fix: several fixes
This commit is contained in:
@@ -421,7 +421,8 @@ function parseTeacherSchedule(
|
||||
currentWeekNumber = weekNumber
|
||||
}
|
||||
|
||||
// Ищем родительскую таблицу с парами (cellpadding="1")
|
||||
// Ищем родительскую таблицу с парами
|
||||
// Сначала пробуем найти по cellpadding="1"
|
||||
let parent: Element | null = anchor as Element
|
||||
for (let i = 0; i < 10 && parent; i++) {
|
||||
parent = parent.parentElement
|
||||
@@ -430,6 +431,24 @@ function parseTeacherSchedule(
|
||||
}
|
||||
}
|
||||
|
||||
// Если не нашли по cellpadding, ищем просто ближайшую таблицу
|
||||
if (!parent || parent.tagName !== 'TABLE') {
|
||||
parent = anchor.closest('table')
|
||||
}
|
||||
|
||||
// Если все еще не нашли, ищем таблицу рядом с якорем
|
||||
if (!parent || parent.tagName !== 'TABLE') {
|
||||
// Ищем следующую таблицу после якоря
|
||||
let nextSibling: Node | null = anchor as Node
|
||||
while (nextSibling) {
|
||||
nextSibling = nextSibling.nextSibling
|
||||
if (nextSibling && nextSibling.nodeType === 1 && (nextSibling as Element).tagName === 'TABLE') {
|
||||
parent = nextSibling as Element
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lessons: Lesson[] = []
|
||||
|
||||
if (parent && parent.tagName === 'TABLE') {
|
||||
@@ -448,7 +467,8 @@ function parseTeacherSchedule(
|
||||
const endTime = (endTimeRaw || '').trim()
|
||||
|
||||
const subjCell = cells[2]
|
||||
const roomText = cells[3].textContent?.trim() || ''
|
||||
// Проверяем наличие ячейки перед доступом к textContent
|
||||
const roomText = cells[3]?.textContent?.trim() || ''
|
||||
|
||||
// Извлекаем предмет, аудиторию и тип занятия по логике python‑парсера
|
||||
let subject = ''
|
||||
@@ -457,19 +477,20 @@ function parseTeacherSchedule(
|
||||
let lessonType = ''
|
||||
let location = ''
|
||||
|
||||
const bold = subjCell.querySelector('b')
|
||||
// Проверяем наличие subjCell перед поиском элементов
|
||||
const bold = subjCell?.querySelector('b')
|
||||
if (bold) {
|
||||
subject = bold.textContent?.trim() || ''
|
||||
}
|
||||
|
||||
const fontGreen = subjCell.querySelector('font.t_green_10')
|
||||
const fontGreen = subjCell?.querySelector('font.t_green_10')
|
||||
if (fontGreen) {
|
||||
location = fontGreen.textContent?.trim() || ''
|
||||
}
|
||||
|
||||
// Всё, что идёт после <b> до <font>, это строка с группой и типом занятия
|
||||
let raw = ''
|
||||
if (bold) {
|
||||
if (bold && subjCell) {
|
||||
let node: ChildNode | null = bold.nextSibling
|
||||
while (node) {
|
||||
const nodeType = (node as any).nodeType
|
||||
@@ -1048,7 +1069,24 @@ export function parsePage(
|
||||
// Для расписания преподавателей используем отдельный, более надежный парсер,
|
||||
// основанный на уже отлаженной python‑версии.
|
||||
if (isTeacherSchedule) {
|
||||
return parseTeacherSchedule(document, url, shouldParseWeekNavigation)
|
||||
try {
|
||||
const result = parseTeacherSchedule(document, url, shouldParseWeekNavigation)
|
||||
// Если парсер не нашел дней, пробуем fallback на parseGroupSchedule
|
||||
if (result.days.length === 0) {
|
||||
logDebug('parsePage: parseTeacherSchedule returned no days, trying fallback')
|
||||
return parseGroupSchedule(document, groupName, url, shouldParseWeekNavigation)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
// При ошибке парсинга преподавателя, пробуем fallback
|
||||
logDebug('parsePage: parseTeacherSchedule failed, trying fallback', { error })
|
||||
try {
|
||||
return parseGroupSchedule(document, groupName, url, shouldParseWeekNavigation)
|
||||
} catch (fallbackError) {
|
||||
// Если и fallback не сработал, выбрасываем оригинальную ошибку
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Для расписания групп используем отдельный парсер, который опирается на структуру
|
||||
|
||||
@@ -13,32 +13,32 @@ export type TeacherListItem = {
|
||||
*/
|
||||
export function parseTeachersList(document: Document): TeacherListItem[] {
|
||||
const teachers: TeacherListItem[] = []
|
||||
|
||||
// Ищем все ссылки, которые содержат ?mn=3&obj=
|
||||
|
||||
// Способ 1: Ищем все ссылки, которые содержат ?mn=3&obj= или mn=3&obj=
|
||||
const links = Array.from(document.querySelectorAll('a[href*="?mn=3&obj="], a[href*="mn=3&obj="]'))
|
||||
|
||||
|
||||
for (const link of links) {
|
||||
const href = link.getAttribute('href')
|
||||
if (!href) continue
|
||||
|
||||
|
||||
// Парсим URL вида ?mn=3&obj=XXX или /?mn=3&obj=XXX
|
||||
const objMatch = href.match(/[?&]obj=(\d+)/)
|
||||
if (!objMatch) continue
|
||||
|
||||
|
||||
const parseId = Number(objMatch[1])
|
||||
if (isNaN(parseId) || parseId <= 0) continue
|
||||
|
||||
|
||||
// Извлекаем имя преподавателя из текста ссылки
|
||||
const name = link.textContent?.trim()
|
||||
if (!name || name.length === 0) continue
|
||||
|
||||
|
||||
// Проверяем, что это не дубликат
|
||||
if (!teachers.find(t => t.parseId === parseId)) {
|
||||
teachers.push({ parseId, name })
|
||||
}
|
||||
}
|
||||
|
||||
// Если не нашли ссылки, пытаемся найти в таблице
|
||||
|
||||
// Способ 2: Если не нашли ссылки, пытаемся найти в таблице
|
||||
if (teachers.length === 0) {
|
||||
const tables = Array.from(document.querySelectorAll('table'))
|
||||
for (const table of tables) {
|
||||
@@ -46,28 +46,71 @@ export function parseTeachersList(document: Document): TeacherListItem[] {
|
||||
for (const row of rows) {
|
||||
const link = row.querySelector('a[href*="obj="]')
|
||||
if (!link) continue
|
||||
|
||||
|
||||
const href = link.getAttribute('href')
|
||||
if (!href || !href.includes('mn=3')) continue
|
||||
|
||||
|
||||
const objMatch = href.match(/[?&]obj=(\d+)/)
|
||||
if (!objMatch) continue
|
||||
|
||||
|
||||
const parseId = Number(objMatch[1])
|
||||
if (isNaN(parseId) || parseId <= 0) continue
|
||||
|
||||
|
||||
const name = link.textContent?.trim() || row.textContent?.trim()
|
||||
if (!name || name.length === 0) continue
|
||||
|
||||
|
||||
if (!teachers.find(t => t.parseId === parseId)) {
|
||||
teachers.push({ parseId, name })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Способ 3: Ищем все ссылки с obj= в URL (более общий поиск)
|
||||
if (teachers.length === 0) {
|
||||
const allLinks = Array.from(document.querySelectorAll('a[href*="obj="]'))
|
||||
for (const link of allLinks) {
|
||||
const href = link.getAttribute('href')
|
||||
if (!href) continue
|
||||
|
||||
// Проверяем, что это mn=3 (преподаватели)
|
||||
if (!href.includes('mn=3')) continue
|
||||
|
||||
const objMatch = href.match(/[?&]obj=(\d+)/)
|
||||
if (!objMatch) continue
|
||||
|
||||
const parseId = Number(objMatch[1])
|
||||
if (isNaN(parseId) || parseId <= 0) continue
|
||||
|
||||
const name = link.textContent?.trim()
|
||||
if (!name || name.length === 0) continue
|
||||
|
||||
if (!teachers.find(t => t.parseId === parseId)) {
|
||||
teachers.push({ parseId, name })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Способ 4: Ищем в формах и input элементах
|
||||
if (teachers.length === 0) {
|
||||
const forms = Array.from(document.querySelectorAll('form[action*="mn=3"]'))
|
||||
for (const form of forms) {
|
||||
const action = form.getAttribute('action') || ''
|
||||
const objMatch = action.match(/[?&]obj=(\d+)/)
|
||||
if (objMatch) {
|
||||
const parseId = Number(objMatch[1])
|
||||
if (!isNaN(parseId) && parseId > 0) {
|
||||
const name = form.textContent?.trim() || `Преподаватель ${parseId}`
|
||||
if (!teachers.find(t => t.parseId === parseId)) {
|
||||
teachers.push({ parseId, name })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Сортируем по имени
|
||||
teachers.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
|
||||
return teachers
|
||||
}
|
||||
|
||||
@@ -88,11 +88,17 @@ async function handler(
|
||||
...(validatedDebug !== undefined && { debug: validatedDebug })
|
||||
}
|
||||
|
||||
saveSettings(settings)
|
||||
// Сбрасываем кеш и загружаем свежие настройки для подтверждения
|
||||
clearSettingsCache()
|
||||
const savedSettings = loadSettings(true)
|
||||
res.status(200).json({ success: true, settings: savedSettings })
|
||||
try {
|
||||
saveSettings(settings)
|
||||
// Сбрасываем кеш и загружаем свежие настройки для подтверждения
|
||||
clearSettingsCache()
|
||||
const savedSettings = loadSettings(true)
|
||||
res.status(200).json({ success: true, settings: savedSettings })
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
res.status(500).json({ error: `Failed to save settings: ${errorMessage}` })
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,31 +27,71 @@ async function handler(
|
||||
// Парсинг и обновление списка преподавателей
|
||||
try {
|
||||
const url = `${PROXY_URL}/?mn=3`
|
||||
|
||||
console.log(`[Teachers API] Fetching teachers list from: ${url}`)
|
||||
console.log(`[Teachers API] PROXY_URL: ${PROXY_URL}`)
|
||||
|
||||
// Добавляем таймаут 10 секунд для fetch запроса
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000)
|
||||
|
||||
const page = await fetch(url, { signal: controller.signal })
|
||||
|
||||
const page = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
redirect: 'follow',
|
||||
headers: {
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
}
|
||||
})
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
console.log(`[Teachers API] Response status: ${page.status}`)
|
||||
console.log(`[Teachers API] Response URL: ${page.url}`)
|
||||
console.log(`[Teachers API] Response redirected: ${page.redirected}`)
|
||||
|
||||
const content = await page.text()
|
||||
const contentType = page.headers.get('content-type')
|
||||
|
||||
console.log(`[Teachers API] Content length: ${content.length}, Content-Type: ${contentType}`)
|
||||
|
||||
if (page.status !== 200 || !contentType || contentTypeParser.parse(contentType).type !== 'text/html') {
|
||||
console.error(`[Teachers API] Invalid response: status ${page.status}, contentType: ${contentType}`)
|
||||
res.status(500).json({ error: `Failed to fetch teachers list: status ${page.status}` })
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, не редирект ли на страницу авторизации
|
||||
if (content.includes('login') || content.includes('auth') || content.includes('Вход') || content.includes('Авторизация')) {
|
||||
console.error('[Teachers API] Response appears to be a login page, not teachers list')
|
||||
}
|
||||
|
||||
const dom = new JSDOM(content, { url })
|
||||
const document = dom.window.document
|
||||
|
||||
|
||||
// Логируем заголовок страницы для отладки
|
||||
const pageTitle = document.title
|
||||
console.log(`[Teachers API] Page title: ${pageTitle}`)
|
||||
|
||||
// Логируем немного HTML для отладки
|
||||
const htmlPreview = content.substring(0, 500).replace(/\n/g, ' ')
|
||||
console.log(`[Teachers API] HTML preview: ${htmlPreview}...`)
|
||||
|
||||
const teachersList = parseTeachersList(document)
|
||||
|
||||
console.log(`[Teachers API] Parsed ${teachersList.length} teachers`)
|
||||
|
||||
// Закрываем JSDOM для освобождения памяти
|
||||
dom.window.close()
|
||||
|
||||
|
||||
if (teachersList.length === 0) {
|
||||
console.error('[Teachers API] No teachers found in HTML')
|
||||
// Логируем больше информации для отладки
|
||||
const hasMn3 = content.includes('mn=3')
|
||||
const hasObj = content.includes('obj=')
|
||||
const hasTeachersTable = content.includes('Преподаватель') || content.includes('преподавател')
|
||||
console.log(`[Teachers API] HTML contains 'mn=3': ${hasMn3}, contains 'obj=': ${hasObj}, contains 'преподавател': ${hasTeachersTable}`)
|
||||
|
||||
// Проверяем, не ошибка ли это
|
||||
if (content.includes('Ошибка') || content.includes('Error') || content.includes('404') || content.includes('500')) {
|
||||
console.error('[Teachers API] Response contains error indicators')
|
||||
}
|
||||
|
||||
res.status(500).json({ error: 'No teachers found on the page' })
|
||||
return
|
||||
}
|
||||
@@ -66,20 +106,23 @@ async function handler(
|
||||
name: teacher.name
|
||||
}
|
||||
}
|
||||
console.log(`[Teachers API] Created TeachersData with ${Object.keys(teachersData).length} entries`)
|
||||
|
||||
// Сохраняем в БД
|
||||
saveTeachers(teachersData)
|
||||
|
||||
console.log('[Teachers API] Saved teachers to database')
|
||||
|
||||
// Сохраняем timestamp последнего обновления
|
||||
const { setTeachersLastUpdateTime } = await import('@/shared/data/database')
|
||||
setTeachersLastUpdateTime(Date.now())
|
||||
|
||||
|
||||
// Сбрасываем кеш и загружаем свежие данные из БД
|
||||
clearTeachersCache()
|
||||
const updatedTeachers = loadTeachers(true)
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
console.log(`[Teachers API] Loaded ${Object.keys(updatedTeachers).length} teachers from database`)
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
teachers: updatedTeachers,
|
||||
parsed: teachersList.length
|
||||
})
|
||||
|
||||
@@ -251,6 +251,7 @@ export function getAllTeachers(): TeachersData {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Database] getAllTeachers: found ${Object.keys(teachers).length} teachers`)
|
||||
return teachers
|
||||
}
|
||||
|
||||
@@ -592,6 +593,48 @@ function migrateFromJSON(): void {
|
||||
console.error('Error hashing default password:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Мигрируем преподавателей из teachers.ts, если БД пустая
|
||||
const teachersCount = database.prepare('SELECT COUNT(*) as count FROM teachers').get() as { count: number }
|
||||
if (teachersCount.count === 0) {
|
||||
try {
|
||||
// Пытаемся импортировать преподавателей из teachers.ts
|
||||
const possiblePaths = [
|
||||
path.join(process.cwd(), 'src/shared/data/teachers.ts'),
|
||||
path.join(process.cwd(), '.next/standalone/src/shared/data/teachers.ts'),
|
||||
path.join(process.cwd(), 'teachers.ts')
|
||||
]
|
||||
|
||||
for (const filePath of possiblePaths) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.log(`Migrating teachers from ${filePath}...`)
|
||||
// Читаем файл и извлекаем JSON массив
|
||||
const fileContents = fs.readFileSync(filePath, 'utf8')
|
||||
const jsonMatch = fileContents.match(/export const teachers = (\[[\s\S]*?\])/)
|
||||
if (jsonMatch && jsonMatch[1]) {
|
||||
const teachersArray = JSON.parse(jsonMatch[1]) as Array<{ name: string }>
|
||||
|
||||
const insertStmt = database.prepare('INSERT INTO teachers (id, parseId, name) VALUES (?, ?, ?)')
|
||||
const transaction = database.transaction((teachers: Array<{ name: string }>) => {
|
||||
teachers.forEach((teacher, index) => {
|
||||
if (teacher.name) {
|
||||
// Используем индекс как parseId, так как в teachers.ts нет parseId
|
||||
const id = String(index + 1)
|
||||
insertStmt.run(id, index + 1, teacher.name)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
transaction(teachersArray)
|
||||
console.log(`Teachers migrated from teachers.ts: ${teachersArray.length} teachers`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error migrating teachers from teachers.ts:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Экспортируем функцию для закрытия соединения (полезно для тестов)
|
||||
|
||||
@@ -11,7 +11,7 @@ const CACHE_TTL_MS = 1000 * 60 // 1 минута
|
||||
export function loadTeachers(forceRefresh: boolean = false): TeachersData {
|
||||
const now = Date.now()
|
||||
const isCacheValid = cachedTeachers !== null && !forceRefresh && (now - cacheTimestamp) < CACHE_TTL_MS
|
||||
|
||||
|
||||
if (isCacheValid && cachedTeachers !== null) {
|
||||
return cachedTeachers
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export function loadTeachers(forceRefresh: boolean = false): TeachersData {
|
||||
try {
|
||||
cachedTeachers = getAllTeachersFromDB()
|
||||
cacheTimestamp = now
|
||||
console.log(`[TeachersLoader] Loaded ${Object.keys(cachedTeachers).length} teachers from database`)
|
||||
return cachedTeachers
|
||||
} catch (error) {
|
||||
console.error('Error loading teachers from database:', error)
|
||||
|
||||
Reference in New Issue
Block a user