fix(groups): исправить синхронизацию и транзакции БД

- Обернуть saveGroups() и saveTeachers() в транзакции SQLite для атомарности операций
      - Экспортировать getDatabase() для использования в других модулях
      - Исправить логику синхронизации в loadGroups() для режима SCHED_MODE=kspsuti
      - Удалить дублирующие вызовы clearGroupsCache() и clearTeachersCache() из API handlers
This commit is contained in:
kilyabin
2026-03-05 23:27:19 +04:00
parent 4a1ec7859f
commit ca77a74d72
6 changed files with 77 additions and 57 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
@@ -74,8 +73,7 @@ async function handler(
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({

View File

@@ -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
} }

View File

@@ -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 {
@@ -59,23 +62,34 @@ export function saveGroups(groups: GroupsData): void {
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) {

View File

@@ -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
@@ -38,23 +39,34 @@ export function saveTeachers(teachers: TeachersData): void {
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) {