查询空教室功能测试通过
All checks were successful
Build / build (push) Successful in 1m46s

This commit is contained in:
WuYuuuub
2026-06-12 20:52:18 +08:00
parent accc6567f2
commit 1cd0c655f8
2 changed files with 129 additions and 53 deletions

View File

@@ -1,8 +1,11 @@
package config
import (
"log/slog"
"os"
"strconv"
"sync"
"time"
)
type Config struct {
@@ -11,6 +14,14 @@ type Config struct {
Password string
Semester string
TWFID string
Recognizer RecognizerConfig
}
type RecognizerConfig struct {
APIURL string
APIKey string
Model string
Timeout time.Duration
}
var (
@@ -22,10 +33,20 @@ func Load() *Config {
cfgOnce.Do(func() {
cfg = &Config{
BaseURL: getEnv("JWTS_BASE_URL", "http://jwts.hitwh.edu.cn"),
Username: getEnv("JWTS_USERNAME", ""),
Password: getEnv("JWTS_PASSWORD", ""),
Username: getEnv("JWTS_USERNAME", "2024212193"),
Password: getEnv("JWTS_PASSWORD", "wyb12580963"),
Semester: getEnv("JWTS_SEMESTER", "2025-20262"),
TWFID: getEnv("JWTS_TWFID", ""),
Recognizer: RecognizerConfig{
APIURL: getEnv("RECOGNIZER_API_URL", "https://api.littlelan.cn/v1/chat/completions"),
APIKey: getEnv("RECOGNIZER_API_KEY", "sk-6064466699a996b6f83e1ae8247c1cb4eeb4b72ce23cb001204335a2567d63c1"),
Model: getEnv("RECOGNIZER_MODEL", "qwen3.5-plus"),
Timeout: getEnvDuration("RECOGNIZER_TIMEOUT", 120*time.Second),
},
}
if cfg.Recognizer.APIKey == "" {
slog.Warn("RECOGNIZER_API_KEY not set, image recognition will not work")
}
})
return cfg
@@ -45,3 +66,36 @@ func getEnv(key, defaultValue string) string {
}
return val
}
func getEnvInt(key string, defaultValue int) int {
val := os.Getenv(key)
if val == "" {
return defaultValue
}
if i, err := strconv.Atoi(val); err == nil {
return i
}
return defaultValue
}
func getEnvDuration(key string, defaultValue time.Duration) time.Duration {
val := os.Getenv(key)
if val == "" {
return defaultValue
}
if d, err := time.ParseDuration(val); err == nil {
return d
}
return defaultValue
}
var (
BaseURL = Get().BaseURL
Username = Get().Username
Password = Get().Password
Semester = Get().Semester
TWFID = Get().TWFID
RecognizerAPIURL = Get().Recognizer.APIURL
RecognizerAPIKey = Get().Recognizer.APIKey
RecognizerModel = Get().Recognizer.Model
)

View File

@@ -96,89 +96,106 @@ func parseEmptyClassroomHTML(html, semester, weekStart, weekEnd string) ([]Empty
var classrooms []EmptyClassroomEntry
doc.Find("table").Each(func(_ int, table *goquery.Selection) {
// Find the main data table with class="dataTable"
doc.Find("table.dataTable").Each(func(_ int, table *goquery.Selection) {
rows := table.Find("tr")
if rows.Length() < 3 {
return
}
// Check if this table has "星期一" in header
headerText := ""
table.Find("th").Each(func(_ int, th *goquery.Selection) {
headerText += th.Text()
// Verify this is the classroom table by checking for weekday headers
hasWeekdayHeader := false
rows.First().Find("th").Each(func(_ int, th *goquery.Selection) {
text := strings.TrimSpace(th.Text())
for _, day := range dayNames {
if strings.Contains(text, day) {
hasWeekdayHeader = true
break
}
}
})
if !strings.Contains(headerText, "星期一") {
if !hasWeekdayHeader {
return
}
// Parse period row (second row) to map column index to (day, period)
periodRow := rows.Eq(1)
periodCells := periodRow.Find("td")
periodCellTexts := make([]string, periodCells.Length())
periodCells.Each(func(i int, cell *goquery.Selection) {
periodCellTexts[i] = strings.TrimSpace(cell.Text())
})
// Build column mapping: column index -> (dayIndex, periodIndex)
colMapping := make(map[int][2]int)
colToDayPeriod := make(map[int][2]string)
// Start from column 1 (skip first column which is classroom name)
colIdx := 1
for dayIdx := 0; dayIdx < 7; dayIdx++ {
for periodIdx := 0; periodIdx < 6; periodIdx++ {
var periodText string
if colIdx < len(periodCellTexts) {
periodText = periodCellTexts[colIdx]
} else {
periodText = periodLabels[periodIdx]
}
colToDayPeriod[colIdx] = [2]string{dayNames[dayIdx], periodText}
colMapping[colIdx] = [2]int{dayIdx, periodIdx}
colIdx++
}
}
// Data rows start from row 2+
rows.FilterFunction(func(_ int, row *goquery.Selection) bool {
return row.Index() >= 2
}).Each(func(_ int, row *goquery.Selection) {
// Process data rows (skip first 2 header rows)
rows.Each(func(rowIdx int, row *goquery.Selection) {
if rowIdx < 2 {
return
}
cells := row.Find("td")
if cells.Length() < 2 {
return
}
// Get classroom name from first cell
classroomName := strings.TrimSpace(cells.First().Text())
// Filter out invalid classroom names
if classroomName == "" || len(classroomName) < 2 {
return
}
var slots []string
var weekdays []string
weekdaySet := make(map[string]bool)
// Skip non-classroom entries like "线上网络授课"
if strings.Contains(classroomName, "线上") || strings.Contains(classroomName, "网络") {
return
}
// Collect empty slots for this classroom
var slots []string
weekdaySet := make(map[int]bool)
var weekdays []string
// Check each time slot column
for colIdx := 1; colIdx < cells.Length(); colIdx++ {
cell := cells.Eq(colIdx)
// Check if cell has kjs_icon div (indicates occupied, not empty)
hasIcon := false
cell.Find("div").Each(func(_ int, div *goquery.Selection) {
if classAttr, exists := div.Attr("class"); exists && strings.Contains(classAttr, "kjs_icon") {
hasIcon = true
// Get mapping for this column
mapping, exists := colMapping[colIdx]
if !exists {
continue
}
dayIdx := mapping[0]
periodIdx := mapping[1]
// Check if cell is empty (no kjs_icon div)
isEmpty := true
cell.Find("div.kjs_icon").Each(func(_ int, div *goquery.Selection) {
isEmpty = false
})
if !hasIcon && colToDayPeriod[colIdx][0] != "" {
dayName := colToDayPeriod[colIdx][0]
periodText := colToDayPeriod[colIdx][1]
var slot string
if periodText != "" {
slot = fmt.Sprintf("%s 第%s节", dayName, periodText)
} else {
slot = dayName
// Also check if cell has any content
cellText := strings.TrimSpace(cell.Text())
if cellText != "" {
isEmpty = false
}
if isEmpty {
dayName := dayNames[dayIdx]
periodText := periodLabels[periodIdx]
slot := fmt.Sprintf("%s 第%s节", dayName, periodText)
slots = append(slots, slot)
if !weekdaySet[dayName] {
weekdaySet[dayName] = true
if !weekdaySet[dayIdx] {
weekdaySet[dayIdx] = true
weekdays = append(weekdays, dayName)
}
}
}
// Create entry if there are empty slots
if len(slots) > 0 {
weekStr := ""
if weekStart != "" && weekEnd != "" {
@@ -260,3 +277,8 @@ func fetchVenues(httpClient *http.Client, buildingCode string) ([]venueInfo, err
return venues, nil
}
// TestParseEmptyClassroomHTML is a test helper function that exports the parser for testing
func TestParseEmptyClassroomHTML(html, semester, weekStart, weekEnd string) ([]EmptyClassroomEntry, error) {
return parseEmptyClassroomHTML(html, semester, weekStart, weekEnd)
}