(阶段二:落地)🧠 CMS 模板系统核心数据结构与流程梳理(SceneStack)

 📌 目标:以 tenantId 为核心构建 theme + scene + section 的配置系统,实现从 schema 显示、内容编辑、再到保存的完整闭环。

🌐 一、核心数据结构(数据库设计)

1. Section 模板定义(结构层)

每个 theme + scene 下的模块结构由系统预设,写在 section_registry 表中:

CREATE TABLE section_registry (
  id SERIAL PRIMARY KEY,
  theme_name TEXT NOT NULL,
  scene_name TEXT NOT NULL,
  section_name TEXT NOT NULL,
  schema JSONB NOT NULL,           -- 字段结构定义
  display_order INTEGER NOT NULL DEFAULT 0,  -- 顺序排列

  UNIQUE (theme_name, scene_name, section_name)
);

 

用于:提供结构化的 schema,用来渲染输入表单。


2. SectionInstance(内容层)

interface SectionInstance {
  id: string;
  tenantId: string;
  themeName: string;       // 冗余,方便检索
  sceneName: string;       // 如 landing、shop
  sectionName: string;     // 如 HeroSection
  order: number;           // 顺序
  props: Record<string, any>;  // 用户填写的数据
  updatedAt: Date;
}

 

用于:保存每一个模块的用户填写内容。

3. PageInstance(页面层)

interface PageInstance {
  id: string;
  pageType: string;      // 如 landing
  themeName: string;
  sections: sectionInstanceId[];
}

 

用于:按页面聚合 SectionInstance 列表。


4. ThemeOptions(入口层)

interface ThemeOptions {
  tenantId: string;
  themeName: string; // "cool", "general"
  pageInstanceMap: Record<string, PageInstanceId>; // 如 { landing: 'abc-123' }
  updatedAt: Date;
  themeVariant?: string;
  version?: number;
}

 

用于:记录当前租户的主题信息和页面入口(页面 ID 映射表)。


🔁 二、前端 CMS 操作流程(开发视角)


✅ Step 1:选择主题 + 页面类型

用户在 CMS 中选择:

  • themeName = "cool"

  • sceneName = "landing"


✅ Step 2:展示

1. 查结构模板 schema(用于展示表单结构)

请求:

GET /api/section-schema?template=cool&pageType=landing

查询语句:

SELECT * FROM section_registry WHERE theme_name = 'cool' AND scene_name = 'landing' ORDER BY display_order ASC;

 

返回:结构列表 section_registry[],用于构建输入表单。
 
目标映射type:
type SectionRegistryEntry = {
  templateId: string;
  pageType: string;
  sectionName: string;
  schema: Record<string, FieldSchema>;
};
type FieldSchema = {
  type: 'string' | 'number' | 'boolean' | 'image' | 'richtext' | 'array' | 'object';
  required?: boolean;
  defaultValue?: any;
  label?: string;
  placeholder?: string;
};

➜ 在后端查询后,返回数据结构如下(伪代码)

例如使用 Prisma / Drizzle / SQL 手写查询,返回:

type SectionRegistryRow = {
  theme_name: string;
  scene_name: string;
  section_name: string;
  schema: any; // JSONB 会解析为 JS 对象
  display_order: number;
};

✨ 映射转换逻辑:

function mapToSectionRegistryEntry(row: SectionRegistryRow): SectionRegistryEntry {
  return {
    templateId: row.theme_name,
    pageType: row.scene_name,
    sectionName: row.section_name,
    schema: row.schema as Record<string, FieldSchema>
  };
}

const sectionRegistryList: SectionRegistryEntry[] = rows.map(mapToSectionRegistryEntry);


 2. 从 theme_options 中获取 pageInstanceId

 const pageInstanceId = themeOptions.pageInstanceMap['landing'] 

 


3. 判断是否存在 pageInstanceId:

❌ 不存在:

→ 说明该页面 尚未创建
return null[],由前端决定显示空表单 or 提示

✅ 存在:

→ 拉取对应的 pageInstance 数据,检查其 themeName:

  const pageInstance = await db.pageInstance.findById(pageInstanceId) 
 
→ 若 pageInstance.themeName !== 当前 themeName:
  • 不再继续查 sectionInstance

  • return null,由前端提示用户是否切换主题或清空数据

→ 若 theme 匹配:

  • 拉取所有 SectionInstance:

 const sectionInstanceList = await db.sectionInstance.findMany({ where: { id: { in: pageInstance.sections } }, orderBy: { order: 'asc' } })  

 

Step 3. 前端渲染逻辑概念:

for (let i = 0; i < sectionRegistryList.length; i++) {
  const schema = sectionRegistryList[i].schema         // Record<string, FieldSchema>
  const userData = sectionInstanceList[i]?.props ?? {} // 用户填写的数据

  Object.entries(schema).forEach(([fieldName, fieldSchema]) => {
    const value = userData[fieldName] ?? fieldSchema.defaultValue
    // 渲染输入框组件
  })
}

 


💾 三、保存逻辑

每个 SectionInstance 可单独保存:

PATCH /api/section-instance/:id

body = {
  props: {
    primaryText: "...",
    imageUrl: "...",
    ...
  }
}

 

也可以批量保存 PageInstance 下所有 section(optional)。

📦 四、总结

层级模型说明
结构层 section_registry 模板定义,用于生成字段结构
内容层 SectionInstance 用户填写的实际内容
页面层 PageInstance 聚合多个模块组成页面
映射层 ThemeOptions.pageInstanceMap 用于识别当前页面入口 ID

⚠️ 所有动态变动都基于数据库,schema 注册在 section_registry,数据存储在 SectionInstance,映射控制由 ThemeOptions 统一管理。

posted @ 2025-07-28 12:34  PEAR2020  阅读(24)  评论(0)    收藏  举报