├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── Doc
└── img
│ └── 香草蛋糕.jpg
├── LICENSE
├── README.md
├── client
├── client.go
├── req.go
├── resp.go
└── service.go
├── cmd
└── HDU-KillCourse
│ ├── getCourse.go
│ ├── killCourse.go
│ ├── login.go
│ ├── main.go
│ └── waitCourse.go
├── config.example.json
├── config
└── config.go
├── go.mod
├── go.sum
├── log
└── log.go
├── util
├── des.go
├── qrCode.go
├── randomString.go
├── rsa.go
└── smtpEmail.go
└── vars
├── const.go
└── portal.go
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v2
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: '1.23'
20 |
21 | - name: Build for Windows
22 | run: GOOS=windows GOARCH=amd64 go build -o build/main-windows-amd64-${{ github.ref_name }}.exe ./cmd/HDU-KillCourse
23 |
24 | - name: Build for Linux
25 | run: GOOS=linux GOARCH=amd64 go build -o build/main-linux-amd64-${{ github.ref_name }} ./cmd/HDU-KillCourse
26 |
27 | - name: Build for macOS (Intel)
28 | run: GOOS=darwin GOARCH=amd64 go build -o build/main-darwin-amd64-${{ github.ref_name }} ./cmd/HDU-KillCourse
29 |
30 | - name: Build for macOS (Apple Silicon)
31 | run: GOOS=darwin GOARCH=arm64 go build -o build/main-darwin-arm64-${{ github.ref_name }} ./cmd/HDU-KillCourse
32 |
33 | - name: Create Release
34 | id: create_release
35 | uses: actions/create-release@v1
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.PERSON_TOKEN }}
38 | with:
39 | tag_name: ${{ github.ref }}
40 | release_name: Release ${{ github.ref }}
41 | draft: false
42 | prerelease: false
43 |
44 | - name: Upload Release Asset Windows
45 | uses: actions/upload-release-asset@v1
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.PERSON_TOKEN }}
48 | with:
49 | upload_url: ${{ steps.create_release.outputs.upload_url }}
50 | asset_path: ./build/main-windows-amd64-${{ github.ref_name }}.exe
51 | asset_name: HDU-KillCourse-windows-amd64-${{ github.ref_name }}.exe
52 | asset_content_type: application/octet-stream
53 |
54 | - name: Upload Release Asset Linux
55 | uses: actions/upload-release-asset@v1
56 | env:
57 | GITHUB_TOKEN: ${{ secrets.PERSON_TOKEN }}
58 | with:
59 | upload_url: ${{ steps.create_release.outputs.upload_url }}
60 | asset_path: ./build/main-linux-amd64-${{ github.ref_name }}
61 | asset_name: HDU-KillCourse-linux-amd64-${{ github.ref_name }}
62 | asset_content_type: application/octet-stream
63 |
64 | - name: Upload Release Asset macOS (Intel)
65 | uses: actions/upload-release-asset@v1
66 | env:
67 | GITHUB_TOKEN: ${{ secrets.PERSON_TOKEN }}
68 | with:
69 | upload_url: ${{ steps.create_release.outputs.upload_url }}
70 | asset_path: ./build/main-darwin-amd64-${{ github.ref_name }}
71 | asset_name: HDU-KillCourse-darwin-amd64-${{ github.ref_name }}
72 | asset_content_type: application/octet-stream
73 |
74 | - name: Upload Release Asset macOS (Apple Silicon)
75 | uses: actions/upload-release-asset@v1
76 | env:
77 | GITHUB_TOKEN: ${{ secrets.PERSON_TOKEN }}
78 | with:
79 | upload_url: ${{ steps.create_release.outputs.upload_url }}
80 | asset_path: ./build/main-darwin-arm64-${{ github.ref_name }}
81 | asset_name: HDU-KillCourse-darwin-arm64-${{ github.ref_name }}
82 | asset_content_type: application/octet-stream
83 |
84 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 |
3 | # Python
4 | HDU-KillCourse_Python/
5 |
6 | # 配置产生文件忽略
7 | config.json
8 | config.example copy.json
9 | course.json
10 | ClientBodyConfig.json
11 |
12 | # 日志文件忽略
13 | log_files/
14 |
15 | # 测试文件忽略
16 | util/rsa_test.go
17 |
18 | # 二进制文件忽略
19 | HDU-KillCourse.exe
--------------------------------------------------------------------------------
/Doc/img/香草蛋糕.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cr4n5/HDU-KillCourse/de2b0e3b798b60513f1462cdc213e2caa68588fc/Doc/img/香草蛋糕.jpg
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
10 |
11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
12 |
13 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
14 |
15 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
16 |
17 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
18 |
19 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
20 |
21 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
22 |
23 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
24 |
25 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
26 |
27 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
28 |
29 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
30 |
31 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
32 |
33 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
34 |
35 | You must give any other recipients of the Work or Derivative Works a copy of this License; and
36 | You must cause any modified files to carry prominent notices stating that You changed the files; and
37 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
38 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
39 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
40 |
41 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
42 |
43 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
44 |
45 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
46 |
47 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
48 |
49 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
50 |
51 | END OF TERMS AND CONDITIONS
52 |
53 | Copyright 2024 crane
54 |
55 | Licensed under the Apache License, Version 2.0 (the "License");
56 | you may not use this file except in compliance with the License.
57 | You may obtain a copy of the License at
58 |
59 | http://www.apache.org/licenses/LICENSE-2.0
60 |
61 | Unless required by applicable law or agreed to in writing, software
62 | distributed under the License is distributed on an "AS IS" BASIS,
63 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
64 | See the License for the specific language governing permissions and
65 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HDU-KillCourse
2 |
3 | >本项目仅供学习和研究使用请于24小时内删除。使用本项目所产生的任何后果由使用者自行承担。在使用本项目之前,请确保您已充分了解相关法律法规,并确保您的行为符合所在国家或地区的法律要求。未经授权的情况下,请勿将本项目用于商业用途或其他非法用途。转载使用请标明出处。
4 |
5 | -
6 |
7 | `杭电 抢课×选课√`
8 |
9 | ## 简介
10 |
11 | - 支持主修,选修,体育课程,特殊课程
12 | - 支持蹲课
13 | - 最近更新时间:2025.3.1
14 |
15 | > [!TIP]
16 | >
17 | > If you are good at using it, you'll discover some pleasant surprises.
18 |
19 | ## 环境
20 |
21 | Go 1.23
22 |
23 | ## 使用
24 |
25 | 1. 下载编译文件
26 |
27 | - 在 [Releases](https://github.com/cr4n5/HDU-KillCourse/releases)中,下载对应系统的可执行文件。
28 |
29 | - Or
30 |
31 | ```shell
32 | go build
33 | ```
34 |
35 | 2. 修改配置
36 |
37 | - 下载 [config.example.json](./config.example.json) 文件。
38 | - 进入 [config.example.json](./config.example.json) 文件,修改对应内容。
39 | - 配置名更改为 config.json。
40 |
41 | ```
42 | {
43 | "cas_login": {
44 | "username": "2201xxxx",//杭电统一身份认证账号密码
45 | "password": "xxxxxxxx",
46 | "dingDingQrLoginEnabled": "0",//置1使用钉钉扫码登录 默认使用账号密码登录
47 | "level:" : "0" //优先级
48 | },
49 | "newjw_login": {
50 | "username": "2201xxxx",//正方教务系统账号密码
51 | "password": "xxxxxxxx",
52 | "level:" : "1" //优先级
53 | }, // 0<1 所以优先使用cas登录 所以0比1大 数学天才
54 | "cookies": { //若 JSESSIONID为空 或 route为空 或 enabled为0,则将不会使用cookies登录
55 | "JSESSIONID": "",// 每次登录cookie参数都会自动更新
56 | "route": "",
57 | "enabled": "1"//如若登录过期,将enabled改为0,将不会使用cookies登录
58 | },
59 | "time": {
60 | "XueNian": "2024",//所选课程所在的学年学期,如2024-2025-1
61 | "XueQi": "1"
62 | },
63 | //课程教学班名称,如(2024-2025-1)-C2092011-01
64 | "course" : {
65 | "(2024-2025-1)-C2092011-01" : "1",//1为选课,0为退课
66 | "(2024-2025-1)-T1300019-04" : "1",
67 | "(2024-2025-1)-T1300019-05" : "1",
68 | "(2024-2025-1)-B2700380-02" : "0",
69 | "(2024-2025-1)-C2892008-02" : "1",
70 | "(2024-2025-1)-W0001321-06" : "0"
71 | },
72 | "wait_course": {
73 | "interval": 60, //查询课程间隔时间,单位秒
74 | "enabled": "0" //是否开启蹲课,开启后将蹲course中值为1的课程,不再进行抢课
75 | },
76 | "smtp_email": { //邮件通知,开启后将会在蹲选课成功后发送邮件通知
77 | "host": "smtp.qq.com", //smtp服务器
78 | "username": "...@qq.com", //发送邮件的邮箱
79 | "password": "xxxxxxxx", //发送邮件的邮箱授权码
80 | "to": "...@qq.com", //接收邮件的邮箱
81 | "enabled": "0" //是否开启邮件通知
82 | },
83 | //课程按顺序执行
84 | "start_time": "2024-07-25 12:00:00",//程序开始时间
85 | }
86 | ```
87 |
88 | 3. 选课
89 |
90 | - 选课之前,可先去杭电课程导出,排好课表,获取课程教学班名称
91 |
92 | > [!NOTE]
93 | >
94 | > 需在任务落实查询开放后,并在选课之前(省去在选课时查询课程请求)执行一次可执行文件获取课程信息
95 |
96 | - 保证可执行文件和config.json在同一级目录下,然后在开始前几分钟执行可执行文件即可
97 |
98 | ## 协议
99 |
100 | [Apache License 2.0](./LICENSE)
101 |
--------------------------------------------------------------------------------
/client/client.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "net/http/cookiejar"
8 | "net/url"
9 | "strings"
10 |
11 | "github.com/cr4n5/HDU-KillCourse/config"
12 | "github.com/cr4n5/HDU-KillCourse/log"
13 | "github.com/cr4n5/HDU-KillCourse/vars"
14 | )
15 |
16 | type ClientBodyConfig struct {
17 | XkkzId map[string]string
18 | Ccdm string
19 | BhId string
20 | JgId string
21 | Xsbj string
22 | Xz string
23 | Mzm string
24 | Xslbdm string
25 | Xbm string
26 | ZyfxId string
27 | XqhId string
28 | }
29 |
30 | // Client 客户端结构体
31 | type Client struct {
32 | client *http.Client
33 | ClientBodyConfig *ClientBodyConfig
34 | }
35 |
36 | // NewClient 创建一个新的客户端
37 | func NewClient(cfg *config.Config) *Client {
38 | // 创建一个cookie jar
39 | jar, _ := cookiejar.New(nil)
40 | return &Client{
41 | client: &http.Client{
42 | Jar: jar,
43 | },
44 | }
45 | }
46 |
47 | func (c *Client) Get(url string, headers map[string]string) ([]byte, int, error) {
48 | req, err := http.NewRequest("GET", url, nil)
49 | if err != nil {
50 | return nil, 0, err
51 | }
52 |
53 | // 添加请求头
54 | for key, value := range headers {
55 | req.Header.Set(key, value)
56 | }
57 |
58 | resp, err := c.client.Do(req)
59 | if err != nil {
60 | return nil, 0, err
61 | }
62 | defer resp.Body.Close()
63 |
64 | result, err := io.ReadAll(resp.Body)
65 | if err != nil {
66 | return nil, 0, err
67 | }
68 |
69 | if !vars.NoDebugUrl[url] {
70 | // 保存请求响应日志
71 | log.Debug(fmt.Sprintf("Request URL: %s [GET]\nRequest Headers: %v\nResponseCode: %d\nResponse: %s",
72 | req.URL.String(),
73 | req.Header,
74 | resp.StatusCode,
75 | string(result)))
76 | }
77 |
78 | return result, resp.StatusCode, nil
79 | }
80 |
81 | func (c *Client) Post(url string, formData string, headers map[string]string) ([]byte, int, error) {
82 | req, err := http.NewRequest("POST", url, strings.NewReader(formData))
83 | if err != nil {
84 | return nil, 0, err
85 | }
86 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
87 |
88 | // 添加请求头
89 | for key, value := range headers {
90 | req.Header.Set(key, value)
91 | }
92 |
93 | resp, err := c.client.Do(req)
94 | if err != nil {
95 | return nil, 0, err
96 | }
97 | defer resp.Body.Close()
98 |
99 | result, err := io.ReadAll(resp.Body)
100 | if err != nil {
101 | return nil, 0, err
102 | }
103 |
104 | if !vars.NoDebugUrl[url] {
105 | // 保存请求响应日志
106 | log.Debug(fmt.Sprintf("Request URL: %s [POST]\nRequest Headers: %v\nRequest Body: %s\nResponseCode: %d\nResponse: %s",
107 | req.URL.String(),
108 | req.Header,
109 | formData,
110 | resp.StatusCode,
111 | string(result)))
112 | }
113 |
114 | return result, resp.StatusCode, nil
115 | }
116 |
117 | // SaveCookies 保存cookies
118 | func (c *Client) SaveCookies(cfg *config.Config) error {
119 | urlStr := "https://newjw.hdu.edu.cn/jwglxt"
120 | parsedURL, err := url.Parse(urlStr)
121 | if err != nil {
122 | return err
123 | }
124 | cookies := c.client.Jar.Cookies(parsedURL)
125 | for _, cookie := range cookies {
126 | if cookie.Name == "JSESSIONID" {
127 | cfg.Cookies.JSESSIONID = cookie.Value
128 | }
129 | if cookie.Name == "route" {
130 | cfg.Cookies.Route = cookie.Value
131 | }
132 | }
133 | // 保存配置文件
134 | err = config.SaveConfig(cfg)
135 | if err != nil {
136 | return err
137 | }
138 |
139 | return nil
140 | }
141 |
142 | // LoadCookies 加载cookies
143 | func (c *Client) LoadCookies(cfg *config.Config) error {
144 | urlStr := "https://newjw.hdu.edu.cn/jwglxt"
145 | parsedURL, err := url.Parse(urlStr)
146 | if err != nil {
147 | return err
148 | }
149 | cookies := []*http.Cookie{
150 | {
151 | Name: "JSESSIONID",
152 | Value: cfg.Cookies.JSESSIONID,
153 | },
154 | {
155 | Name: "route",
156 | Value: cfg.Cookies.Route,
157 | },
158 | }
159 | c.client.Jar.SetCookies(parsedURL, cookies)
160 |
161 | return nil
162 | }
163 |
--------------------------------------------------------------------------------
/client/req.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import "net/url"
4 |
5 | // LoginReq 登录请求
6 | type LoginReq struct {
7 | Csrftoken string
8 | Username string
9 | Password string
10 | }
11 |
12 | // ToFormData 转换为表单数据
13 | func (req *LoginReq) ToFormData() url.Values {
14 | return url.Values{
15 | "csrftoken": {req.Csrftoken},
16 | "yhm": {req.Username},
17 | "mm": {req.Password},
18 | }
19 | }
20 |
21 | // CasLoginReq cas登录请求
22 | type CasLoginReq struct {
23 | Username string
24 | Type string
25 | EventID string
26 | Geolocation string
27 | Execution string
28 | CaptchaCode string
29 | Croypto string
30 | Password string
31 | }
32 |
33 | // ToFormData 转换为表单数据
34 | func (req *CasLoginReq) ToFormData() url.Values {
35 | return url.Values{
36 | "username": {req.Username},
37 | "type": {req.Type},
38 | "_eventId": {req.EventID},
39 | "geolocation": {req.Geolocation},
40 | "execution": {req.Execution},
41 | "captcha_code": {req.CaptchaCode},
42 | "croypto": {req.Croypto},
43 | "password": {req.Password},
44 | }
45 | }
46 |
47 | // GetCourseReq 获取课程请求
48 | type GetCourseReq struct {
49 | Kkbm string
50 | Kch string
51 | Kcfzr string
52 | Xsxy string
53 | ZyhID string
54 | BhID string
55 | ZyfxID string
56 | NjdmID string
57 | Xsdm string
58 | Jxdd string
59 | Kklxdm string
60 | XqhID string
61 | Xkbj string
62 | Kkzt string
63 | Kclbdm string
64 | Kcgsdm string
65 | Kcxzdm string
66 | Apksfsdm string
67 | Ksfsdm string
68 | Khfsdm string
69 | Cxfs string
70 | Jsssbm string
71 | Zcm string
72 | Xbdm string
73 | CdlbID string
74 | CdejlbID string
75 | Jxbmc string
76 | Sfzjxb string
77 | Sfhbbj string
78 | Zymc string
79 | Xnmc string
80 | Xqmc string
81 | Kkxymc string
82 | Jgmc string
83 | Njmc string
84 | Sfpk string
85 | Sfwp string
86 | Ywtk string
87 | Skfs string
88 | Dylx string
89 | Jzglbm string
90 | Jxms string
91 | Skpt string
92 | Sfhxkc string
93 | Sfxwkc string
94 | Sknr string
95 | Bz string
96 | Xkbz string
97 | Sfzj string
98 | Qsz string
99 | ZykfkcbjCx string
100 | SfgssxbkCx string
101 | Zzz string
102 | Xf string
103 | JysID string
104 | Xnm string
105 | Xqm string
106 | Js string
107 | Kclxdm string
108 | Search string
109 | Nd string
110 | ShowCount string
111 | CurrentPage string
112 | QsortName string
113 | SortOrder string
114 | Time string
115 | }
116 |
117 | // ToFormData 转换为表单数据
118 | func (req *GetCourseReq) ToFormData() url.Values {
119 | return url.Values{
120 | "kkbm": {req.Kkbm},
121 | "kch": {req.Kch},
122 | "kcfzr": {req.Kcfzr},
123 | "xsxy": {req.Xsxy},
124 | "zyh_id": {req.ZyhID},
125 | "bh_id": {req.BhID},
126 | "zyfx_id": {req.ZyfxID},
127 | "njdm_id": {req.NjdmID},
128 | "xsdm": {req.Xsdm},
129 | "jxdd": {req.Jxdd},
130 | "kklxdm": {req.Kklxdm},
131 | "xqh_id": {req.XqhID},
132 | "xkbj": {req.Xkbj},
133 | "kkzt": {req.Kkzt},
134 | "kclbdm": {req.Kclbdm},
135 | "kcgsdm": {req.Kcgsdm},
136 | "kcxzdm": {req.Kcxzdm},
137 | "apksfsdm": {req.Apksfsdm},
138 | "ksfsdm": {req.Ksfsdm},
139 | "khfsdm": {req.Khfsdm},
140 | "cxfs": {req.Cxfs},
141 | "jsssbm": {req.Jsssbm},
142 | "zcm": {req.Zcm},
143 | "xbdm": {req.Xbdm},
144 | "cdlb_id": {req.CdlbID},
145 | "cdejlb_id": {req.CdejlbID},
146 | "jxbmc": {req.Jxbmc},
147 | "sfzjxb": {req.Sfzjxb},
148 | "sfhbbj": {req.Sfhbbj},
149 | "zymc": {req.Zymc},
150 | "xnmc": {req.Xnmc},
151 | "xqmc": {req.Xqmc},
152 | "kkxymc": {req.Kkxymc},
153 | "jgmc": {req.Jgmc},
154 | "njmc": {req.Njmc},
155 | "sfpk": {req.Sfpk},
156 | "sfwp": {req.Sfwp},
157 | "ywtk": {req.Ywtk},
158 | "skfs": {req.Skfs},
159 | "dylx": {req.Dylx},
160 | "jzglbm": {req.Jzglbm},
161 | "jxms": {req.Jxms},
162 | "skpt": {req.Skpt},
163 | "sfhxkc": {req.Sfhxkc},
164 | "sfxwkc": {req.Sfxwkc},
165 | "sknr": {req.Sknr},
166 | "bz": {req.Bz},
167 | "xkbz": {req.Xkbz},
168 | "sfzj": {req.Sfzj},
169 | "qsz": {req.Qsz},
170 | "zykfkcbj_cx": {req.ZykfkcbjCx},
171 | "sfgssxbk_cx": {req.SfgssxbkCx},
172 | "zzz": {req.Zzz},
173 | "xf": {req.Xf},
174 | "jys_id": {req.JysID},
175 | "xnm": {req.Xnm},
176 | "xqm": {req.Xqm},
177 | "js": {req.Js},
178 | "kclxdm": {req.Kclxdm},
179 | "_search": {req.Search},
180 | "nd": {req.Nd},
181 | "queryModel.showCount": {req.ShowCount},
182 | "queryModel.currentPage": {req.CurrentPage},
183 | "queryModel.sortName": {req.QsortName},
184 | "queryModel.sortOrder": {req.SortOrder},
185 | "time": {req.Time},
186 | }
187 | }
188 |
189 | // GetDoJxbIdReq 获取do_jxb_id请求
190 | type GetDoJxbIdReq struct {
191 | BklxID string
192 | NjdmID string
193 | Xkxnm string
194 | Xkxqm string
195 | Kklxdm string
196 | KchID string
197 | XkkzID string
198 | Xsbj string
199 | Ccdm string
200 | Xz string
201 | Mzm string
202 | Xslbdm string
203 | Xbm string
204 | BhID string
205 | ZyfxID string
206 | JgID string
207 | XqhID string
208 | }
209 |
210 | // ToFormData 转换为表单数据
211 | func (req *GetDoJxbIdReq) ToFormData() url.Values {
212 | return url.Values{
213 | "bklx_id": {req.BklxID},
214 | "njdm_id": {req.NjdmID},
215 | "xkxnm": {req.Xkxnm},
216 | "xkxqm": {req.Xkxqm},
217 | "kklxdm": {req.Kklxdm},
218 | "kch_id": {req.KchID},
219 | "xkkz_id": {req.XkkzID},
220 | "xsbj": {req.Xsbj},
221 | "ccdm": {req.Ccdm},
222 | "xz": {req.Xz},
223 | "mzm": {req.Mzm},
224 | "xslbdm": {req.Xslbdm},
225 | "xbm": {req.Xbm},
226 | "bh_id": {req.BhID},
227 | "zyfx_id": {req.ZyfxID},
228 | "jg_id": {req.JgID},
229 | "xqh_id": {req.XqhID},
230 | }
231 | }
232 |
233 | // SelectCourseReq 选课请求
234 | type SelectCourseReq struct {
235 | JxbIDs string
236 | KchID string
237 | Qz string
238 | NjdmID string
239 | ZyhID string
240 | }
241 |
242 | // ToFormData 转换为表单数据
243 | func (req *SelectCourseReq) ToFormData() url.Values {
244 | return url.Values{
245 | "jxb_ids": {req.JxbIDs},
246 | "kch_id": {req.KchID},
247 | "qz": {req.Qz},
248 | "njdm_id": {req.NjdmID},
249 | "zyh_id": {req.ZyhID},
250 | }
251 | }
252 |
253 | // CancelCourseReq 退课请求
254 | type CancelCourseReq struct {
255 | JxbIDs string
256 | KchID string
257 | Xkxnm string
258 | Xkxqm string
259 | }
260 |
261 | // ToFormData 转换为表单数据
262 | func (req *CancelCourseReq) ToFormData() url.Values {
263 | return url.Values{
264 | "jxb_ids": {req.JxbIDs},
265 | "kch_id": {req.KchID},
266 | "xkxnm": {req.Xkxnm},
267 | "xkxqm": {req.Xkxqm},
268 | }
269 | }
270 |
271 | // SearchCourseReq 搜索课程请求
272 | type SearchCourseReq struct {
273 | Xkxnm string
274 | Xkxqm string
275 | Kklxdm string
276 | Jspage string
277 | Kspage string
278 | Yllist string // 是否有余量
279 | Filterlist string // 搜索内容
280 | }
281 |
282 | // ToFormData 转换为表单数据
283 | func (req *SearchCourseReq) ToFormData() url.Values {
284 | return url.Values{
285 | "xkxnm": {req.Xkxnm},
286 | "xkxqm": {req.Xkxqm},
287 | "kklxdm": {req.Kklxdm},
288 | "jspage": {req.Jspage},
289 | "kspage": {req.Kspage},
290 | "yl_list[0]": {req.Yllist},
291 | "filter_list[0]": {req.Filterlist},
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/client/resp.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | type GetPublicKeyResp struct {
4 | Modules string `json:"modulus"`
5 | Exponent string `json:"exponent"`
6 | }
7 |
8 | type GetCourseResp struct {
9 | Items []struct {
10 | Jxbmc string `json:"jxbmc"`
11 | KchID string `json:"kch_id"`
12 | JxbID string `json:"jxb_id"`
13 | Jxbzc string `json:"jxbzc"`
14 | Kklxmc string `json:"kklxmc"`
15 | Kcmc string `json:"kcmc"` // 课程名称
16 | Sksj string `json:"sksj"` // 上课时间
17 | } `json:"items"`
18 | }
19 |
20 | type GetDoJxbIdResp struct {
21 | JxbID string `json:"jxb_id"`
22 | DoJxbID string `json:"do_jxb_id"`
23 | }
24 |
25 | type SelectCourseResq struct {
26 | Flag string `json:"flag"`
27 | Msg string `json:"msg"`
28 | }
29 |
30 | type SearchCourseResp struct {
31 | TmpList []struct {
32 | Jxbmc string `json:"jxbmc"`
33 | } `json:"tmpList"`
34 | }
35 |
36 | type QrLoginIdResp struct {
37 | Code int `json:"code"`
38 | Message string `json:"message"`
39 | Data string `json:"data"`
40 | }
41 |
42 | type QrLoginStatusResp struct {
43 | Code int `json:"code"`
44 | Message string `json:"message"`
45 | Data string `json:"data"`
46 | }
47 |
--------------------------------------------------------------------------------
/client/service.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "regexp"
8 | "strings"
9 | "time"
10 |
11 | "github.com/antchfx/htmlquery"
12 | "github.com/cr4n5/HDU-KillCourse/util"
13 | "golang.org/x/net/html"
14 | )
15 |
16 | // GetCsrftoken 获取csrftoken
17 | func (c *Client) GetCsrftoken() (string, error) {
18 | login_url := "https://newjw.hdu.edu.cn/jwglxt/xtgl/login_slogin.html"
19 |
20 | // 获取csrftoken
21 | result, _, err := c.Get(login_url, nil)
22 | if err != nil {
23 | return "", err
24 | }
25 | // 解析csrftoken
26 | doc, err := htmlquery.Parse(strings.NewReader(string(result)))
27 | if err != nil {
28 | return "", err
29 | }
30 | node := htmlquery.FindOne(doc, `//input[@name="csrftoken"]/@value`)
31 | var csrftoken string
32 | if node != nil {
33 | csrftoken = htmlquery.InnerText(node)
34 | } else {
35 | return "", errors.New("获取csrftoken失败")
36 | }
37 |
38 | return csrftoken, nil
39 | }
40 |
41 | // GetCasLoginConfig 获取cas登录配置
42 | func (c *Client) GetCasLoginConfig() (execution string, croypto string, err error) {
43 | result, _, err := c.Get("https://sso.hdu.edu.cn/login", nil)
44 | if err != nil {
45 | return "", "", err
46 | }
47 |
48 | // 解析cas登录配置
49 | doc, err := htmlquery.Parse(strings.NewReader(string(result)))
50 | if err != nil {
51 | return "", "", err
52 | }
53 | node := htmlquery.FindOne(doc, `//*[@id="login-page-flowkey"]/text()`)
54 | if node != nil {
55 | execution = htmlquery.InnerText(node)
56 | } else {
57 | return "", "", errors.New("获取cas登录配置失败")
58 | }
59 | node = htmlquery.FindOne(doc, `//*[@id="login-croypto"]/text()`)
60 | if node != nil {
61 | croypto = htmlquery.InnerText(node)
62 | } else {
63 | return "", "", errors.New("获取cas登录配置失败")
64 | }
65 | if execution == "" || croypto == "" {
66 | return "", "", errors.New("获取cas登录配置失败")
67 | }
68 |
69 | return execution, croypto, nil
70 | }
71 |
72 | // GetQrLoginId 获取二维码登录ID
73 | func (c *Client) GetQrLoginId() (*QrLoginIdResp, error) {
74 | url := "https://sso.hdu.edu.cn/api/protected/qrlogin/loginid"
75 | // 设置请求头
76 | CsrfKey := util.GenerateRandomString(32)
77 | CsrfValue := util.GenerateCsrfValue(CsrfKey)
78 | headers := map[string]string{
79 | "Csrf-Key": CsrfKey,
80 | "Csrf-Value": CsrfValue,
81 | }
82 | // 获取二维码登录ID
83 | result, _, err := c.Get(url, headers)
84 | if err != nil {
85 | return nil, err
86 | }
87 | // 解析二维码登录ID
88 | var qrLoginIdResp QrLoginIdResp
89 | err = json.Unmarshal(result, &qrLoginIdResp)
90 | if err != nil {
91 | return nil, err
92 | }
93 |
94 | return &qrLoginIdResp, nil
95 | }
96 |
97 | // GetLoginQr 获取二维码
98 | func (c *Client) GetQrCode(id string) ([]byte, error) {
99 | url := fmt.Sprintf("https://sso.hdu.edu.cn/api/public/qrlogin/qrgen/%s/dingDingQr", id)
100 | // 获取二维码
101 | result, _, err := c.Get(url, nil)
102 | if err != nil {
103 | return nil, err
104 | }
105 |
106 | return result, nil
107 | }
108 |
109 | // GetQrLoginStatus 获取二维码登录状态
110 | func (c *Client) GetQrLoginStatus(id string) (*QrLoginStatusResp, error) {
111 | url := fmt.Sprintf("https://sso.hdu.edu.cn/api/protected/qrlogin/scan/%s", id)
112 | // 设置请求头
113 | CsrfKey := util.GenerateRandomString(32)
114 | CsrfValue := util.GenerateCsrfValue(CsrfKey)
115 | headers := map[string]string{
116 | "Csrf-Key": CsrfKey,
117 | "Csrf-Value": CsrfValue,
118 | }
119 | // 获取二维码登录状态
120 | result, _, err := c.Get(url, headers)
121 | if err != nil {
122 | return nil, err
123 | }
124 | // 解析二维码登录状态
125 | var qrLoginStatusResp QrLoginStatusResp
126 | err = json.Unmarshal(result, &qrLoginStatusResp)
127 | if err != nil {
128 | return nil, err
129 | }
130 |
131 | return &qrLoginStatusResp, nil
132 | }
133 |
134 | // CasLoginPost cas登录请求
135 | func (c *Client) CasLoginPost(req *CasLoginReq) (string, error) {
136 | login_url := "https://sso.hdu.edu.cn/login"
137 | // 登录
138 | formData := req.ToFormData()
139 | result, _, err := c.Post(login_url, formData.Encode(), nil)
140 | if err != nil {
141 | return "", err
142 | }
143 |
144 | return string(result), nil
145 | }
146 |
147 | // CasLoginNewjw cas登录newjw
148 | func (c *Client) CasLoginNewjw() (string, error) {
149 | new_jw := "https://newjw.hdu.edu.cn/sso/driot4login"
150 | // 通过cas登录newjw
151 | result, _, err := c.Get(new_jw, nil)
152 | if err != nil {
153 | return "", err
154 | }
155 |
156 | return string(result), nil
157 | }
158 |
159 | // GetPublicKey 获取公钥
160 | func (c *Client) GetPublicKey() (*GetPublicKeyResp, error) {
161 | result, _, err := c.Get(fmt.Sprintf("https://newjw.hdu.edu.cn/jwglxt/xtgl/login_getPublicKey.html?time=%d", time.Now().Unix()), nil)
162 | if err != nil {
163 | return nil, err
164 | }
165 | // 解析公钥
166 | var PublicKeyResp GetPublicKeyResp
167 | err = json.Unmarshal(result, &PublicKeyResp)
168 | if err != nil {
169 | return nil, err
170 | }
171 |
172 | return &PublicKeyResp, nil
173 | }
174 |
175 | // NewjwLoginPost Newjw登录请求
176 | func (c *Client) NewjwLoginPost(req *LoginReq) (string, error) {
177 | login_url := "https://newjw.hdu.edu.cn/jwglxt/xtgl/login_slogin.html"
178 | // 登录
179 | formData := req.ToFormData()
180 | result, _, err := c.Post(login_url, formData.Encode(), nil)
181 | if err != nil {
182 | return "", err
183 | }
184 |
185 | return string(result), nil
186 | }
187 |
188 | // GetCourse 获取课程
189 | func (c *Client) GetCourse(req *GetCourseReq) (*GetCourseResp, error) {
190 | course_url := "https://newjw.hdu.edu.cn/jwglxt/rwlscx/rwlscx_cxRwlsIndex.html?doType=query&gnmkdm=N1548"
191 | // 获取课程
192 | formData := req.ToFormData()
193 | result, _, err := c.Post(course_url, formData.Encode(), nil)
194 | if err != nil {
195 | return nil, err
196 | }
197 | if strings.Contains(string(result), "统一身份认证") {
198 | return nil, errors.New("可能登录过期")
199 | }
200 | // 检验是否可以获取课程
201 | if strings.Contains(string(result), "无功能权限") {
202 | return nil, errors.New("任务落实查询并未开放")
203 | }
204 | // 解析课程
205 | var courseResp GetCourseResp
206 | err = json.Unmarshal(result, &courseResp)
207 | if err != nil {
208 | return nil, err
209 | }
210 |
211 | return &courseResp, nil
212 | }
213 |
214 | // GetClientBodyConfig 获取选课配置
215 | func (c *Client) GetClientBodyConfig() error {
216 | // 解析选课配置函数
217 | AnalysisConfig := func(doc *html.Node) error {
218 | // 解析XkkzId
219 | AnalysisXkkzId := func(node []*html.Node) error {
220 | pattern := regexp.MustCompile(`queryCourse\(this,'(\d+)'`)
221 | pattern1 := regexp.MustCompile(`queryCourse\(this,'(?:[^']*)','(\w+)'`)
222 | for _, n := range node {
223 | match := pattern.FindStringSubmatch(htmlquery.InnerText(n))
224 | match1 := pattern1.FindStringSubmatch(htmlquery.InnerText(n))
225 | if match == nil || match1 == nil {
226 | return errors.New("XkkzId解析失败") // 待测试
227 | }
228 | c.ClientBodyConfig.XkkzId[match[1]] = match1[1]
229 | }
230 | return nil
231 | }
232 |
233 | c.ClientBodyConfig = &ClientBodyConfig{
234 | XkkzId: make(map[string]string),
235 | }
236 | // 如果未找到对应的xpath,报错
237 | if node := htmlquery.FindOne(doc, `//input[@name="ccdm"]/@value`); node != nil {
238 | c.ClientBodyConfig.Ccdm = htmlquery.InnerText(node)
239 | } else {
240 | return errors.New("ccdm获取失败")
241 | }
242 | if node := htmlquery.FindOne(doc, `//input[@name="bh_id"]/@value`); node != nil {
243 | c.ClientBodyConfig.BhId = htmlquery.InnerText(node)
244 | } else {
245 | return errors.New("bh_id获取失败")
246 | }
247 | if node := htmlquery.FindOne(doc, `//input[@name="jg_id_1"]/@value`); node != nil {
248 | c.ClientBodyConfig.JgId = htmlquery.InnerText(node)
249 | } else {
250 | return errors.New("jg_id获取失败")
251 | }
252 | if node := htmlquery.FindOne(doc, `//input[@name="xsbj"]/@value`); node != nil {
253 | c.ClientBodyConfig.Xsbj = htmlquery.InnerText(node)
254 | } else {
255 | return errors.New("xsbj获取失败")
256 | }
257 | if node := htmlquery.FindOne(doc, `//input[@name="xz"]/@value`); node != nil {
258 | c.ClientBodyConfig.Xz = htmlquery.InnerText(node)
259 | } else {
260 | return errors.New("xz获取失败")
261 | }
262 | if node := htmlquery.FindOne(doc, `//input[@name="mzm"]/@value`); node != nil {
263 | c.ClientBodyConfig.Mzm = htmlquery.InnerText(node)
264 | } else {
265 | return errors.New("mzm获取失败")
266 | }
267 | if node := htmlquery.FindOne(doc, `//input[@name="xslbdm"]/@value`); node != nil {
268 | c.ClientBodyConfig.Xslbdm = htmlquery.InnerText(node)
269 | } else {
270 | return errors.New("xslbdm获取失败")
271 | }
272 | if node := htmlquery.FindOne(doc, `//input[@name="xbm"]/@value`); node != nil {
273 | c.ClientBodyConfig.Xbm = htmlquery.InnerText(node)
274 | } else {
275 | return errors.New("xbm获取失败")
276 | }
277 | if node := htmlquery.FindOne(doc, `//input[@name="zyfx_id"]/@value`); node != nil {
278 | c.ClientBodyConfig.ZyfxId = htmlquery.InnerText(node)
279 | } else {
280 | return errors.New("zyfx_id获取失败")
281 | }
282 | if node := htmlquery.FindOne(doc, `//input[@name="xqh_id"]/@value`); node != nil {
283 | c.ClientBodyConfig.XqhId = htmlquery.InnerText(node)
284 | } else {
285 | return errors.New("xqh_id获取失败")
286 | }
287 |
288 | // 获取选课控制ID
289 | if node := htmlquery.Find(doc, `//a[@role="tab"]/@onclick`); node != nil {
290 | err := AnalysisXkkzId(node)
291 | if err != nil {
292 | return err
293 | }
294 | } else {
295 | return errors.New("XkkzID获取失败")
296 | }
297 |
298 | return nil
299 | }
300 |
301 | url := "https://newjw.hdu.edu.cn/jwglxt/xsxk/zzxkyzb_cxZzxkYzbIndex.html?gnmkdm=N253512&layout=default"
302 | // 获取选课配置
303 | result, _, err := c.Get(url, nil)
304 | if err != nil {
305 | return err
306 | }
307 | if strings.Contains(string(result), "统一身份认证") {
308 | return errors.New("可能登录过期")
309 | }
310 | // 检验是否可以获取选课配置
311 | if strings.Contains(string(result), "对不起,当前不属于选课阶段") {
312 | return errors.New("当前不属于选课阶段")
313 | }
314 | // 解析选课配置
315 | doc, err := htmlquery.Parse(strings.NewReader(string(result)))
316 | if err != nil {
317 | return err
318 | }
319 | err = AnalysisConfig(doc)
320 | if err != nil {
321 | return err
322 | }
323 |
324 | return nil
325 | }
326 |
327 | // GetDoJxbId 获取do_jxb_id
328 | func (c *Client) GetDoJxbId(req *GetDoJxbIdReq) ([]GetDoJxbIdResp, error) {
329 | url := "https://newjw.hdu.edu.cn/jwglxt/xsxk/zzxkyzbjk_cxJxbWithKchZzxkYzb.html?gnmkdm=N253512"
330 | // 获取do_jxb_id
331 | formData := req.ToFormData()
332 | result, _, err := c.Post(url, formData.Encode(), nil)
333 | if err != nil {
334 | return nil, err
335 | }
336 | if strings.Contains(string(result), "统一身份认证") {
337 | return nil, errors.New("可能登录过期")
338 | }
339 | // 解析do_jxb_id
340 | var doJxbIdResp []GetDoJxbIdResp
341 | err = json.Unmarshal(result, &doJxbIdResp)
342 | if err != nil {
343 | return nil, err
344 | }
345 |
346 | return doJxbIdResp, nil
347 | }
348 |
349 | // SelectCourse 选课
350 | func (c *Client) SelectCourse(req *SelectCourseReq) (*SelectCourseResq, error) {
351 | url := "https://newjw.hdu.edu.cn/jwglxt/xsxk/zzxkyzbjk_xkBcZyZzxkYzb.html?gnmkdm=N253512"
352 | // 选课
353 | formData := req.ToFormData()
354 | result, _, err := c.Post(url, formData.Encode(), nil)
355 | if err != nil {
356 | return nil, err
357 | }
358 | if strings.Contains(string(result), "统一身份认证") {
359 | return nil, errors.New("可能登录过期")
360 | }
361 | // 解析选课结果
362 | var selectCourseResq SelectCourseResq
363 | err = json.Unmarshal(result, &selectCourseResq)
364 | if err != nil {
365 | return nil, err
366 | }
367 |
368 | return &selectCourseResq, nil
369 | }
370 |
371 | // CancelCourse 退课
372 | func (c *Client) CancelCourse(req *CancelCourseReq) (string, error) {
373 | url := "https://newjw.hdu.edu.cn/jwglxt/xsxk/zzxkyzb_tuikBcZzxkYzb.html?gnmkdm=N253512"
374 | // 退课
375 | formData := req.ToFormData()
376 | result, _, err := c.Post(url, formData.Encode(), nil)
377 | if err != nil {
378 | return "", err
379 | }
380 | if strings.Contains(string(result), "统一身份认证") {
381 | return "", errors.New("可能登录过期")
382 | }
383 |
384 | return string(result), nil
385 | }
386 |
387 | // SearchCourse 搜索课程
388 | func (c *Client) SearchCourse(req *SearchCourseReq) (*SearchCourseResp, error) {
389 | url := "https://newjw.hdu.edu.cn/jwglxt/xsxk/zzxkyzb_cxZzxkYzbPartDisplay.html?gnmkdm=N253512"
390 | // 搜索课程
391 | formData := req.ToFormData()
392 | result, _, err := c.Post(url, formData.Encode(), nil)
393 | if err != nil {
394 | return nil, err
395 | }
396 | if strings.Contains(string(result), "统一身份认证") {
397 | return nil, errors.New("可能登录过期")
398 | }
399 | // 解析搜索结果
400 | var searchCourseResp SearchCourseResp
401 | err = json.Unmarshal(result, &searchCourseResp)
402 | if err != nil {
403 | return nil, err
404 | }
405 |
406 | return &searchCourseResp, nil
407 | }
408 |
--------------------------------------------------------------------------------
/cmd/HDU-KillCourse/getCourse.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/cr4n5/HDU-KillCourse/client"
7 | "github.com/cr4n5/HDU-KillCourse/config"
8 | "github.com/cr4n5/HDU-KillCourse/log"
9 | "strconv"
10 | "time"
11 | )
12 |
13 | func GetCourse(c *client.Client, cfg *config.Config) (*config.Course, error) {
14 | // 先从本地course.json读取课程信息
15 | courses, err := config.ReadCourse()
16 | if err == nil {
17 | return courses, nil
18 | }
19 |
20 | // 本地课程信息读取失败,从服务器获取课程信息
21 | log.Error("本地课程信息读取失败,正在从服务器获取课程信息...")
22 | log.Info("Notice!: 等待时间可能较长,请耐心等待...")
23 | // 初始化请求
24 | XueNian := cfg.Time.XueNian
25 | intXueNian, err := strconv.Atoi(XueNian)
26 | if err != nil {
27 | return nil, errors.New("学年格式错误")
28 | }
29 | xnmc := fmt.Sprintf("%s-%s", XueNian, strconv.Itoa(intXueNian+1))
30 | xqmc := cfg.Time.XueQi
31 | var xqm string
32 | if xqmc == "1" {
33 | xqm = "3"
34 | } else if xqmc == "2" {
35 | xqm = "12"
36 | } else {
37 | return nil, errors.New("学期格式错误")
38 | }
39 | req := &client.GetCourseReq{
40 | Cxfs: "1",
41 | Zymc: "全部",
42 | Xnmc: xnmc,
43 | Xqmc: xqmc,
44 | Kkxymc: "全部",
45 | Jgmc: "全部",
46 | Ywtk: "0",
47 | Skfs: "0",
48 | Xnm: XueNian,
49 | Xqm: xqm,
50 | Search: "false",
51 | Nd: fmt.Sprintf("%d", time.Now().Unix()),
52 | ShowCount: "9999",
53 | CurrentPage: "1",
54 | SortOrder: "asc",
55 | Time: "0",
56 | }
57 |
58 | // 获取课程信息
59 | courseResp, err := c.GetCourse(req)
60 | if err != nil {
61 | return nil, err
62 | }
63 | // 转换为config.Course
64 | courses = &config.Course{
65 | Items: courseResp.Items,
66 | }
67 |
68 | // 保存课程信息到本地
69 | err = config.SaveCourse(courses)
70 | if err != nil {
71 | return nil, err
72 | }
73 |
74 | return courses, nil
75 | }
76 |
--------------------------------------------------------------------------------
/cmd/HDU-KillCourse/killCourse.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "os"
8 | "time"
9 |
10 | "github.com/cr4n5/HDU-KillCourse/client"
11 | "github.com/cr4n5/HDU-KillCourse/config"
12 | "github.com/cr4n5/HDU-KillCourse/log"
13 | )
14 |
15 | // GetDoJxbId 获取doJxbId
16 | func GetDoJxbId(c *client.Client, KchId string, JxbId string, Kklxdm string, NjdmId string, XueNian string, Xqm string) (string, error) {
17 | // 检查c.ClientBodyConfig是否为nil,测试用
18 | if c.ClientBodyConfig == nil {
19 | return "", errors.New("ClientBodyConfig未初始化")
20 | }
21 |
22 | // 设置请求参数
23 | req := &client.GetDoJxbIdReq{
24 | BklxID: "0",
25 | NjdmID: NjdmId,
26 | Xkxnm: XueNian,
27 | Xkxqm: Xqm,
28 | Kklxdm: Kklxdm,
29 | KchID: KchId,
30 | XkkzID: c.ClientBodyConfig.XkkzId[Kklxdm],
31 | Xsbj: c.ClientBodyConfig.Xsbj,
32 | Ccdm: c.ClientBodyConfig.Ccdm,
33 | Xz: c.ClientBodyConfig.Xz,
34 | Mzm: c.ClientBodyConfig.Mzm,
35 | Xslbdm: c.ClientBodyConfig.Xslbdm,
36 | Xbm: c.ClientBodyConfig.Xbm,
37 | BhID: c.ClientBodyConfig.BhId,
38 | ZyfxID: c.ClientBodyConfig.ZyfxId,
39 | JgID: c.ClientBodyConfig.JgId,
40 | XqhID: c.ClientBodyConfig.XqhId,
41 | }
42 |
43 | // 发送请求
44 | resp, err := c.GetDoJxbId(req)
45 | if err != nil {
46 | return "", err
47 | }
48 |
49 | // 解析doJxbId
50 | for _, v := range resp {
51 | if v.JxbID == JxbId {
52 | return v.DoJxbID, nil
53 | }
54 | }
55 |
56 | return "", errors.New("doJxbId不存在")
57 |
58 | }
59 |
60 | // SelectCourse 选课
61 | func SelectCourse(c *client.Client, JxbIds string, KchId string, Kklxdm string, Jxbzc string, cfg *config.Config) error {
62 | // 设置请求参数
63 | req := &client.SelectCourseReq{
64 | JxbIDs: JxbIds,
65 | KchID: KchId,
66 | Qz: "0",
67 | }
68 |
69 | // 若为主修课程
70 | if Kklxdm == "01" {
71 | if cfg.DontTouchForDebug == "1" {
72 | // req.NjdmID = "20" + Jxbzc[0:2]
73 | // req.ZyhID = Jxbzc[2:6]
74 | req.NjdmID = "20" + Jxbzc[len(Jxbzc)-8:len(Jxbzc)-6]
75 | req.ZyhID = Jxbzc[len(Jxbzc)-6 : len(Jxbzc)-2]
76 | } else {
77 | req.NjdmID = "20" + c.ClientBodyConfig.BhId[0:2]
78 | req.ZyhID = c.ClientBodyConfig.BhId[2:6]
79 | }
80 | }
81 |
82 | // 发送请求
83 | result, err := c.SelectCourse(req)
84 | if err != nil {
85 | return err
86 | }
87 |
88 | if result.Flag == "1" {
89 | log.Info("选课成功")
90 | } else if result.Flag == "0" {
91 | log.Error("选课失败: ", result.Msg)
92 | } else {
93 | log.Error("选课失败: 人数可能已满", result)
94 | }
95 |
96 | return nil
97 | }
98 |
99 | // CancelCourse 退课
100 | func CancelCourse(c *client.Client, JxbIds string, KchId string, XueNian string, Xqm string) error {
101 | // 设置请求参数
102 | req := &client.CancelCourseReq{
103 | JxbIDs: JxbIds,
104 | KchID: KchId,
105 | Xkxnm: XueNian,
106 | Xkxqm: Xqm,
107 | }
108 |
109 | // 发送请求
110 | result, err := c.CancelCourse(req)
111 | if err != nil {
112 | return err
113 | }
114 |
115 | if result == "\"1\"" {
116 | log.Info("退课成功(可能?)")
117 | } else {
118 | log.Error("退课失败:", result)
119 | }
120 |
121 | return nil
122 | }
123 |
124 | // HandleCourse 处理课程
125 | func HandleCourse(c *client.Client, cfg *config.Config, course *config.Course, CourseName string, SelectFlag interface{}) error {
126 | for _, v := range course.Items {
127 | if v.Jxbmc == CourseName {
128 | // 更改Kklxdm
129 | Kklxdm := v.Kklxmc
130 | if Kklxdm == "主修课程" {
131 | Kklxdm = "01"
132 | } else if Kklxdm == "通识选修课" {
133 | Kklxdm = "10"
134 | } else if Kklxdm == "体育分项" {
135 | Kklxdm = "05"
136 | } else if Kklxdm == "特殊课程" {
137 | Kklxdm = "09"
138 | } else {
139 | return errors.New("课程类型错误")
140 | }
141 |
142 | // 打印课程信息
143 | log.Info("课程名称: ", v.Kcmc)
144 | log.Info("上课时间: ", v.Sksj)
145 |
146 | // 设置NjdmId
147 | NjdmId := "20" + c.ClientBodyConfig.BhId[0:2]
148 |
149 | // 设置Xqm
150 | Xqm := cfg.Time.XueQi
151 | if Xqm == "1" {
152 | Xqm = "3"
153 | } else if Xqm == "2" {
154 | Xqm = "12"
155 | } else {
156 | return errors.New("学期格式错误")
157 | }
158 |
159 | // 获取doJxbId
160 | doJxbId, err := GetDoJxbId(c, v.KchID, v.JxbID, Kklxdm, NjdmId, cfg.Time.XueNian, Xqm)
161 | if err != nil {
162 | return err
163 | }
164 |
165 | // 选课
166 | if SelectFlag == "1" {
167 | err = SelectCourse(c, doJxbId, v.KchID, Kklxdm, v.Jxbzc, cfg)
168 | if err != nil {
169 | return err
170 | }
171 | } else {
172 | // 退课
173 | err = CancelCourse(c, doJxbId, v.KchID, cfg.Time.XueNian, Xqm)
174 | if err != nil {
175 | return err
176 | }
177 | }
178 |
179 | return nil
180 | }
181 | }
182 | return errors.New(CourseName + "课程不存在")
183 | }
184 |
185 | // KillCourse 选退课
186 | func KillCourse(ctx context.Context, c *client.Client, cfg *config.Config, course *config.Course) {
187 | // 计算需要等待的时间
188 | // 时区
189 | loc, err := time.LoadLocation("Asia/Shanghai")
190 | if err != nil {
191 | log.Error("初始化时间地区失败,正在使用手动定义的时区信息 :", err)
192 | loc = time.FixedZone("CST", 8*3600)
193 | }
194 | t, err := time.ParseInLocation("2006-01-02 15:04:05", cfg.StartTime, loc)
195 | if err != nil {
196 | log.Error("时间格式错误: ", err)
197 | return
198 | }
199 | log.Info("选课开始时间: ", t)
200 | waitTime := t.Unix() - time.Now().Unix()
201 |
202 | select {
203 | case <-ctx.Done():
204 | return
205 | case <-time.After(time.Duration(waitTime) * time.Second):
206 | log.Info("时间已到,开始处理课程...")
207 | //go func() {
208 | for {
209 | select {
210 | case <-ctx.Done():
211 | return
212 | default:
213 | // 获取选课配置
214 | err = ReadClientBodyConfig(c)
215 | if err != nil {
216 | err = c.GetClientBodyConfig()
217 | if err != nil {
218 | log.Error("获取选课配置失败: ", err)
219 | continue
220 | }
221 | }
222 | // 保存选课配置
223 | if cfg.ClientBodyConfigEnabled == "1" {
224 | err = SaveClientBodyConfig(c)
225 | if err != nil {
226 | log.Error("保存选课配置失败: ", err)
227 | return
228 | }
229 | }
230 | log.Info("选课配置获取成功")
231 | // 选退课
232 | for _, k := range cfg.Course.Keys() {
233 | v, _ := cfg.Course.Get(k)
234 | // 处理课程
235 | log.Info("----------------------------------------")
236 | log.Info("正在处理课程: ", k)
237 | err = HandleCourse(c, cfg, course, k, v)
238 | if err != nil {
239 | log.Error("处理课程失败: ", err)
240 | continue
241 | }
242 | }
243 | // 完成
244 | channel <- "完成"
245 | return
246 | }
247 | }
248 | //}()
249 | }
250 | }
251 |
252 | // SaveClientBodyConfig 保存选课配置
253 | func SaveClientBodyConfig(c *client.Client) error {
254 | // 将c.ClientBodyConfig保存到文件CLientBodyConfig.json
255 | clientBodyConfig := c.ClientBodyConfig
256 | bytes, err := json.Marshal(clientBodyConfig)
257 | if err != nil {
258 | return err
259 | }
260 |
261 | err = os.WriteFile("ClientBodyConfig.json", bytes, 0666)
262 | if err != nil {
263 | return err
264 | }
265 |
266 | return nil
267 | }
268 |
269 | // ReadClientBodyConfig 读取选课配置
270 | func ReadClientBodyConfig(c *client.Client) error {
271 | // 读取文件CLientBodyConfig.json到c.ClientBodyConfig
272 | bytes, err := os.ReadFile("ClientBodyConfig.json")
273 | if err != nil {
274 | return err
275 | }
276 |
277 | var clientBodyConfig client.ClientBodyConfig
278 | err = json.Unmarshal(bytes, &clientBodyConfig)
279 | if err != nil {
280 | return err
281 | }
282 |
283 | c.ClientBodyConfig = &clientBodyConfig
284 |
285 | return nil
286 | }
287 |
--------------------------------------------------------------------------------
/cmd/HDU-KillCourse/login.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "strings"
6 |
7 | "github.com/cr4n5/HDU-KillCourse/client"
8 | "github.com/cr4n5/HDU-KillCourse/config"
9 | "github.com/cr4n5/HDU-KillCourse/log"
10 | "github.com/cr4n5/HDU-KillCourse/util"
11 | )
12 |
13 | // NewjwLogin newjw登录
14 | func NewjwLogin(c *client.Client, cfg *config.Config) error {
15 | log.Info("获取csrftoken...")
16 | // 获取csrftoken
17 | csrftoken, err := c.GetCsrftoken()
18 | if err != nil {
19 | return err
20 | }
21 |
22 | log.Info("获取公钥...")
23 | // 获取公钥
24 | publicKey, err := c.GetPublicKey()
25 | if err != nil {
26 | return err
27 | }
28 |
29 | // 加密密码
30 | encryptedPassword, err := util.RsaEncrypt(publicKey.Modules, cfg.NewjwLogin.Password)
31 | if err != nil {
32 | return err
33 | }
34 |
35 | log.Info("正在登录...")
36 | // 登录
37 | loginReq := &client.LoginReq{
38 | Csrftoken: csrftoken,
39 | Username: cfg.NewjwLogin.Username,
40 | Password: encryptedPassword,
41 | }
42 | result, err := c.NewjwLoginPost(loginReq)
43 | if err != nil {
44 | return err
45 | }
46 |
47 | // 判断是否登录成功
48 | if strings.Contains(result, "用户名或密码不正确,请重新输入") {
49 | return errors.New("用户名或密码不正确!")
50 | }
51 |
52 | return nil
53 | }
54 |
55 | // CasPassWordLogin cas使用用户名密码登录 !不可用 需手机验证码
56 | func CasPassWordLogin(c *client.Client, cfg *config.Config) error {
57 | log.Info("获取cas登录配置...")
58 | // 获取cas登录配置
59 | execution, croypto, err := c.GetCasLoginConfig()
60 | if err != nil {
61 | return err
62 | }
63 |
64 | // 加密密码
65 | encryptedPassword, err := util.DesEncrypt(croypto, cfg.CasLogin.Password)
66 | if err != nil {
67 | return err
68 | }
69 |
70 | log.Info("正在cas登录...")
71 | // cas登录
72 | casLoginReq := &client.CasLoginReq{
73 | Username: cfg.CasLogin.Username,
74 | Type: "UsernamePassword",
75 | EventID: "submit",
76 | Geolocation: "",
77 | Execution: execution,
78 | CaptchaCode: "",
79 | Croypto: croypto,
80 | Password: encryptedPassword,
81 | }
82 | result, err := c.CasLoginPost(casLoginReq)
83 | if err != nil {
84 | return err
85 | }
86 | // 判断是否登录成功
87 | if strings.Contains(result, "用户名密码登录") {
88 | return errors.New("用户名或密码不正确!")
89 | }
90 |
91 | // 通过cas登录newjw
92 | log.Info("正在通过cas登录newjw...")
93 | result, err = c.CasLoginNewjw()
94 | if err != nil {
95 | return err
96 | }
97 | // 判断是否登录成功
98 | if !strings.Contains(result, "杭州电子科技大学本科教学管理服务平台") {
99 | return errors.New("未知错误, cas登录newjw失败, 请重新尝试登录")
100 | }
101 |
102 | return nil
103 | }
104 |
105 | // CasQrLogin cas使用钉钉扫码登录
106 | func CasQrLogin(c *client.Client, cfg *config.Config) error {
107 | log.Info("获取cas登录配置...")
108 | // 获取cas登录配置
109 | execution, _, err := c.GetCasLoginConfig()
110 | if err != nil {
111 | return err
112 | }
113 |
114 | log.Info("正在获取QrLoginId...")
115 | // 获取QrLoginId
116 | qrLoginIdResp, err := c.GetQrLoginId()
117 | if err != nil {
118 | return err
119 | }
120 |
121 | log.Info("正在获取二维码...")
122 | log.Info("请使用" + log.ErrorColor("钉钉") + "扫码登录...")
123 | var qrLoginStatus *client.QrLoginStatusResp
124 | for {
125 | // 获取二维码
126 | qrCodeBytes, err := c.GetQrCode(qrLoginIdResp.Data)
127 | if err != nil {
128 | return err
129 | }
130 | // 解析二维码
131 | qrCode, err := util.QrCodeDecode(qrCodeBytes)
132 | if err != nil {
133 | return err
134 | }
135 | // 打印二维码
136 | err = util.QrCodePrint(qrCode)
137 | if err != nil {
138 | return err
139 | }
140 |
141 | // 检查登录状态
142 | qrLoginStatus, err = c.GetQrLoginStatus(qrLoginIdResp.Data)
143 | if err != nil {
144 | return err
145 | }
146 | if qrLoginStatus.Code == 200 {
147 | break
148 | }
149 |
150 | // 二维码过期
151 | util.ClearQrCode()
152 | log.Error("二维码已过期, 请重新扫码登录")
153 | }
154 |
155 | log.Info("正在cas登录...")
156 | // cas登录
157 | casLoginReq := &client.CasLoginReq{
158 | Username: qrLoginStatus.Data,
159 | Type: "dingDingQr",
160 | EventID: "submit",
161 | Geolocation: "",
162 | Execution: execution,
163 | }
164 | result, err := c.CasLoginPost(casLoginReq)
165 | if err != nil {
166 | return err
167 | }
168 | // 判断是否登录成功
169 | if strings.Contains(result, "用户名密码登录") {
170 | return errors.New("cas使用钉钉扫码登录失败!")
171 | }
172 |
173 | // 通过cas登录newjw
174 | log.Info("正在通过cas登录newjw...")
175 | result, err = c.CasLoginNewjw()
176 | if err != nil {
177 | return err
178 | }
179 | // 判断是否登录成功
180 | if !strings.Contains(result, "杭州电子科技大学本科教学管理服务平台") {
181 | return errors.New("未知错误, cas登录newjw失败, 请重新尝试登录")
182 | }
183 |
184 | return nil
185 | }
186 |
187 | // Login 根据Level优先级登录
188 | func Login(cfg *config.Config) (*client.Client, error) {
189 | // 创建一个新的客户端
190 | c := client.NewClient(cfg)
191 |
192 | // 使用保存的cookies登录
193 | if cfg.Cookies.JSESSIONID != "" && cfg.Cookies.Route != "" && cfg.Cookies.Enabled == "1" {
194 | log.Info("正在使用保存的cookies登录...")
195 | err := c.LoadCookies(cfg)
196 | if err != nil {
197 | return nil, err
198 | }
199 | log.Info(log.ErrorColor("Notice!: 若登录过期,将cookies.enabled置为0,重新登录"))
200 | return c, nil
201 | }
202 |
203 | // 根据Level优先级登录
204 | if cfg.CasLogin.Level < cfg.NewjwLogin.Level {
205 | // cas登录
206 | log.Info("正在通过cas登录...")
207 | var err error
208 | if cfg.CasLogin.DingDingQrLoginEnabled == "1" {
209 | err = CasQrLogin(c, cfg)
210 | } else {
211 | err = CasPassWordLogin(c, cfg)
212 | }
213 | if err != nil {
214 | log.Error("cas登录失败: ", err)
215 | // newjw登录
216 | log.Info("正在通过newjw登录...")
217 | // 重置client
218 | c = client.NewClient(cfg)
219 | err := NewjwLogin(c, cfg)
220 | if err != nil {
221 | log.Error("newjw登录失败: ", err)
222 | return nil, err
223 | }
224 | }
225 | } else {
226 | // newjw登录
227 | log.Info("正在通过newjw登录...")
228 | err := NewjwLogin(c, cfg)
229 | if err != nil {
230 | log.Error("newjw登录失败: ", err)
231 | // cas登录
232 | log.Info("正在通过cas登录...")
233 | // 重置client
234 | c = client.NewClient(cfg)
235 | var err error
236 | if cfg.CasLogin.DingDingQrLoginEnabled == "1" {
237 | err = CasQrLogin(c, cfg)
238 | } else {
239 | err = CasPassWordLogin(c, cfg)
240 | }
241 | if err != nil {
242 | log.Error("cas登录失败: ", err)
243 | return nil, err
244 | }
245 | }
246 | }
247 |
248 | // 保存cookies
249 | err := c.SaveCookies(cfg)
250 | if err != nil {
251 | return nil, err
252 | }
253 |
254 | return c, nil
255 | }
256 |
--------------------------------------------------------------------------------
/cmd/HDU-KillCourse/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "os/signal"
8 | "syscall"
9 |
10 | "github.com/cr4n5/HDU-KillCourse/config"
11 | "github.com/cr4n5/HDU-KillCourse/log"
12 | "github.com/cr4n5/HDU-KillCourse/vars"
13 | )
14 |
15 | // 程序结束信号
16 | var channel = make(chan string)
17 |
18 | func main() {
19 | // 用于结束程序
20 | defer func() {
21 | fmt.Println("Press Enter to exit...")
22 | fmt.Scanln()
23 | }()
24 | ctx := context.Background()
25 |
26 | vars.ShowPortal()
27 |
28 | // 读取配置文件
29 | log.Info("开始读取配置文件...")
30 | cfg, err := config.InitCfg()
31 | if err != nil {
32 | log.Error("读取配置文件失败: ", err)
33 | return
34 | }
35 | log.Info("读取配置文件成功")
36 |
37 | // 登录
38 | log.Info("开始登录...")
39 | c, err := Login(cfg)
40 | if err != nil {
41 | log.Error("登录失败...")
42 | return
43 | }
44 | log.Info("登录成功...")
45 |
46 | // 获取课程信息
47 | log.Info("开始获取课程信息...")
48 | courses, err := GetCourse(c, cfg)
49 | if err != nil {
50 | log.Error("获取课程信息失败: ", err)
51 | return
52 | }
53 | log.Info("获取课程信息成功...")
54 | log.Info(log.ErrorColor("Notice!: 在下学期选课开始前,请删除course.json文件,获取最新课程信息"))
55 |
56 | cancelCtx, cancel := context.WithCancel(ctx)
57 | // 捕获终止信号
58 | stopChan := make(chan os.Signal, 1)
59 | signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM)
60 |
61 | // 选退课
62 | if cfg.WaitCourse.Enabled == "1" {
63 | go WaitCourse(cancelCtx, c, cfg, courses)
64 | } else {
65 | go KillCourse(cancelCtx, c, cfg, courses)
66 | }
67 |
68 | select {
69 | case <-stopChan:
70 | log.Info("收到终止信号,正在退出...")
71 | cancel()
72 | case <-channel:
73 | log.Info("此程序已完成,正在退出...")
74 | cancel()
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/cmd/HDU-KillCourse/waitCourse.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "time"
7 |
8 | "github.com/cr4n5/HDU-KillCourse/client"
9 | "github.com/cr4n5/HDU-KillCourse/config"
10 | "github.com/cr4n5/HDU-KillCourse/log"
11 | "github.com/cr4n5/HDU-KillCourse/util"
12 | )
13 |
14 | // StartWaitCourse 开始蹲课
15 | func StartWaitCourse(ctx context.Context, c *client.Client, cfg *config.Config, courses *config.Course, CourseName string, waitCourseChannel chan string) {
16 | defer func() {
17 | waitCourseChannel <- "完成"
18 | }()
19 |
20 | firstRun := true
21 |
22 | for {
23 | if firstRun {
24 | firstRun = false
25 | } else {
26 | select {
27 | case <-ctx.Done():
28 | return
29 | case <-time.After(time.Duration(cfg.WaitCourse.Interval) * time.Second):
30 | }
31 | }
32 |
33 | // 检验是否有余量
34 | isOk, err := GetIsCourseOk(c, cfg, courses, CourseName)
35 | if err != nil {
36 | log.Error(CourseName+"查询失败: ", err)
37 | if err.Error() == "可能登录过期" {
38 | waitCourseChannel <- "登录过期"
39 | return
40 | }
41 | SendEmail(cfg, "查询失败", CourseName+"查询失败,将会继续蹲课: "+err.Error())
42 | continue
43 | }
44 |
45 | if isOk {
46 | // 选课
47 | err := HandleCourse(c, cfg, courses, CourseName, "1")
48 | if err != nil {
49 | log.Error(CourseName+"选课失败: ", err)
50 | if err.Error() == "可能登录过期" {
51 | waitCourseChannel <- "登录过期"
52 | return
53 | }
54 | SendEmail(cfg, "蹲选课失败", CourseName+"选课失败,将会继续蹲课: "+err.Error())
55 | continue
56 | }
57 |
58 | log.Info(CourseName + "蹲选课成功")
59 | // 发送邮件
60 | SendEmail(cfg, "蹲选课成功", CourseName+"选课成功?(,请自行查看确认")
61 |
62 | // 将此CourseName设置为0
63 | cfg.Course.Set(CourseName, "0")
64 |
65 | return
66 | }
67 | }
68 | }
69 |
70 | // SendEmail 发送邮件
71 | func SendEmail(cfg *config.Config, subject string, body string) {
72 | if cfg.SmtpEmail.Enabled == "1" {
73 | err := util.SendEmail(cfg.SmtpEmail.Host, cfg.SmtpEmail.Username, cfg.SmtpEmail.Password, cfg.SmtpEmail.To, subject, body)
74 | if err != nil {
75 | log.Error("发送邮件失败: ", err)
76 | }
77 | }
78 | }
79 |
80 | // GetIsCourseOk 检验是否有余量
81 | func GetIsCourseOk(c *client.Client, cfg *config.Config, course *config.Course, CourseName string) (bool, error) {
82 | for _, v := range course.Items {
83 | if v.Jxbmc == CourseName {
84 | // 更改Kklxdm
85 | Kklxdm := v.Kklxmc
86 | if Kklxdm == "主修课程" {
87 | Kklxdm = "01"
88 | } else if Kklxdm == "通识选修课" {
89 | Kklxdm = "10"
90 | } else if Kklxdm == "体育分项" {
91 | Kklxdm = "05"
92 | } else if Kklxdm == "特殊课程" {
93 | Kklxdm = "09"
94 | } else {
95 | return false, errors.New("课程类型错误")
96 | }
97 |
98 | // 设置Xqm
99 | Xqm := cfg.Time.XueQi
100 | if Xqm == "1" {
101 | Xqm = "3"
102 | } else if Xqm == "2" {
103 | Xqm = "12"
104 | } else {
105 | return false, errors.New("学期格式错误")
106 | }
107 |
108 | // 获取课程是否有余量
109 | req := &client.SearchCourseReq{
110 | Xkxnm: cfg.Time.XueNian,
111 | Xkxqm: Xqm,
112 | Kklxdm: Kklxdm,
113 | Kspage: "1",
114 | Jspage: "10",
115 | Yllist: "1",
116 | Filterlist: CourseName,
117 | }
118 |
119 | // 发送请求
120 | result, err := c.SearchCourse(req)
121 | if err != nil {
122 | return false, err
123 | }
124 |
125 | // 检验是否有余量, TmpList是否长度为0
126 | if len(result.TmpList) == 0 {
127 | log.Info(CourseName + "" + v.Kcmc + ": " + "课程无余量")
128 | return false, nil
129 | } else {
130 | log.Info(CourseName + "" + v.Kcmc + ": " + "课程有余量")
131 | return true, nil
132 | }
133 |
134 | }
135 | }
136 | return false, errors.New(CourseName + "课程不存在")
137 | }
138 |
139 | // WaitCourse 蹲课
140 | func WaitCourse(ctx context.Context, c *client.Client, cfg *config.Config, course *config.Course) {
141 | defer func() {
142 | channel <- "完成"
143 | }()
144 |
145 | log.Info("开始蹲课...")
146 |
147 | // 关闭Cookies
148 | cfg.Cookies.Enabled = "0"
149 |
150 | // 获取选课配置
151 | err := ReadClientBodyConfig(c)
152 | if err != nil {
153 | err = c.GetClientBodyConfig()
154 | if err != nil {
155 | log.Error("获取选课配置失败: ", err)
156 | return
157 | }
158 | }
159 | log.Info("选课配置获取成功")
160 |
161 | for {
162 | waitCourseChannel := make(chan string)
163 | numWaitCourse := 0
164 | waitCourseCtx, cancel := context.WithCancel(ctx)
165 |
166 | // 蹲课
167 | for _, k := range cfg.Course.Keys() {
168 | v, _ := cfg.Course.Get(k)
169 | // 开始蹲课
170 | if v == "1" {
171 | numWaitCourse++
172 | go StartWaitCourse(waitCourseCtx, c, cfg, course, k, waitCourseChannel)
173 | }
174 | }
175 |
176 | // 等待蹲课结束
177 | outerLoop:
178 | for {
179 | select {
180 | case <-ctx.Done():
181 | cancel()
182 | return
183 | case message := <-waitCourseChannel:
184 | if message == "登录过期" {
185 | log.Error("登录过期")
186 | cancel()
187 | SendEmail(cfg, "登录过期", "登录过期,自动重新登录")
188 | close(waitCourseChannel)
189 |
190 | log.Info("重新登录...")
191 | // 重新登录
192 | c, err = Login(cfg)
193 | if err != nil {
194 | log.Error("登录失败...")
195 | SendEmail(cfg, "登录失败", "登录失败,程序停止,请检查")
196 | return
197 | }
198 | break outerLoop
199 | }
200 |
201 | numWaitCourse--
202 | if numWaitCourse == 0 {
203 | cancel()
204 | return
205 | }
206 | }
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "cas_login": {
3 | "username": "2201xxxx",
4 | "password": "xxxxxxxx",
5 | "dingDingQrLoginEnabled": "0",
6 | "level": "0"
7 | },
8 | "newjw_login": {
9 | "username": "2201xxxx",
10 | "password": "xxxxxxxx",
11 | "level": "1"
12 | },
13 | "cookies": {
14 | "JSESSIONID": "",
15 | "route": "",
16 | "enabled": "1"
17 | },
18 | "time": {
19 | "XueNian": "2024",
20 | "XueQi": "1"
21 | },
22 | "course": {
23 | "(2024-2025-1)-C2092011-01" : "1",
24 | "(2024-2025-1)-T1300019-04" : "1",
25 | "(2024-2025-1)-T1300019-05" : "1",
26 | "(2024-2025-1)-B2700380-02" : "0",
27 | "(2024-2025-1)-C2892008-02" : "1",
28 | "(2024-2025-1)-W0001321-06" : "0"
29 | },
30 | "wait_course": {
31 | "interval": 60,
32 | "enabled": "0"
33 | },
34 | "smtp_email": {
35 | "host": "smtp.qq.com",
36 | "username": "...@qq.com",
37 | "password": "xxxxxxxx",
38 | "to": "...@qq.com",
39 | "enabled": "0"
40 | },
41 | "start_time": "2024-07-25 12:00:00"
42 | }
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "os"
7 |
8 | "github.com/cr4n5/HDU-KillCourse/log"
9 | "github.com/iancoleman/orderedmap"
10 | )
11 |
12 | // Config 配置文件结构体
13 | type Config struct {
14 | CasLogin struct {
15 | Username string `json:"username"`
16 | Password string `json:"password"`
17 | DingDingQrLoginEnabled string `json:"dingDingQrLoginEnabled"`
18 | Level string `json:"level"`
19 | } `json:"cas_login"`
20 | NewjwLogin struct {
21 | Username string `json:"username"`
22 | Password string `json:"password"`
23 | Level string `json:"level"`
24 | } `json:"newjw_login"`
25 | Cookies struct {
26 | JSESSIONID string `json:"JSESSIONID"`
27 | Route string `json:"route"`
28 | Enabled string `json:"enabled"`
29 | } `json:"cookies"`
30 | Time struct {
31 | XueNian string `json:"XueNian"`
32 | XueQi string `json:"XueQi"`
33 | } `json:"time"`
34 | Course *orderedmap.OrderedMap `json:"course"`
35 | WaitCourse struct {
36 | Interval int `json:"interval"`
37 | Enabled string `json:"enabled"`
38 | } `json:"wait_course"`
39 | SmtpEmail struct {
40 | Host string `json:"host"`
41 | Username string `json:"username"`
42 | Password string `json:"password"`
43 | To string `json:"to"`
44 | Enabled string `json:"enabled"`
45 | } `json:"smtp_email"`
46 | StartTime string `json:"start_time"`
47 | ClientBodyConfigEnabled string `json:"ClientBodyConfigEnabled,omitempty"`
48 | DontTouchForDebug string `json:"DontTouchForDebug,omitempty"`
49 | }
50 |
51 | // Course 课程信息结构体
52 | type Course struct {
53 | Items []struct {
54 | Jxbmc string `json:"jxbmc"`
55 | KchID string `json:"kch_id"`
56 | JxbID string `json:"jxb_id"`
57 | Jxbzc string `json:"jxbzc"`
58 | Kklxmc string `json:"kklxmc"`
59 | Kcmc string `json:"kcmc"` // 课程名称
60 | Sksj string `json:"sksj"` // 上课时间
61 | } `json:"items"`
62 | }
63 |
64 | func InitCfg() (*Config, error) {
65 | // 读取配置文件
66 | bytes, err := os.ReadFile("config.json")
67 | if err != nil {
68 | return nil, err
69 | }
70 |
71 | // 解析配置文件
72 | var cfg Config
73 | if err := json.Unmarshal(bytes, &cfg); err != nil {
74 | return nil, err
75 | }
76 |
77 | // 验证配置文件
78 | if err := cfg.Validate(); err != nil {
79 | return nil, err
80 | }
81 |
82 | return &cfg, nil
83 | }
84 |
85 | // Validate 验证配置文件
86 | func (cfg *Config) Validate() error {
87 | if (cfg.CasLogin.Username == "" || cfg.CasLogin.Password == "") && (cfg.NewjwLogin.Username == "" || cfg.NewjwLogin.Password == "") {
88 | return errors.New("用户名或密码为空")
89 | }
90 | if cfg.Time.XueNian == "" || cfg.Time.XueQi == "" {
91 | return errors.New("学年或学期为空")
92 | }
93 | if cfg.Course == nil {
94 | return errors.New("课程为空")
95 | }
96 | if cfg.WaitCourse.Interval == 0 || cfg.WaitCourse.Enabled == "" {
97 | return errors.New("WaitCourse为空")
98 | }
99 | if cfg.SmtpEmail.Enabled == "1" {
100 | if cfg.SmtpEmail.Host == "" || cfg.SmtpEmail.Username == "" || cfg.SmtpEmail.Password == "" || cfg.SmtpEmail.To == "" {
101 | return errors.New("SmtpEmail为空")
102 | }
103 | }
104 | if cfg.StartTime == "" {
105 | return errors.New("StartTime为空")
106 | }
107 |
108 | // 打印配置文件
109 | // 空行
110 | log.Info("")
111 |
112 | log.Info(log.InfoColor("CasLogin:"))
113 | log.Info(" Username: ", cfg.CasLogin.Username)
114 | log.Info(" Password: ", cfg.CasLogin.Password)
115 | log.Info(" DingDingQrLoginEnabled: ", cfg.CasLogin.DingDingQrLoginEnabled)
116 | log.Info(" Level: ", cfg.CasLogin.Level)
117 | log.Info(log.InfoColor("NewjwLogin:"))
118 | log.Info(" Username: ", cfg.NewjwLogin.Username)
119 | log.Info(" Password: ", cfg.NewjwLogin.Password)
120 | log.Info(" Level: ", cfg.NewjwLogin.Level)
121 | log.Info(log.InfoColor("XueNian: "), cfg.Time.XueNian)
122 | log.Info(log.InfoColor("XueQi: "), cfg.Time.XueQi)
123 | log.Info(log.InfoColor("WaitCourse:"))
124 | log.Info(" Interval: ", cfg.WaitCourse.Interval)
125 | log.Info(" Enabled: ", cfg.WaitCourse.Enabled)
126 | log.Info(log.InfoColor("SmtpEmail:"))
127 | if cfg.SmtpEmail.Enabled == "1" {
128 | log.Info(" Host: ", cfg.SmtpEmail.Host)
129 | log.Info(" Username: ", cfg.SmtpEmail.Username)
130 | log.Info(" Password: ", cfg.SmtpEmail.Password)
131 | log.Info(" To: ", cfg.SmtpEmail.To)
132 | } else {
133 | log.Info(" SmtpEmailEnabled: ", cfg.SmtpEmail.Enabled)
134 | }
135 | log.Info(log.InfoColor("StartTime: "), cfg.StartTime)
136 | log.Info(log.InfoColor("Course:"))
137 | for _, k := range cfg.Course.Keys() {
138 | v, _ := cfg.Course.Get(k)
139 | log.Info(k, ": ", v)
140 | }
141 |
142 | // 空行
143 | log.Info("")
144 |
145 | return nil
146 | }
147 |
148 | // ReadCourse 读取课程信息
149 | func ReadCourse() (*Course, error) {
150 | // 读取课程信息
151 | bytes, err := os.ReadFile("course.json")
152 | if err != nil {
153 | return nil, err
154 | }
155 |
156 | // 解析课程信息
157 | var course Course
158 | if err := json.Unmarshal(bytes, &course); err != nil {
159 | return nil, err
160 | }
161 |
162 | return &course, nil
163 | }
164 |
165 | // SaveCourse 保存课程信息
166 | func SaveCourse(course *Course) error {
167 | // 转换为json
168 | bytes, err := json.Marshal(course)
169 | if err != nil {
170 | return err
171 | }
172 |
173 | // 保存课程信息
174 | if err := os.WriteFile("course.json", bytes, 0666); err != nil {
175 | return err
176 | }
177 |
178 | return nil
179 | }
180 |
181 | // SaveConfig 保存配置文件
182 | func SaveConfig(cfg *Config) error {
183 | // 转换为json
184 | bytes, err := json.MarshalIndent(cfg, "", " ")
185 | if err != nil {
186 | return err
187 | }
188 |
189 | // 保存配置文件
190 | if err := os.WriteFile("config.json", bytes, 0666); err != nil {
191 | return err
192 | }
193 |
194 | return nil
195 | }
196 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/cr4n5/HDU-KillCourse
2 |
3 | go 1.23
4 |
5 | require (
6 | github.com/antchfx/htmlquery v1.3.3
7 | github.com/fatih/color v1.18.0
8 | github.com/iancoleman/orderedmap v0.3.0
9 | github.com/makiuchi-d/gozxing v0.1.1
10 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
11 | golang.org/x/net v0.7.0
12 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
13 | )
14 |
15 | require (
16 | github.com/antchfx/xpath v1.3.2 // indirect
17 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
18 | github.com/mattn/go-colorable v0.1.13 // indirect
19 | github.com/mattn/go-isatty v0.0.20 // indirect
20 | golang.org/x/sys v0.25.0 // indirect
21 | golang.org/x/text v0.7.0 // indirect
22 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
23 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
24 | )
25 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/antchfx/htmlquery v1.3.3 h1:x6tVzrRhVNfECDaVxnZi1mEGrQg3mjE/rxbH2Pe6dNE=
2 | github.com/antchfx/htmlquery v1.3.3/go.mod h1:WeU3N7/rL6mb6dCwtE30dURBnBieKDC/fR8t6X+cKjU=
3 | github.com/antchfx/xpath v1.3.2 h1:LNjzlsSjinu3bQpw9hWMY9ocB80oLOWuQqFvO6xt51U=
4 | github.com/antchfx/xpath v1.3.2/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
5 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
6 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
7 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
8 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
9 | github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
10 | github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
11 | github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I=
12 | github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU=
13 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
14 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
15 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
16 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
17 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
18 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
19 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
20 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
21 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
22 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
23 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
24 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
25 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
26 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
27 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
28 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
29 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
30 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
31 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
32 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
33 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
34 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
35 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
36 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
37 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
38 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
40 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
41 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
42 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
43 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
44 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
45 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
46 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
47 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
48 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
49 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
50 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
51 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
52 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
53 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
54 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
55 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
56 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
57 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
58 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
59 |
--------------------------------------------------------------------------------
/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "io"
5 | "log"
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/fatih/color"
10 | )
11 |
12 | var (
13 | infoLogger *log.Logger
14 | errorLogger *log.Logger
15 | debugLogger *log.Logger
16 | )
17 |
18 | var (
19 | InfoColor = color.New(color.FgGreen).SprintFunc()
20 | ErrorColor = color.New(color.FgRed).SprintFunc()
21 | )
22 |
23 | var logDir = "log_files"
24 |
25 | func init() {
26 | // 创建日志目录
27 | if err := os.MkdirAll(logDir, 0755); err != nil {
28 | log.Fatalf("无法创建日志目录: %v", err)
29 | }
30 |
31 | appFile, err := os.OpenFile(filepath.Join(logDir, "app.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
32 | if err != nil {
33 | log.Fatalf("无法打开app.log文件: %v", err)
34 | }
35 |
36 | debugFile, err := os.OpenFile(filepath.Join(logDir, "debug.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
37 | if err != nil {
38 | log.Fatalf("无法打开debug.log文件: %v", err)
39 | }
40 |
41 | appMultiWriter := io.MultiWriter(appFile, os.Stdout)
42 | debugMultiWriter := io.MultiWriter(debugFile)
43 |
44 | infoLogger = log.New(appMultiWriter, "", log.Ldate|log.Ltime|log.Lmicroseconds)
45 | errorLogger = log.New(appMultiWriter, "", log.Ldate|log.Ltime|log.Lmicroseconds)
46 | debugLogger = log.New(debugMultiWriter, "", log.Ldate|log.Ltime|log.Lmicroseconds)
47 | }
48 |
49 | func Info(v ...interface{}) {
50 | infoLogger.Println(append([]interface{}{InfoColor("[INFO]")}, v...)...)
51 | }
52 |
53 | func Error(v ...interface{}) {
54 | errorLogger.Println(append([]interface{}{ErrorColor("[ERROR]")}, v...)...)
55 | }
56 |
57 | func Debug(v ...interface{}) {
58 | debugLogger.Println(append([]interface{}{"[DEBUG]"}, v...)...)
59 | }
60 |
--------------------------------------------------------------------------------
/util/des.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "crypto/des"
6 | "encoding/base64"
7 | )
8 |
9 | // PKCS7Padding pads the plaintext to be a multiple of the block size
10 | func PKCS7Padding(plainText []byte, blockSize int) []byte {
11 | padding := blockSize - len(plainText)%blockSize
12 | padText := bytes.Repeat([]byte{byte(padding)}, padding)
13 | return append(plainText, padText...)
14 | }
15 |
16 | // Encrypt encrypts the plaintext using DES algorithm with the given key
17 | func DesEncrypt(key, plainText string) (string, error) {
18 | keyBytes, err := base64.StdEncoding.DecodeString(key)
19 | if err != nil {
20 | return "", err
21 | }
22 |
23 | block, err := des.NewCipher(keyBytes)
24 | if err != nil {
25 | return "", err
26 | }
27 |
28 | paddedText := PKCS7Padding([]byte(plainText), block.BlockSize())
29 | cipherText := make([]byte, len(paddedText))
30 |
31 | for bs, be := 0, block.BlockSize(); bs < len(paddedText); bs, be = bs+block.BlockSize(), be+block.BlockSize() {
32 | block.Encrypt(cipherText[bs:be], paddedText[bs:be])
33 | }
34 |
35 | encodedCipherText := base64.StdEncoding.EncodeToString(cipherText)
36 | return encodedCipherText, nil
37 | }
38 |
--------------------------------------------------------------------------------
/util/qrCode.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "image"
7 | _ "image/png"
8 |
9 | "github.com/makiuchi-d/gozxing"
10 | "github.com/makiuchi-d/gozxing/qrcode"
11 | qrcodeGen "github.com/skip2/go-qrcode"
12 | )
13 |
14 | // QrCodeDecode 解码二维码
15 | func QrCodeDecode(pngBytes []byte) (string, error) {
16 | img, _, err := image.Decode(bytes.NewReader(pngBytes))
17 | if err != nil {
18 | return "", err
19 | }
20 |
21 | // 准备 BinaryBitmap
22 | bmp, err := gozxing.NewBinaryBitmapFromImage(img)
23 | if err != nil {
24 | return "", err
25 | }
26 |
27 | // 解码图像
28 | qrReader := qrcode.NewQRCodeReader()
29 | result, err := qrReader.Decode(bmp, nil)
30 | if err != nil {
31 | return "", err
32 | }
33 |
34 | return result.String(), nil
35 | }
36 |
37 | // QrCodePrint 打印二维码
38 | func QrCodePrint(content string) error {
39 | // 生成二维码并输出到命令行
40 | qr, err := qrcodeGen.New(content, qrcodeGen.Low)
41 | if err != nil {
42 | return err
43 | }
44 | fmt.Println(qr.ToSmallString(false))
45 |
46 | return nil
47 | }
48 |
49 | // ClearQrCode 清空二维码
50 | func ClearQrCode() {
51 | // 清空26行
52 | for i := 0; i < 26; i++ {
53 | fmt.Print("\033[1A\033[K")
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/util/randomString.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/base64"
6 | "encoding/hex"
7 | "math/rand"
8 | "time"
9 | )
10 |
11 | func GenerateRandomString(n int) string {
12 | const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
13 | var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
14 | b := make([]byte, n)
15 | for i := range b {
16 | b[i] = charset[seededRand.Intn(len(charset))]
17 | }
18 | return string(b)
19 | }
20 |
21 | // GenerateCsrfValue 生成 Csrf-Value
22 | func GenerateCsrfValue(n string) string {
23 | // Base64 编码
24 | t := base64.StdEncoding.EncodeToString([]byte(n))
25 | // 插入 t 本身
26 | o := t[:len(t)/2] + t + t[len(t)/2:]
27 | // 计算 MD5 哈希值
28 | hash := md5.Sum([]byte(o))
29 | return hex.EncodeToString(hash[:])
30 | }
31 |
--------------------------------------------------------------------------------
/util/rsa.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/rsa"
6 | "encoding/base64"
7 | "encoding/hex"
8 | "math/big"
9 | )
10 |
11 | func RsaEncrypt(publicKey string, data string) (string, error) {
12 | // 解码公钥
13 | pubKey, err := base64.StdEncoding.DecodeString(publicKey)
14 | if err != nil {
15 | return "", err
16 | }
17 | // 转化成十六进制
18 | pubKeyHex := hex.EncodeToString(pubKey)
19 |
20 | // 创建公钥
21 | pub := new(rsa.PublicKey)
22 | pub.N = new(big.Int)
23 | pub.N.SetString(pubKeyHex, 16)
24 | pub.E = 65537
25 |
26 | // 加密
27 | cipher, err := rsa.EncryptPKCS1v15(rand.Reader, pub, []byte(data))
28 | if err != nil {
29 | return "", err
30 | }
31 |
32 | // 转换为16进制
33 | cipherHex := hex.EncodeToString(cipher)
34 | cipherByte, err := hex.DecodeString(cipherHex)
35 | if err != nil {
36 | return "", err
37 | }
38 | // 转换为base64
39 | return base64.StdEncoding.EncodeToString(cipherByte), nil
40 | }
41 |
--------------------------------------------------------------------------------
/util/smtpEmail.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "crypto/tls"
5 | "gopkg.in/gomail.v2"
6 | )
7 |
8 | func SendEmail(host string, username string, password string, to string, subject string, body string) error {
9 | m := gomail.NewMessage()
10 | m.SetHeader("From", username)
11 | m.SetHeader("To", to)
12 | m.SetHeader("Subject", subject)
13 | m.SetBody("text/plain", body)
14 |
15 | d := gomail.NewDialer(host, 25, username, password)
16 |
17 | // 关闭ssl
18 | d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
19 |
20 | // 发送邮件
21 | if err := d.DialAndSend(m); err != nil {
22 | return err
23 | }
24 | return nil
25 | }
26 |
--------------------------------------------------------------------------------
/vars/const.go:
--------------------------------------------------------------------------------
1 | package vars
2 |
3 | var (
4 | // 不需要debug的url
5 | NoDebugUrl = map[string]bool{
6 | "https://sso.hdu.edu.cn/login": true,
7 | "https://newjw.hdu.edu.cn/sso/driot4login": true,
8 | "https://newjw.hdu.edu.cn/jwglxt/xtgl/login_slogin.html": true,
9 | "https://newjw.hdu.edu.cn/jwglxt/rwlscx/rwlscx_cxRwlsIndex.html?doType=query&gnmkdm=N1548": true, // 获取课程
10 | "https://newjw.hdu.edu.cn/jwglxt/xsxk/zzxkyzb_cxZzxkYzbIndex.html?gnmkdm=N253512&layout=default": true, // 选课配置
11 | }
12 | )
13 |
--------------------------------------------------------------------------------
/vars/portal.go:
--------------------------------------------------------------------------------
1 | package vars
2 |
3 | import "fmt"
4 |
5 | var portal = `
6 | _ _ ____ _ _ _ __ _ _ _ ____
7 | | | | | | _ \ | | | | | |/ / (_) | | | | / ___| ___ _ _ _ __ ___ ___
8 | | |_| | | | | | | | | | _____ | ' / | | | | | | | | / _ \ | | | | | '__| / __| / _ \
9 | | _ | | |_| | | |_| | |_____| | . \ | | | | | | | |___ | (_) | | |_| | | | \__ \ | __/
10 | |_| |_| |____/ \___/ |_|\_\ |_| |_| |_| \____| \___/ \__,_| |_| |___/ \___|
11 |
12 | HDU-KillCourse[https://github.com/cr4n5/HDU-KillCourse]
13 | `
14 |
15 | func ShowPortal() {
16 | fmt.Println(portal)
17 | }
18 |
--------------------------------------------------------------------------------