概览

后端 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
2
3
4
5
6
7
export const baseFields = {
createdBy: integer().default(0), // 创建者 ID,系统任务写 0
createdAt: timestamp({ mode: "date" }).defaultNow(),
updatedBy: integer().default(0),
updatedAt: timestamp({ mode: "date" }).defaultNow(),
deletedAt: timestamp({ mode: "date" }), // 软删除,NULL = 未删除
};

软删除(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
2
3
4
5
6
7
8
9
10
RBAC / 租户域          模板与相册域              订单与提交域           素材域
─────────────── ─────────────── ─────────────── ──────────────
tenants template_types orders background_categories
system_users templates order_templates background_assets
system_roles albums order_albums font_variants
system_menus album_sizes order_template_submissions
system_user_roles album_page_counts order_template_submission_items
system_role_menus album_size_relations
system_role_buttons album_page_count_relations 商品域
album_templates shops / products / product_specs

多租户是一等公民:业务表普遍带 tenantId,并建了 (tenantId, createdAt)、(tenantId, status) 等复合索引做租户隔离 + 分页。

表关系图(核心)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
albums ─┬─< album_size_relations >── album_sizes
├─< album_page_count_relations >── album_page_counts
└─< album_templates >── templates
│ templates.sceneJson (Leafer 场景树)
│ templates.width/height/templateType
▼
products ──< product_specs >── (templateRules / albumRules 规则 jsonb)
orders ──┬── order_templates >── templates
├── order_albums >── albums
└── order_template_submissions (1)
│
│ (N)
order_template_submission_items
├── templateId / albumId / albumInstanceNo
├── sceneJson (提交时快照)
├── userUploads (用户上传素材)
├── userTexts (用户填入文字)
├── attemptCount / nextRetryAt (重试台账)
└── composedImageUrls (合成产物)

关键表速查

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
2
3
4
5
6
7
8
9
{
"children": [
{ "tag": "Rect", "x": 0, "y": 0, "width": 800, "height": 600, "fill": "#fff" },
{ "tag": "Image", "x": 50, "y": 50, "width": 400, "height": 400,
"editable": true, "placeholder": "请上传图片", "faceSafe": true },
{ "tag": "Text", "x": 50, "y": 500, "text": "在此处编辑文字",
"editable": true, "fontFamily": "SourceHanSans" }
]
}

相册体系(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_code on (shopId, lower(trim(code))) where deletedAt is null;两个 GIN 索引加速规则 jsonb 查询。
  • shops — 店铺维度,规格按店隔离。

订单与提交(orders + 提交主/明细表)

1
2
3
4
5
orders                            ← 业务订单(金额、商品/规格、相册尺寸/P数、访问令牌、过期时间、tenantId)
├ order_templates ← 订单包含哪些模板
├ order_albums ← 订单包含哪些相册
└ order_template_submissions ← 一次"提交合成"一条(首次且唯一,重复提交 409)
└ order_template_submission_items ← 每模板实例一条明细

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
2
3
4
5
cd apps/pf-service
bun db:generate # drizzle-kit generate 生成迁移 SQL
bun db:push # 推送 schema(开发环境)
bun db:migrate # 执行迁移(drizzle-kit migrate)
bun db:studio # Drizzle Studio 可视化

种子分三层:

  1. src/db/seed.ts —— 目前是空占位(export {})。
  2. 运行时 RBAC 种子 services/seedService.ts::ensureDefaultRbacData() —— 幂等建默认租户、超管账号(Bun.password.hash)、超级管理员/管理员 两角色、菜单、角色-菜单/按钮授权。
  3. 批量演示数据 scripts/seed_*.ts(bun scripts/...):seed_ecommerce.ts(店铺/商品/规格)、seed_albums.ts(相册尺寸/P数/模板包)、seed_ai_templates.ts(风格套系 + MinIO 素材上传)、seed_assets.ts(背景/贴纸 SVG)。

下一步

站内搜索

没有找到内容!