This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -11,6 +14,14 @@ type Config struct {
|
|||||||
Password string
|
Password string
|
||||||
Semester string
|
Semester string
|
||||||
TWFID string
|
TWFID string
|
||||||
|
Recognizer RecognizerConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecognizerConfig struct {
|
||||||
|
APIURL string
|
||||||
|
APIKey string
|
||||||
|
Model string
|
||||||
|
Timeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -22,10 +33,20 @@ func Load() *Config {
|
|||||||
cfgOnce.Do(func() {
|
cfgOnce.Do(func() {
|
||||||
cfg = &Config{
|
cfg = &Config{
|
||||||
BaseURL: getEnv("JWTS_BASE_URL", "http://jwts.hitwh.edu.cn"),
|
BaseURL: getEnv("JWTS_BASE_URL", "http://jwts.hitwh.edu.cn"),
|
||||||
Username: getEnv("JWTS_USERNAME", ""),
|
Username: getEnv("JWTS_USERNAME", "2024212193"),
|
||||||
Password: getEnv("JWTS_PASSWORD", ""),
|
Password: getEnv("JWTS_PASSWORD", "wyb12580963"),
|
||||||
Semester: getEnv("JWTS_SEMESTER", "2025-20262"),
|
Semester: getEnv("JWTS_SEMESTER", "2025-20262"),
|
||||||
TWFID: getEnv("JWTS_TWFID", ""),
|
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
|
return cfg
|
||||||
@@ -45,3 +66,36 @@ func getEnv(key, defaultValue string) string {
|
|||||||
}
|
}
|
||||||
return val
|
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
|
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")
|
rows := table.Find("tr")
|
||||||
if rows.Length() < 3 {
|
if rows.Length() < 3 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this table has "星期一" in header
|
// Verify this is the classroom table by checking for weekday headers
|
||||||
headerText := ""
|
hasWeekdayHeader := false
|
||||||
table.Find("th").Each(func(_ int, th *goquery.Selection) {
|
rows.First().Find("th").Each(func(_ int, th *goquery.Selection) {
|
||||||
headerText += th.Text()
|
text := strings.TrimSpace(th.Text())
|
||||||
|
for _, day := range dayNames {
|
||||||
|
if strings.Contains(text, day) {
|
||||||
|
hasWeekdayHeader = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
if !strings.Contains(headerText, "星期一") {
|
if !hasWeekdayHeader {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse period row (second row) to map column index to (day, period)
|
// Build column mapping: column index -> (dayIndex, periodIndex)
|
||||||
periodRow := rows.Eq(1)
|
colMapping := make(map[int][2]int)
|
||||||
periodCells := periodRow.Find("td")
|
|
||||||
periodCellTexts := make([]string, periodCells.Length())
|
|
||||||
periodCells.Each(func(i int, cell *goquery.Selection) {
|
|
||||||
periodCellTexts[i] = strings.TrimSpace(cell.Text())
|
|
||||||
})
|
|
||||||
|
|
||||||
colToDayPeriod := make(map[int][2]string)
|
// Start from column 1 (skip first column which is classroom name)
|
||||||
colIdx := 1
|
colIdx := 1
|
||||||
for dayIdx := 0; dayIdx < 7; dayIdx++ {
|
for dayIdx := 0; dayIdx < 7; dayIdx++ {
|
||||||
for periodIdx := 0; periodIdx < 6; periodIdx++ {
|
for periodIdx := 0; periodIdx < 6; periodIdx++ {
|
||||||
var periodText string
|
colMapping[colIdx] = [2]int{dayIdx, periodIdx}
|
||||||
if colIdx < len(periodCellTexts) {
|
|
||||||
periodText = periodCellTexts[colIdx]
|
|
||||||
} else {
|
|
||||||
periodText = periodLabels[periodIdx]
|
|
||||||
}
|
|
||||||
colToDayPeriod[colIdx] = [2]string{dayNames[dayIdx], periodText}
|
|
||||||
colIdx++
|
colIdx++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data rows start from row 2+
|
// Process data rows (skip first 2 header rows)
|
||||||
rows.FilterFunction(func(_ int, row *goquery.Selection) bool {
|
rows.Each(func(rowIdx int, row *goquery.Selection) {
|
||||||
return row.Index() >= 2
|
if rowIdx < 2 {
|
||||||
}).Each(func(_ int, row *goquery.Selection) {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cells := row.Find("td")
|
cells := row.Find("td")
|
||||||
if cells.Length() < 2 {
|
if cells.Length() < 2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get classroom name from first cell
|
||||||
classroomName := strings.TrimSpace(cells.First().Text())
|
classroomName := strings.TrimSpace(cells.First().Text())
|
||||||
|
// Filter out invalid classroom names
|
||||||
if classroomName == "" || len(classroomName) < 2 {
|
if classroomName == "" || len(classroomName) < 2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var slots []string
|
// Skip non-classroom entries like "线上网络授课"
|
||||||
var weekdays []string
|
if strings.Contains(classroomName, "线上") || strings.Contains(classroomName, "网络") {
|
||||||
weekdaySet := make(map[string]bool)
|
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++ {
|
for colIdx := 1; colIdx < cells.Length(); colIdx++ {
|
||||||
cell := cells.Eq(colIdx)
|
cell := cells.Eq(colIdx)
|
||||||
// Check if cell has kjs_icon div (indicates occupied, not empty)
|
|
||||||
hasIcon := false
|
// Get mapping for this column
|
||||||
cell.Find("div").Each(func(_ int, div *goquery.Selection) {
|
mapping, exists := colMapping[colIdx]
|
||||||
if classAttr, exists := div.Attr("class"); exists && strings.Contains(classAttr, "kjs_icon") {
|
if !exists {
|
||||||
hasIcon = true
|
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] != "" {
|
// Also check if cell has any content
|
||||||
dayName := colToDayPeriod[colIdx][0]
|
cellText := strings.TrimSpace(cell.Text())
|
||||||
periodText := colToDayPeriod[colIdx][1]
|
if cellText != "" {
|
||||||
var slot string
|
isEmpty = false
|
||||||
if periodText != "" {
|
|
||||||
slot = fmt.Sprintf("%s 第%s节", dayName, periodText)
|
|
||||||
} else {
|
|
||||||
slot = dayName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isEmpty {
|
||||||
|
dayName := dayNames[dayIdx]
|
||||||
|
periodText := periodLabels[periodIdx]
|
||||||
|
slot := fmt.Sprintf("%s 第%s节", dayName, periodText)
|
||||||
slots = append(slots, slot)
|
slots = append(slots, slot)
|
||||||
if !weekdaySet[dayName] {
|
|
||||||
weekdaySet[dayName] = true
|
if !weekdaySet[dayIdx] {
|
||||||
|
weekdaySet[dayIdx] = true
|
||||||
weekdays = append(weekdays, dayName)
|
weekdays = append(weekdays, dayName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create entry if there are empty slots
|
||||||
if len(slots) > 0 {
|
if len(slots) > 0 {
|
||||||
weekStr := ""
|
weekStr := ""
|
||||||
if weekStart != "" && weekEnd != "" {
|
if weekStart != "" && weekEnd != "" {
|
||||||
@@ -260,3 +277,8 @@ func fetchVenues(httpClient *http.Client, buildingCode string) ([]venueInfo, err
|
|||||||
|
|
||||||
return venues, nil
|
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