fix(groups): исправить синхронизацию и транзакции БД
- Обернуть saveGroups() и saveTeachers() в транзакции SQLite для атомарности операций
- Экспортировать getDatabase() для использования в других модулях
- Исправить логику синхронизации в loadGroups() для режима SCHED_MODE=kspsuti
- Удалить дублирующие вызовы clearGroupsCache() и clearTeachersCache() из API handlers
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { withAuth, ApiResponse } from '@/shared/utils/api-wrapper'
|
import { withAuth, ApiResponse } from '@/shared/utils/api-wrapper'
|
||||||
import { loadGroups, saveGroups, clearGroupsCache, GroupsData } from '@/shared/data/groups-loader'
|
import { loadGroups, saveGroups, GroupsData } from '@/shared/data/groups-loader'
|
||||||
import { validateGroupId, validateCourse } from '@/shared/utils/validation'
|
import { validateGroupId, validateCourse } from '@/shared/utils/validation'
|
||||||
import { SCHED_MODE } from '@/shared/constants/urls'
|
import { SCHED_MODE } from '@/shared/constants/urls'
|
||||||
|
|
||||||
@@ -19,7 +19,6 @@ async function handler(
|
|||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
// Получение списка групп (всегда свежие данные для админ-панели)
|
// Получение списка групп (всегда свежие данные для админ-панели)
|
||||||
clearGroupsCache()
|
|
||||||
const groups = await loadGroups(true)
|
const groups = await loadGroups(true)
|
||||||
res.status(200).json({ groups })
|
res.status(200).json({ groups })
|
||||||
return
|
return
|
||||||
@@ -72,8 +71,7 @@ async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveGroups(groups)
|
saveGroups(groups)
|
||||||
// Сбрасываем кеш и загружаем свежие данные из БД
|
// Загружаем свежие данные из БД (кеш уже сброшен в saveGroups)
|
||||||
clearGroupsCache()
|
|
||||||
const updatedGroups = await loadGroups(true)
|
const updatedGroups = await loadGroups(true)
|
||||||
res.status(200).json({ success: true, groups: updatedGroups })
|
res.status(200).json({ success: true, groups: updatedGroups })
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { withAuth, ApiResponse } from '@/shared/utils/api-wrapper'
|
import { withAuth, ApiResponse } from '@/shared/utils/api-wrapper'
|
||||||
import { loadGroups, saveGroups, clearGroupsCache, GroupsData } from '@/shared/data/groups-loader'
|
import { loadGroups, saveGroups, GroupsData } from '@/shared/data/groups-loader'
|
||||||
import { validateCourse } from '@/shared/utils/validation'
|
import { validateCourse } from '@/shared/utils/validation'
|
||||||
import { SCHED_MODE } from '@/shared/constants/urls'
|
import { SCHED_MODE } from '@/shared/constants/urls'
|
||||||
|
|
||||||
@@ -60,8 +60,7 @@ async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveGroups(groups)
|
saveGroups(groups)
|
||||||
// Сбрасываем кеш и загружаем свежие данные из БД
|
// Загружаем свежие данные из БД (кеш уже сброшен в saveGroups)
|
||||||
clearGroupsCache()
|
|
||||||
const updatedGroups = await loadGroups(true)
|
const updatedGroups = await loadGroups(true)
|
||||||
res.status(200).json({ success: true, groups: updatedGroups })
|
res.status(200).json({ success: true, groups: updatedGroups })
|
||||||
return
|
return
|
||||||
@@ -77,8 +76,7 @@ async function handler(
|
|||||||
delete groups[id]
|
delete groups[id]
|
||||||
|
|
||||||
saveGroups(groups)
|
saveGroups(groups)
|
||||||
// Сбрасываем кеш и загружаем свежие данные из БД
|
// Загружаем свежие данные из БД (кеш уже сброшен в saveGroups)
|
||||||
clearGroupsCache()
|
|
||||||
const updatedGroups = await loadGroups(true)
|
const updatedGroups = await loadGroups(true)
|
||||||
res.status(200).json({ success: true, groups: updatedGroups })
|
res.status(200).json({ success: true, groups: updatedGroups })
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ async function handler(
|
|||||||
) {
|
) {
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
// Получение списка преподавателей (всегда свежие данные для админ-панели)
|
// Получение списка преподавателей (всегда свежие данные для админ-панели)
|
||||||
clearTeachersCache()
|
|
||||||
const teachers = loadTeachers(true)
|
const teachers = loadTeachers(true)
|
||||||
res.status(200).json({ teachers })
|
res.status(200).json({ teachers })
|
||||||
return
|
return
|
||||||
@@ -69,17 +68,16 @@ async function handler(
|
|||||||
|
|
||||||
// Сохраняем в БД
|
// Сохраняем в БД
|
||||||
saveTeachers(teachersData)
|
saveTeachers(teachersData)
|
||||||
|
|
||||||
// Сохраняем timestamp последнего обновления
|
// Сохраняем timestamp последнего обновления
|
||||||
const { setTeachersLastUpdateTime } = await import('@/shared/data/database')
|
const { setTeachersLastUpdateTime } = await import('@/shared/data/database')
|
||||||
setTeachersLastUpdateTime(Date.now())
|
setTeachersLastUpdateTime(Date.now())
|
||||||
|
|
||||||
// Сбрасываем кеш и загружаем свежие данные из БД
|
// Загружаем свежие данные из БД (кеш уже сброшен в saveTeachers)
|
||||||
clearTeachersCache()
|
|
||||||
const updatedTeachers = loadTeachers(true)
|
const updatedTeachers = loadTeachers(true)
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
teachers: updatedTeachers,
|
teachers: updatedTeachers,
|
||||||
parsed: teachersList.length
|
parsed: teachersList.length
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ function migrateDatabaseLocation(): void {
|
|||||||
// Инициализация базы данных
|
// Инициализация базы данных
|
||||||
let db: Database.Database | null = null
|
let db: Database.Database | null = null
|
||||||
|
|
||||||
function getDatabase(): Database.Database {
|
export function getDatabase(): Database.Database {
|
||||||
if (db) {
|
if (db) {
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getAllGroups as getAllGroupsFromDB, createGroup, updateGroup, deleteGroup, getGroup } from './database'
|
import { getAllGroups as getAllGroupsFromDB, createGroup, updateGroup, deleteGroup, getGroup, getDatabase } from './database'
|
||||||
import { SCHED_MODE } from '@/shared/constants/urls'
|
import { SCHED_MODE } from '@/shared/constants/urls'
|
||||||
import { syncGroupsFromKspsutiIfNeeded } from '@/app/agregator/groups'
|
import { syncGroupsFromKspsutiIfNeeded } from '@/app/agregator/groups'
|
||||||
|
import type { Database } from 'better-sqlite3'
|
||||||
|
|
||||||
export type GroupInfo = {
|
export type GroupInfo = {
|
||||||
parseId: number
|
parseId: number
|
||||||
@@ -24,17 +25,19 @@ export async function loadGroups(forceRefresh: boolean = false): Promise<GroupsD
|
|||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const isCacheValid = cachedGroups !== null && !forceRefresh && (now - cacheTimestamp) < CACHE_TTL_MS
|
const isCacheValid = cachedGroups !== null && !forceRefresh && (now - cacheTimestamp) < CACHE_TTL_MS
|
||||||
|
|
||||||
if (isCacheValid && cachedGroups !== null) {
|
// В режиме kspsuti всегда проверяем синхронизацию, даже если кэш валиден
|
||||||
return cachedGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
// В авто‑режиме сначала пробуем синхронизировать группы с lk.ks.psuti.ru.
|
|
||||||
if (SCHED_MODE === 'kspsuti') {
|
if (SCHED_MODE === 'kspsuti') {
|
||||||
const synced = await syncGroupsFromKspsutiIfNeeded(KSPSUTI_SYNC_TTL_MS)
|
const synced = await syncGroupsFromKspsutiIfNeeded(KSPSUTI_SYNC_TTL_MS)
|
||||||
if (synced) {
|
if (synced) {
|
||||||
saveGroups(synced)
|
saveGroups(synced)
|
||||||
clearGroupsCache()
|
// После saveGroups кеш уже сброшен, продолжаем загрузку из БД
|
||||||
|
} else if (isCacheValid && cachedGroups !== null) {
|
||||||
|
// Синхронизация не проводилась (TTL не истёк), используем кэш
|
||||||
|
return cachedGroups
|
||||||
}
|
}
|
||||||
|
} else if (isCacheValid && cachedGroups !== null) {
|
||||||
|
// В других режимах используем обычную логику кэширования
|
||||||
|
return cachedGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -54,28 +57,39 @@ export async function loadGroups(forceRefresh: boolean = false): Promise<GroupsD
|
|||||||
export function saveGroups(groups: GroupsData): void {
|
export function saveGroups(groups: GroupsData): void {
|
||||||
try {
|
try {
|
||||||
const existingGroups = getAllGroupsFromDB()
|
const existingGroups = getAllGroupsFromDB()
|
||||||
|
|
||||||
// Определяем, какие группы нужно добавить, обновить или удалить
|
// Определяем, какие группы нужно добавить, обновить или удалить
|
||||||
const existingIds = new Set(Object.keys(existingGroups))
|
const existingIds = new Set(Object.keys(existingGroups))
|
||||||
const newIds = new Set(Object.keys(groups))
|
const newIds = new Set(Object.keys(groups))
|
||||||
|
|
||||||
// Добавляем или обновляем группы
|
// Получаем ссылки на подготовленные выражения для транзакции
|
||||||
for (const [id, group] of Object.entries(groups)) {
|
const database = getDatabase() as Database
|
||||||
if (existingIds.has(id)) {
|
const insertStmt = database.prepare('INSERT INTO groups (id, parseId, name, course) VALUES (?, ?, ?, ?)')
|
||||||
updateGroup(id, group)
|
const updateStmt = database.prepare('UPDATE groups SET parseId = ?, name = ?, course = ? WHERE id = ?')
|
||||||
} else {
|
const deleteStmt = database.prepare('DELETE FROM groups WHERE id = ?')
|
||||||
createGroup(id, group)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаляем группы, которых больше нет
|
// Выполняем все операции в транзакции для атомарности
|
||||||
for (const id of existingIds) {
|
const saveTransaction = database.transaction((groupsData: GroupsData) => {
|
||||||
if (!newIds.has(id)) {
|
// Добавляем или обновляем группы
|
||||||
deleteGroup(id)
|
for (const [id, group] of Object.entries(groupsData)) {
|
||||||
|
if (existingIds.has(id)) {
|
||||||
|
updateStmt.run(group.parseId, group.name, group.course, id)
|
||||||
|
} else {
|
||||||
|
insertStmt.run(id, group.parseId, group.name, group.course)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Сбрасываем кеш и timestamp
|
// Удаляем группы, которых больше нет
|
||||||
|
for (const id of existingIds) {
|
||||||
|
if (!newIds.has(id)) {
|
||||||
|
deleteStmt.run(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
saveTransaction(groups)
|
||||||
|
|
||||||
|
// Сбрасываем кеш и timestamp после успешной транзакции
|
||||||
cachedGroups = null
|
cachedGroups = null
|
||||||
cacheTimestamp = 0
|
cacheTimestamp = 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getAllTeachers as getAllTeachersFromDB, createTeacher, updateTeacher, deleteTeacher, getTeacher, type TeacherInfo, type TeachersData } from './database'
|
import { getAllTeachers as getAllTeachersFromDB, createTeacher, updateTeacher, deleteTeacher, getTeacher, getDatabase, type TeacherInfo, type TeachersData } from './database'
|
||||||
|
import type { Database } from 'better-sqlite3'
|
||||||
|
|
||||||
let cachedTeachers: TeachersData | null = null
|
let cachedTeachers: TeachersData | null = null
|
||||||
let cacheTimestamp: number = 0
|
let cacheTimestamp: number = 0
|
||||||
@@ -11,7 +12,7 @@ const CACHE_TTL_MS = 1000 * 60 // 1 минута
|
|||||||
export function loadTeachers(forceRefresh: boolean = false): TeachersData {
|
export function loadTeachers(forceRefresh: boolean = false): TeachersData {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const isCacheValid = cachedTeachers !== null && !forceRefresh && (now - cacheTimestamp) < CACHE_TTL_MS
|
const isCacheValid = cachedTeachers !== null && !forceRefresh && (now - cacheTimestamp) < CACHE_TTL_MS
|
||||||
|
|
||||||
if (isCacheValid && cachedTeachers !== null) {
|
if (isCacheValid && cachedTeachers !== null) {
|
||||||
return cachedTeachers
|
return cachedTeachers
|
||||||
}
|
}
|
||||||
@@ -33,28 +34,39 @@ export function loadTeachers(forceRefresh: boolean = false): TeachersData {
|
|||||||
export function saveTeachers(teachers: TeachersData): void {
|
export function saveTeachers(teachers: TeachersData): void {
|
||||||
try {
|
try {
|
||||||
const existingTeachers = getAllTeachersFromDB()
|
const existingTeachers = getAllTeachersFromDB()
|
||||||
|
|
||||||
// Определяем, каких преподавателей нужно добавить, обновить или удалить
|
// Определяем, каких преподавателей нужно добавить, обновить или удалить
|
||||||
const existingIds = new Set(Object.keys(existingTeachers))
|
const existingIds = new Set(Object.keys(existingTeachers))
|
||||||
const newIds = new Set(Object.keys(teachers))
|
const newIds = new Set(Object.keys(teachers))
|
||||||
|
|
||||||
// Добавляем или обновляем преподавателей
|
// Получаем ссылки на подготовленные выражения для транзакции
|
||||||
for (const [id, teacher] of Object.entries(teachers)) {
|
const database = getDatabase() as Database
|
||||||
if (existingIds.has(id)) {
|
const insertStmt = database.prepare('INSERT INTO teachers (id, parseId, name) VALUES (?, ?, ?)')
|
||||||
updateTeacher(id, teacher)
|
const updateStmt = database.prepare('UPDATE teachers SET parseId = ?, name = ? WHERE id = ?')
|
||||||
} else {
|
const deleteStmt = database.prepare('DELETE FROM teachers WHERE id = ?')
|
||||||
createTeacher(id, teacher)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаляем преподавателей, которых больше нет
|
// Выполняем все операции в транзакции для атомарности
|
||||||
for (const id of existingIds) {
|
const saveTransaction = database.transaction((teachersData: TeachersData) => {
|
||||||
if (!newIds.has(id)) {
|
// Добавляем или обновляем преподавателей
|
||||||
deleteTeacher(id)
|
for (const [id, teacher] of Object.entries(teachersData)) {
|
||||||
|
if (existingIds.has(id)) {
|
||||||
|
updateStmt.run(teacher.parseId, teacher.name, id)
|
||||||
|
} else {
|
||||||
|
insertStmt.run(id, teacher.parseId, teacher.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Сбрасываем кеш и timestamp
|
// Удаляем преподавателей, которых больше нет
|
||||||
|
for (const id of existingIds) {
|
||||||
|
if (!newIds.has(id)) {
|
||||||
|
deleteStmt.run(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
saveTransaction(teachers)
|
||||||
|
|
||||||
|
// Сбрасываем кеш и timestamp после успешной транзакции
|
||||||
cachedTeachers = null
|
cachedTeachers = null
|
||||||
cacheTimestamp = 0
|
cacheTimestamp = 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user