├── .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 | - huohuo 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 | --------------------------------------------------------------------------------