Initial commit

This commit is contained in:
2026-03-13 20:43:44 +08:00
commit 17cdb7ad0d
19 changed files with 4744 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
converter
converter.exe

68
auth/login.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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">&nbsp;</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]周,[910]周</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]周,[910]周</br>N楼-335</td>
<td width="118">&nbsp</td>
<td width="122">&nbsp</td>
</tr>
<tr class="bgcol">
<td width="20">上午</td>
<td width="20">3-4</td>
<td width="118">泛函分析基础</br>蒋心蕊[11]周,[12]周,[2-8]周,[910]周</br>N楼-335</td>
<td width="118">&nbsp</td>
<td width="118">泛函分析基础</br>蒋心蕊[2-8]周N楼-335</br>体育6(定向运动)</br>张晓秋[9-16]周</td>
<td width="118">&nbsp</td>
<td width="118">微分几何</br>李鹏程[2-8]周N楼-335[9]周N楼-331</td>
<td width="118">&nbsp</td>
<td width="122">&nbsp</td>
</tr>
<tr >
<td width="20">下午</td>
<td width="20">5-6</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">数理统计</br>黄春茂[1011]周研究院-中405</td>
<td width="118">&nbsp</td>
<td width="122">&nbsp</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>黄春茂[1011]周,[12]周</br>研究院-中405</td>
<td width="118">数理统计</br>黄春茂[1-9]周N楼-116</td>
<td width="118">数理统计</br>黄春茂[1011]周,[12]周</br>研究院-中405</br>形势与政策3</br>田金花[5-8]周N楼-116</td>
<td width="118">数理统计</br>黄春茂[1-9]周N楼-116</td>
<td width="118">&nbsp</td>
<td width="122">&nbsp</td>
</tr>
<tr >
<td width="20">晚上</td>
<td width="20">9-10</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="122">&nbsp</td>
</tr>
<tr class="bgcol">
<td width="20">晚上</td>
<td width="20">11-12</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="122">&nbsp</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
View 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
View 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
View 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
View 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
View 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
View 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 == "&nbsp" {
continue
}
// 跳过&nbsp
if strings.TrimSpace(cellHTML) == "&nbsp" {
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-60=周一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
View 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, "大学物理X1", "王小赛", 1)
checkCourseHasWeek(t, courses, "大学物理X1", "王小赛", 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

File diff suppressed because it is too large Load Diff

224
proto/runner/runner.proto Normal file
View 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; // 年级
}

View 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
View 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">&nbsp;</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">&nbsp</td>
<td width="122">&nbsp</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">&nbsp</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">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="122">&nbsp</td>
</tr>
<tr class="bgcol">
<td width="20">下午</td>
<td width="20">7-8</td>
<td width="118">大学物理X1</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">大学物理X1</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">大学物理X1</br>王小赛[1-8]周M楼-307</td>
<td width="118">&nbsp</td>
<td width="122">&nbsp</td>
</tr>
<tr >
<td width="20">晚上</td>
<td width="20">9-10</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="122">&nbsp</td>
</tr>
<tr class="bgcol">
<td width="20">晚上</td>
<td width="20">11-12</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="118">&nbsp</td>
<td width="122">&nbsp</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>