This commit is contained in:
@@ -1,16 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
BaseURL string
|
||||
Username string
|
||||
Password string
|
||||
Semester string
|
||||
TWFID string
|
||||
BaseURL string
|
||||
Username string
|
||||
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
|
||||
)
|
||||
|
||||
@@ -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 != "" {
|
||||
@@ -259,4 +276,9 @@ 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user