164 lines
6.4 KiB
TypeScript
164 lines
6.4 KiB
TypeScript
import {
|
||
boolean,
|
||
int,
|
||
json,
|
||
mysqlTable,
|
||
text,
|
||
timestamp,
|
||
varchar,
|
||
} from "drizzle-orm/mysql-core";
|
||
|
||
/**
|
||
* msg 服务数据库 Schema。
|
||
*
|
||
* 仲裁依据:
|
||
* - 02-architecture-design.md §3.1(表清单 + 字段 + 索引)
|
||
* - G11:ID 用 cuid2(varchar(32))
|
||
* - proto msg.proto 字段对齐(Notification / NotificationPreference / NotificationTemplate)
|
||
*
|
||
* 表清单:
|
||
* - msg_notifications(通知主表,扩展字段)
|
||
* - msg_notification_preferences(用户偏好,扩展字段)
|
||
* - msg_notification_templates(通知模板,新增)
|
||
* - msg_notification_deliveries(投递记录,新增)
|
||
*
|
||
* 注:msg_outbox_events / processed_events 在 shared/outbox/outbox.schema.ts
|
||
*/
|
||
|
||
// ============================================================
|
||
// 通知状态 / 渠道 / 类型 枚举(字符串枚举,DB 存 varchar)
|
||
// ============================================================
|
||
|
||
export type NotificationStatus =
|
||
"pending" | "sent" | "delivered" | "read" | "recalled" | "failed";
|
||
|
||
export type NotificationChannel =
|
||
"in_app" | "email" | "sms" | "push" | "wechat";
|
||
|
||
export type NotificationType =
|
||
"system" | "exam" | "homework" | "grade" | "attendance" | "mastery";
|
||
|
||
export type DeliveryStatus =
|
||
"pending" | "sent" | "delivered" | "failed" | "retrying";
|
||
|
||
export type TemplateStatus = "draft" | "active" | "archived";
|
||
|
||
// ============================================================
|
||
// msg_notifications(通知主表 · 扩展)
|
||
// ============================================================
|
||
|
||
export const notifications = mysqlTable("msg_notifications", {
|
||
id: varchar("id", { length: 32 }).notNull().primaryKey(),
|
||
userId: varchar("user_id", { length: 32 }).notNull(),
|
||
type: varchar("type", { length: 32 }).notNull().$type<NotificationType>(),
|
||
title: varchar("title", { length: 255 }).notNull(),
|
||
content: text("content").notNull(),
|
||
channel: varchar("channel", { length: 32 })
|
||
.notNull()
|
||
.$type<NotificationChannel>(),
|
||
isRead: boolean("is_read").notNull().default(false),
|
||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||
// 扩展字段(P5)
|
||
status: varchar("status", { length: 32 })
|
||
.notNull()
|
||
.default("pending")
|
||
.$type<NotificationStatus>(),
|
||
metadata: json("metadata").$type<Record<string, string> | null>(),
|
||
relatedEntityType: varchar("related_entity_type", { length: 64 }),
|
||
relatedEntityId: varchar("related_entity_id", { length: 32 }),
|
||
groupId: varchar("group_id", { length: 32 }),
|
||
senderId: varchar("sender_id", { length: 32 }),
|
||
templateId: varchar("template_id", { length: 32 }),
|
||
eventId: varchar("event_id", { length: 64 }),
|
||
readAt: timestamp("read_at"),
|
||
updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
|
||
});
|
||
|
||
export type Notification = typeof notifications.$inferSelect;
|
||
export type NewNotification = typeof notifications.$inferInsert;
|
||
|
||
// ============================================================
|
||
// msg_notification_preferences(用户偏好 · 扩展)
|
||
// ============================================================
|
||
|
||
export const notificationPreferences = mysqlTable(
|
||
"msg_notification_preferences",
|
||
{
|
||
id: varchar("id", { length: 32 }).notNull().primaryKey(),
|
||
userId: varchar("user_id", { length: 32 }).notNull(),
|
||
type: varchar("type", { length: 32 }).notNull().$type<NotificationType>(),
|
||
channels: json("channels").notNull().$type<NotificationChannel[]>(),
|
||
frequencyLimit: int("frequency_limit"),
|
||
quietHoursStart: varchar("quiet_hours_start", { length: 8 }),
|
||
quietHoursEnd: varchar("quiet_hours_end", { length: 8 }),
|
||
quietHoursTimezone: varchar("quiet_hours_timezone", { length: 64 }).default(
|
||
"Asia/Shanghai",
|
||
),
|
||
enabled: boolean("enabled").notNull().default(true),
|
||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||
updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
|
||
},
|
||
);
|
||
|
||
export type NotificationPreference =
|
||
typeof notificationPreferences.$inferSelect;
|
||
export type NewNotificationPreference =
|
||
typeof notificationPreferences.$inferInsert;
|
||
|
||
// ============================================================
|
||
// msg_notification_templates(通知模板 · 新增)
|
||
// ============================================================
|
||
|
||
export const notificationTemplates = mysqlTable("msg_notification_templates", {
|
||
id: varchar("id", { length: 32 }).notNull().primaryKey(),
|
||
code: varchar("code", { length: 64 }).notNull(),
|
||
type: varchar("type", { length: 32 }).notNull().$type<NotificationType>(),
|
||
titleTemplate: varchar("title_template", { length: 255 }).notNull(),
|
||
contentTemplate: text("content_template").notNull(),
|
||
defaultChannels: json("default_channels")
|
||
.notNull()
|
||
.$type<NotificationChannel[]>(),
|
||
variables: json("variables").notNull().$type<string[]>(),
|
||
locale: varchar("locale", { length: 16 }).notNull().default("zh-CN"),
|
||
status: varchar("status", { length: 32 })
|
||
.notNull()
|
||
.default("draft")
|
||
.$type<TemplateStatus>(),
|
||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||
updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
|
||
});
|
||
|
||
export type NotificationTemplate = typeof notificationTemplates.$inferSelect;
|
||
export type NewNotificationTemplate = typeof notificationTemplates.$inferInsert;
|
||
|
||
// ============================================================
|
||
// msg_notification_deliveries(投递记录 · 新增)
|
||
// ============================================================
|
||
|
||
export const notificationDeliveries = mysqlTable(
|
||
"msg_notification_deliveries",
|
||
{
|
||
id: varchar("id", { length: 32 }).notNull().primaryKey(),
|
||
notificationId: varchar("notification_id", { length: 32 }).notNull(),
|
||
channel: varchar("channel", { length: 32 })
|
||
.notNull()
|
||
.$type<NotificationChannel>(),
|
||
status: varchar("status", { length: 32 })
|
||
.notNull()
|
||
.default("pending")
|
||
.$type<DeliveryStatus>(),
|
||
externalId: varchar("external_id", { length: 128 }),
|
||
attemptCount: int("attempt_count").notNull().default(0),
|
||
maxRetry: int("max_retry").notNull().default(3),
|
||
lastError: text("last_error"),
|
||
nextRetryAt: timestamp("next_retry_at"),
|
||
deliveredAt: timestamp("delivered_at"),
|
||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||
updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
|
||
},
|
||
);
|
||
|
||
export type NotificationDelivery = typeof notificationDeliveries.$inferSelect;
|
||
export type NewNotificationDelivery =
|
||
typeof notificationDeliveries.$inferInsert;
|