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 { 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 { SCHED_MODE } from '@/shared/constants/urls'
@@ -19,7 +19,6 @@ async function handler(
if (req.method === 'GET') {
// Получение списка групп (всегда свежие данные для админ-панели)
clearGroupsCache()
const groups = await loadGroups(true)
res.status(200).json({ groups })
return
@@ -72,8 +71,7 @@ async function handler(
}
saveGroups(groups)
// Сбрасываем кеш и загружаем свежие данные из БД
clearGroupsCache()
// Загружаем свежие данные из БД (кеш уже сброшен в saveGroups)
const updatedGroups = await loadGroups(true)
res.status(200).json({ success: true, groups: updatedGroups })
return

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next'
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 { SCHED_MODE } from '@/shared/constants/urls'
@@ -60,8 +60,7 @@ async function handler(
}
saveGroups(groups)
// Сбрасываем кеш и загружаем свежие данные из БД
clearGroupsCache()
// Загружаем свежие данные из БД (кеш уже сброшен в saveGroups)
const updatedGroups = await loadGroups(true)
res.status(200).json({ success: true, groups: updatedGroups })
return
@@ -77,8 +76,7 @@ async function handler(
delete groups[id]
saveGroups(groups)
// Сбрасываем кеш и загружаем свежие данные из БД
clearGroupsCache()
// Загружаем свежие данные из БД (кеш уже сброшен в saveGroups)
const updatedGroups = await loadGroups(true)
res.status(200).json({ success: true, groups: updatedGroups })
return

View File

@@ -17,7 +17,6 @@ async function handler(
) {
if (req.method === 'GET') {
// Получение списка преподавателей (всегда свежие данные для админ-панели)
clearTeachersCache()
const teachers = loadTeachers(true)
res.status(200).json({ teachers })
return
@@ -74,8 +73,7 @@ async function handler(
const { setTeachersLastUpdateTime } = await import('@/shared/data/database')
setTeachersLastUpdateTime(Date.now())
// Сбрасываем кеш и загружаем свежие данные из БД
clearTeachersCache()
// Загружаем свежие данные из БД (кеш уже сброшен в saveTeachers)
const updatedTeachers = loadTeachers(true)
res.status(200).json({

View File

@@ -91,7 +91,7 @@ function migrateDatabaseLocation(): void {
// Инициализация базы данных
let db: Database.Database | null = null
function getDatabase(): Database.Database {
export function getDatabase(): Database.Database {
if (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 { syncGroupsFromKspsutiIfNeeded } from '@/app/agregator/groups'
import type { Database } from 'better-sqlite3'
export type GroupInfo = {
parseId: number
@@ -24,17 +25,19 @@ export async function loadGroups(forceRefresh: boolean = false): Promise<GroupsD
const now = Date.now()
const isCacheValid = cachedGroups !== null && !forceRefresh && (now - cacheTimestamp) < CACHE_TTL_MS
if (isCacheValid && cachedGroups !== null) {
return cachedGroups
}
// В авто‑режиме сначала пробуем синхронизировать группы с lk.ks.psuti.ru.
// В режиме kspsuti всегда проверяем синхронизацию, даже если кэш валиден
if (SCHED_MODE === 'kspsuti') {
const synced = await syncGroupsFromKspsutiIfNeeded(KSPSUTI_SYNC_TTL_MS)
if (synced) {
saveGroups(synced)
clearGroupsCache()
// После saveGroups кеш уже сброшен, продолжаем загрузку из БД
} else if (isCacheValid && cachedGroups !== null) {
// Синхронизация не проводилась (TTL не истёк), используем кэш
return cachedGroups
}
} else if (isCacheValid && cachedGroups !== null) {
// В других режимах используем обычную логику кэширования
return cachedGroups
}
try {
@@ -59,23 +62,34 @@ export function saveGroups(groups: GroupsData): void {
const existingIds = new Set(Object.keys(existingGroups))
const newIds = new Set(Object.keys(groups))
// Получаем ссылки на подготовленные выражения для транзакции
const database = getDatabase() as Database
const insertStmt = database.prepare('INSERT INTO groups (id, parseId, name, course) VALUES (?, ?, ?, ?)')
const updateStmt = database.prepare('UPDATE groups SET parseId = ?, name = ?, course = ? WHERE id = ?')
const deleteStmt = database.prepare('DELETE FROM groups WHERE id = ?')
// Выполняем все операции в транзакции для атомарности
const saveTransaction = database.transaction((groupsData: GroupsData) => {
// Добавляем или обновляем группы
for (const [id, group] of Object.entries(groups)) {
for (const [id, group] of Object.entries(groupsData)) {
if (existingIds.has(id)) {
updateGroup(id, group)
updateStmt.run(group.parseId, group.name, group.course, id)
} else {
createGroup(id, group)
insertStmt.run(id, group.parseId, group.name, group.course)
}
}
// Удаляем группы, которых больше нет
for (const id of existingIds) {
if (!newIds.has(id)) {
deleteGroup(id)
deleteStmt.run(id)
}
}
})
// Сбрасываем кеш и timestamp
saveTransaction(groups)
// Сбрасываем кеш и timestamp после успешной транзакции
cachedGroups = null
cacheTimestamp = 0
} 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 cacheTimestamp: number = 0
@@ -38,23 +39,34 @@ export function saveTeachers(teachers: TeachersData): void {
const existingIds = new Set(Object.keys(existingTeachers))
const newIds = new Set(Object.keys(teachers))
// Получаем ссылки на подготовленные выражения для транзакции
const database = getDatabase() as Database
const insertStmt = database.prepare('INSERT INTO teachers (id, parseId, name) VALUES (?, ?, ?)')
const updateStmt = database.prepare('UPDATE teachers SET parseId = ?, name = ? WHERE id = ?')
const deleteStmt = database.prepare('DELETE FROM teachers WHERE id = ?')
// Выполняем все операции в транзакции для атомарности
const saveTransaction = database.transaction((teachersData: TeachersData) => {
// Добавляем или обновляем преподавателей
for (const [id, teacher] of Object.entries(teachers)) {
for (const [id, teacher] of Object.entries(teachersData)) {
if (existingIds.has(id)) {
updateTeacher(id, teacher)
updateStmt.run(teacher.parseId, teacher.name, id)
} else {
createTeacher(id, teacher)
insertStmt.run(id, teacher.parseId, teacher.name)
}
}
// Удаляем преподавателей, которых больше нет
for (const id of existingIds) {
if (!newIds.has(id)) {
deleteTeacher(id)
deleteStmt.run(id)
}
}
})
// Сбрасываем кеш и timestamp
saveTransaction(teachers)
// Сбрасываем кеш и timestamp после успешной транзакции
cachedTeachers = null
cacheTimestamp = 0
} catch (error) {