Schema 变更同步 (Schema Change Sync)
1. 适用场景
当 apps/type/src/business/**/schema.ts 发生以下变更时,必须按本清单逐项检查并同步:
- 场景 A: 现有表新增/删除/重命名字段
- 场景 B: 新增数据库表
- 场景 C: 删除数据库表
2. 同步检查清单
复制此清单跟踪进度:
Schema 变更同步进度:
- [ ] 1. Schema 文件 (Trinity Pattern)
- [ ] 2. 导出链完整性
- [ ] 3. 前端业务类型
- [ ] 4. 数据库迁移
- [ ] 5. 技能文档
- [ ] 6. 后端 API 接口
- [ ] 7. 种子数据
- [ ] 8. 前端列表页面
- [ ] 9. 前端表单组件
- [ ] 10. 前端 API Hook
2.1. Schema 文件 (Trinity Pattern)
文件: apps/type/src/business/{domain}/{module}/schema.ts
Schema 文件必须同时更新三个部分:
| 部分 | 内容 | 示例 |
|---|---|---|
| Part A | Drizzle Table 定义 | pgTable("dt_cache_configs", { ... }) |
| Part B | Zod Schemas (insert/select/update) | insertDtCacheConfigSchema, updateDtCacheConfigSchema |
| Part C | TypeScript Types | DtCacheConfig, NewDtCacheConfig, UpdateDtCacheConfig |
注意: update schema 手动使用 z.object({...}),新增字段需要手动添加对应的 optional 字段。
2.2. 导出链完整性
仅新增表时需要。确保从 schema.ts 到 src/index.ts 的完整导出链:
schema.ts → module/index.ts → domain/index.ts → business/index.ts → src/index.ts
每一层 index.ts 必须包含 export * from "./xxx";。
2.3. 前端业务类型
重要: 前端业务类型应从 Schema 推导生成,存放在独立的 .ts 文件中(见第 6 节规范)。
文件: apps/type/src/business/{domain}/{module}/index.ts 或 {module}/xxx-types.ts
| 类型 | 生成位置 | 说明 |
|---|---|---|
XxxListItem |
独立 .ts 文件 (index.ts) | 列表页每行数据 |
XxxQueryParams |
独立 .ts 文件 (index.ts) | 搜索/筛选参数 |
XxxFormVO |
独立 .ts 文件 (index.ts) | 表单数据对象 |
| Options 常量 | common/business-options.ts |
下拉选项 |
迁移状态: 存量放在 schema.ts 中的前端类型应逐步迁移到独立 .ts 文件(见 6.6 节)。
2.4. 数据库迁移
pnpm -F @01s-11comm/api db:generate
检查生成的 apps/api/drizzle/xxxx_*.sql 迁移文件内容是否正确。apps/type 只负责 schema 单一事实源;Drizzle Kit 配置、drizzle/** 迁移目录、db:* 脚本、Neon readiness/drift 诊断和受控迁移执行归 apps/api。apps/admin 的旧入口仅作为 legacy source 或兼容参考,不再作为长期权威项目。
2.5. 技能文档
| 文件 | 更新内容 |
|---|---|
.claude/skills/neon-db-list/SKILL.md |
新增表: 添加条目;修改字段: 更新备注;删除表: 移除条目 |
.claude/skills/project-schema-registry/references/{domain}.md |
更新对应领域的 Scenario 描述 |
2.6. 后端 API 接口
文件: apps/api/server/routes/api/{业务路径}/*.ts(旧 apps/admin/server/api/{业务路径}/*.ts 仅作 legacy source 对照)
| 变更点 | 说明 |
|---|---|
列表接口 list.post.ts |
select 查询包含新字段;映射函数处理字段名差异;querySchema 包含新筛选参数 |
| 新增/更新接口 | readValidatedBody 使用的 Zod schema 已更新 |
| 新增表时 | 创建完整的 CRUD 接口文件 (list/create/update/delete) |
2.7. 种子数据
文件: apps/admin/server/db/seed/modules/{module}.seed.ts(当前 seed source 仍在旧目录时仅作兼容维护;不要把它扩展为新的长期 DB 运维入口)
- 在对应 seed 模块的
seed(db)函数中添加新表的db.insert(table).values([...]) - 数据直接使用 Drizzle Insert 类型,TypeScript 编译器会检查字段匹配
- 使用
sid(scope, key)生成确定性 UUID - 如果是全新领域,创建新的
.seed.ts文件并在_registry.ts中注册 - 在
apps/api提供专门 seed 能力前,使用现有 legacy seed 脚本验证数据填充,并在任务记录中标明兼容边界
2.8. 前端列表页面
文件: apps/admin/src/pages/{业务路径}/index.vue
| 变更点 | 说明 |
|---|---|
columns |
表格列配置包含新字段 |
plusSearchColumns |
搜索栏包含新筛选条件 |
plusSearchModelRef |
搜索默认值包含新字段 |
2.9. 前端表单组件
文件: apps/admin/src/pages/{业务路径}/components/form.ts + form.vue
| 文件 | 变更点 |
|---|---|
form.ts |
defaultForm 包含新字段默认值 |
form.vue |
plusFormColumns 包含新表单项;plusFormRules 包含新校验规则 |
2.10. 前端 API Hook
文件: apps/admin/src/api/{业务路径}/index.ts
- 修改字段时: 通常无需更改(Hook 透传参数)
- 新增表时: 创建
useXxxListQueryHook
3. 字段名映射规范
3.1. DB 字段与前端类型字段的常见差异
后端 API 的映射函数需处理以下差异:
| 差异类型 | DB 字段 (Drizzle) | 前端字段 (ListItem) |
|---|---|---|
| 主键 | id |
xxxId (如 cacheId) |
| 创建时间 | createTime (Date) |
createTime (string) |
| 更新时间 | updateTime (Date) |
updateTime (string) |
| 语义重命名 | refreshStrategy |
refreshPolicy |
日期格式化统一为 YYYY-MM-DD HH:mm:ss。
3.2. 枚举值映射规范
| 存储层 | 格式 | 示例 |
|---|---|---|
| 数据库 | 英文枚举值 (options 的 value) |
"enabled", "scheduled" |
| 前端展示 | 中文标签 (options 的 label) |
"启用", "定时刷新" |
种子数据如果来自 Mock(使用中文标签),必须建立 label→value 映射表:
const statusMap: Record<string, string> = {
启用: "enabled",
禁用: "disabled",
维护中: "maintenance",
};
4. 场景速查表
4.1. 场景 A: 现有表新增字段
| 序号 | 检查项 | 必须 |
|---|---|---|
| 1 | Schema 文件 Part A + B + C | 是 |
| 2 | 数据库迁移 db:generate |
是 |
| 3 | 技能文档更新 | 是 |
| 4 | 后端 API 映射函数 | 是 |
| 5 | 前端业务类型 | 视情况 |
| 6 | 种子数据 | 视情况 |
| 7 | 前端列表/表单 | 视情况 |
4.2. 场景 B: 新增数据库表
全部 10 项检查项均为必须。额外注意:
- 导出链完整性(2.2)
- 创建完整 CRUD 接口(2.6)
- 创建前端 API Hook(2.10)
5. 实战示例
参考 apps/admin/src/docs/reports/2026-02-13-schema-sync-progress.md,记录了 dtCacheConfigs 表新增 5 个字段时的完整同步过程,涉及 7 类文件共 15 个文件的同步更改。
6. 前端业务类型生成规范
核心原则: schema.ts 只存储 DB 层类型,前端层类型存放在独立的 .ts 文件中。
6.1. 类型体系架构
项目类型分为两个层次:
| 层次 | 类型名称 | 存放位置 | 示例 |
|---|---|---|---|
| DB 层 | Xxx, NewXxx, UpdateXxx |
schema.ts |
DtCacheConfig, NewDtCacheConfig |
| 前端层 | XxxListItem, XxxQueryParams, XxxFormVO |
独立 .ts 文件 (index.ts/xxx-types.ts) | DtCacheConfigListItem |
6.2. 文件组织规范
6.2.1. schema.ts - 只放 DB 层类型
// ==========================================
// Part C: TypeScript Types (仅 DB 层)
// ==========================================
// --- DB 层类型 ---
export type DtCacheConfig = typeof dtCacheConfigs.$inferSelect;
export type NewDtCacheConfig = typeof dtCacheConfigs.$inferInsert;
export type UpdateDtCacheConfig = z.infer<typeof updateDtCacheConfigSchema>;
注意: schema.ts 中只能包含以上三种 DB 层类型,禁止在此文件中定义前端层类型。
6.2.2. 独立 .ts 文件 - 存放前端层类型
前端层类型存放在以下位置之一:
- 推荐:
{module}/index.ts- 与 schema.ts 同目录下的 index.ts - 可选:
{module}/xxx-types.ts- 专门的类型文件
// apps/type/src/business/setting-manage/dictionary-manage/index.ts
// 从 schema.ts 导入 DB 层类型
export type { DtCacheConfig, NewDtCacheConfig, UpdateDtCacheConfig } from "./schema";
// --- 前端层类型 ---
/** 缓存配置列表数据 */
export type DtCacheConfigListItem = Omit<DtCacheConfig, "createTime " | "updateTime" | "deletedAt"> & {
/** 创建时间 */
createTime: string;
/** 更新时间 */
updateTime: string;
};
/** 缓存配置查询参数 */
export interface DtCacheConfigQueryParams extends BaseListQueryParams {
/** 缓存名称 */
cacheName?: string;
/** 缓存类型 */
cacheType?: string;
/** 状态 */
status?: string;
}
/** 缓存配置表单数据 */
export interface DtCacheConfigFormVO {
/** 缓存编码 */
cacheCode: string;
/** 缓存名称 */
cacheName: string;
/** 缓存键 */
cacheKey: string;
/** 缓存类型 */
cacheType?: string;
/** 缓存分组 */
cacheGroup?: string;
/** 过期时间(秒) */
expireTime?: number;
/** 缓存描述 */
description?: string;
/** 刷新策略 */
refreshStrategy?: string;
/** 状态 */
status?: string;
}
6.3. 类型推导公式
6.3.1. ListItem 类型推导
// 从 Select 类型推导,移除系统字段,转换时间格式
export type DtCacheConfigListItem = Omit<DtCacheConfig, "createTime " | "updateTime" | "deletedAt"> & {
createTime: string; // 从 createTime 转换
updateTime: string; // 从 updateTime 转换
};
推导规则:
- 移除
deletedAt(软删除字段不展示) - 将
createTime : Date转换为createTime: string - 将
updateTime: Date转换为updateTime: string - 主键
id保留(无需转换为xxxId,除非业务需要)
6.3.2. QueryParams 类型推导
// 继承分页参数,添加业务筛选字段
export interface DtCacheConfigQueryParams extends BaseListQueryParams {
/** 缓存名称(模糊搜索) */
cacheName?: string;
/** 缓存类型 */
cacheType?: string;
/** 状态 */
status?: string;
}
推导规则:
- 必须继承
BaseListQueryParams(包含pageIndex,pageSize) - 业务筛选字段从 Table 的可筛选列推导
- 使用可选字段(
?)
6.3.3. FormVO 类型推导
// 基于 Insert 类型,添加展示用字段
export interface DtCacheConfigFormVO {
/** 缓存编码 */
cacheCode: string;
/** 缓存名称 */
cacheName: string;
/** 缓存键 */
cacheKey: string;
/** 缓存类型 */
cacheType?: string;
/** 缓存分组 */
cacheGroup?: string;
/** 过期时间(秒) */
expireTime?: number;
/** 缓存描述 */
description?: string;
/** 刷新策略 */
refreshStrategy?: string;
/** 状态 */
status?: string;
}
推导规则:
- 基于
NewXxx类型 - 必填字段保留原类型
- 可选字段使用
? - 不包含
id,createTime,updateTime
6.4. 字段映射规范
6.4.1. 系统字段映射表
| DB 字段 | 前端字段 | 转换规则 |
|---|---|---|
id |
id |
保持不变 |
createTime |
createTime |
Date → string (格式: YYYY-MM-DD HH:mm:ss) |
updateTime |
updateTime |
Date → string (格式: YYYY-MM-DD HH:mm:ss) |
deletedAt |
- | 移除(不展示) |
remark |
remark |
保持不变 |
6.4.2. 自定义字段映射
对于需要语义化重命名的字段,在前端类型文件中使用 交叉类型:
// 示例:主键语义化映射(在独立 .ts 文件中)
export type PtPatrolTaskListItem = Omit<PtPatrolTask, "id"> & {
/** 任务ID */
taskId: string;
};
6.5. Options 常量存放位置
Options 常量应存放在 apps/type/src/common/business-options.ts 文件中,不应放在 schema.ts 或模块的独立类型文件中。
// apps/type/src/common/business-options.ts
/** 缓存类型选项 */
export const dtCacheConfigTypeOptions: OptionsType = [
{ label: "内存", value: "memory" },
{ label: "Redis", value: "redis" },
{ label: "本地", value: "local" },
];
/** 状态选项 */
export const dtCacheConfigStatusOptions: OptionsType = [
{ label: "启用", value: "enabled" },
{ label: "禁用", value: "disabled" },
{ label: "维护中", value: "maintenance" },
];
存放规则:
- 从 Zod Schema 的
.enum()推导枚举值 - 中文
label需要人工补充 - 存放在
common/business-options.ts,通过导出链统一导出
6.6. 迁移流程
6.6.1. 迁移阶段划分
| 阶段 | 目标 | 操作 |
|---|---|---|
| Phase 1 | 新增 Schema 时 | schema.ts 只放 DB 类型,前端类型放 index.ts |
| Phase 2 | 存量放在 schema.ts 的前端类型 | 迁移到独立 .ts 文件 |
| Phase 3 | 清理独立文件中重复的类型定义 | 删除已迁移的重复类型 |
6.6.2. Phase 1: 新增 Schema 时的操作步骤
步骤 1: 在 schema.ts Part C 中生成仅 DB 层类型
// schema.ts Part C - 只放 DB 层类型
export type DtCacheConfig = typeof dtCacheConfigs.$inferSelect;
export type NewDtCacheConfig = typeof dtCacheConfigs.$inferInsert;
export type UpdateDtCacheConfig = z.infer<typeof updateDtCacheConfigSchema>;
步骤 2: 在 index.ts 中生成前端层类型
// index.ts - 前端层类型
// ListItem: 移除系统字段,转换时间格式
export type DtCacheConfigListItem = Omit<DtCacheConfig, "createTime " | "updateTime" | "deletedAt"> & {
createTime: string;
updateTime: string;
};
// QueryParams: 继承 BaseListQueryParams
export interface DtCacheConfigQueryParams extends BaseListQueryParams {
cacheName?: string;
cacheType?: string;
status?: string;
}
// FormVO: 基于 NewXxx,可选化
export type DtCacheConfigFormVO = Partial<NewDtCacheConfig> &
Pick<NewDtCacheConfig, "cacheCode" | "cacheName" | "cacheKey">;
步骤 3: 导入必要的类型
import type { BaseListQueryParams, OptionsType } from "../../../common";
步骤 4: 更新导出链
确保 module/index.ts 正确导出:
export * from "./schema";
export * from "./index"; // 包含前端层类型
6.6.3. Phase 2: 存量迁移操作步骤
目标: 将存放在 schema.ts 中的前端类型迁移到独立 .ts 文件
步骤 1: 识别需要迁移的模块
存量前端类型存放位置:
├── community-manage/
│ ├── schema.ts (含前端类型) → 迁移到 index.ts
│ └── index.ts
├── patrol-manage/
│ ├── schema.ts (含前端类型) → 迁移到 index.ts
│ └── index.ts
步骤 2: 移动前端类型到 index.ts
- 从 schema.ts 中移除前端层类型(XxxListItem, XxxQueryParams, XxxFormVO)
- 在 index.ts 中添加前端层类型定义
- 从 schema.ts 导入 DB 层类型供前端类型使用
步骤 3: 更新前端引用
在需要使用的地方,从 index.ts 导入:
// 旧写法(从 schema.ts 导入)
import type { PatrolTaskListItem, PatrolTaskQueryParams } from "../patrol-manage/schema";
// 新写法(从 index.ts 导入)
import type { PtPatrolTaskListItem, PtPatrolTaskQueryParams } from "../patrol-manage";
步骤 4: 运行类型检查
pnpm -F @01s-11comm/type typecheck
步骤 5: 确认无类型错误后,更新 schema.ts
从 schema.ts 中删除已迁移的前端类型定义。
6.7. 迁移检查清单
前端业务类型迁移进度:
- [ ] 1. 在 schema.ts Part C 中生成 DB 层类型(仅三种)
- [ ] 2. 在 index.ts 中生成前端层类型(ListItem/QueryParams/FormVO)
- [ ] 3. 从 schema.ts 导入 DB 层类型供前端类型使用
- [ ] 4. 导入 BaseListQueryParams
- [ ] 5. Options 常量放在 common/business-options.ts
- [ ] 6. 迁移类型别名(如 NoticeType、枚举类型)到 index.ts
- [ ] 7. 迁移业务逻辑函数(如 noticeListDataToFormData)到独立文件
- [ ] 8. 处理继承关系(如 PatrolTaskListItem extends TaskListItem)
- [ ] 9. 更新后端 API 映射函数(mapToListItem)
- [ ] 10. 更新前端引用(form.ts、hooks、composables、Vue 组件)
- [ ] 11. 运行类型检查确认无错误
- [ ] 12. 从 schema.ts 中删除已迁移的前端类型
6.8. 常见问题处理
Q1: 字段名需要语义化重命名怎么办?
问题: Schema 的 id 字段在前端需要显示为 taskId
解决: 使用交叉类型覆盖(在 index.ts 中)
export type PtPatrolTaskListItem = Omit<PtPatrolTask, "id"> & {
taskId: string;
};
Q2: 前端只需要部分字段怎么办?
问题: ListItem 不需要 Schema 的所有字段
解决: 使用 Pick 或 Omit
export type DtCacheConfigListItem = Pick<DtCacheConfig, "id" | "cacheCode" | "cacheName" | "cacheType" | "status"> & {
createTime: string;
updateTime: string;
};
Q3: 如何处理继承关系(如 PatrolTaskListItem extends TaskListItem)?
问题: 现有代码使用 PatrolTaskListItem extends TaskListItem
解决: 迁移后改为独立定义,消除继承依赖
// 旧写法
export interface TaskListItem {
id: string;
name: string;
status: string;
createTime: string;
}
export interface PatrolTaskListItem extends TaskListItem {
taskCode: string;
patrolPlan: string;
}
// 新写法 - 全部独立定义,不依赖继承
export interface PtPatrolTaskListItem {
id: string;
taskCode: string;
taskName: string;
patrolPlan: string;
status: string;
createTime: string;
updateTime: string;
}
注意: 继承关系会使类型推导复杂化,迁移后应使用独立定义。
Q4: 类型别名(如 NoticeType、MenuItemType)如何迁移?
问题: 存放在 schema.ts 中的类型别名
// schema.ts 中(错误做法)
export type NoticeType = "notification" | "announcement" | "reminder";
export type MenuItemType = "catalog" | "menu" | "button";
解决: 迁移到 index.ts
// index.ts 中(正确做法)
/** 公告类型 */
export type NoticeType = "notification" | "announcement" | "reminder" | "activity" | "maintenance" | "safety";
/** 菜单项类型 */
export type MenuItemType = "catalog" | "menu" | "button" | "interface";
Q5: 业务逻辑函数(如 noticeListDataToFormData)如何处理?
问题: 独立文件中存在数据转换函数
// 独立文件中
export function noticeListDataToFormData(listData: CommunityNoticeListItem): CommunityNoticeFormVO {
return {
noticeTitle: listData.noticeTitle,
// ...
};
}
解决: 保留在独立文件中,类型引用从 index.ts 导入
// 保留在独立文件中,但更新类型引用
import type { CmNoticeListItem, CmNoticeFormVO } from "./index";
export function noticeListDataToFormData(listData: CmNoticeListItem): CmNoticeFormVO {
return {
noticeTitle: listData.noticeTitle,
// ...
};
}
Q6: 后端 API 映射函数需要更新吗?
问题: 当 ListItem 类型变化时,后端的映射函数也需要更新
解决: 检查并更新后端 API 的映射函数
// 后端 API 中的映射函数
function mapToListItem(row: CmNotice): CmNoticeListItem {
return {
...row,
createTime: row.createTime ? format(row.createTime, "yyyy-MM-dd HH:mm:ss") : "",
updateTime: row.updateTime ? format(row.updateTime, "yyyy-MM-dd HH:mm:ss") : "",
};
}
注意: 当 Schema 字段变化时,映射函数需要同步更新。
Q7: 前端引用范围有哪些?
问题: 需要更新哪些文件的引用?
解决: 检查以下位置的类型引用
| 位置 | 文件类型 | 示例 |
|---|---|---|
| 表单组件 | form.ts |
defaultForm: XxxFormVO |
| 列表页面 | index.vue |
columns 配置 |
| API Hooks | api/xxx/index.ts |
返回值类型 |
| Composables | composables/*.ts |
参数/返回值类型 |
| 其他组件 | components/*.vue |
Props 类型 |
迁移步骤:
- 搜索所有使用旧类型的地方
- 将导入路径从
./schema改为从./index导入 - 运行类型检查确认无错误
export type DtCacheConfigListItem = Pick<DtCacheConfig, "id" | "cacheCode" | "cacheName" | "cacheType" | "status"> & {
createTime: string;
updateTime: string;
};
Q8: FormVO 需要额外的展示字段怎么办?
问题: 表单需要显示关联名称(如 communityName),但 Schema 只有 communityId
解决: 使用交叉类型扩展(在 index.ts 中)
export interface CmNoticeFormVO extends Omit<NewCmNotice, "communityId"> {
/** 社区名称(展示用) */
communityName?: string;
communityId?: string;
}
Q9: 如何处理日期格式转换?
问题: DB 返回 Date 类型,前端需要 string
解决: 在后端 API 的映射函数中处理
function mapToListItem(row: CmNotice): CmNoticeListItem {
return {
...row,
createTime: row.createTime ? format(row.createTime, "yyyy-MM-dd HH:mm:ss") : "",
updateTime: row.updateTime ? format(row.updateTime, "yyyy-MM-dd HH:mm:ss") : "",
};
}
6.9. 迁移示例:dtCacheConfigs
以 dtCacheConfigs 为例,展示正确的文件组织方式:
schema.ts - 仅包含 DB 层类型:
// apps/type/src/business/setting-manage/dictionary-manage/schema.ts
// Part A: Drizzle Table
export const dtCacheConfigs = pgTable("dt_cache_configs", {
// ... 字段定义
});
// Part B: Zod Schemas
export const insertDtCacheConfigSchema = createInsertSchema(dtCacheConfigs);
export const updateDtCacheConfigSchema = createUpdateSchema(dtCacheConfigs);
// Part C: DB 层类型(仅此三种)
export type DtCacheConfig = typeof dtCacheConfigs.$inferSelect;
export type NewDtCacheConfig = typeof dtCacheConfigs.$inferInsert;
export type UpdateDtCacheConfig = z.infer<typeof updateDtCacheConfigSchema>;
index.ts - 包含前端层类型:
// apps/type/src/business/setting-manage/dictionary-manage/index.ts
// 从 schema.ts 导入 DB 层类型
export type { DtCacheConfig, NewDtCacheConfig, UpdateDtCacheConfig } from "./schema";
// --- 前端层类型 ---
/** 缓存配置列表数据 */
export type DtCacheConfigListItem = Omit<DtCacheConfig, "createTime " | "updateTime" | "deletedAt"> & {
createTime: string;
updateTime: string;
};
export interface DtCacheConfigQueryParams extends BaseListQueryParams {
cacheName?: string;
cacheType?: string;
status?: string;
}
export type DtCacheConfigFormVO = Partial<NewDtCacheConfig>;
// Options 常量(从 common 导入)
export { dtCacheConfigStatusOptions, dtCacheConfigTypeOptions } from "../../../common/business-options";
前端引用方式:
// 从 index.ts 导入前端层类型
import type {
DtCacheConfigListItem,
DtCacheConfigQueryParams,
DtCacheConfigFormVO,
} from "@01s-11comm/type/setting-manage/dictionary-manage";
// 或使用路径别名
import type { DtCacheConfigListItem } from "@/types/setting-manage/dictionary-manage";
7. 迁移状态追踪
迁移原则: 渐进式迁移,每次只处理一个模块,确保类型检查通过后再处理下一个模块。