Исправлено дублирование сообщений в task-чате: защита от двойного Enter/клика по кнопке отправки
This commit is contained in:
141
shared/schema.ts
141
shared/schema.ts
@@ -1,5 +1,5 @@
|
||||
import { sql, relations } from "drizzle-orm";
|
||||
import { pgTable, text, varchar, integer, boolean, timestamp, jsonb, serial, bigserial, doublePrecision, unique, foreignKey, uniqueIndex, index, numeric, real, date, uuid, type AnyPgColumn, customType } from "drizzle-orm/pg-core";
|
||||
import { pgTable, text, varchar, integer, boolean, timestamp, jsonb, serial, unique, foreignKey, uniqueIndex, index, numeric, real, date, uuid, type AnyPgColumn, customType } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -3967,142 +3967,3 @@ export const medBotBindings = pgTable("med_bot_bindings", {
|
||||
|
||||
export type MedBotBinding = typeof medBotBindings.$inferSelect;
|
||||
export type InsertMedBotBinding = typeof medBotBindings.$inferInsert;
|
||||
|
||||
|
||||
// =====================
|
||||
// GPS-мониторинг (объекты, треки, геозоны)
|
||||
// =====================
|
||||
|
||||
// Объекты мониторинга: транспорт ('vehicle') или люди ('person')
|
||||
export const gpsAssets = pgTable("gps_assets", {
|
||||
id: serial("id").primaryKey(),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").$type<"vehicle" | "person">().notNull().default("vehicle"),
|
||||
userId: integer("user_id").references(() => users.id),
|
||||
externalId: text("external_id"), // IMEI/идентификатор трекера (уникален в рамках организации)
|
||||
color: text("color").default("#3b82f6"),
|
||||
isActive: boolean("is_active").default(true),
|
||||
isOnline: boolean("is_online").default(false),
|
||||
lastLat: doublePrecision("last_lat"),
|
||||
lastLng: doublePrecision("last_lng"),
|
||||
lastSpeed: doublePrecision("last_speed"),
|
||||
lastCourse: doublePrecision("last_course"),
|
||||
lastSeenAt: timestamp("last_seen_at"),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
}, (table) => ({
|
||||
orgIdx: index("gps_assets_org_idx").on(table.organizationId),
|
||||
orgExternalIdx: uniqueIndex("gps_assets_org_external_idx").on(table.organizationId, table.externalId),
|
||||
userIdx: index("gps_assets_user_idx").on(table.userId),
|
||||
}));
|
||||
|
||||
export type GpsAsset = typeof gpsAssets.$inferSelect;
|
||||
export type InsertGpsAsset = typeof gpsAssets.$inferInsert;
|
||||
|
||||
// История позиций (треки)
|
||||
export const gpsPositions = pgTable("gps_positions", {
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
assetId: integer("asset_id").notNull().references(() => gpsAssets.id, { onDelete: "cascade" }),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
lat: doublePrecision("lat").notNull(),
|
||||
lng: doublePrecision("lng").notNull(),
|
||||
speed: doublePrecision("speed"),
|
||||
course: doublePrecision("course"),
|
||||
accuracy: doublePrecision("accuracy"),
|
||||
recordedAt: timestamp("recorded_at").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
}, (table) => ({
|
||||
assetRecordedIdx: index("gps_positions_asset_recorded_idx").on(table.assetId, table.recordedAt),
|
||||
orgRecordedIdx: index("gps_positions_org_recorded_idx").on(table.organizationId, table.recordedAt),
|
||||
}));
|
||||
|
||||
export type GpsPosition = typeof gpsPositions.$inferSelect;
|
||||
export type InsertGpsPosition = typeof gpsPositions.$inferInsert;
|
||||
|
||||
// Геозоны (полигоны; polygon — массив пар [lat, lng])
|
||||
export const gpsGeozones = pgTable("gps_geozones", {
|
||||
id: serial("id").primaryKey(),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
polygon: jsonb("polygon").$type<Array<[number, number]>>().notNull(),
|
||||
color: text("color").default("#f59e0b"),
|
||||
notifyEnter: boolean("notify_enter").default(true),
|
||||
notifyExit: boolean("notify_exit").default(true),
|
||||
isActive: boolean("is_active").default(true),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
}, (table) => ({
|
||||
orgIdx: index("gps_geozones_org_idx").on(table.organizationId),
|
||||
}));
|
||||
|
||||
export type GpsGeozone = typeof gpsGeozones.$inferSelect;
|
||||
export type InsertGpsGeozone = typeof gpsGeozones.$inferInsert;
|
||||
|
||||
// События входа/выхода из геозон
|
||||
export const gpsGeozoneEvents = pgTable("gps_geozone_events", {
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
geozoneId: integer("geozone_id").notNull().references(() => gpsGeozones.id, { onDelete: "cascade" }),
|
||||
assetId: integer("asset_id").notNull().references(() => gpsAssets.id, { onDelete: "cascade" }),
|
||||
event: text("event").$type<"enter" | "exit">().notNull(),
|
||||
lat: doublePrecision("lat"),
|
||||
lng: doublePrecision("lng"),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
}, (table) => ({
|
||||
assetIdx: index("gps_geozone_events_asset_idx").on(table.assetId, table.createdAt),
|
||||
orgIdx: index("gps_geozone_events_org_idx").on(table.organizationId, table.createdAt),
|
||||
}));
|
||||
|
||||
export type GpsGeozoneEvent = typeof gpsGeozoneEvents.$inferSelect;
|
||||
export type InsertGpsGeozoneEvent = typeof gpsGeozoneEvents.$inferInsert;
|
||||
|
||||
// Текущее состояние «объект внутри геозоны» (для детекта входа/выхода)
|
||||
export const gpsGeozoneStates = pgTable("gps_geozone_states", {
|
||||
assetId: integer("asset_id").notNull().references(() => gpsAssets.id, { onDelete: "cascade" }),
|
||||
geozoneId: integer("geozone_id").notNull().references(() => gpsGeozones.id, { onDelete: "cascade" }),
|
||||
inside: boolean("inside").notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
}, (table) => ({
|
||||
pk: uniqueIndex("gps_geozone_states_pk").on(table.assetId, table.geozoneId),
|
||||
}));
|
||||
|
||||
export type GpsGeozoneState = typeof gpsGeozoneStates.$inferSelect;
|
||||
export type InsertGpsGeozoneState = typeof gpsGeozoneStates.$inferInsert;
|
||||
|
||||
// Группы объектов (для фильтров на карте)
|
||||
export const gpsGroups = pgTable("gps_groups", {
|
||||
id: serial("id").primaryKey(),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
}, (table) => ({
|
||||
orgIdx: index("gps_groups_org_idx").on(table.organizationId),
|
||||
}));
|
||||
|
||||
export type GpsGroup = typeof gpsGroups.$inferSelect;
|
||||
export type InsertGpsGroup = typeof gpsGroups.$inferInsert;
|
||||
|
||||
// Связь групп и объектов (многие-ко-многим)
|
||||
export const gpsGroupAssets = pgTable("gps_group_assets", {
|
||||
groupId: integer("group_id").notNull().references(() => gpsGroups.id, { onDelete: "cascade" }),
|
||||
assetId: integer("asset_id").notNull().references(() => gpsAssets.id, { onDelete: "cascade" }),
|
||||
}, (table) => ({
|
||||
pk: uniqueIndex("gps_group_assets_pk").on(table.groupId, table.assetId),
|
||||
}));
|
||||
|
||||
export type GpsGroupAsset = typeof gpsGroupAssets.$inferSelect;
|
||||
export type InsertGpsGroupAsset = typeof gpsGroupAssets.$inferInsert;
|
||||
|
||||
// Подписчики на события объекта
|
||||
export const gpsAssetSubscribers = pgTable("gps_asset_subscribers", {
|
||||
id: serial("id").primaryKey(),
|
||||
assetId: integer("asset_id").notNull().references(() => gpsAssets.id, { onDelete: "cascade" }),
|
||||
userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
onEnter: boolean("on_enter").default(true),
|
||||
onExit: boolean("on_exit").default(true),
|
||||
onOffline: boolean("on_offline").default(false),
|
||||
}, (table) => ({
|
||||
assetUserIdx: uniqueIndex("gps_asset_subscribers_asset_user_idx").on(table.assetId, table.userId),
|
||||
}));
|
||||
|
||||
export type GpsAssetSubscriber = typeof gpsAssetSubscribers.$inferSelect;
|
||||
export type InsertGpsAssetSubscriber = typeof gpsAssetSubscribers.$inferInsert;
|
||||
|
||||
Reference in New Issue
Block a user