概览
后端 Drizzle Schema 位于 apps/pf-service/src/db/schema.ts(~920 行、26 张表),已整体从 MySQL 迁移到 PostgreSQL,全部用 drizzle-orm/pg-core 定义。连接在 src/db/postgres.ts,走 postgres.js 驱动(postgres(config.db.url, { max: 10 }))。
所有业务表共享 baseFields:
1 | export const baseFields = { |
软删除(deletedAt)覆盖所有业务表;查询时必带 isNull(table.deletedAt)。
PostgreSQL 列型约定(重要)
迁移后不再有 MySQL 的痕迹(无 mysqlEnum / datetime / mysql2)。约定:
| 语义 | 用法 |
|---|---|
| 主键 | serial().primaryKey()(自增) |
| 整数 / 金额 | integer()(金额存最小货币单位「分」) |
| JSON | jsonb()(承载 sceneJson、用户上传/文字、合成结果、规则),配 .$type<T>() 标 TS 类型 |
| 时间 | timestamp({ mode: "date" })(不用 datetime) |
| 布尔 | boolean() |
| 字符串 | varchar({ length: N }) |
| 枚举 | 不用 pgEnum——统一 varchar + TypeScript .$type<union>() 收窄 |
常用「枚举」工厂:statusFlag()("1"|"2")、normalStatus()("normal"|"disabled")、taskStatus()("pending"|"processing"|"completed"|"failed")、tenantStatus()("active"|"disabled")。
还用到 PG 专属能力:check() 约束、带 .where(sql\…`)的**部分唯一索引**、以及product_specs上两个 **GIN 索引**(覆盖templateRules/albumRules` 两个 jsonb 列)。
表分域(26 张)
1 | RBAC / 租户域 模板与相册域 订单与提交域 素材域 |
多租户是一等公民:业务表普遍带 tenantId,并建了 (tenantId, createdAt)、(tenantId, status) 等复合索引做租户隔离 + 分页。
表关系图(核心)
1 | albums ─┬─< album_size_relations >── album_sizes |
关键表速查
templates — 模板表
id serial · +baseFields · userId int NN · name varchar(100) · groupId/groupName · canvasOrder int · categoryId int NN · templateType varchar(50) NN · width/height int NN · sceneJson jsonb NN · previewUrl varchar(500) · status normalStatus · tenantId int。
索引:idx_templates_category/type/status/tenant、idx_templates_tenant_status (tenantId,status)、idx_templates_tenant_created (tenantId,createdAt)、idx_templates_group_order (groupId,canvasOrder)。
sceneJson 是整个项目的”协议中枢”——编辑器写、用户端读、合成服务消费。结构示例:
1 | { |
相册体系(albums + 关联表)
相册是”多模板成册”的一等公民:
albums— 相册定义(bindingType、pageMode single/spread、coverMaterial、tags jsonb、status、tenantId)album_sizes— 可选尺寸(width/height/sort),uk(name)album_page_counts— 可选 P 数(pageCount),uk(pageCount)、uk(name)album_size_relations/album_page_count_relations— 相册↔尺寸/P数(uk(album,size)等)album_templates— 相册由哪些模板按relationType ∈ {inner, cover, back_cover}组成,含sort
用户选一本相册 + 尺寸 + P 数后,系统按 album_templates 展开成具体模板列表逐张套用。
商品与规格(products + product_specs)
products— 商品;product_specs— 商品规格,绑定模板规则templateRules jsonb({templateId, templateNum?}[])与相册规则albumRules jsonb({albumId}[])。- 约束示例:
check强制albumSizeId与albumPageCountId同为空或同有值;部分唯一索引uk_product_specs_shop_codeon(shopId, lower(trim(code)))wheredeletedAt is null;两个 GIN 索引加速规则 jsonb 查询。 shops— 店铺维度,规格按店隔离。
订单与提交(orders + 提交主/明细表)
1 | orders ← 业务订单(金额、商品/规格、相册尺寸/P数、访问令牌、过期时间、tenantId) |
orders 关键列:productId/productSpecId int NN、orderNo varchar(50)、buyerName/buyerPhone、totalAmount int NN、orderAccessTokenHash/ExpiresAt/RevokedAt(一次性访问令牌)、accessTokenRequired bool、status normalStatus、uploadStatus "pending"|"submitted"、composeStatus taskStatus、hasDownloaded bool、firstDownloadedAt timestamp、expireAt、tenantId。索引含 uk(orderNo)、idx_orders_compose_status、idx_orders_tenant_created、idx_orders_tenant_downloaded (tenantId,hasDownloaded)。
order_template_submissions:orderId NN、orderNo NN、status taskStatus、downloadedAt timestamp、originalUploads jsonb、tenantId。索引含 idx_ots_tenant_status、idx_ots_downloaded_at。
order_template_submission_items:submissionId/orderId/templateId NN、albumId/albumInstanceNo/albumPageNo、instanceNo、templateNum int、sceneJson jsonb NN、userUploads jsonb NN、userTexts jsonb NN、status taskStatus、failReason、attemptCount int、nextRetryAt timestamp、composedImageUrls jsonb (string[])。索引含 idx_otsi_submission_status (submissionId,status)、idx_otsi_album_instance、idx_otsi_template_instance。
合成状态 vs 下载状态(解耦)
这是近期一次重要重构(8bedb52 合成状态与下载状态解耦):
| 载体 | 取值 | 谁维护 | |
|---|---|---|---|
| 合成状态 | orders.composeStatus / submissions.status / items.status(taskStatus) |
pending → processing → completed | failed |
后台合成 Job(可重试、状态聚合) |
| 下载状态 | orders.hasDownloaded(bool) / orders.firstDownloadedAt / submissions.downloadedAt |
时间戳/布尔,首次写入即固化 | 下载 API 同步写(无 Job,前置条件是合成 completed) |
明细的 attemptCount / nextRetryAt 是重试台账:失败后 nextRetryAt = now + 2^attempt s,attemptCount ≥ 3 永久失败;processing 超 10 分钟视为僵尸被下一轮重新抢占。
提交明细的快照字段
order_template_submission_items 中存了三份快照:sceneJson(提交时刻的模板场景树,防止管理员后续改模板影响历史合成)、userUploads({ instanceKey: { elementKey: { url } } })、userTexts(同结构存 text)、composedImageUrls(合成产物 URL 列表)。
为什么二级索引 instanceKey + elementKey? 同一张模板可能被同一订单引用多次(templateNum > 1 / instanceNo),每”次”实例分别上传;元素 key 对应 sceneJson 中可编辑节点的 id。
RBAC 与租户
tenants + system_users / system_roles / system_menus + 三张关联表(system_user_roles、system_role_menus、system_role_buttons)构成后台权限体系,与 soybean-admin 结构对齐。
素材库
background_categories/background_assets(userId=0全局,否则个人/租户)font_variants(ttf/otf/woff/woff2)template_types— 模板分类字典
字段约定
- 主键:
serial自增 - 外键:仅在 schema 注释体现,不建物理外键约束(保留迁移弹性)
- 金额:
integer存分 - 状态:
varchar + $type收窄;业务无关字段统一normal / disabled - JSON:一律
jsonb,配.$type<T>() - 审计:
baseFields强制每张表都有 created/updated/deleted - 租户:业务表普遍带
tenantId,配复合索引
迁移与种子
1 | cd apps/pf-service |
种子分三层:
src/db/seed.ts—— 目前是空占位(export {})。- 运行时 RBAC 种子
services/seedService.ts::ensureDefaultRbacData()—— 幂等建默认租户、超管账号(Bun.password.hash)、超级管理员/管理员两角色、菜单、角色-菜单/按钮授权。 - 批量演示数据
scripts/seed_*.ts(bun scripts/...):seed_ecommerce.ts(店铺/商品/规格)、seed_albums.ts(相册尺寸/P数/模板包)、seed_ai_templates.ts(风格套系 + MinIO 素材上传)、seed_assets.ts(背景/贴纸 SVG)。
下一步
- 想看后端如何消费这些表 → pf-service 后端
- 想看合成 Job 如何用提交明细 → 自动合成流水线
- 想看 sceneJson 怎么被编辑/渲染 → 模板编辑器