Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
converter
|
||||||
|
converter.exe
|
||||||
68
auth/login.go
Normal file
68
auth/login.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"jwts/client"
|
||||||
|
"jwts/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
func Login(captchaCode string) (bool, error) {
|
||||||
|
return LoginWithClient(client.Client, captchaCode, config.Username, config.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginWithClient 使用指定的 HTTP 客户端和凭据登录
|
||||||
|
func LoginWithClient(httpClient *http.Client, captchaCode, username, password string) (bool, error) {
|
||||||
|
fmt.Println("[4] 执行登录...")
|
||||||
|
|
||||||
|
data := fmt.Sprintf("usercode=%s&password=%s&code=%s", username, password, captchaCode)
|
||||||
|
req, err := http.NewRequest("POST", config.BaseURL+"/login", strings.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header = client.GetHeadersWithContentType("application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.CheckLoginSuccess(string(body)) {
|
||||||
|
fmt.Println(" ✓ 登录成功!")
|
||||||
|
|
||||||
|
// 提取用户名
|
||||||
|
nameMatch := regexp.MustCompile(`(\w+)同学`).FindStringSubmatch(string(body))
|
||||||
|
if len(nameMatch) > 1 {
|
||||||
|
fmt.Printf(" 欢迎, %s!\n", nameMatch[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存登录后的页面
|
||||||
|
if err := os.WriteFile("login_result.html", body, 0644); err != nil {
|
||||||
|
fmt.Printf(" 警告: 无法保存登录页面: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println(" 登录后页面已保存到: login_result.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(" ✗ 登录失败")
|
||||||
|
if client.IsCaptchaError(string(body)) {
|
||||||
|
fmt.Println(" 错误: 验证码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
72
captcha/recognize.go
Normal file
72
captcha/recognize.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package captcha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"jwts/client"
|
||||||
|
"jwts/config"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 保存验证码图片
|
||||||
|
func SaveCaptchaImage(httpClient *http.Client) ([]byte, error) {
|
||||||
|
fmt.Println("[2] 获取验证码...")
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", config.BaseURL+"/captchaImage", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header = client.GetHeaders()
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile("captcha.png", body, 0644); err != nil {
|
||||||
|
fmt.Printf(" 警告: 无法保存验证码图片: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println(" 验证码已保存到: captcha.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 识别验证码 - 使用固定验证码用于测试
|
||||||
|
func Recognize(imagePath string) (string, error) {
|
||||||
|
fmt.Println("[3] 使用固定验证码...")
|
||||||
|
// 固定验证码用于测试
|
||||||
|
return "....", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取并识别验证码
|
||||||
|
func GetAndRecognize(client *http.Client) (string, error) {
|
||||||
|
_, err := SaveCaptchaImage(client)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaCode, err := Recognize("captcha.png")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" 识别结果: %s\n", captchaCode)
|
||||||
|
return captchaCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于解析HTML(登录页面)
|
||||||
|
func ParseHTML(html string) (*goquery.Document, error) {
|
||||||
|
return goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||||
|
}
|
||||||
89
client/http.go
Normal file
89
client/http.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"jwts/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 全局HTTP客户端
|
||||||
|
var Client *http.Client
|
||||||
|
|
||||||
|
// 初始化HTTP客户端
|
||||||
|
func InitClient() {
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create cookie jar: %v", err)
|
||||||
|
}
|
||||||
|
Client = &http.Client{
|
||||||
|
Jar: jar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
func GetHeaders() http.Header {
|
||||||
|
return http.Header{
|
||||||
|
"User-Agent": []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"},
|
||||||
|
"Accept": []string{"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"},
|
||||||
|
"Accept-Language": []string{"zh-CN,zh;q=0.9,en;q=0.8"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取带Content-Type的请求头
|
||||||
|
func GetHeadersWithContentType(contentType string) http.Header {
|
||||||
|
headers := GetHeaders()
|
||||||
|
headers["Content-Type"] = []string{contentType}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤1: 获取初始Cookie
|
||||||
|
func GetInitialCookies() error {
|
||||||
|
fmt.Println("[1] 访问登录页面获取初始Cookie...")
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", config.BaseURL+"/loginNOCAS", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header = GetHeaders()
|
||||||
|
|
||||||
|
resp, err := Client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
fmt.Printf(" 登录页面状态码: %d\n", resp.StatusCode)
|
||||||
|
PrintCookies()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印Cookies
|
||||||
|
func PrintCookies() {
|
||||||
|
fmt.Printf(" 当前Cookies: ")
|
||||||
|
for _, cookie := range Client.Jar.Cookies(parseURL(config.BaseURL)) {
|
||||||
|
fmt.Printf("%s=%s; ", cookie.Name, cookie.Value)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析URL
|
||||||
|
func parseURL(rawURL string) *url.URL {
|
||||||
|
u, _ := url.Parse(rawURL)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录是否成功
|
||||||
|
func CheckLoginSuccess(body string) bool {
|
||||||
|
return strings.Contains(body, "本科教学管理服务系统")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否验证码错误
|
||||||
|
func IsCaptchaError(body string) bool {
|
||||||
|
return strings.Contains(body, "验证码")
|
||||||
|
}
|
||||||
23
config/config.go
Normal file
23
config/config.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 配置变量
|
||||||
|
var (
|
||||||
|
BaseURL = getEnv("JWTS_BASE_URL", "http://jwts.hitwh.edu.cn")
|
||||||
|
Username = getEnv("JWTS_USERNAME", "")
|
||||||
|
Password = getEnv("JWTS_PASSWORD", "")
|
||||||
|
Semester = getEnv("JWTS_SEMESTER", "2025-20262")
|
||||||
|
TWFID = getEnv("JWTS_TWFID", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
// getEnv 从环境变量获取配置,提供默认值
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
value := os.Getenv(key)
|
||||||
|
if value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
43
go.mod
Normal file
43
go.mod
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
module jwts
|
||||||
|
|
||||||
|
go 1.25.7
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/PuerkitoBio/goquery v1.11.0
|
||||||
|
github.com/gin-gonic/gin v1.12.0
|
||||||
|
google.golang.org/grpc v1.70.0
|
||||||
|
google.golang.org/protobuf v1.36.10
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
golang.org/x/net v0.51.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect
|
||||||
|
)
|
||||||
180
go.sum
Normal file
180
go.sum
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||||
|
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||||
|
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||||
|
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||||
|
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||||
|
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||||
|
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||||
|
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||||
|
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||||
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
303
grkb.html
Normal file
303
grkb.html
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<title>学生个人课表查询</title>
|
||||||
|
|
||||||
|
|
||||||
|
<link href="/resources/css/common/style.css" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="/resources/css/common/content.css" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="/resources/css/common/pageTag.css" rel="stylesheet" type="text/css" />
|
||||||
|
<script type="text/javascript" src="/resources/js/jquery/jquery-1.7.2.min.js"></script>
|
||||||
|
|
||||||
|
<script src="/resources/js/jquery/jquery.form.js" type="text/javascript"></script>
|
||||||
|
<script type="text/javascript" src="/resources/js/common/validator.js" ></script>
|
||||||
|
<script type="text/javascript" src="/resources/js/common/jqcheckBox.js"></script>
|
||||||
|
<script type="text/javascript" src="/resources/js/jquery/jquery.chained.remote.js"></script>
|
||||||
|
<script type="text/javascript" src="/resources/js/common/jqreload.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/resources/js/sweetalert2/sweetalert2.css">
|
||||||
|
<script src="/resources/js/sweetalert2/sweetalert2.min.js"></script>
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
.Contentbox { width:100%; min-height:100%;height:auto }
|
||||||
|
*html .Contentbox { height:100% }
|
||||||
|
</style>
|
||||||
|
<script type="text/javascript" src="/resources/js/jquery/jquery.form.js"></script>
|
||||||
|
<script type="text/javascript" src="/resources/js/common/modalpopup.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
jQuery().ready(function (){
|
||||||
|
|
||||||
|
|
||||||
|
$("a[id^='jcgl']").on("click",function( ){
|
||||||
|
var rwh=$(this).attr('id').split('_')[1];
|
||||||
|
|
||||||
|
document.queryform.action = "/jcgl/queryJcrwdyList_xs?pageRwh="+rwh;
|
||||||
|
document.queryform.submit();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//查询
|
||||||
|
function queryLike(){
|
||||||
|
document.queryform.action = "/kbcx/queryGrkb";
|
||||||
|
document.queryform.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
//导出
|
||||||
|
function exportExcel(){
|
||||||
|
document.queryform.action = "/kbcx/ExportGrKbxx";
|
||||||
|
document.queryform.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
//查询周课表
|
||||||
|
function queryZkb(){
|
||||||
|
document.queryform.action = "/kbcx/queryXszkb";
|
||||||
|
document.queryform.submit();
|
||||||
|
}
|
||||||
|
function kbqr(){
|
||||||
|
document.queryform.action = "/kbcx/kbqr";
|
||||||
|
$("#queryform").ajaxSubmit(function(result){
|
||||||
|
if(result==null || result =="" ){
|
||||||
|
alert("课表确认成功!");
|
||||||
|
document.queryform.action = "/kbcx/queryGrkb";
|
||||||
|
document.queryform.submit();
|
||||||
|
}else{
|
||||||
|
alert("课表确认失败!");
|
||||||
|
document.queryform.action = "/kbcx/queryGrkb";
|
||||||
|
document.queryform.submit();
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//弹出层
|
||||||
|
function wqrkcxx(){
|
||||||
|
var src = "/kbcx/queryWqrkbList?xnxq="+$('#xnxq').val();
|
||||||
|
jQuery("#iframename_js").attr("src",src);
|
||||||
|
BOX_show('NeworEdit1');
|
||||||
|
}
|
||||||
|
|
||||||
|
//查询
|
||||||
|
function queryLike1(){
|
||||||
|
document.queryform.action = "/kbcx/queryXsxkXq";
|
||||||
|
document.queryform.submit();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="Contentbox">
|
||||||
|
<div class="pd10">
|
||||||
|
<div class="address">当前位置:学生选课 >> <a href="javascript:void(0);" onclick="javascript:queryLike();return false;">个人课表查询</a></div>
|
||||||
|
<div class="clr"></div>
|
||||||
|
<div class="butsea" >
|
||||||
|
<form id="queryform" name="queryform" method="post">
|
||||||
|
|
||||||
|
<input type="hidden" name="fhlj" value="kbcx/queryGrkb"/>
|
||||||
|
<table border="0" align="left" cellpadding="0" cellspacing="0" >
|
||||||
|
<tr>
|
||||||
|
<td>学年学期:</td>
|
||||||
|
<td>
|
||||||
|
<select name="xnxq" id="xnxq" class="XNXQ_CON" onchange="javascript:queryLike();return false;">
|
||||||
|
|
||||||
|
<option value="2026-20271" >2026秋季</option>
|
||||||
|
|
||||||
|
<option value="2025-20263" >2026夏季</option>
|
||||||
|
|
||||||
|
<option value="2025-20262" selected>2026春季</option>
|
||||||
|
|
||||||
|
<option value="2025-20261" >2025秋季</option>
|
||||||
|
|
||||||
|
<option value="2024-20253" >2025夏季</option>
|
||||||
|
|
||||||
|
<option value="2024-20252" >2025春季</option>
|
||||||
|
|
||||||
|
<option value="2024-20251" >2024秋季</option>
|
||||||
|
|
||||||
|
<option value="2023-20243" >2024夏季</option>
|
||||||
|
|
||||||
|
<option value="2023-20242" >2024春季</option>
|
||||||
|
|
||||||
|
<option value="2023-20241" >2023秋季</option>
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="addlist_button1 ml15"><a href="javascript:void(0);" onclick="javascript:exportExcel();return false;"><span>导出课表</span></a></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
<td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div><span >课表确认时间:~</span></div>
|
||||||
|
<div class="clr"></div>
|
||||||
|
<div class="Menubox" >
|
||||||
|
<ul>
|
||||||
|
<li class="hover"><a href="javascript:void(0);" onclick="javascript:queryLike();return false;" >学生总课表</a></li>
|
||||||
|
<li><a href="javascript:void(0);" onclick="javascript:queryZkb();return false;" >学生周课表</a></li>
|
||||||
|
<li><a href="javascript:void(0);" onclick="javascript:queryLike1();return false;" >学生选课信息查询</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="clr"></div>
|
||||||
|
|
||||||
|
<div class="xfyq_area mt10">
|
||||||
|
<div class="xfyq_top" style="text-align:center"><span class="ml10 bold">2026春季学期(2023210517)胡浩烨课表(2026-03-13 16:27:14)</span></div>
|
||||||
|
<div class="xfyq_con">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse" class="addlist_01">
|
||||||
|
<tr>
|
||||||
|
<th width="40" height="30" colspan="2"> </th>
|
||||||
|
<th>星期一</th>
|
||||||
|
<th>星期二</th>
|
||||||
|
<th>星期三</th>
|
||||||
|
<th>星期四</th>
|
||||||
|
<th>星期五</th>
|
||||||
|
<th>星期六</th>
|
||||||
|
<th>星期日</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr >
|
||||||
|
<td width="20">上午</td>
|
||||||
|
<td width="20">1-2</td>
|
||||||
|
<td width="118">点集拓扑学</br>李鹏程[2-8]周,[9]周</br>N楼-335</br>微分几何</br>李鹏程[10]周N楼-335</td>
|
||||||
|
<td width="118">点集拓扑学</br>李鹏程[2-8]周,[9]周</br>N楼-335</td>
|
||||||
|
<td width="118">泛函分析基础</br>蒋心蕊[11]周,[12]周,[9,10]周</br>N楼-335</br>微分几何</br>李鹏程[2-8]周N楼-335</td>
|
||||||
|
<td width="118">微分几何</br>李鹏程[2-8]周,[9]周</br>N楼-335</td>
|
||||||
|
<td width="118">泛函分析基础</br>蒋心蕊[11]周,[2-8]周,[9,10]周</br>N楼-335</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="122"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="bgcol">
|
||||||
|
<td width="20">上午</td>
|
||||||
|
<td width="20">3-4</td>
|
||||||
|
<td width="118">泛函分析基础</br>蒋心蕊[11]周,[12]周,[2-8]周,[9,10]周</br>N楼-335</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118">泛函分析基础</br>蒋心蕊[2-8]周N楼-335</br>体育(6)(定向运动)</br>张晓秋[9-16]周</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118">微分几何</br>李鹏程[2-8]周N楼-335,[9]周N楼-331</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="122"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr >
|
||||||
|
<td width="20">下午</td>
|
||||||
|
<td width="20">5-6</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118">数理统计</br>黄春茂[10,11]周研究院-中405</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="122"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="bgcol">
|
||||||
|
<td width="20">下午</td>
|
||||||
|
<td width="20">7-8</td>
|
||||||
|
<td width="118">数理统计</br>黄春茂[1-9]周,[10]周</br>N楼-116</td>
|
||||||
|
<td width="118">数理统计</br>黄春茂[10,11]周,[12]周</br>研究院-中405</td>
|
||||||
|
<td width="118">数理统计</br>黄春茂[1-9]周N楼-116</td>
|
||||||
|
<td width="118">数理统计</br>黄春茂[10,11]周,[12]周</br>研究院-中405</br>形势与政策(3)</br>田金花[5-8]周N楼-116</td>
|
||||||
|
<td width="118">数理统计</br>黄春茂[1-9]周N楼-116</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="122"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr >
|
||||||
|
<td width="20">晚上</td>
|
||||||
|
<td width="20">9-10</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="122"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="bgcol">
|
||||||
|
<td width="20">晚上</td>
|
||||||
|
<td width="20">11-12</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="122"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<tr><td colspan="9">其它课程: 生产实习◇马强◇1◇'
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="clr"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="clr"></div>
|
||||||
|
<div style="color: red;font-size:16px">
|
||||||
|
注意:“个人课表查询”是按照您的选课进行显示,请仔细与“班级推荐课表查询”页面进行对比,如发现个人课表中缺少本班级的推荐课程(特别是必修课),则说明您可能存在漏选情况,请认真核实,如有需要请及时进行补选。
|
||||||
|
</div>
|
||||||
|
<div class="clr"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="NeworEdit1" style="display:none; width: 750px;border: 1px solid #2c4f72;-webkit-box-shadow: 0px 0px 5px #000;-moz-box-shadow: 0px 0px 5px #000;box-shadow: 0px 0px 5px #000; " >
|
||||||
|
<form name="addOrUpdfrom" id="addOrUpdfrom" method="post">
|
||||||
|
<div class="Floatleft Pct100 popup_bg">
|
||||||
|
<div class="popup_left">未确认课程</div>
|
||||||
|
<a href="#" class="btn_close" onclick="javascript:BOX_remove('NeworEdit1');return false;"></a> </div>
|
||||||
|
<div class="clr"></div>
|
||||||
|
<div class="popup">
|
||||||
|
<div class="clr"></div>
|
||||||
|
<iframe id="iframename_js" name="iframename_js" border="0" framespacing="0" marginheight="0" marginwidth="0" style="width: 730px; height: 410px;" frameborder="0" allowTransparency="true"></iframe>
|
||||||
|
<div class="clr"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="setting"> </div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
205
grpc/client.go
Normal file
205
grpc/client.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
pb "jwts/proto/runner"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client gRPC 客户端
|
||||||
|
type Client struct {
|
||||||
|
serverAddr string
|
||||||
|
runnerID string
|
||||||
|
version string
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
stream pb.RunnerHub_ConnectClient
|
||||||
|
mu sync.Mutex
|
||||||
|
handler *TaskHandler
|
||||||
|
|
||||||
|
// 配置
|
||||||
|
heartbeatInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient 创建新的 gRPC 客户端
|
||||||
|
func NewClient(serverAddr, runnerID, version string) *Client {
|
||||||
|
return &Client{
|
||||||
|
serverAddr: serverAddr,
|
||||||
|
runnerID: runnerID,
|
||||||
|
version: version,
|
||||||
|
heartbeatInterval: 30 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHandler 设置任务处理器
|
||||||
|
func (c *Client) SetHandler(handler *TaskHandler) {
|
||||||
|
c.handler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect 连接到 gRPC 服务器
|
||||||
|
func (c *Client) Connect(ctx context.Context) error {
|
||||||
|
log.Printf("正在连接到 gRPC 服务器: %s", c.serverAddr)
|
||||||
|
|
||||||
|
conn, err := grpc.Dial(c.serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("连接失败: %w", err)
|
||||||
|
}
|
||||||
|
c.conn = conn
|
||||||
|
|
||||||
|
client := pb.NewRunnerHubClient(conn)
|
||||||
|
stream, err := client.Connect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建流失败: %w", err)
|
||||||
|
}
|
||||||
|
c.stream = stream
|
||||||
|
|
||||||
|
// 发送注册请求
|
||||||
|
if err := c.register(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("gRPC 连接成功")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// register 发送注册请求
|
||||||
|
func (c *Client) register() error {
|
||||||
|
msg := &pb.StreamMessage{
|
||||||
|
Message: &pb.StreamMessage_Register{
|
||||||
|
Register: &pb.RegisterRequest{
|
||||||
|
RunnerId: c.runnerID,
|
||||||
|
Version: c.version,
|
||||||
|
Capabilities: map[string]string{
|
||||||
|
"TASK_TYPE_GET_SCHEDULE": "true",
|
||||||
|
"TASK_TYPE_LOGIN": "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return c.stream.Send(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run 运行主循环,接收消息
|
||||||
|
func (c *Client) Run(ctx context.Context) error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
msg, err := c.stream.Recv()
|
||||||
|
if err == io.EOF {
|
||||||
|
log.Println("服务器关闭了连接")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("接收消息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.handleMessage(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMessage 处理接收到的消息
|
||||||
|
func (c *Client) handleMessage(msg *pb.StreamMessage) {
|
||||||
|
switch m := msg.Message.(type) {
|
||||||
|
case *pb.StreamMessage_RegisterResponse:
|
||||||
|
log.Printf("注册成功: %s, 会话ID: %s, 心跳间隔: %d秒",
|
||||||
|
m.RegisterResponse.Message,
|
||||||
|
m.RegisterResponse.SessionId,
|
||||||
|
m.RegisterResponse.HeartbeatInterval)
|
||||||
|
if m.RegisterResponse.HeartbeatInterval > 0 {
|
||||||
|
c.heartbeatInterval = time.Duration(m.RegisterResponse.HeartbeatInterval) * time.Second
|
||||||
|
}
|
||||||
|
case *pb.StreamMessage_HeartbeatAck:
|
||||||
|
// 心跳确认,可以用于计算延迟
|
||||||
|
log.Printf("心跳确认,延迟: %dms", m.HeartbeatAck.Latency)
|
||||||
|
case *pb.StreamMessage_Task:
|
||||||
|
log.Printf("收到任务: %s, 类型: %v", m.Task.TaskId, m.Task.Type)
|
||||||
|
go c.handleTask(m.Task)
|
||||||
|
case *pb.StreamMessage_TaskCancel:
|
||||||
|
log.Printf("收到取消任务请求: %s, 原因: %s", m.TaskCancel.TaskId, m.TaskCancel.Reason)
|
||||||
|
if c.handler != nil {
|
||||||
|
c.handler.CancelTask(m.TaskCancel.TaskId)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Printf("收到未知消息类型: %T", msg.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTask 处理任务
|
||||||
|
func (c *Client) handleTask(task *pb.Task) {
|
||||||
|
if c.handler == nil {
|
||||||
|
log.Println("错误: 未设置任务处理器")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := c.handler.Execute(task)
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
err := c.stream.Send(&pb.StreamMessage{
|
||||||
|
Message: &pb.StreamMessage_Result{
|
||||||
|
Result: result,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("发送任务结果失败: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("任务结果已发送: %s, 状态: %v", result.TaskId, result.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendHeartbeat 发送心跳
|
||||||
|
func (c *Client) sendHeartbeat() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if c.stream == nil {
|
||||||
|
return fmt.Errorf("流未连接")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.stream.Send(&pb.StreamMessage{
|
||||||
|
Message: &pb.StreamMessage_Heartbeat{
|
||||||
|
Heartbeat: &pb.Heartbeat{
|
||||||
|
Timestamp: time.Now().UnixMilli(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeartbeatLoop 心跳循环
|
||||||
|
func (c *Client) HeartbeatLoop(ctx context.Context, interval time.Duration) {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := c.sendHeartbeat(); err != nil {
|
||||||
|
log.Printf("发送心跳失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭连接
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
if c.stream != nil {
|
||||||
|
c.stream.CloseSend()
|
||||||
|
}
|
||||||
|
if c.conn != nil {
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
305
grpc/handler.go
Normal file
305
grpc/handler.go
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
pb "jwts/proto/runner"
|
||||||
|
|
||||||
|
"jwts/auth"
|
||||||
|
"jwts/captcha"
|
||||||
|
"jwts/config"
|
||||||
|
"jwts/models"
|
||||||
|
"jwts/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TaskHandler 任务处理器
|
||||||
|
type TaskHandler struct {
|
||||||
|
cancelledTasks map[string]bool
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTaskHandler 创建新的任务处理器
|
||||||
|
func NewTaskHandler() *TaskHandler {
|
||||||
|
return &TaskHandler{
|
||||||
|
cancelledTasks: make(map[string]bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute 执行任务
|
||||||
|
func (h *TaskHandler) Execute(task *pb.Task) *pb.TaskResult {
|
||||||
|
result := &pb.TaskResult{
|
||||||
|
TaskId: task.TaskId,
|
||||||
|
CompletedAt: time.Now().UnixMilli(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查任务是否已取消
|
||||||
|
if h.IsCancelled(task.TaskId) {
|
||||||
|
result.Status = pb.TaskStatus_TASK_STATUS_CANCELLED
|
||||||
|
result.ErrorMessage = "任务已取消"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
switch task.Type {
|
||||||
|
case pb.TaskType_TASK_TYPE_GET_SCHEDULE:
|
||||||
|
data, err := h.getSchedule(task.Payload)
|
||||||
|
if err != nil {
|
||||||
|
result.Status = pb.TaskStatus_TASK_STATUS_FAILED
|
||||||
|
result.ErrorMessage = err.Error()
|
||||||
|
} else {
|
||||||
|
result.Status = pb.TaskStatus_TASK_STATUS_SUCCESS
|
||||||
|
result.Data = data
|
||||||
|
}
|
||||||
|
case pb.TaskType_TASK_TYPE_LOGIN:
|
||||||
|
data, err := h.login(task.Payload)
|
||||||
|
if err != nil {
|
||||||
|
result.Status = pb.TaskStatus_TASK_STATUS_FAILED
|
||||||
|
result.ErrorMessage = err.Error()
|
||||||
|
} else {
|
||||||
|
result.Status = pb.TaskStatus_TASK_STATUS_SUCCESS
|
||||||
|
result.Data = data
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result.Status = pb.TaskStatus_TASK_STATUS_FAILED
|
||||||
|
result.ErrorMessage = fmt.Sprintf("未知的任务类型: %v", task.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelTask 取消任务
|
||||||
|
func (h *TaskHandler) CancelTask(taskID string) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
h.cancelledTasks[taskID] = true
|
||||||
|
log.Printf("任务已标记为取消: %s", taskID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCancelled 检查任务是否已取消
|
||||||
|
func (h *TaskHandler) IsCancelled(taskID string) bool {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
return h.cancelledTasks[taskID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSchedule 获取课表
|
||||||
|
func (h *TaskHandler) getSchedule(payload []byte) ([]byte, error) {
|
||||||
|
var req pb.GetSchedulePayload
|
||||||
|
if err := json.Unmarshal(payload, &req); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析任务载荷失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
semester := strings.TrimSpace(req.Semester)
|
||||||
|
if semester == "" {
|
||||||
|
semester = config.Semester
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("获取课表: 用户=%s, 学期=%s", req.Username, semester)
|
||||||
|
|
||||||
|
// 创建新的 HTTP 客户端
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建 cookie jar 失败: %w", err)
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Jar: jar,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 TWFID cookie (使用默认配置)
|
||||||
|
setTWFIDCookie(httpClient, config.BaseURL, "")
|
||||||
|
|
||||||
|
// 步骤1: 获取初始 Cookie
|
||||||
|
if err := getInitialCookies(httpClient); err != nil {
|
||||||
|
return nil, fmt.Errorf("获取初始 Cookie 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试登录
|
||||||
|
maxRetries := 3
|
||||||
|
var loginSuccess bool
|
||||||
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||||
|
log.Printf("尝试登录 (第%d次)...", attempt)
|
||||||
|
|
||||||
|
// 获取并识别验证码
|
||||||
|
captchaCode, err := captcha.GetAndRecognize(httpClient)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" 验证码获取失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
success, err := loginWithCredentials(httpClient, captchaCode, req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" 登录失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if success {
|
||||||
|
loginSuccess = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(" 登录失败,3秒后重试...")
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !loginSuccess {
|
||||||
|
return nil, fmt.Errorf("登录失败,已重试 %d 次", maxRetries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取课表
|
||||||
|
courses, err := parser.FetchScheduleWithClient(httpClient, semester)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取课表失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建结果
|
||||||
|
result := &pb.ScheduleResultData{
|
||||||
|
StudentId: req.Username,
|
||||||
|
Semester: semester,
|
||||||
|
Courses: convertCourses(courses),
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// login 登录验证
|
||||||
|
func (h *TaskHandler) login(payload []byte) ([]byte, error) {
|
||||||
|
var req pb.LoginPayload
|
||||||
|
if err := json.Unmarshal(payload, &req); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析任务载荷失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("登录验证: 用户=%s", req.Username)
|
||||||
|
|
||||||
|
// 创建新的 HTTP 客户端
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建 cookie jar 失败: %w", err)
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Jar: jar,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 TWFID cookie
|
||||||
|
setTWFIDCookie(httpClient, config.BaseURL, req.Twfid)
|
||||||
|
|
||||||
|
// 步骤1: 获取初始 Cookie
|
||||||
|
if err := getInitialCookies(httpClient); err != nil {
|
||||||
|
return nil, fmt.Errorf("获取初始 Cookie 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试登录
|
||||||
|
maxRetries := 3
|
||||||
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||||
|
log.Printf("尝试登录 (第%d次)...", attempt)
|
||||||
|
|
||||||
|
// 获取并识别验证码
|
||||||
|
captchaCode, err := captcha.GetAndRecognize(httpClient)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" 验证码获取失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
success, err := loginWithCredentials(httpClient, captchaCode, req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" 登录失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if success {
|
||||||
|
return json.Marshal(map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"message": "登录成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(" 登录失败,3秒后重试...")
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("登录失败,已重试 %d 次", maxRetries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInitialCookies 获取初始 Cookie
|
||||||
|
func getInitialCookies(httpClient *http.Client) error {
|
||||||
|
log.Println("[1] 访问登录页面获取初始 Cookie...")
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", config.BaseURL+"/loginNOCAS", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header = getHeaders()
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
log.Printf(" 登录页面状态码: %d", resp.StatusCode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loginWithCredentials 使用凭据登录
|
||||||
|
func loginWithCredentials(httpClient *http.Client, captchaCode, username, password string) (bool, error) {
|
||||||
|
return auth.LoginWithClient(httpClient, captchaCode, username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setTWFIDCookie 设置 TWFID Cookie
|
||||||
|
func setTWFIDCookie(httpClient *http.Client, baseURL, twfid string) {
|
||||||
|
if twfid == "" {
|
||||||
|
twfid = config.TWFID
|
||||||
|
}
|
||||||
|
u, _ := url.Parse(baseURL)
|
||||||
|
httpClient.Jar.SetCookies(u, []*http.Cookie{
|
||||||
|
{Name: "TWFID", Value: twfid},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHeaders 获取请求头
|
||||||
|
func getHeaders() http.Header {
|
||||||
|
return http.Header{
|
||||||
|
"User-Agent": []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"},
|
||||||
|
"Accept": []string{"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"},
|
||||||
|
"Accept-Language": []string{"zh-CN,zh;q=0.9,en;q=0.8"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertCourses 转换课程格式
|
||||||
|
func convertCourses(courses []models.Course) []*pb.Course {
|
||||||
|
var result []*pb.Course
|
||||||
|
for _, c := range courses {
|
||||||
|
var schedule []*pb.ScheduleTime
|
||||||
|
for _, s := range c.Schedule {
|
||||||
|
schedule = append(schedule, &pb.ScheduleTime{
|
||||||
|
Day: s.Day,
|
||||||
|
Sections: convertToInt32(s.Section),
|
||||||
|
Weeks: convertToInt32(s.Weeks),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
result = append(result, &pb.Course{
|
||||||
|
Name: c.Name,
|
||||||
|
Teacher: c.Teacher,
|
||||||
|
Room: c.Room,
|
||||||
|
Schedule: schedule,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertToInt32 将 []int 转换为 []int32
|
||||||
|
func convertToInt32(arr []int) []int32 {
|
||||||
|
result := make([]int32, len(arr))
|
||||||
|
for i, v := range arr {
|
||||||
|
result[i] = int32(v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
162
main.go
Normal file
162
main.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"jwts/auth"
|
||||||
|
"jwts/captcha"
|
||||||
|
"jwts/client"
|
||||||
|
"jwts/config"
|
||||||
|
grpcClient "jwts/grpc"
|
||||||
|
"jwts/parser"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mode := flag.String("mode", "http", "运行模式: http 或 grpc")
|
||||||
|
grpcAddr := flag.String("grpc-addr", "localhost:50051", "gRPC 服务器地址")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *mode == "grpc" {
|
||||||
|
runGRPCClient(*grpcAddr)
|
||||||
|
} else {
|
||||||
|
runHTTPServer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runGRPCClient 运行 gRPC 客户端模式
|
||||||
|
func runGRPCClient(serverAddr string) {
|
||||||
|
runnerID := os.Getenv("RUNNER_ID")
|
||||||
|
if runnerID == "" {
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
runnerID = "runner-" + hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
version := os.Getenv("RUNNER_VERSION")
|
||||||
|
if version == "" {
|
||||||
|
version = "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("启动 gRPC 客户端, Runner ID: %s, 版本: %s", runnerID, version)
|
||||||
|
|
||||||
|
client := grpcClient.NewClient(serverAddr, runnerID, version)
|
||||||
|
handler := grpcClient.NewTaskHandler()
|
||||||
|
client.SetHandler(handler)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 处理信号
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
if err := client.Connect(ctx); err != nil {
|
||||||
|
log.Printf("连接失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sigChan
|
||||||
|
log.Println("正在关闭...")
|
||||||
|
client.Close()
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 启动心跳
|
||||||
|
go client.HeartbeatLoop(ctx, 30*time.Second)
|
||||||
|
|
||||||
|
// 运行主循环
|
||||||
|
if err := client.Run(ctx); err != nil {
|
||||||
|
log.Printf("客户端错误: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runHTTPServer 运行 HTTP 服务器模式
|
||||||
|
func runHTTPServer() {
|
||||||
|
// 初始化客户端
|
||||||
|
client.InitClient()
|
||||||
|
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// 健康检查接口
|
||||||
|
r.GET("/ping", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "pong",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 任务处理接口
|
||||||
|
r.POST("/task", func(c *gin.Context) {
|
||||||
|
// 这里可以根据请求体参数决定执行什么任务
|
||||||
|
// 目前默认执行原有的登录和获取课表逻辑
|
||||||
|
if config.Username == "" || config.Password == "" {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "环境变量未设置"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行任务
|
||||||
|
err := runTask()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "任务执行成功"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 启动服务
|
||||||
|
log.Println("Server starting on :8080")
|
||||||
|
if err := r.Run(":8080"); err != nil {
|
||||||
|
log.Fatalf("Failed to run server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTask() error {
|
||||||
|
// 步骤1: 获取初始Cookie
|
||||||
|
err := client.GetInitialCookies()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("获取Cookie失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试登录
|
||||||
|
maxRetries := 3
|
||||||
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||||
|
log.Printf("尝试登录 (第%d次)...", attempt)
|
||||||
|
|
||||||
|
// 获取并识别验证码
|
||||||
|
captchaCode, err := captcha.GetAndRecognize(client.Client)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" 验证码获取失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
success, err := auth.Login(captchaCode)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" 登录失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if success {
|
||||||
|
// 获取课表
|
||||||
|
_, err = parser.FetchSchedule(client.Client)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("获取课表失败: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(" 登录失败,3秒后重试...")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
36
models/api.go
Normal file
36
models/api.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// APIRequest API请求结构
|
||||||
|
type APIRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Messages []Message `json:"messages"`
|
||||||
|
MaxTokens int `json:"max_tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message 消息结构
|
||||||
|
type Message struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content interface{} `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentItem 内容项
|
||||||
|
type ContentItem struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
ImageURL *ImageURL `json:"image_url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageURL 图片URL
|
||||||
|
type ImageURL struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIResponse API响应结构
|
||||||
|
type APIResponse struct {
|
||||||
|
Choices []Choice `json:"choices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choice 选择
|
||||||
|
type Choice struct {
|
||||||
|
Message Message `json:"message"`
|
||||||
|
}
|
||||||
16
models/course.go
Normal file
16
models/course.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Course 课程结构
|
||||||
|
type Course struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Teacher string `json:"teacher"`
|
||||||
|
Room string `json:"room"`
|
||||||
|
Schedule []Schedule `json:"schedule"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule 上课时间
|
||||||
|
type Schedule struct {
|
||||||
|
Day string `json:"day"`
|
||||||
|
Section []int `json:"section"`
|
||||||
|
Weeks []int `json:"weeks"`
|
||||||
|
}
|
||||||
531
parser/schedule.go
Normal file
531
parser/schedule.go
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"jwts/client"
|
||||||
|
"jwts/config"
|
||||||
|
"jwts/models"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
var weekTokenRegexp = regexp.MustCompile(`\[?\d+(?:[-,,]\d+)*\]?周`)
|
||||||
|
|
||||||
|
type weekRoomEntry struct {
|
||||||
|
weeksStr string
|
||||||
|
room string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取课表
|
||||||
|
func FetchSchedule(httpClient *http.Client) ([]models.Course, error) {
|
||||||
|
return FetchScheduleWithClient(httpClient, config.Semester)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchScheduleWithClient 使用指定的 HTTP 客户端和学期获取课表
|
||||||
|
func FetchScheduleWithClient(httpClient *http.Client, semester string) ([]models.Course, error) {
|
||||||
|
fmt.Println("[5] 获取课表数据...")
|
||||||
|
|
||||||
|
// POST请求获取课表
|
||||||
|
data := fmt.Sprintf("xnxq=%s", semester)
|
||||||
|
req, err := http.NewRequest("POST", config.BaseURL+"/kbcx/queryGrkb", strings.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header = client.GetHeadersWithContentType("application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile("grkb.html", body, 0644); err != nil {
|
||||||
|
fmt.Printf(" 警告: 无法保存课表页面: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println(" 课表页面已保存到: grkb.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析课表
|
||||||
|
courses, err := ParseScheduleHTML(string(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" 共提取 %d 门课程\n", len(courses))
|
||||||
|
|
||||||
|
// 输出到JSON
|
||||||
|
jsonData, err := os.Create("schedule_result.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer jsonData.Close()
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(jsonData)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
err = encoder.Encode(courses)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Println(" JSON结果已保存到: schedule_result.json")
|
||||||
|
|
||||||
|
// 打印课程信息
|
||||||
|
for i, course := range courses {
|
||||||
|
fmt.Printf(" 课程 %d: %s - %s\n", i+1, course.Name, course.Teacher)
|
||||||
|
}
|
||||||
|
|
||||||
|
return courses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析课表HTML
|
||||||
|
func ParseScheduleHTML(html string) ([]models.Course, error) {
|
||||||
|
// 创建一个带Reader的Document
|
||||||
|
reader := strings.NewReader(html)
|
||||||
|
doc, err := goquery.NewDocumentFromReader(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
courseMap := make(map[string]*models.Course)
|
||||||
|
|
||||||
|
// 找到课表table
|
||||||
|
doc.Find("table").Each(func(tableIdx int, table *goquery.Selection) {
|
||||||
|
class, _ := table.Attr("class")
|
||||||
|
if !strings.Contains(class, "addlist") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Find("tr").Each(func(trIdx int, tr *goquery.Selection) {
|
||||||
|
tds := tr.Find("td")
|
||||||
|
if tds.Length() < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一个td是上午/下午/晚上,第二个是节次
|
||||||
|
section := tds.Eq(1).Text()
|
||||||
|
if section == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从第3个td开始是星期一到星期日 (索引2-8)
|
||||||
|
for dayIndex := 2; dayIndex <= 8; dayIndex++ {
|
||||||
|
if dayIndex >= tds.Length() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dayTd := tds.Eq(dayIndex)
|
||||||
|
|
||||||
|
// 获取HTML内容
|
||||||
|
cellHTML, err := dayTd.Html()
|
||||||
|
if err != nil || cellHTML == "" || cellHTML == " " {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过 
|
||||||
|
if strings.TrimSpace(cellHTML) == " " {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
day := getDayName(dayIndex - 1)
|
||||||
|
|
||||||
|
// 解析课程信息
|
||||||
|
parseCourseCellHTML(cellHTML, day, section, courseMap)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 将map转为slice
|
||||||
|
var courses []models.Course
|
||||||
|
for _, course := range courseMap {
|
||||||
|
courses = append(courses, *course)
|
||||||
|
}
|
||||||
|
|
||||||
|
return courses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析课程单元格HTML
|
||||||
|
func parseCourseCellHTML(cellHTML, day, section string, courseMap map[string]*models.Course) {
|
||||||
|
|
||||||
|
// 检查是否有多个课程(用<br/>分割)
|
||||||
|
if strings.Contains(cellHTML, "<br>") || strings.Contains(cellHTML, "<br/>") || strings.Contains(cellHTML, "<br />") {
|
||||||
|
// 分割成多个课程
|
||||||
|
// 先统一替换
|
||||||
|
cellHTML = strings.ReplaceAll(cellHTML, "<br>", "|SPLIT|")
|
||||||
|
cellHTML = strings.ReplaceAll(cellHTML, "<br/>", "|SPLIT|")
|
||||||
|
cellHTML = strings.ReplaceAll(cellHTML, "<br />", "|SPLIT|")
|
||||||
|
|
||||||
|
courses := strings.Split(cellHTML, "|SPLIT|")
|
||||||
|
|
||||||
|
var courseName string
|
||||||
|
for i, part := range courses {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeWeek, weekEntries := extractWeekEntries(part)
|
||||||
|
if len(weekEntries) == 0 {
|
||||||
|
// 没有周数信息,这是课程名(教室会在周数后面或下一个部分)
|
||||||
|
courseName = part
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有周数信息,说明这行包含老师+周数+地点
|
||||||
|
// 需要结合之前的课程名
|
||||||
|
if courseName == "" {
|
||||||
|
courseName = part
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先取片段中的第一个有效教室作为回退值
|
||||||
|
fallbackRoom := ""
|
||||||
|
for _, entry := range weekEntries {
|
||||||
|
if entry.room != "" {
|
||||||
|
fallbackRoom = entry.room
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果片段里没有教室,查看下一个部分是否是教室
|
||||||
|
if fallbackRoom == "" {
|
||||||
|
// 找下一个部分
|
||||||
|
for j := i + 1; j < len(courses); j++ {
|
||||||
|
nextPart := strings.TrimSpace(courses[j])
|
||||||
|
if nextPart == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 检查下一个部分是否是教室(包含楼/室/厅/院)
|
||||||
|
if strings.Contains(nextPart, "楼") || strings.Contains(nextPart, "室") || strings.Contains(nextPart, "厅") || strings.Contains(nextPart, "院") {
|
||||||
|
fallbackRoom = nextPart
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// 如果下一个部分不包含周数信息,说明是课程名,停止查找
|
||||||
|
if !regexp.MustCompile(`\d+.*周`).MatchString(nextPart) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析老师(在beforeWeek中)
|
||||||
|
teacher := ""
|
||||||
|
// 去掉课程名部分
|
||||||
|
if strings.HasPrefix(beforeWeek, courseName) {
|
||||||
|
teacher = strings.TrimSpace(beforeWeek[len(courseName):])
|
||||||
|
} else {
|
||||||
|
// 老师可能直接在整个字符串中
|
||||||
|
teacherPattern := regexp.MustCompile(`([一-龥]{2,4})$`)
|
||||||
|
teacherMatch := teacherPattern.FindStringSubmatch(beforeWeek)
|
||||||
|
if teacherMatch != nil {
|
||||||
|
teacher = teacherMatch[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析节次
|
||||||
|
sectionNums := parseSection(section)
|
||||||
|
|
||||||
|
// 每个“周次-地点”片段分别入库,避免不同周次地点混淆
|
||||||
|
for _, entry := range weekEntries {
|
||||||
|
room := strings.TrimSpace(entry.room)
|
||||||
|
if room == "" {
|
||||||
|
room = fallbackRoom
|
||||||
|
}
|
||||||
|
weeks := parseWeeks(entry.weeksStr)
|
||||||
|
if len(weeks) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用课程名+老师+教室作为 key,避免不同地点被错误合并
|
||||||
|
key := buildCourseKey(courseName, teacher, room)
|
||||||
|
|
||||||
|
// 检查是否已存在
|
||||||
|
if existing, exists := courseMap[key]; exists {
|
||||||
|
// 添加新的上课时间
|
||||||
|
existing.Schedule = append(existing.Schedule, models.Schedule{
|
||||||
|
Day: day,
|
||||||
|
Section: sectionNums,
|
||||||
|
Weeks: weeks,
|
||||||
|
})
|
||||||
|
if existing.Room == "" && room != "" {
|
||||||
|
existing.Room = room
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 创建新课程
|
||||||
|
courseMap[key] = &models.Course{
|
||||||
|
Name: courseName,
|
||||||
|
Teacher: teacher,
|
||||||
|
Room: room,
|
||||||
|
Schedule: []models.Schedule{
|
||||||
|
{
|
||||||
|
Day: day,
|
||||||
|
Section: sectionNums,
|
||||||
|
Weeks: weeks,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这个课程处理完后,重置courseName
|
||||||
|
courseName = ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 只有一个课程,直接解析
|
||||||
|
line := strings.TrimSpace(cellHTML)
|
||||||
|
|
||||||
|
beforeWeek, weekEntries := extractWeekEntries(line)
|
||||||
|
if len(weekEntries) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackRoom := ""
|
||||||
|
for _, entry := range weekEntries {
|
||||||
|
if entry.room != "" {
|
||||||
|
fallbackRoom = entry.room
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从beforeWeek中提取老师和课程名
|
||||||
|
teacherPattern := regexp.MustCompile(`([一-龥]{2,4})$`)
|
||||||
|
teacherMatch := teacherPattern.FindStringSubmatch(beforeWeek)
|
||||||
|
|
||||||
|
var courseName, teacher string
|
||||||
|
if teacherMatch != nil {
|
||||||
|
teacher = teacherMatch[1]
|
||||||
|
courseName = strings.TrimSpace(beforeWeek[:len(beforeWeek)-len(teacher)])
|
||||||
|
} else {
|
||||||
|
courseName = beforeWeek
|
||||||
|
}
|
||||||
|
|
||||||
|
if courseName == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析节次
|
||||||
|
sectionNums := parseSection(section)
|
||||||
|
|
||||||
|
for _, entry := range weekEntries {
|
||||||
|
room := strings.TrimSpace(entry.room)
|
||||||
|
if room == "" {
|
||||||
|
room = fallbackRoom
|
||||||
|
}
|
||||||
|
weeks := parseWeeks(entry.weeksStr)
|
||||||
|
if len(weeks) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用课程名+老师+教室作为 key,避免不同地点被错误合并
|
||||||
|
key := buildCourseKey(courseName, teacher, room)
|
||||||
|
|
||||||
|
if existing, exists := courseMap[key]; exists {
|
||||||
|
existing.Schedule = append(existing.Schedule, models.Schedule{
|
||||||
|
Day: day,
|
||||||
|
Section: sectionNums,
|
||||||
|
Weeks: weeks,
|
||||||
|
})
|
||||||
|
if existing.Room == "" && room != "" {
|
||||||
|
existing.Room = room
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
courseMap[key] = &models.Course{
|
||||||
|
Name: courseName,
|
||||||
|
Teacher: teacher,
|
||||||
|
Room: room,
|
||||||
|
Schedule: []models.Schedule{
|
||||||
|
{
|
||||||
|
Day: day,
|
||||||
|
Section: sectionNums,
|
||||||
|
Weeks: weeks,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCourseKey(name, teacher, room string) string {
|
||||||
|
return strings.TrimSpace(name) + "|" + strings.TrimSpace(teacher) + "|" + strings.TrimSpace(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractWeekInfo 提取一段文本中的完整周次片段
|
||||||
|
// 例如:于战华[15]周,[2-14]周 -> before=于战华, weeks=[15]周,[2-14]周, after=
|
||||||
|
func extractWeekInfo(text string) (beforeWeek, weeksPart, afterWeek string, ok bool) {
|
||||||
|
indexes := weekTokenRegexp.FindAllStringIndex(text, -1)
|
||||||
|
if len(indexes) == 0 {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
firstStart := indexes[0][0]
|
||||||
|
beforeWeek = strings.TrimSpace(text[:firstStart])
|
||||||
|
|
||||||
|
parts := weekTokenRegexp.FindAllString(text, -1)
|
||||||
|
weeksPart = strings.Join(parts, ",")
|
||||||
|
|
||||||
|
// 周次后(含中间)非周次内容作为教室候选
|
||||||
|
tail := strings.TrimSpace(text[firstStart:])
|
||||||
|
afterWeek = strings.TrimSpace(weekTokenRegexp.ReplaceAllString(tail, ""))
|
||||||
|
afterWeek = strings.Trim(afterWeek, ",, ")
|
||||||
|
return beforeWeek, weeksPart, afterWeek, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractWeekEntries 将一段文本拆成多个“周次-地点”片段
|
||||||
|
// 例如:李鹏程[2-8]周N楼-335,[9]周N楼-331
|
||||||
|
// 会拆成:
|
||||||
|
// 1) weeks=[2-8]周 room=N楼-335
|
||||||
|
// 2) weeks=[9]周 room=N楼-331
|
||||||
|
func extractWeekEntries(text string) (beforeWeek string, entries []weekRoomEntry) {
|
||||||
|
indexes := weekTokenRegexp.FindAllStringIndex(text, -1)
|
||||||
|
if len(indexes) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
firstStart := indexes[0][0]
|
||||||
|
beforeWeek = strings.TrimSpace(text[:firstStart])
|
||||||
|
|
||||||
|
for i, idx := range indexes {
|
||||||
|
start := idx[0]
|
||||||
|
end := idx[1]
|
||||||
|
nextStart := len(text)
|
||||||
|
if i+1 < len(indexes) {
|
||||||
|
nextStart = indexes[i+1][0]
|
||||||
|
}
|
||||||
|
|
||||||
|
weeksStr := strings.TrimSpace(text[start:end])
|
||||||
|
room := strings.TrimSpace(text[end:nextStart])
|
||||||
|
room = weekTokenRegexp.ReplaceAllString(room, "")
|
||||||
|
room = strings.Trim(room, ",, ")
|
||||||
|
room = strings.TrimSpace(room)
|
||||||
|
|
||||||
|
entries = append(entries, weekRoomEntry{
|
||||||
|
weeksStr: weeksStr,
|
||||||
|
room: room,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return beforeWeek, entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDayName(index int) string {
|
||||||
|
// 返回0-6,0=周一,6=周日
|
||||||
|
days := []string{"0", "1", "2", "3", "4", "5", "6"}
|
||||||
|
if index >= 1 && index <= 7 {
|
||||||
|
return days[index-1]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSection(section string) []int {
|
||||||
|
// 格式: 1-2 或 3-4
|
||||||
|
re := regexp.MustCompile(`(\d+)-(\d+)`)
|
||||||
|
match := re.FindStringSubmatch(section)
|
||||||
|
if match != nil {
|
||||||
|
start, _ := strconv.Atoi(match[1])
|
||||||
|
end, _ := strconv.Atoi(match[2])
|
||||||
|
var nums []int
|
||||||
|
for i := start; i <= end; i++ {
|
||||||
|
nums = append(nums, i)
|
||||||
|
}
|
||||||
|
return nums
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单独数字
|
||||||
|
num, err := strconv.Atoi(section)
|
||||||
|
if err == nil {
|
||||||
|
return []int{num}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWeeks(weeksStr string) []int {
|
||||||
|
if weeksStr == "" {
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一格式:去掉括号/周/空白,统一中文分隔符
|
||||||
|
raw := strings.TrimSpace(weeksStr)
|
||||||
|
oddOnly := strings.Contains(raw, "单")
|
||||||
|
evenOnly := strings.Contains(raw, "双")
|
||||||
|
|
||||||
|
replacer := strings.NewReplacer(
|
||||||
|
"[", "", "]", "",
|
||||||
|
"(", "", ")", "",
|
||||||
|
"(", "", ")", "",
|
||||||
|
"周", "",
|
||||||
|
",", ",",
|
||||||
|
"、", ",",
|
||||||
|
" ", "",
|
||||||
|
)
|
||||||
|
normalized := replacer.Replace(raw)
|
||||||
|
if normalized == "" {
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
weekSet := make(map[int]struct{})
|
||||||
|
for _, part := range strings.Split(normalized, ",") {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 区间格式:1-8
|
||||||
|
if strings.Contains(part, "-") {
|
||||||
|
rangeParts := strings.SplitN(part, "-", 2)
|
||||||
|
if len(rangeParts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
start, err1 := strconv.Atoi(strings.TrimSpace(rangeParts[0]))
|
||||||
|
end, err2 := strconv.Atoi(strings.TrimSpace(rangeParts[1]))
|
||||||
|
if err1 != nil || err2 != nil || start > end {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := start; i <= end; i++ {
|
||||||
|
if oddOnly && i%2 == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if evenOnly && i%2 != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
weekSet[i] = struct{}{}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单周次:例如 9
|
||||||
|
num, err := strconv.Atoi(part)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if oddOnly && num%2 == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if evenOnly && num%2 != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
weekSet[num] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(weekSet) == 0 {
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
weeks := make([]int, 0, len(weekSet))
|
||||||
|
for w := range weekSet {
|
||||||
|
weeks = append(weeks, w)
|
||||||
|
}
|
||||||
|
sort.Ints(weeks)
|
||||||
|
return weeks
|
||||||
|
}
|
||||||
234
parser/schedule_test.go
Normal file
234
parser/schedule_test.go
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"jwts/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseScheduleHTML_FromTestHTML_MergedWeeks(t *testing.T) {
|
||||||
|
htmlPath := filepath.Join("..", "test.html")
|
||||||
|
body, err := os.ReadFile(htmlPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("读取 test.html 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
courses, err := ParseScheduleHTML(string(body))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("解析课表失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCourseHasWeek(t, courses, "大学物理X(1)", "王小赛", 1)
|
||||||
|
checkCourseHasWeek(t, courses, "大学物理X(1)", "王小赛", 16)
|
||||||
|
checkCourseHasWeek(t, courses, "数学分析(2)", "于战华", 2)
|
||||||
|
checkCourseHasWeek(t, courses, "数学分析(2)", "于战华", 15)
|
||||||
|
checkCourseHasRooms(t, courses, "中国近现代史纲要", "苏克", []string{"H楼-447", "H楼-434"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseScheduleHTML_FromGrkbHTML_ChineseCommaWeeks(t *testing.T) {
|
||||||
|
htmlPath := filepath.Join("..", "grkb.html")
|
||||||
|
body, err := os.ReadFile(htmlPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("读取 grkb.html 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
courses, err := ParseScheduleHTML(string(body))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("解析课表失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCourseHasWeek(t, courses, "数理统计", "黄春茂", 10)
|
||||||
|
checkCourseHasWeek(t, courses, "数理统计", "黄春茂", 11)
|
||||||
|
checkCourseHasWeek(t, courses, "数理统计", "黄春茂", 12)
|
||||||
|
checkCourseHasWeek(t, courses, "微分几何", "李鹏程", 2)
|
||||||
|
checkCourseHasWeek(t, courses, "微分几何", "李鹏程", 8)
|
||||||
|
checkCourseHasWeek(t, courses, "微分几何", "李鹏程", 9)
|
||||||
|
checkCourseRoomForWeek(t, courses, "微分几何", "李鹏程", 9, "N楼-331")
|
||||||
|
checkCourseRoomForWeek(t, courses, "微分几何", "李鹏程", 8, "N楼-335")
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCourseWeeks(
|
||||||
|
t *testing.T,
|
||||||
|
courses []models.Course,
|
||||||
|
name, teacher, day string,
|
||||||
|
section []int,
|
||||||
|
wantStart, wantEnd int,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, c := range courses {
|
||||||
|
if c.Name != name || c.Teacher != teacher {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, s := range c.Schedule {
|
||||||
|
if s.Day != day || !sameInts(s.Section, section) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(s.Weeks) == 0 {
|
||||||
|
t.Fatalf("%s/%s 周次为空", name, teacher)
|
||||||
|
}
|
||||||
|
if s.Weeks[0] != wantStart || s.Weeks[len(s.Weeks)-1] != wantEnd {
|
||||||
|
t.Fatalf("%s/%s 周次范围错误: got=%v want=%d..%d", name, teacher, s.Weeks, wantStart, wantEnd)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatalf("未找到课程: %s/%s day=%s section=%v", name, teacher, day, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameInts(a, b []int) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkScheduleContainsWeeks(
|
||||||
|
t *testing.T,
|
||||||
|
courses []models.Course,
|
||||||
|
name, teacher, day string,
|
||||||
|
section []int,
|
||||||
|
wantWeeks []int,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, c := range courses {
|
||||||
|
if c.Name != name || c.Teacher != teacher {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, s := range c.Schedule {
|
||||||
|
if s.Day != day || !sameInts(s.Section, section) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(s.Weeks) == 0 {
|
||||||
|
t.Fatalf("%s/%s 周次为空", name, teacher)
|
||||||
|
}
|
||||||
|
for _, w := range wantWeeks {
|
||||||
|
if !containsInt(s.Weeks, w) {
|
||||||
|
t.Fatalf("%s/%s 周次缺失: want include=%d got=%v", name, teacher, w, s.Weeks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatalf("未找到课程: %s/%s day=%s section=%v", name, teacher, day, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsInt(arr []int, target int) bool {
|
||||||
|
for _, v := range arr {
|
||||||
|
if v == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCourseHasWeek(
|
||||||
|
t *testing.T,
|
||||||
|
courses []models.Course,
|
||||||
|
name, teacher string,
|
||||||
|
week int,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, c := range courses {
|
||||||
|
if c.Name != name || c.Teacher != teacher {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, s := range c.Schedule {
|
||||||
|
if containsInt(s.Weeks, week) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatalf("%s/%s 缺少第%d周", name, teacher, week)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCourseRoomForWeek(
|
||||||
|
t *testing.T,
|
||||||
|
courses []models.Course,
|
||||||
|
name, teacher string,
|
||||||
|
week int,
|
||||||
|
wantRoom string,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, c := range courses {
|
||||||
|
if c.Name != name || c.Teacher != teacher {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, s := range c.Schedule {
|
||||||
|
if containsInt(s.Weeks, week) && c.Room == wantRoom {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatalf("%s/%s 第%d周未匹配到地点 %s", name, teacher, week, wantRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCourseHasRooms(
|
||||||
|
t *testing.T,
|
||||||
|
courses []models.Course,
|
||||||
|
name, teacher string,
|
||||||
|
wantRooms []string,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
roomSet := map[string]bool{}
|
||||||
|
for _, c := range courses {
|
||||||
|
if c.Name == name && c.Teacher == teacher {
|
||||||
|
roomSet[c.Room] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, room := range wantRooms {
|
||||||
|
if !roomSet[room] {
|
||||||
|
t.Fatalf("%s/%s 缺少地点: %s, 当前地点集合: %v", name, teacher, room, roomSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCourseAnyScheduleContainsWeeks(
|
||||||
|
t *testing.T,
|
||||||
|
courses []models.Course,
|
||||||
|
name, teacher string,
|
||||||
|
wantWeeks []int,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
foundCourse := false
|
||||||
|
for _, c := range courses {
|
||||||
|
if c.Name != name || c.Teacher != teacher {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
foundCourse = true
|
||||||
|
for _, s := range c.Schedule {
|
||||||
|
allFound := true
|
||||||
|
for _, w := range wantWeeks {
|
||||||
|
if !containsInt(s.Weeks, w) {
|
||||||
|
allFound = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allFound {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundCourse {
|
||||||
|
t.Fatalf("未找到课程: %s/%s", name, teacher)
|
||||||
|
}
|
||||||
|
t.Fatalf("%s/%s 所有排课都不包含预期周次: want=%v", name, teacher, wantWeeks)
|
||||||
|
}
|
||||||
1835
proto/runner/runner.pb.go
Normal file
1835
proto/runner/runner.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
224
proto/runner/runner.proto
Normal file
224
proto/runner/runner.proto
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package runner;
|
||||||
|
|
||||||
|
option go_package = "jwts/proto/runner;runner";
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 服务定义
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// RunnerHub 服务 - 由 Backend 服务器实现
|
||||||
|
// Runner 作为客户端主动连接,建立双向流通信
|
||||||
|
service RunnerHub {
|
||||||
|
// Connect 建立双向流连接
|
||||||
|
// Runner 通过此连接注册、接收任务、返回结果
|
||||||
|
rpc Connect(stream StreamMessage) returns (stream StreamMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 流消息包装 - 所有消息通过 oneof 包装
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// StreamMessage 双向流消息包装
|
||||||
|
message StreamMessage {
|
||||||
|
oneof message {
|
||||||
|
RegisterRequest register = 1; // Runner 注册请求
|
||||||
|
RegisterResponse register_response = 2; // 注册响应
|
||||||
|
Heartbeat heartbeat = 3; // 心跳请求
|
||||||
|
HeartbeatAck heartbeat_ack = 4; // 心跳响应
|
||||||
|
Task task = 5; // 下发的任务
|
||||||
|
TaskResult result = 6; // 任务结果
|
||||||
|
TaskCancel task_cancel = 7; // 取消任务
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 注册相关消息
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// RegisterRequest Runner 注册请求
|
||||||
|
message RegisterRequest {
|
||||||
|
string runner_id = 1; // Runner 唯一标识 (UUID)
|
||||||
|
string version = 2; // Runner 版本号
|
||||||
|
map<string, string> capabilities = 3; // 能力标签
|
||||||
|
// 例如: {"login": "true", "schedule": "true", "grades": "true"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterResponse 注册响应
|
||||||
|
message RegisterResponse {
|
||||||
|
bool success = 1; // 是否成功
|
||||||
|
string session_id = 2; // 会话ID,用于标识本次连接
|
||||||
|
string message = 3; // 消息说明
|
||||||
|
int32 heartbeat_interval = 4; // 心跳间隔 (秒)
|
||||||
|
int32 task_timeout = 5; // 默认任务超时时间 (秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 心跳消息
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Heartbeat 心跳请求
|
||||||
|
message Heartbeat {
|
||||||
|
int64 timestamp = 1; // 客户端时间戳 (毫秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeartbeatAck 心跳响应
|
||||||
|
message HeartbeatAck {
|
||||||
|
int64 timestamp = 1; // 服务端时间戳 (毫秒)
|
||||||
|
int64 latency = 2; // 往返延迟 (毫秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 任务定义
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// TaskType 任务类型枚举
|
||||||
|
enum TaskType {
|
||||||
|
TASK_TYPE_UNKNOWN = 0; // 未知类型
|
||||||
|
TASK_TYPE_LOGIN = 1; // 登录验证
|
||||||
|
TASK_TYPE_GET_SCHEDULE = 2; // 获取课表
|
||||||
|
TASK_TYPE_GET_GRADES = 3; // 获取成绩
|
||||||
|
TASK_TYPE_GET_EXAMS = 4; // 获取考试安排
|
||||||
|
TASK_TYPE_GET_USER_INFO = 5; // 获取用户信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskStatus 任务状态枚举
|
||||||
|
enum TaskStatus {
|
||||||
|
TASK_STATUS_UNKNOWN = 0; // 未知状态
|
||||||
|
TASK_STATUS_SUCCESS = 1; // 成功
|
||||||
|
TASK_STATUS_FAILED = 2; // 失败
|
||||||
|
TASK_STATUS_TIMEOUT = 3; // 超时
|
||||||
|
TASK_STATUS_CANCELLED = 4; // 已取消
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task 任务定义
|
||||||
|
message Task {
|
||||||
|
string task_id = 1; // 任务唯一ID (UUID)
|
||||||
|
TaskType type = 2; // 任务类型
|
||||||
|
int64 created_at = 3; // 创建时间戳 (毫秒)
|
||||||
|
int32 timeout_seconds = 4; // 超时时间 (秒)
|
||||||
|
bytes payload = 5; // 任务载荷 (JSON 编码)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskCancel 取消任务
|
||||||
|
message TaskCancel {
|
||||||
|
string task_id = 1; // 要取消的任务ID
|
||||||
|
string reason = 2; // 取消原因
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskResult 任务结果
|
||||||
|
message TaskResult {
|
||||||
|
string task_id = 1; // 任务ID
|
||||||
|
TaskStatus status = 2; // 执行状态
|
||||||
|
int64 completed_at = 3; // 完成时间戳 (毫秒)
|
||||||
|
bytes data = 4; // 结果数据 (JSON 编码)
|
||||||
|
string error_code = 5; // 错误码
|
||||||
|
string error_message = 6; // 错误消息
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 任务载荷定义 - 用于 JSON 序列化到 bytes
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// LoginPayload 登录任务载荷
|
||||||
|
message LoginPayload {
|
||||||
|
string username = 1; // 学号
|
||||||
|
string password = 2; // 密码
|
||||||
|
string twfid = 3; // 初始 Cookie (可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSchedulePayload 获取课表任务载荷
|
||||||
|
message GetSchedulePayload {
|
||||||
|
string username = 1; // 学号
|
||||||
|
string password = 2; // 密码
|
||||||
|
string semester = 3; // 学期,如 "2025-20262"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGradesPayload 获取成绩任务载荷
|
||||||
|
message GetGradesPayload {
|
||||||
|
string username = 1; // 学号
|
||||||
|
string password = 2; // 密码
|
||||||
|
string semester = 3; // 学期 (可选,为空则获取全部)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExamsPayload 获取考试安排任务载荷
|
||||||
|
message GetExamsPayload {
|
||||||
|
string username = 1; // 学号
|
||||||
|
string password = 2; // 密码
|
||||||
|
string semester = 3; // 学期
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 结果数据定义 - 用于 JSON 序列化到 bytes
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// ScheduleResultData 课表结果数据
|
||||||
|
message ScheduleResultData {
|
||||||
|
string semester = 1; // 学期
|
||||||
|
string student_id = 2; // 学号
|
||||||
|
string student_name = 3; // 学生姓名
|
||||||
|
repeated Course courses = 4; // 课程列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course 课程信息
|
||||||
|
message Course {
|
||||||
|
string code = 1; // 课程代码
|
||||||
|
string name = 2; // 课程名称
|
||||||
|
string teacher = 3; // 授课教师
|
||||||
|
string room = 4; // 教室
|
||||||
|
float credit = 5; // 学分
|
||||||
|
repeated ScheduleTime schedule = 6; // 上课时间安排
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScheduleTime 上课时间安排
|
||||||
|
message ScheduleTime {
|
||||||
|
string day = 1; // 星期几,"0"-"6" 表示周日到周六
|
||||||
|
repeated int32 sections = 2; // 节次,如 [1, 2, 3]
|
||||||
|
repeated int32 weeks = 3; // 周次,如 [1, 2, 3, 4, 5]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GradesResultData 成绩结果数据
|
||||||
|
message GradesResultData {
|
||||||
|
string semester = 1; // 学期
|
||||||
|
string student_id = 2; // 学号
|
||||||
|
repeated Grade grades = 3; // 成绩列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grade 成绩信息
|
||||||
|
message Grade {
|
||||||
|
string course_code = 1; // 课程代码
|
||||||
|
string course_name = 2; // 课程名称
|
||||||
|
float credit = 3; // 学分
|
||||||
|
string grade = 4; // 成绩 (可能是等级或分数)
|
||||||
|
float grade_point = 5; // 绩点
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExamsResultData 考试安排结果数据
|
||||||
|
message ExamsResultData {
|
||||||
|
string semester = 1; // 学期
|
||||||
|
string student_id = 2; // 学号
|
||||||
|
repeated Exam exams = 3; // 考试列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exam 考试信息
|
||||||
|
message Exam {
|
||||||
|
string course_code = 1; // 课程代码
|
||||||
|
string course_name = 2; // 课程名称
|
||||||
|
string exam_type = 3; // 考试类型 (期末/期中/补考)
|
||||||
|
string date = 4; // 考试日期 (YYYY-MM-DD)
|
||||||
|
string time = 5; // 考试时间 (HH:MM-HH:MM)
|
||||||
|
string room = 6; // 考场
|
||||||
|
string seat = 7; // 座位号
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInfoResultData 用户信息结果数据
|
||||||
|
message UserInfoResultData {
|
||||||
|
string student_id = 1; // 学号
|
||||||
|
string name = 2; // 姓名
|
||||||
|
string gender = 3; // 性别
|
||||||
|
string college = 4; // 学院
|
||||||
|
string major = 5; // 专业
|
||||||
|
string class_name = 6; // 班级
|
||||||
|
string grade = 7; // 年级
|
||||||
|
}
|
||||||
125
proto/runner/runner_grpc.pb.go
Normal file
125
proto/runner/runner_grpc.pb.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
|
// - protoc v6.33.1
|
||||||
|
// source: proto/runner/runner.proto
|
||||||
|
|
||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.64.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
|
const (
|
||||||
|
RunnerHub_Connect_FullMethodName = "/runner.RunnerHub/Connect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunnerHubClient is the client API for RunnerHub service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
//
|
||||||
|
// RunnerHub 服务 - 由 Backend 服务器实现
|
||||||
|
// Runner 作为客户端主动连接,建立双向流通信
|
||||||
|
type RunnerHubClient interface {
|
||||||
|
// Connect 建立双向流连接
|
||||||
|
// Runner 通过此连接注册、接收任务、返回结果
|
||||||
|
Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StreamMessage, StreamMessage], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type runnerHubClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRunnerHubClient(cc grpc.ClientConnInterface) RunnerHubClient {
|
||||||
|
return &runnerHubClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *runnerHubClient) Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StreamMessage, StreamMessage], error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
stream, err := c.cc.NewStream(ctx, &RunnerHub_ServiceDesc.Streams[0], RunnerHub_Connect_FullMethodName, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &grpc.GenericClientStream[StreamMessage, StreamMessage]{ClientStream: stream}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||||
|
type RunnerHub_ConnectClient = grpc.BidiStreamingClient[StreamMessage, StreamMessage]
|
||||||
|
|
||||||
|
// RunnerHubServer is the server API for RunnerHub service.
|
||||||
|
// All implementations must embed UnimplementedRunnerHubServer
|
||||||
|
// for forward compatibility.
|
||||||
|
//
|
||||||
|
// RunnerHub 服务 - 由 Backend 服务器实现
|
||||||
|
// Runner 作为客户端主动连接,建立双向流通信
|
||||||
|
type RunnerHubServer interface {
|
||||||
|
// Connect 建立双向流连接
|
||||||
|
// Runner 通过此连接注册、接收任务、返回结果
|
||||||
|
Connect(grpc.BidiStreamingServer[StreamMessage, StreamMessage]) error
|
||||||
|
mustEmbedUnimplementedRunnerHubServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedRunnerHubServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedRunnerHubServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedRunnerHubServer) Connect(grpc.BidiStreamingServer[StreamMessage, StreamMessage]) error {
|
||||||
|
return status.Error(codes.Unimplemented, "method Connect not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedRunnerHubServer) mustEmbedUnimplementedRunnerHubServer() {}
|
||||||
|
func (UnimplementedRunnerHubServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeRunnerHubServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to RunnerHubServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeRunnerHubServer interface {
|
||||||
|
mustEmbedUnimplementedRunnerHubServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterRunnerHubServer(s grpc.ServiceRegistrar, srv RunnerHubServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedRunnerHubServer was
|
||||||
|
// embedded by pointer and is nil. This will cause panics if an
|
||||||
|
// unimplemented method is ever invoked, so we test this at initialization
|
||||||
|
// time to prevent it from happening at runtime later due to I/O.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&RunnerHub_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _RunnerHub_Connect_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
return srv.(RunnerHubServer).Connect(&grpc.GenericServerStream[StreamMessage, StreamMessage]{ServerStream: stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||||
|
type RunnerHub_ConnectServer = grpc.BidiStreamingServer[StreamMessage, StreamMessage]
|
||||||
|
|
||||||
|
// RunnerHub_ServiceDesc is the grpc.ServiceDesc for RunnerHub service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var RunnerHub_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "runner.RunnerHub",
|
||||||
|
HandlerType: (*RunnerHubServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{},
|
||||||
|
Streams: []grpc.StreamDesc{
|
||||||
|
{
|
||||||
|
StreamName: "Connect",
|
||||||
|
Handler: _RunnerHub_Connect_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
ClientStreams: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: "proto/runner/runner.proto",
|
||||||
|
}
|
||||||
291
test.html
Normal file
291
test.html
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<title>学生个人课表查询</title><script src=/sf-webproxy/api/vpn-config></script><script src=/sf-webproxy/resource/web_proxy.js?v=1.4.0></script><script>var postMessage=fakePostMessage;</script>
|
||||||
|
|
||||||
|
|
||||||
|
<link href="/resources/css/common/style.css" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="/resources/css/common/content.css" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="/resources/css/common/pageTag.css" rel="stylesheet" type="text/css" />
|
||||||
|
<script type="text/javascript" src="/resources/js/jquery/jquery-1.7.2.min.js"></script>
|
||||||
|
|
||||||
|
<script src="/resources/js/jquery/jquery.form.js" type="text/javascript"></script>
|
||||||
|
<script type="text/javascript" src="/resources/js/common/validator.js" ></script>
|
||||||
|
<script type="text/javascript" src="/resources/js/common/jqcheckBox.js"></script>
|
||||||
|
<script type="text/javascript" src="/resources/js/jquery/jquery.chained.remote.js"></script>
|
||||||
|
<script type="text/javascript" src="/resources/js/common/jqreload.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/resources/js/sweetalert2/sweetalert2.css">
|
||||||
|
<script src="/resources/js/sweetalert2/sweetalert2.min.js"></script>
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
.Contentbox { width:100%; min-height:100%;height:auto }
|
||||||
|
*html .Contentbox { height:100% }
|
||||||
|
</style>
|
||||||
|
<script type="text/javascript" src="/resources/js/jquery/jquery.form.js"></script>
|
||||||
|
<script type="text/javascript" src="/resources/js/common/modalpopup.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
jQuery().ready(function (){
|
||||||
|
|
||||||
|
|
||||||
|
$("a[id^='jcgl']").on("click",function( ){
|
||||||
|
var rwh=$(this).attr('id').split('_')[1];
|
||||||
|
|
||||||
|
document.queryform.action = "/jcgl/queryJcrwdyList_xs?pageRwh="+rwh;
|
||||||
|
document.queryform.submit();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//查询
|
||||||
|
function queryLike(){
|
||||||
|
document.queryform.action = "/kbcx/queryGrkb";
|
||||||
|
document.queryform.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
//导出
|
||||||
|
function exportExcel(){
|
||||||
|
document.queryform.action = "/kbcx/ExportGrKbxx";
|
||||||
|
document.queryform.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
//查询周课表
|
||||||
|
function queryZkb(){
|
||||||
|
document.queryform.action = "/kbcx/queryXszkb";
|
||||||
|
document.queryform.submit();
|
||||||
|
}
|
||||||
|
function kbqr(){
|
||||||
|
document.queryform.action = "/kbcx/kbqr";
|
||||||
|
$("#queryform").ajaxSubmit(function(result){
|
||||||
|
if(result==null || result =="" ){
|
||||||
|
alert("课表确认成功!");
|
||||||
|
document.queryform.action = "/kbcx/queryGrkb";
|
||||||
|
document.queryform.submit();
|
||||||
|
}else{
|
||||||
|
alert("课表确认失败!");
|
||||||
|
document.queryform.action = "/kbcx/queryGrkb";
|
||||||
|
document.queryform.submit();
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//弹出层
|
||||||
|
function wqrkcxx(){
|
||||||
|
var src = "/kbcx/queryWqrkbList?xnxq="+$('#xnxq').val();
|
||||||
|
jQuery("#iframename_js").attr("src",src);
|
||||||
|
BOX_show('NeworEdit1');
|
||||||
|
}
|
||||||
|
|
||||||
|
//查询
|
||||||
|
function queryLike1(){
|
||||||
|
document.queryform.action = "/kbcx/queryXsxkXq";
|
||||||
|
document.queryform.submit();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="Contentbox">
|
||||||
|
<div class="pd10">
|
||||||
|
<div class="address">当前位置:学生选课 >> <a href="javascript:void(0);" onclick="javascript:queryLike();return false;">个人课表查询</a></div>
|
||||||
|
<div class="clr"></div>
|
||||||
|
<div class="butsea" >
|
||||||
|
<form id="queryform" name="queryform" method="post">
|
||||||
|
|
||||||
|
<input type="hidden" name="fhlj" value="kbcx/queryGrkb"/>
|
||||||
|
<table border="0" align="left" cellpadding="0" cellspacing="0" >
|
||||||
|
<tr>
|
||||||
|
<td>学年学期:</td>
|
||||||
|
<td>
|
||||||
|
<select name="xnxq" id="xnxq" class="XNXQ_CON" onchange="javascript:queryLike();return false;">
|
||||||
|
|
||||||
|
<option value="2026-20271" >2026秋季</option>
|
||||||
|
|
||||||
|
<option value="2025-20263" >2026夏季</option>
|
||||||
|
|
||||||
|
<option value="2025-20262" selected>2026春季</option>
|
||||||
|
|
||||||
|
<option value="2025-20261" >2025秋季</option>
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="addlist_button1 ml15"><a href="javascript:void(0);" onclick="javascript:exportExcel();return false;"><span>导出课表</span></a></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
<td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div><span >课表确认时间:~</span></div>
|
||||||
|
<div class="clr"></div>
|
||||||
|
<div class="Menubox" >
|
||||||
|
<ul>
|
||||||
|
<li class="hover"><a href="javascript:void(0);" onclick="javascript:queryLike();return false;" >学生总课表</a></li>
|
||||||
|
<li><a href="javascript:void(0);" onclick="javascript:queryZkb();return false;" >学生周课表</a></li>
|
||||||
|
<li><a href="javascript:void(0);" onclick="javascript:queryLike1();return false;" >学生选课信息查询</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="clr"></div>
|
||||||
|
|
||||||
|
<div class="xfyq_area mt10">
|
||||||
|
<div class="xfyq_top" style="text-align:center"><span class="ml10 bold">2026春季学期(2025211232)宋长霖课表(2026-03-13 15:40:22)</span></div>
|
||||||
|
<div class="xfyq_con">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse" class="addlist_01">
|
||||||
|
<tr>
|
||||||
|
<th width="40" height="30" colspan="2"> </th>
|
||||||
|
<th>星期一</th>
|
||||||
|
<th>星期二</th>
|
||||||
|
<th>星期三</th>
|
||||||
|
<th>星期四</th>
|
||||||
|
<th>星期五</th>
|
||||||
|
<th>星期六</th>
|
||||||
|
<th>星期日</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr >
|
||||||
|
<td width="20">上午</td>
|
||||||
|
<td width="20">1-2</td>
|
||||||
|
<td width="118">中国近现代史纲要</br>苏克[1-16]周H楼-447</td>
|
||||||
|
<td width="118">体育(2)(高尔夫)</br>毕剑峰[1-16]周</td>
|
||||||
|
<td width="118">计算与智能程序设计</br>陈浩[9-12]周N楼-118</td>
|
||||||
|
<td width="118">形势与政策(1)</br>刘文颖[1-4]周H楼-463</br>中国近现代史纲要</br>苏克[9-12]周H楼-434</td>
|
||||||
|
<td width="118">计算与智能程序设计</br>陈浩[9-12]周N楼-118</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="122"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="bgcol">
|
||||||
|
<td width="20">上午</td>
|
||||||
|
<td width="20">3-4</td>
|
||||||
|
<td width="118">数学分析(2)</br>于战华[15]周,[2-14]周</br>M楼-303</td>
|
||||||
|
<td width="118">学术英语读写(2025版大一下)</br>苏敏[1-17]周N楼-255</td>
|
||||||
|
<td width="118">数学分析(2)</br>于战华[2-14]周M楼-303</td>
|
||||||
|
<td width="118">数学分析(2)</br>于战华[2-14]周M楼-303</td>
|
||||||
|
<td width="118">悦己人生</br>黄靖涵[1-8]周大活-北301</td>
|
||||||
|
<td width="118">学术英语读写(2025版大一下)</br>苏敏[9]周N楼-255</td>
|
||||||
|
<td width="122"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr >
|
||||||
|
<td width="20">下午</td>
|
||||||
|
<td width="20">5-6</td>
|
||||||
|
<td width="118">写作与沟通</br>侯薇[1-8]周N楼-208</td>
|
||||||
|
<td width="118">爱情与婚姻</br>师韵茗[1-8]周M楼-201</br>外汇交易原理与实务</br>王继东[9-16]周M楼-106</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="122"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="bgcol">
|
||||||
|
<td width="20">下午</td>
|
||||||
|
<td width="20">7-8</td>
|
||||||
|
<td width="118">大学物理X(1)</br>王小赛[1-8]周,[9-16]周</br>M楼-307</td>
|
||||||
|
<td width="118">电路与电子技术(1)</br>菅维乐[1-8]周,[9-12]周</br>M楼-205</td>
|
||||||
|
<td width="118">大学物理X(1)</br>王小赛[1-8]周,[9-16]周</br>M楼-307</td>
|
||||||
|
<td width="118">电路与电子技术(1)</br>菅维乐[1-8]周,[9-12]周</br>M楼-205</td>
|
||||||
|
<td width="118">大学物理X(1)</br>王小赛[1-8]周M楼-307</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="122"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr >
|
||||||
|
<td width="20">晚上</td>
|
||||||
|
<td width="20">9-10</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="122"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="bgcol">
|
||||||
|
<td width="20">晚上</td>
|
||||||
|
<td width="20">11-12</td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="118"> </td>
|
||||||
|
<td width="122"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<tr><td colspan="9">其它课程: 新型储能电源前沿与进展◇李旭东◇0◇2' 大学物理实验x(1)◇李盛凤◇1-16◇847290108334833812 APP群号'
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="clr"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="clr"></div>
|
||||||
|
<div style="color: red;font-size:16px">
|
||||||
|
注意:“个人课表查询”是按照您的选课进行显示,请仔细与“班级推荐课表查询”页面进行对比,如发现个人课表中缺少本班级的推荐课程(特别是必修课),则说明您可能存在漏选情况,请认真核实,如有需要请及时进行补选。
|
||||||
|
</div>
|
||||||
|
<div class="clr"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="NeworEdit1" style="display:none; width: 750px;border: 1px solid #2c4f72;-webkit-box-shadow: 0px 0px 5px #000;-moz-box-shadow: 0px 0px 5px #000;box-shadow: 0px 0px 5px #000; " >
|
||||||
|
<form name="addOrUpdfrom" id="addOrUpdfrom" method="post">
|
||||||
|
<div class="Floatleft Pct100 popup_bg">
|
||||||
|
<div class="popup_left">未确认课程</div>
|
||||||
|
<a href="#" class="btn_close" onclick="javascript:BOX_remove('NeworEdit1');return false;"></a> </div>
|
||||||
|
<div class="clr"></div>
|
||||||
|
<div class="popup">
|
||||||
|
<div class="clr"></div>
|
||||||
|
<iframe id="iframename_js" name="iframename_js" border="0" framespacing="0" marginheight="0" marginwidth="0" style="width: 730px; height: 410px;" frameborder="0" allowTransparency="true"></iframe>
|
||||||
|
<div class="clr"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="setting"> </div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Reference in New Issue
Block a user