✨ 四川教师刷课
This commit is contained in:
@@ -2,4 +2,4 @@ ent-new:
|
|||||||
cd internal;go run -mod=mod entgo.io/ent/cmd/ent new $(Name)
|
cd internal;go run -mod=mod entgo.io/ent/cmd/ent new $(Name)
|
||||||
|
|
||||||
ent-gen:
|
ent-gen:
|
||||||
go run . g
|
go run main.go g
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# 四川智慧教育刷课 API 文档
|
||||||
|
|
||||||
|
## 基础信息
|
||||||
|
|
||||||
|
- 基础路径: `/v1/course`
|
||||||
|
- 需要认证: 是 (Bearer Token)
|
||||||
|
- Content-Type: `application/json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 查询课程列表
|
||||||
|
|
||||||
|
登录四川智慧教育平台,获取用户的课程列表。
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /v1/course/smartedu-sc/query
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求参数**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| username | string | 是 | 登录账号 |
|
||||||
|
| password | string | 是 | 登录密码 |
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "13800138000",
|
||||||
|
"password": "your_password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "course_id_1",
|
||||||
|
"courseName": "课程名称1",
|
||||||
|
"image": "https://xxx.com/image.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "course_id_2",
|
||||||
|
"courseName": "课程名称2",
|
||||||
|
"image": "https://xxx.com/image2.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 添加刷课任务
|
||||||
|
|
||||||
|
添加一个新的刷课任务,提交后立即开始秒刷。
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /v1/course/smartedu-sc
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求参数**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| username | string | 是 | 登录账号 |
|
||||||
|
| password | string | 是 | 登录密码 |
|
||||||
|
| course_name | string | 是 | 课程名称 |
|
||||||
|
| course_id | string | 是 | 课程ID(从查询接口获取) |
|
||||||
|
| desc | string | 否 | 备注说明 |
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "13800138000",
|
||||||
|
"password": "your_password",
|
||||||
|
"course_name": "教师培训课程",
|
||||||
|
"course_id": "abc123",
|
||||||
|
"desc": "2024年培训"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "任务已提交,正在刷课中"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 获取刷课任务列表
|
||||||
|
|
||||||
|
获取当前用户的四川智慧教育刷课任务列表。
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /v1/course/smartedu-sc
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求参数**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| username | string | 否 | 按账号筛选 |
|
||||||
|
| status | string | 否 | 按状态筛选: not_start/waiting/doing/done/fail/pause |
|
||||||
|
| page | int | 否 | 页码,默认1 |
|
||||||
|
| size | int | 否 | 每页数量,默认10 |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"createTime": "2024-12-09 10:00:00",
|
||||||
|
"updateTime": "2024-12-09 10:05:00",
|
||||||
|
"username": "13800138000",
|
||||||
|
"type": "smartedu_sc",
|
||||||
|
"course_name": "教师培训课程",
|
||||||
|
"course_id": "abc123",
|
||||||
|
"need": "task",
|
||||||
|
"status": "done",
|
||||||
|
"task_count": 20,
|
||||||
|
"success_count": 20,
|
||||||
|
"violence": "yes",
|
||||||
|
"desc": "2024年培训"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**状态说明**
|
||||||
|
|
||||||
|
| 状态值 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| not_start | 未开始 |
|
||||||
|
| waiting | 等待中 |
|
||||||
|
| doing | 进行中 |
|
||||||
|
| done | 已完成 |
|
||||||
|
| fail | 失败 |
|
||||||
|
| pause | 已暂停 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 删除刷课任务
|
||||||
|
|
||||||
|
删除指定的刷课任务。
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /v1/course/smartedu-sc/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| id | int64 | 是 | 任务ID |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "删除成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误码说明
|
||||||
|
|
||||||
|
| 错误码 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| 200 | 成功 |
|
||||||
|
| 400 | 请求参数错误 |
|
||||||
|
| 401 | 未授权 |
|
||||||
|
| 403 | 无权限 |
|
||||||
|
| 500 | 服务器内部错误 |
|
||||||
|
|
||||||
|
## 使用流程
|
||||||
|
|
||||||
|
1. 调用 **查询课程列表** 接口,获取用户在四川智慧教育平台的课程
|
||||||
|
2. 选择需要刷的课程,调用 **添加刷课任务** 接口
|
||||||
|
3. 任务提交后自动开始秒刷,可通过 **获取刷课任务列表** 查看进度
|
||||||
|
4. 刷课完成后状态变为 `done`
|
||||||
@@ -66,6 +66,7 @@ func StartServer(ctx context.Context) {
|
|||||||
// 网课
|
// 网课
|
||||||
courseGroup := group.Group("/course").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
courseGroup := group.Group("/course").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||||
courseGroup.Bind(course.NewXxtCtrlV1())
|
courseGroup.Bind(course.NewXxtCtrlV1())
|
||||||
|
courseGroup.Bind(course.NewSmartEduSCCtrlV1())
|
||||||
})
|
})
|
||||||
|
|
||||||
s.SetPort(port.Int())
|
s.SetPort(port.Int())
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SmartEduSCQueryCourseReq 查询四川智慧教育课程列表
|
||||||
|
type SmartEduSCQueryCourseReq struct {
|
||||||
|
g.Meta `path:"/smartedu-sc/query" method:"post" tags:"SmartEduSC" summary:"查询四川智慧教育课程列表"`
|
||||||
|
Username string `json:"username" v:"required#账号不能为空"`
|
||||||
|
Password string `json:"password" v:"required#密码不能为空"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SmartEduSCAddCourseReq 添加四川智慧教育刷课任务
|
||||||
|
type SmartEduSCAddCourseReq struct {
|
||||||
|
g.Meta `path:"/smartedu-sc" method:"post" tags:"SmartEduSC" summary:"添加四川智慧教育刷课任务"`
|
||||||
|
Username string `json:"username" v:"required#账号不能为空"`
|
||||||
|
Password string `json:"password" v:"required#密码不能为空"`
|
||||||
|
CourseName string `json:"course_name" v:"required#课程名称不能为空"`
|
||||||
|
CourseID string `json:"course_id" v:"required#课程ID不能为空"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SmartEduSCListReq 获取四川智慧教育刷课列表
|
||||||
|
type SmartEduSCListReq struct {
|
||||||
|
g.Meta `path:"/smartedu-sc" method:"get" tags:"SmartEduSC" summary:"获取四川智慧教育刷课列表"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Page int `json:"page" d:"1"`
|
||||||
|
Size int `json:"size" d:"10"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SmartEduSCDeleteReq 删除四川智慧教育刷课任务
|
||||||
|
type SmartEduSCDeleteReq struct {
|
||||||
|
g.Meta `path:"/smartedu-sc/{id}" method:"delete" tags:"SmartEduSC" summary:"删除四川智慧教育刷课任务"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"likedream-goframe/internal/application/cookiejar"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SmartEdu = resty.New().
|
||||||
|
SetCookieJar(cookiejar.NewAllSavePersistCookieJar()).
|
||||||
|
SetRedirectPolicy(resty.FlexibleRedirectPolicy(20)).
|
||||||
|
SetTimeout(30 * time.Second).
|
||||||
|
SetTLSClientConfig(&tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if !utils.IsDebug() {
|
||||||
|
Xuexitong.SetDebug(false)
|
||||||
|
} else {
|
||||||
|
Xuexitong.SetProxy("http://127.0.0.1:9000")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSmartEdu 创建一个全新的独立 client 实例,避免 Clone 可能的浅拷贝问题
|
||||||
|
// 每个实例都是完全独立的,适用于并发场景,不会共享状态
|
||||||
|
func NewSmartEdu() *resty.Client {
|
||||||
|
client := resty.New().
|
||||||
|
SetCookieJar(nil).
|
||||||
|
SetTimeout(30*time.Second).
|
||||||
|
SetDoNotParseResponse(false).
|
||||||
|
SetTLSClientConfig(&tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}).
|
||||||
|
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
|
||||||
|
|
||||||
|
// 根据调试模式设置代理和调试选项
|
||||||
|
if !utils.IsDebug() {
|
||||||
|
client.SetDebug(false)
|
||||||
|
} else {
|
||||||
|
client.SetProxy("http://127.0.0.1:9000")
|
||||||
|
}
|
||||||
|
jar := cookiejar.NewAllSavePersistCookieJar()
|
||||||
|
client.SetCookieJar(jar)
|
||||||
|
return client
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/base64"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
scAESKey = "m1xp9slgfa|su@m2"
|
||||||
|
scAESIV = "pa!st17m@04h5u#0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncryptSC AES-CBC加密,使用固定的key和iv
|
||||||
|
func EncryptSC(plaintext string) string {
|
||||||
|
key := []byte(scAESKey)
|
||||||
|
iv := []byte(scAESIV)
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// PKCS7填充
|
||||||
|
blockSize := block.BlockSize()
|
||||||
|
padding := blockSize - len(plaintext)%blockSize
|
||||||
|
padText := make([]byte, padding)
|
||||||
|
for i := range padText {
|
||||||
|
padText[i] = byte(padding)
|
||||||
|
}
|
||||||
|
plaintextBytes := append([]byte(plaintext), padText...)
|
||||||
|
|
||||||
|
// CBC加密
|
||||||
|
ciphertext := make([]byte, len(plaintextBytes))
|
||||||
|
mode := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
mode.CryptBlocks(ciphertext, plaintextBytes)
|
||||||
|
|
||||||
|
// Base64编码
|
||||||
|
return base64.StdEncoding.EncodeToString(ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptSC AES-CBC解密,使用固定的key和iv
|
||||||
|
func DecryptSC(ciphertext string) string {
|
||||||
|
key := []byte(scAESKey)
|
||||||
|
iv := []byte(scAESIV)
|
||||||
|
|
||||||
|
// Base64解码
|
||||||
|
ciphertextBytes, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// CBC解密
|
||||||
|
blockSize := block.BlockSize()
|
||||||
|
if len(ciphertextBytes)%blockSize != 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext := make([]byte, len(ciphertextBytes))
|
||||||
|
mode := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
mode.CryptBlocks(plaintext, ciphertextBytes)
|
||||||
|
|
||||||
|
// 去除PKCS7填充
|
||||||
|
if len(plaintext) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
padding := int(plaintext[len(plaintext)-1])
|
||||||
|
if padding <= 0 || padding > blockSize || padding > len(plaintext) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(plaintext[:len(plaintext)-padding])
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncData 实现与JavaScript版本相同的加密功能
|
||||||
|
// clientToken参数用于替代JavaScript中的cookie值
|
||||||
|
func EncData(data interface{}, clientToken string) map[string]string {
|
||||||
|
// 检查数据是否为空
|
||||||
|
if data == nil || data == "" || data == "{}" || data == " " || data == "null" {
|
||||||
|
return map[string]string{
|
||||||
|
"ciphertext": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取axes_max
|
||||||
|
axesMax := len(clientToken)
|
||||||
|
if axesMax < 4 {
|
||||||
|
axesMax = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建axes
|
||||||
|
axes := creatAxes(axesMax)
|
||||||
|
|
||||||
|
// 替换逗号为连字符
|
||||||
|
aesaxes := strings.ReplaceAll(axes, ",", "-")
|
||||||
|
|
||||||
|
// 生成AES密钥
|
||||||
|
aesKey := encryptKey(axes, clientToken)
|
||||||
|
|
||||||
|
// 将数据转换为JSON字符串
|
||||||
|
var dataStr string
|
||||||
|
switch v := data.(type) {
|
||||||
|
case string:
|
||||||
|
dataStr = v
|
||||||
|
default:
|
||||||
|
jsonBytes, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return map[string]string{
|
||||||
|
"ciphertext": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataStr = string(jsonBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密数据
|
||||||
|
encdata := Encrypt(dataStr, aesKey)
|
||||||
|
|
||||||
|
// 构造密文
|
||||||
|
ciphertext := encdata + "," + aesaxes
|
||||||
|
|
||||||
|
return map[string]string{
|
||||||
|
"ciphertext": ciphertext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DescData 实现与JavaScript版本相同的解密功能
|
||||||
|
// clientToken参数用于替代JavaScript中的cookie值
|
||||||
|
func DescData(JR map[string]interface{}, clientToken string) map[string]interface{} {
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"flag": false,
|
||||||
|
"data": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
if JR == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取flag
|
||||||
|
flag, ok := JR["flag"].(bool)
|
||||||
|
if !ok {
|
||||||
|
flag = false
|
||||||
|
}
|
||||||
|
|
||||||
|
result["flag"] = flag
|
||||||
|
|
||||||
|
// 如果flag为false,直接返回data
|
||||||
|
if !flag {
|
||||||
|
if data, ok := JR["data"]; ok {
|
||||||
|
result["data"] = data
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查key是否存在
|
||||||
|
key, keyExists := JR["key"].(string)
|
||||||
|
if !keyExists || key == "" {
|
||||||
|
if data, ok := JR["data"]; ok {
|
||||||
|
result["data"] = data
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理解密
|
||||||
|
axes := key
|
||||||
|
aesaxes := strings.ReplaceAll(axes, "-", ",")
|
||||||
|
aesKey := encryptKey(aesaxes, clientToken)
|
||||||
|
|
||||||
|
// 获取加密数据
|
||||||
|
outdata, ok := JR["data"].(string)
|
||||||
|
if !ok {
|
||||||
|
result["data"] = ""
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL解码(与JS版本的decodeURIComponent一致)
|
||||||
|
decodedData, err := url.QueryUnescape(outdata)
|
||||||
|
if err != nil {
|
||||||
|
// 如果URL解码失败,使用原始数据
|
||||||
|
decodedData = outdata
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密数据
|
||||||
|
decryptedData := Decrypt(decodedData, aesKey)
|
||||||
|
result["data"] = decryptedData
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// encryptKey 根据axes生成加密密钥
|
||||||
|
func encryptKey(axes string, clientToken string) string {
|
||||||
|
aaxes := strings.Split(axes, ",")
|
||||||
|
key := ""
|
||||||
|
|
||||||
|
for _, axis := range aaxes {
|
||||||
|
axisInt := parseInt(axis)
|
||||||
|
// 确保axisInt不会导致越界
|
||||||
|
if axisInt >= 0 && axisInt < len(clientToken) {
|
||||||
|
// 与JavaScript版本一致:substring(aaxes[i], aaxes[i]*1+4)
|
||||||
|
end := axisInt + 4
|
||||||
|
if end <= len(clientToken) {
|
||||||
|
key += clientToken[axisInt:end]
|
||||||
|
} else {
|
||||||
|
// 如果end超出范围,只取到末尾
|
||||||
|
key += clientToken[axisInt:]
|
||||||
|
// 填充到4个字符
|
||||||
|
for len(key)%4 != 0 {
|
||||||
|
key += "A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果起始位置越界,使用默认值
|
||||||
|
key += "abcd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保密钥长度至少为16字节,如果超过则截断
|
||||||
|
if len(key) < 16 {
|
||||||
|
// 用'A'填充到16字节
|
||||||
|
for len(key) < 16 {
|
||||||
|
key += "A"
|
||||||
|
}
|
||||||
|
} else if len(key) > 16 {
|
||||||
|
// 截断到16字节
|
||||||
|
key = key[:16]
|
||||||
|
}
|
||||||
|
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// creatAxes 创建随机axes
|
||||||
|
func creatAxes(max int) string {
|
||||||
|
if max < 4 {
|
||||||
|
max = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
axes := ""
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
// 生成0到max-4之间的随机数
|
||||||
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(max-4)))
|
||||||
|
if err != nil {
|
||||||
|
// 出错时使用默认值
|
||||||
|
axes += "0,"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
axes += fmt.Sprintf("%d,", n.Int64())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除末尾的逗号
|
||||||
|
if len(axes) > 0 {
|
||||||
|
axes = axes[:len(axes)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return axes
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInt 简单的字符串转整数函数
|
||||||
|
func parseInt(s string) int {
|
||||||
|
var result int
|
||||||
|
fmt.Sscanf(s, "%d", &result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt AES加密函数
|
||||||
|
func Encrypt(data, key string) string {
|
||||||
|
// 确保密钥长度为16字节
|
||||||
|
keyBytes := make([]byte, 16)
|
||||||
|
if len(key) >= 16 {
|
||||||
|
copy(keyBytes, []byte(key)[:16])
|
||||||
|
} else {
|
||||||
|
copy(keyBytes, []byte(key))
|
||||||
|
// 如果密钥不足16字节,用'A'填充
|
||||||
|
for i := len(key); i < 16; i++ {
|
||||||
|
keyBytes[i] = 'A'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建AES密码块
|
||||||
|
block, err := aes.NewCipher(keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// PKCS7填充
|
||||||
|
blockSize := block.BlockSize()
|
||||||
|
padding := blockSize - len(data)%blockSize
|
||||||
|
padText := make([]byte, padding)
|
||||||
|
for i := range padText {
|
||||||
|
padText[i] = byte(padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext := append([]byte(data), padText...)
|
||||||
|
|
||||||
|
// CBC模式加密
|
||||||
|
ciphertext := make([]byte, len(plaintext))
|
||||||
|
// 使用固定的IV(与JavaScript版本一致,使用相同的key作为IV)
|
||||||
|
iv := make([]byte, blockSize)
|
||||||
|
copy(iv, keyBytes[:blockSize])
|
||||||
|
|
||||||
|
mode := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
mode.CryptBlocks(ciphertext, plaintext)
|
||||||
|
|
||||||
|
// Base64编码(使用URL编码兼容CryptoJS)
|
||||||
|
return base64.StdEncoding.EncodeToString(ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt AES解密函数
|
||||||
|
func Decrypt(data, key string) string {
|
||||||
|
// Base64解码
|
||||||
|
ciphertext, err := base64.StdEncoding.DecodeString(data)
|
||||||
|
if err != nil {
|
||||||
|
// 尝试URL解码
|
||||||
|
ciphertext, err = base64.URLEncoding.DecodeString(data)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保密钥长度为16字节
|
||||||
|
keyBytes := make([]byte, 16)
|
||||||
|
if len(key) >= 16 {
|
||||||
|
copy(keyBytes, []byte(key)[:16])
|
||||||
|
} else {
|
||||||
|
copy(keyBytes, []byte(key))
|
||||||
|
// 如果密钥不足16字节,用'A'填充
|
||||||
|
for i := len(key); i < 16; i++ {
|
||||||
|
keyBytes[i] = 'A'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建AES密码块
|
||||||
|
block, err := aes.NewCipher(keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// CBC模式解密
|
||||||
|
blockSize := block.BlockSize()
|
||||||
|
if len(ciphertext)%blockSize != 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用固定的IV(与JavaScript版本一致,使用相同的key作为IV)
|
||||||
|
iv := make([]byte, blockSize)
|
||||||
|
copy(iv, keyBytes[:blockSize])
|
||||||
|
|
||||||
|
mode := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
plaintext := make([]byte, len(ciphertext))
|
||||||
|
mode.CryptBlocks(plaintext, ciphertext)
|
||||||
|
|
||||||
|
// 去除PKCS7填充
|
||||||
|
if len(plaintext) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
padding := int(plaintext[len(plaintext)-1])
|
||||||
|
// 更宽松的填充检查
|
||||||
|
if padding <= 0 || padding > blockSize || padding > len(plaintext) {
|
||||||
|
// 如果填充不合法,返回原始数据
|
||||||
|
return string(plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证填充是否正确(更宽松的检查)
|
||||||
|
validPadding := true
|
||||||
|
for i := len(plaintext) - padding; i < len(plaintext); i++ {
|
||||||
|
if plaintext[i] != byte(padding) {
|
||||||
|
validPadding = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validPadding {
|
||||||
|
// 填充验证失败,返回尽可能多的有效数据
|
||||||
|
return string(plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常去除填充
|
||||||
|
if len(plaintext) <= padding {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(plaintext[:len(plaintext)-padding])
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
var SAFETOCKEN="Safe-Tocken";//for RSA key cookie
|
||||||
|
var encryptAxes = function(axes){
|
||||||
|
RSAUtils.setMaxDigits(1024);
|
||||||
|
var safeTocken = getCookie(SAFETOCKEN);
|
||||||
|
if(safeTocken=="" || safeTocken ==''){
|
||||||
|
throw new Error("Cannot read Cookie '"+SAFETOCKEN+"' values,System need run RSAData() again!");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
var safe_tocken = safeTocken.split(",");
|
||||||
|
var pubexp = safe_tocken[1];
|
||||||
|
var priexp = safe_tocken[0];
|
||||||
|
var modulus = safe_tocken[2];
|
||||||
|
var key = RSAUtils.getKeyPair(safe_tocken[1], safe_tocken[0], safe_tocken[2]);
|
||||||
|
axes = axes.split("").reverse().join("");
|
||||||
|
return RSAUtils.encryptedString(key,axes);
|
||||||
|
}
|
||||||
|
var axesMax = function(){
|
||||||
|
var client_tocken = getCookie(CLIENTTOCKEN);
|
||||||
|
if(client_tocken=="" || client_tocken==''){
|
||||||
|
throw new Error("Cannot read Cookie '"+CLIENTTOCKEN+"' values,System need run RSAData() again!");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
return client_tocken.length;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* decrypt axes use ras key form http back
|
||||||
|
*/
|
||||||
|
var decryptAxes = function(rsakey){
|
||||||
|
RSAUtils.setMaxDigits(1024);
|
||||||
|
var safeTocken = getCookie(SAFETOCKEN);
|
||||||
|
if(safeTocken=="" || safeTocken ==''){
|
||||||
|
throw new Error("Cannot read Cookie '"+SAFETOCKEN+"' values,System need run RSAData() again!");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
var safe_tocken = safeTocken.split(",");
|
||||||
|
var pubexp = safe_tocken[1];
|
||||||
|
var priexp = safe_tocken[0];
|
||||||
|
var modulus = safe_tocken[2];
|
||||||
|
var key = RSAUtils.getKeyPair(safe_tocken[1], safe_tocken[0], safe_tocken[2]);
|
||||||
|
return RSAUtils.decryptedString(key,rsakey);
|
||||||
|
}
|
||||||
|
var CLIENTTOCKEN = "Client-Tocken";
|
||||||
|
/**
|
||||||
|
* Get String key as axes
|
||||||
|
*/
|
||||||
|
var encryptKey = function(axes){
|
||||||
|
//1.get client-tocken
|
||||||
|
//2. realse this axes get this encryptKey
|
||||||
|
var key="";
|
||||||
|
var client_tocken = getCookie(CLIENTTOCKEN);
|
||||||
|
if(client_tocken=="" || client_tocken==''){
|
||||||
|
throw new Error("Cannot read Cookie '"+CLIENTTOCKEN+"' values,System need run RSAData() again!");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
var aaxes = axes.split(",");
|
||||||
|
for(var i=0;i<aaxes.length;i++){
|
||||||
|
key += client_tocken.substring(aaxes[i],aaxes[i]*1+4);
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
var getCookie = function(name){
|
||||||
|
var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
|
||||||
|
if(arr=document.cookie.match(reg))
|
||||||
|
return decodeURIComponent(arr[2]);
|
||||||
|
else
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* get a random axes
|
||||||
|
*/
|
||||||
|
var creatAxes = function(max){
|
||||||
|
var axes = "";
|
||||||
|
for(var i=0;i<4;i++ ){
|
||||||
|
axes += parseInt(Math.random()*(max-4),10)+",";
|
||||||
|
}
|
||||||
|
axes = axes.substring(0,axes.length-1);
|
||||||
|
return axes;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* just encdata for Ajax
|
||||||
|
*/
|
||||||
|
var EncData = function(data){
|
||||||
|
if(data=="" || data =="{}" || data==" " || data==null || data=="null"){
|
||||||
|
ciphertext="";
|
||||||
|
}else{//存在数据
|
||||||
|
//获取aeskey
|
||||||
|
var axes_max = axesMax();
|
||||||
|
var axes = creatAxes(axes_max);
|
||||||
|
var reg=new RegExp(",","g");
|
||||||
|
var aesaxes = axes.replace(reg,'-');
|
||||||
|
var aesKey = encryptKey(axes);
|
||||||
|
data = JSON.stringify(data);
|
||||||
|
encdata = Encrypt(data,aesKey);
|
||||||
|
ciphertext = encdata+","+aesaxes;
|
||||||
|
}
|
||||||
|
return {'ciphertext':ciphertext};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* get data from back http
|
||||||
|
*/
|
||||||
|
var DescData = function(JR){
|
||||||
|
var _data="";
|
||||||
|
if(JR){//data is not empty
|
||||||
|
var flag = JR.flag;
|
||||||
|
if(flag){//成功
|
||||||
|
if(JR.key=="" || JR.key==''){
|
||||||
|
_data = JR.data;
|
||||||
|
}else{//解密数据
|
||||||
|
var axes = JR.key;
|
||||||
|
var reg=new RegExp("-","g");
|
||||||
|
var aesaxes = axes.replace(reg,',');
|
||||||
|
var aesKey = encryptKey(aesaxes);
|
||||||
|
var outdata=decodeURIComponent(JR.data);
|
||||||
|
_data = Decrypt(outdata,aesKey);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
_data = JR.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {'flag':JR.flag,'data':_data};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* post data use as AJAX
|
||||||
|
*/
|
||||||
|
var SubData = function(data,URL){
|
||||||
|
var result="";
|
||||||
|
var ciphertext = EncData(data);
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
cache : false,
|
||||||
|
async:false,
|
||||||
|
url: URL,//传入后台的地址/方法
|
||||||
|
data: ciphertext,//参数,这里是一个json语句
|
||||||
|
success: function (RS) {
|
||||||
|
result = DescData(RS);
|
||||||
|
},
|
||||||
|
error: function (err) {
|
||||||
|
throw new Error("Post data was Error!");
|
||||||
|
return ;
|
||||||
|
},
|
||||||
|
fail:function(){
|
||||||
|
throw new Error("Post data was fail,website need NetWork!");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var Encrypt = function(data,key){
|
||||||
|
var aeskey = CryptoJS.enc.Utf8.parse(key);
|
||||||
|
var aesiv = CryptoJS.enc.Utf8.parse(key);
|
||||||
|
var srcs = CryptoJS.enc.Utf8.parse(data);
|
||||||
|
return CryptoJS.AES.encrypt(srcs, aeskey, { iv: aesiv,mode:CryptoJS.mode.CBC}).toString();
|
||||||
|
}
|
||||||
|
function Decrypt(word,key){
|
||||||
|
var aeskey = CryptoJS.enc.Utf8.parse(key);
|
||||||
|
var aesiv = CryptoJS.enc.Utf8.parse(key);
|
||||||
|
var decrypt = CryptoJS.AES.decrypt(word, aeskey, { iv: aesiv,mode:CryptoJS.mode.CBC});
|
||||||
|
var decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
|
||||||
|
return decryptedStr.toString();
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncdata(t *testing.T) {
|
||||||
|
// 使用与JavaScript版本相同的clientToken
|
||||||
|
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "123456",
|
||||||
|
}
|
||||||
|
encData := EncData(data, clientToken)
|
||||||
|
fmt.Println("EncData result:", encData)
|
||||||
|
|
||||||
|
// 创建一个模拟的JR对象来测试DescData
|
||||||
|
jr := map[string]interface{}{
|
||||||
|
"flag": true,
|
||||||
|
"key": getKeyFromCiphertext(encData["ciphertext"]),
|
||||||
|
"data": getDataFromCiphertext(encData["ciphertext"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
descResult := DescData(jr, clientToken)
|
||||||
|
fmt.Println("DescData result:", descResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecdata(t *testing.T) {
|
||||||
|
// 使用与JavaScript版本相同的clientToken
|
||||||
|
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"flag": true,
|
||||||
|
"data": "YN4tHIF4S3OQgfhTICpFqR425z2sHwCEmlsGdVYt5m6ssyIagtqEvf0hEXdzhc0u",
|
||||||
|
"key": "120-54-61-117",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调试信息
|
||||||
|
fmt.Println("Input data:", data)
|
||||||
|
|
||||||
|
// 手动测试密钥生成
|
||||||
|
axes := "120-54-61-117"
|
||||||
|
aesaxes := strings.ReplaceAll(axes, "-", ",")
|
||||||
|
aesKey := encryptKey(aesaxes, clientToken)
|
||||||
|
fmt.Println("Generated key:", aesKey)
|
||||||
|
fmt.Println("Key length:", len(aesKey))
|
||||||
|
|
||||||
|
// 直接测试解密
|
||||||
|
encryptedData := "YN4tHIF4S3OQgfhTICpFqR425z2sHwCEmlsGdVYt5m6ssyIagtqEvf0hEXdzhc0u"
|
||||||
|
decrypted := Decrypt(encryptedData, aesKey)
|
||||||
|
fmt.Println("Direct decrypt result:", decrypted)
|
||||||
|
fmt.Printf("Direct decrypt result (bytes): %v\n", []byte(decrypted))
|
||||||
|
|
||||||
|
result := DescData(data, clientToken)
|
||||||
|
fmt.Println("TestDecdata result:", result)
|
||||||
|
|
||||||
|
// 检查结果
|
||||||
|
if result["flag"] != true {
|
||||||
|
t.Errorf("Expected flag to be true, got %v", result["flag"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getKeyFromCiphertext 从ciphertext中提取key部分
|
||||||
|
func getKeyFromCiphertext(ciphertext string) string {
|
||||||
|
parts := strings.Split(ciphertext, ",")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
return parts[1]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDataFromCiphertext 从ciphertext中提取data部分
|
||||||
|
func getDataFromCiphertext(ciphertext string) string {
|
||||||
|
parts := strings.Split(ciphertext, ",")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
return parts[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加一个简单的加密/解密测试
|
||||||
|
func TestEncryptDecrypt(t *testing.T) {
|
||||||
|
originalData := `{"username":"admin","password":"123456"}`
|
||||||
|
key := "abcdabcdabcdabcd" // 16字节密钥
|
||||||
|
|
||||||
|
// 加密
|
||||||
|
encrypted := Encrypt(originalData, key)
|
||||||
|
fmt.Println("Encrypted:", encrypted)
|
||||||
|
|
||||||
|
// 解密
|
||||||
|
decrypted := Decrypt(encrypted, key)
|
||||||
|
fmt.Println("Decrypted:", decrypted)
|
||||||
|
|
||||||
|
if decrypted != originalData {
|
||||||
|
t.Errorf("Decrypt failed. Expected: %s, Got: %s", originalData, decrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试特定的axes生成密钥
|
||||||
|
func TestEncryptKey(t *testing.T) {
|
||||||
|
// 使用与JavaScript版本相同的clientToken
|
||||||
|
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
// 测试提供的axes
|
||||||
|
axes := "120,54,61,117"
|
||||||
|
|
||||||
|
key := encryptKey(axes, clientToken)
|
||||||
|
fmt.Println("Test axes:", axes)
|
||||||
|
fmt.Println("Generated key:", key)
|
||||||
|
fmt.Println("Key length:", len(key))
|
||||||
|
|
||||||
|
// 验证每个部分
|
||||||
|
fmt.Println("Client token length:", len(clientToken))
|
||||||
|
|
||||||
|
// 分别计算每个部分
|
||||||
|
parts := strings.Split(axes, ",")
|
||||||
|
for i, part := range parts {
|
||||||
|
axisInt := parseInt(part)
|
||||||
|
fmt.Printf("Part %d: %s -> %d\n", i, part, axisInt)
|
||||||
|
|
||||||
|
if axisInt >= 0 && axisInt < len(clientToken) {
|
||||||
|
end := axisInt + 4
|
||||||
|
if end <= len(clientToken) {
|
||||||
|
fmt.Printf(" Extracted: %s\n", clientToken[axisInt:end])
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Extracted: %s (partial)\n", clientToken[axisInt:])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Out of range, using default\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试具体案例
|
||||||
|
func TestSpecificCase(t *testing.T) {
|
||||||
|
// 使用与JavaScript版本相同的clientToken
|
||||||
|
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
// 使用你提供的具体数据
|
||||||
|
axes := "120,54,61,117"
|
||||||
|
key := encryptKey(axes, clientToken)
|
||||||
|
|
||||||
|
fmt.Println("=== Specific Case Test ===")
|
||||||
|
fmt.Printf("Axes: %s\n", axes)
|
||||||
|
fmt.Printf("Generated Key: %s\n", key)
|
||||||
|
fmt.Printf("Key Length: %d\n", len(key))
|
||||||
|
|
||||||
|
// 测试加密
|
||||||
|
testData := "test data"
|
||||||
|
encrypted := Encrypt(testData, key)
|
||||||
|
fmt.Printf("Encrypted: %s\n", encrypted)
|
||||||
|
|
||||||
|
// 测试解密
|
||||||
|
decrypted := Decrypt(encrypted, key)
|
||||||
|
fmt.Printf("Decrypted: %s\n", decrypted)
|
||||||
|
|
||||||
|
if testData != decrypted {
|
||||||
|
fmt.Println("WARNING: Encryption/Decryption mismatch!")
|
||||||
|
} else {
|
||||||
|
fmt.Println("SUCCESS: Encryption/Decryption works correctly!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试你提供的实际加密数据
|
||||||
|
func TestActualEncryptedData(t *testing.T) {
|
||||||
|
// 使用与JavaScript版本相同的clientToken
|
||||||
|
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
// 你提供的实际加密数据
|
||||||
|
encryptedData := "YN4tHIF4S3OQgfhTICpFqR425z2sHwCEmlsGdVYt5m6ssyIagtqEvf0hEXdzhc0u"
|
||||||
|
|
||||||
|
// 根据axes "120,54,61,117" 生成密钥
|
||||||
|
axes := "120,54,61,117"
|
||||||
|
key := encryptKey(axes, clientToken)
|
||||||
|
|
||||||
|
fmt.Println("=== Actual Encrypted Data Test ===")
|
||||||
|
fmt.Printf("Encrypted Data: %s\n", encryptedData)
|
||||||
|
fmt.Printf("Key: %s\n", key)
|
||||||
|
|
||||||
|
// 尝试解密
|
||||||
|
decrypted := Decrypt(encryptedData, key)
|
||||||
|
fmt.Printf("Decrypted Result: '%s'\n", decrypted)
|
||||||
|
fmt.Printf("Decrypted Length: %d\n", len(decrypted))
|
||||||
|
fmt.Printf("Decrypted Bytes: %v\n", []byte(decrypted))
|
||||||
|
|
||||||
|
// 检查是否是有效的JSON
|
||||||
|
if len(decrypted) > 0 {
|
||||||
|
fmt.Println("Possible JSON data detected")
|
||||||
|
} else {
|
||||||
|
fmt.Println("No data returned from decryption")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最终验证测试
|
||||||
|
func TestFinalVerification(t *testing.T) {
|
||||||
|
// 使用与JavaScript版本相同的clientToken
|
||||||
|
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
fmt.Println("=== Final Verification Test ===")
|
||||||
|
|
||||||
|
// 使用你提供的axes生成密钥
|
||||||
|
axes := "120,54,61,117"
|
||||||
|
key := encryptKey(axes, clientToken)
|
||||||
|
fmt.Printf("Generated key for axes '%s': '%s'\n", axes, key)
|
||||||
|
|
||||||
|
// 你提供的加密数据
|
||||||
|
encryptedData := "YN4tHIF4S3OQgfhTICpFqR425z2sHwCEmlsGdVYt5m6ssyIagtqEvf0hEXdzhc0u"
|
||||||
|
|
||||||
|
// 尝试解密
|
||||||
|
decrypted := Decrypt(encryptedData, key)
|
||||||
|
fmt.Printf("Decrypted result: '%s'\n", decrypted)
|
||||||
|
|
||||||
|
// 如果解密结果为空,尝试不同的方法
|
||||||
|
if len(decrypted) == 0 {
|
||||||
|
fmt.Println("Trying alternative decryption approaches...")
|
||||||
|
|
||||||
|
// 尝试不同的填充处理
|
||||||
|
fmt.Println("Testing raw decryption without padding removal...")
|
||||||
|
// 这里我们可以添加更多的调试信息
|
||||||
|
|
||||||
|
// 输出一些调试信息
|
||||||
|
fmt.Printf("Key bytes: %v\n", []byte(key))
|
||||||
|
fmt.Printf("Encrypted data length: %d\n", len(encryptedData))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Course struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
CourseName string `json:"courseName"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Chapter struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
ChapterName string `json:"chapterName"`
|
||||||
|
SubSection []SubSection `json:"studySubsections"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubSection struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
SubsectionName string `json:"subsectionName"`
|
||||||
|
VideoTime string `json:"videoTime"`
|
||||||
|
StudyFiles StudyFile `json:"studyFiles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StudyFile struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
FileName string `json:"fileName"`
|
||||||
|
FileSize string `json:"fileSize"`
|
||||||
|
FileTime string `json:"fileTime"`
|
||||||
|
FileType string `json:"fileType"`
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
VideoTime string `json:"videoTime"`
|
||||||
|
StudyTime string `json:"studyTime"`
|
||||||
|
State string `json:"state"` // 2是学习中 1是未开始 3是已完成
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package sc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"likedream-goframe/internal/application/smartedu/model"
|
||||||
|
"likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/framework/logger"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/samber/oops"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HomeUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/home"
|
||||||
|
CourseDetailUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/studyCourse/getCourseDetails"
|
||||||
|
RecordProcessUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/studyCourseUser/recordProcess"
|
||||||
|
RefresProcessUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/studyCourseUser/refreshProcess"
|
||||||
|
ApiCourseFinishedHour = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/studyCourse/getCourseFinishedHour"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Course struct {
|
||||||
|
Client *resty.Client
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCourse(token string, client *resty.Client) *Course {
|
||||||
|
return &Course{
|
||||||
|
Token: token,
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Course) GetCourseList(ctx context.Context) []model.Course {
|
||||||
|
jsUrl := c.GetJsUrl(ctx, HomeUrl)
|
||||||
|
get, err := c.Client.R().Get(jsUrl)
|
||||||
|
utils.OkOrPanic(err, oops.WithContext(ctx).With("jsUrl", jsUrl).Wrap)
|
||||||
|
jsonString := ParseJsonString(get.String())
|
||||||
|
returnData := gjson.Get(jsonString, "returnData").Array()
|
||||||
|
var courseList []model.Course
|
||||||
|
for _, one := range returnData {
|
||||||
|
courseList = append(courseList, model.Course{
|
||||||
|
Id: one.Get("id").String(),
|
||||||
|
CourseName: one.Get("courseName").String(),
|
||||||
|
Image: one.Get("studyFiles.filePath").String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return courseList
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Course) GetChapterList(ctx context.Context, courseId string) []model.Chapter {
|
||||||
|
get, err := c.Client.R().SetQueryParam("courseId", courseId).SetHeader("X-Token", c.Token).Get(CourseDetailUrl)
|
||||||
|
utils.OkOrPanic(err, oops.WithContext(ctx).With("GetChapterList", courseId).Wrap)
|
||||||
|
chapterJson := gjson.Get(get.String(), "returnData.chapters").Array()
|
||||||
|
var chapterList []model.Chapter
|
||||||
|
for _, ch := range chapterJson {
|
||||||
|
chapter := model.Chapter{
|
||||||
|
Id: ch.Get("id").String(),
|
||||||
|
ChapterName: ch.Get("chapterName").String(),
|
||||||
|
}
|
||||||
|
subSectionJson := ch.Get("studySubsections").Array()
|
||||||
|
var subSectionList []model.SubSection
|
||||||
|
for _, sec := range subSectionJson {
|
||||||
|
studyFileJson := sec.Get("studyFiles")
|
||||||
|
studyFile := model.StudyFile{
|
||||||
|
Id: studyFileJson.Get("id").String(),
|
||||||
|
FileName: studyFileJson.Get("fileName").String(),
|
||||||
|
FileSize: studyFileJson.Get("fileSize").String(),
|
||||||
|
FileTime: studyFileJson.Get("fileTime").String(),
|
||||||
|
FileType: studyFileJson.Get("fileType").String(),
|
||||||
|
FilePath: studyFileJson.Get("filePath").String(),
|
||||||
|
VideoTime: studyFileJson.Get("videoTime").String(),
|
||||||
|
State: studyFileJson.Get("state").String(),
|
||||||
|
StudyTime: studyFileJson.Get("studyTime").String(),
|
||||||
|
}
|
||||||
|
subSection := model.SubSection{
|
||||||
|
Id: sec.Get("id").String(),
|
||||||
|
SubsectionName: sec.Get("subsectionName").String(),
|
||||||
|
VideoTime: sec.Get("videoTime").String(),
|
||||||
|
StudyFiles: studyFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
subSectionList = append(subSectionList, subSection)
|
||||||
|
}
|
||||||
|
chapter.SubSection = subSectionList
|
||||||
|
chapterList = append(chapterList, chapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapterList
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseJsonString(str string) string {
|
||||||
|
// eval("module.exports = /*#__PURE__*/JSON.parse('{...}');\n\n//# sourceURL=...
|
||||||
|
re := regexp.MustCompile(`#__PURE__\*/JSON\.parse\('(.+?)'\);`)
|
||||||
|
matches := re.FindStringSubmatch(str)
|
||||||
|
if len(matches) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// 处理转义的引号
|
||||||
|
return strings.ReplaceAll(matches[1], `\"`, `"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Course) GetJsUrl(ctx context.Context, url string) string {
|
||||||
|
get, err := c.Client.R().Get(url)
|
||||||
|
utils.OkOrPanic(err, oops.WithContext(ctx).With("GetJsUrl", url).Wrap)
|
||||||
|
return ParseJsUrl(get.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Course) Study(ctx context.Context, c2 model.Course, ch model.Chapter, sec model.SubSection) {
|
||||||
|
// 如果已完成
|
||||||
|
if sec.StudyFiles.State == "3" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := map[string]any{
|
||||||
|
"chapterId": ch.Id,
|
||||||
|
"courseId": c2.Id,
|
||||||
|
"subsectionId": sec.Id,
|
||||||
|
}
|
||||||
|
if sec.StudyFiles.State == "1" {
|
||||||
|
form["state"] = "1"
|
||||||
|
c.Refresh(ctx, c2, ch, sec)
|
||||||
|
form["studyTime"] = 5
|
||||||
|
// 如果state为
|
||||||
|
post, err := c.Client.R().SetHeader("X-Token", c.Token).SetBody(form).Post(RecordProcessUrl)
|
||||||
|
utils.OkOrPanic(err, oops.WithContext(ctx).With("Study", form).Wrap)
|
||||||
|
str := post.String()
|
||||||
|
if gjson.Get(str, "returnCode").String() != "200" {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "学习失败,"+gjson.Get(str, "returnMessage").String()))
|
||||||
|
}
|
||||||
|
logger.Logger.Info("学习成功", zap.String("课程名", c2.CourseName), zap.String("章节名", ch.ChapterName), zap.String("小节名", sec.SubsectionName))
|
||||||
|
}
|
||||||
|
c.Refresh(ctx, c2, ch, sec)
|
||||||
|
atoi, _ := strconv.Atoi(sec.StudyFiles.VideoTime)
|
||||||
|
form["state"] = "3"
|
||||||
|
form["studyTime"] = atoi
|
||||||
|
// 如果state为
|
||||||
|
post, err := c.Client.R().SetHeader("X-Token", c.Token).SetBody(form).Post(RecordProcessUrl)
|
||||||
|
utils.OkOrPanic(err, oops.WithContext(ctx).With("Study", form).Wrap)
|
||||||
|
str := post.String()
|
||||||
|
if gjson.Get(str, "returnCode").String() != "200" {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "学习失败,"+gjson.Get(str, "returnMessage").String()))
|
||||||
|
}
|
||||||
|
logger.Logger.Info("学习成功", zap.String("课程名", c2.CourseName), zap.String("章节名", ch.ChapterName), zap.String("小节名", sec.SubsectionName))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Course) Refresh(ctx context.Context, c2 model.Course, ch model.Chapter, sec model.SubSection) {
|
||||||
|
form := map[string]any{
|
||||||
|
"chapterId": ch.Id,
|
||||||
|
"courseId": c2.Id,
|
||||||
|
"subsectionId": sec.Id,
|
||||||
|
"studyTime": 0,
|
||||||
|
}
|
||||||
|
post, err := c.Client.R().SetHeader("X-Token", c.Token).SetBody(form).Post(RefresProcessUrl)
|
||||||
|
utils.OkOrPanic(err, oops.WithContext(ctx).With("Refresh", form).Wrap)
|
||||||
|
str := post.String()
|
||||||
|
if gjson.Get(str, "returnCode").String() != "200" {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "刷新嫉妒失败,"+gjson.Get(str, "returnMessage").String()))
|
||||||
|
}
|
||||||
|
logger.Logger.Info("刷新成功", zap.String("课程名", c2.CourseName), zap.String("章节名", ch.ChapterName), zap.String("小节名", sec.SubsectionName))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseJsUrl(str string) string {
|
||||||
|
// <script defer src="https://cdnbasic.scbdc.edu.cn/%E7%9C%81%E7%BA%A7%E8%B5%84%E6%BA%90/hd/chunk-vendors.5e79f5ce155849.js">
|
||||||
|
re := regexp.MustCompile(`https://[^"'\s]+chunk-vendors[^"'\s]+\.js`)
|
||||||
|
match := re.FindString(str)
|
||||||
|
if match == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Replace(match, "chunk-vendors", "src_views_home_index_vue", 1)
|
||||||
|
}
|
||||||
|
func (c *Course) GetCourseProcees(ctx context.Context, courseId string) (finished string, total string) {
|
||||||
|
post, err := c.Client.R().SetHeader("X-Token", c.Token).SetBody(map[string]any{
|
||||||
|
"courseIds": []string{courseId},
|
||||||
|
}).Post(ApiCourseFinishedHour)
|
||||||
|
utils.OkOrPanic(err, oops.WithContext(ctx).With("GetCourseProcees", courseId).Wrap)
|
||||||
|
str := gjson.Get(post.String(), "returnData.finishedHours").String()
|
||||||
|
split := strings.Split(str, ",")
|
||||||
|
return split[0], split[1]
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package sc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"likedream-goframe/internal/application/cookiejar"
|
||||||
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
"likedream-goframe/internal/application/smartedu/cryptoutil"
|
||||||
|
"likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/gogf/gf/v2/container/gvar"
|
||||||
|
"github.com/samber/oops"
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LoginPageUrl = "https://basic.sc.smartedu.cn/ThirdPortalService/user/otherlogin!login.ac?appkey=C56DA16ECBC56FBEEC908DA09E45C72C917A80118F057FA1F0B5BAE41CC9CC9DECD5BDB7133FE17C328C5D37B37CA8E7&pkey=5D79CA42E45C5273DF8532D09E1F158B15E25919CDB958940F84D5E63F5F53A1ECD5BDB7133FE17C328C5D37B37CA8E7¶ms=718F83A5347CBFDB7D1A9065FA090FE949D92330BB9A3351FE0715C5B8A3E86F37916C1004E835C7C7F964E3F301477F7D37F04485FA8707845DAAA23356236ED1D326CF5A5E3C263470516EE9B4A2ED"
|
||||||
|
LoginActionUrl = "https://basic.sc.smartedu.cn/ThirdPortalService/login!login.ac"
|
||||||
|
LoginTicketUrl = "https://basic.sc.smartedu.cn/ThirdPortalService/login!getTicket.ac"
|
||||||
|
LoginOOSUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/sso/oauth/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Login struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Client *resty.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogin(username, password string) *Login {
|
||||||
|
return &Login{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
Client: httpclient.NewSmartEdu(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (l *Login) GetCookieValue(key string) string {
|
||||||
|
cookies := l.Client.CookieJar().(*cookiejar.AllSavePersistCookieJar)
|
||||||
|
for _, cookie := range cookies.Cookies(nil) {
|
||||||
|
if cookie.Name == key {
|
||||||
|
return cookie.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func (l *Login) GetClient() *resty.Client {
|
||||||
|
return l.Client
|
||||||
|
}
|
||||||
|
func (l *Login) Login(ctx context.Context) string {
|
||||||
|
form := l.getLoginInput(ctx)
|
||||||
|
form["loginName"] = gvar.New(l.Username)
|
||||||
|
form["password"] = gvar.New(l.Password)
|
||||||
|
token := l.GetCookieValue("Client-Tocken")
|
||||||
|
data := cryptoutil.EncData(form, token)
|
||||||
|
post, err := l.Client.R().SetFormData(data).Post(LoginActionUrl)
|
||||||
|
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginActionUrl", LoginActionUrl).Wrap)
|
||||||
|
descData := cryptoutil.DescData(utils.ReadJson[map[string]interface{}](post.Body), token)
|
||||||
|
result := descData["data"].(string)
|
||||||
|
if !strings.Contains(result, `"Flag":0`) {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "登录失败"))
|
||||||
|
}
|
||||||
|
get, err := l.Client.R().Get(LoginTicketUrl)
|
||||||
|
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginTicketUrl", LoginTicketUrl).Wrap)
|
||||||
|
return get.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Login) OOSLogin(ctx context.Context, ticket string) string {
|
||||||
|
post, err := l.Client.R().SetQueryParam("ticket", ticket).Post(LoginOOSUrl)
|
||||||
|
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginOOSUrl", LoginOOSUrl).Wrap)
|
||||||
|
defer post.Body.Close()
|
||||||
|
json := utils.ReadJson[map[string]string](post.Body)
|
||||||
|
if json["returnCode"] != "200" {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, json["returnMessage"]))
|
||||||
|
}
|
||||||
|
return json["returnData"]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Login) getLoginInput(ctx context.Context) map[string]*gvar.Var {
|
||||||
|
get, err := l.Client.R().WithContext(ctx).Get(LoginPageUrl)
|
||||||
|
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginPageUrl", LoginPageUrl).Wrap)
|
||||||
|
defer get.Body.Close()
|
||||||
|
reader, err := goquery.NewDocumentFromReader(get.Body)
|
||||||
|
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginPageUrl", LoginPageUrl).Wrap)
|
||||||
|
find := reader.Find("#loginContent input")
|
||||||
|
var inputMap = make(map[string]*gvar.Var)
|
||||||
|
find.Each(func(i int, input *goquery.Selection) {
|
||||||
|
key, exists := input.Attr("name")
|
||||||
|
if exists {
|
||||||
|
val, _ := input.Attr("value")
|
||||||
|
inputMap[key] = gvar.New(val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return inputMap
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package sc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogin(t *testing.T) {
|
||||||
|
login := NewLogin("13551503012", "XIAOzhi520")
|
||||||
|
login.Login(context.Background())
|
||||||
|
login.GetClient()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStudy(t *testing.T) {
|
||||||
|
login := NewLogin("13551503012", "XIAOzhi520")
|
||||||
|
//ticket := login.Login(context.Background())
|
||||||
|
//token := login.OOSLogin(context.Background(), ticket)
|
||||||
|
client := login.GetClient()
|
||||||
|
course := NewCourse("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkZXZpY2VUeXBlIjoid2ViIiwibG9naW5UeXBlIjoidXNlcnJlYWxtIiwiZXhwIjoxNzY1MjU3NDI1LCJ1c2VySWQiOiIxOTk4MDI1NjQ1NzU5ODM2MTYwIn0.PL5Roy0t9v0mK-ucK73IZg2qTZfw0YhW1sVxG1SYetU", client)
|
||||||
|
courseList := course.GetCourseList(context.Background())
|
||||||
|
for _, c := range courseList {
|
||||||
|
chapterList := course.GetChapterList(context.Background(), c.Id)
|
||||||
|
for _, ch := range chapterList {
|
||||||
|
for _, sec := range ch.SubSection {
|
||||||
|
course.Study(context.Background(), c, ch, sec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetProcess(t *testing.T) {
|
||||||
|
login := NewLogin("13551503012", "XIAOzhi520")
|
||||||
|
//ticket := login.Login(context.Background())
|
||||||
|
//token := login.OOSLogin(context.Background(), ticket)
|
||||||
|
client := login.GetClient()
|
||||||
|
course := NewCourse("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkZXZpY2VUeXBlIjoid2ViIiwibG9naW5UeXBlIjoidXNlcnJlYWxtIiwiZXhwIjoxNzY1MjU3NDI1LCJ1c2VySWQiOiIxOTk4MDI1NjQ1NzU5ODM2MTYwIn0.PL5Roy0t9v0mK-ucK73IZg2qTZfw0YhW1sVxG1SYetU", client)
|
||||||
|
courseList := course.GetCourseList(context.Background())
|
||||||
|
for _, c := range courseList {
|
||||||
|
finished, total := course.GetCourseProcees(context.Background(), c.Id)
|
||||||
|
t.Log(finished, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package course
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
v1 "likedream-goframe/internal/api/v1"
|
||||||
|
"likedream-goframe/internal/application/smartedu/model"
|
||||||
|
"likedream-goframe/internal/ent"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ISmartEduSCCtrlV1 interface {
|
||||||
|
List(ctx context.Context, req *v1.SmartEduSCListReq) (*utils.Response[[]*ent.CourseEntity], error)
|
||||||
|
QueryCourses(ctx context.Context, req *v1.SmartEduSCQueryCourseReq) (*utils.Response[[]model.Course], error)
|
||||||
|
AddCourse(ctx context.Context, req *v1.SmartEduSCAddCourseReq) (*utils.Response[any], error)
|
||||||
|
DeleteCourse(ctx context.Context, req *v1.SmartEduSCDeleteReq) (*utils.Response[any], error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package course
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
v1 "likedream-goframe/internal/api/v1"
|
||||||
|
"likedream-goframe/internal/application/smartedu/model"
|
||||||
|
"likedream-goframe/internal/ent"
|
||||||
|
"likedream-goframe/internal/service"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SmartEduSCCtrlV1 struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取四川智慧教育刷课列表
|
||||||
|
func (c SmartEduSCCtrlV1) List(ctx context.Context, req *v1.SmartEduSCListReq) (res *utils.Response[[]*ent.CourseEntity], err error) {
|
||||||
|
defer utils.Pass(&err)
|
||||||
|
return service.Global.SmartEduSC.List(ctx, req), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryCourses 查询四川智慧教育课程列表
|
||||||
|
func (c SmartEduSCCtrlV1) QueryCourses(ctx context.Context, req *v1.SmartEduSCQueryCourseReq) (res *utils.Response[[]model.Course], err error) {
|
||||||
|
defer utils.Pass(&err)
|
||||||
|
return service.Global.SmartEduSC.QueryCourses(ctx, req), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCourse 添加四川智慧教育刷课任务
|
||||||
|
func (c SmartEduSCCtrlV1) AddCourse(ctx context.Context, req *v1.SmartEduSCAddCourseReq) (res *utils.Response[any], err error) {
|
||||||
|
defer utils.Pass(&err)
|
||||||
|
return service.Global.SmartEduSC.AddCourse(ctx, req), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCourse 删除四川智慧教育刷课任务
|
||||||
|
func (c SmartEduSCCtrlV1) DeleteCourse(ctx context.Context, req *v1.SmartEduSCDeleteReq) (res *utils.Response[any], err error) {
|
||||||
|
defer utils.Pass(&err)
|
||||||
|
return service.Global.SmartEduSC.DeleteCourse(ctx, req), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSmartEduSCCtrlV1() ISmartEduSCCtrlV1 {
|
||||||
|
return &SmartEduSCCtrlV1{}
|
||||||
|
}
|
||||||
@@ -117,7 +117,8 @@ type Type string
|
|||||||
|
|
||||||
// Type values.
|
// Type values.
|
||||||
const (
|
const (
|
||||||
TypeXxt Type = "xxt"
|
TypeXxt Type = "xxt"
|
||||||
|
TypeSmarteduSc Type = "smartedu_sc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (_type Type) String() string {
|
func (_type Type) String() string {
|
||||||
@@ -127,7 +128,7 @@ func (_type Type) String() string {
|
|||||||
// TypeValidator is a validator for the "type" field enum values. It is called by the builders before save.
|
// TypeValidator is a validator for the "type" field enum values. It is called by the builders before save.
|
||||||
func TypeValidator(_type Type) error {
|
func TypeValidator(_type Type) error {
|
||||||
switch _type {
|
switch _type {
|
||||||
case TypeXxt:
|
case TypeXxt, TypeSmarteduSc:
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("courseentity: invalid enum value for type field: %q", _type)
|
return fmt.Errorf("courseentity: invalid enum value for type field: %q", _type)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ var (
|
|||||||
{Name: "update_time", Type: field.TypeString, SchemaType: map[string]string{"mysql": "datetime", "postgres": "timestamp", "sqlite3": "datetime"}},
|
{Name: "update_time", Type: field.TypeString, SchemaType: map[string]string{"mysql": "datetime", "postgres": "timestamp", "sqlite3": "datetime"}},
|
||||||
{Name: "username", Type: field.TypeString},
|
{Name: "username", Type: field.TypeString},
|
||||||
{Name: "password", Type: field.TypeString},
|
{Name: "password", Type: field.TypeString},
|
||||||
{Name: "type", Type: field.TypeEnum, Enums: []string{"xxt"}},
|
{Name: "type", Type: field.TypeEnum, Enums: []string{"xxt", "smartedu_sc"}},
|
||||||
{Name: "course_name", Type: field.TypeString},
|
{Name: "course_name", Type: field.TypeString},
|
||||||
{Name: "course_id", Type: field.TypeString},
|
{Name: "course_id", Type: field.TypeString},
|
||||||
{Name: "need", Type: field.TypeEnum, Enums: []string{"task", "exam", "all"}},
|
{Name: "need", Type: field.TypeEnum, Enums: []string{"task", "exam", "all"}},
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func (CourseEntity) Fields() []ent.Field {
|
|||||||
field.Int64("user_id"),
|
field.Int64("user_id"),
|
||||||
field.String("username"), // 账号
|
field.String("username"), // 账号
|
||||||
field.String("password").Sensitive(), //密码
|
field.String("password").Sensitive(), //密码
|
||||||
field.Enum("type").Values("xxt"), // 类型:目前只有学习通
|
field.Enum("type").Values("xxt", "smartedu_sc"), // 类型:学习通、四川智慧教育
|
||||||
field.String("course_name"), // 课程名称
|
field.String("course_name"), // 课程名称
|
||||||
field.String("course_id"), // 课程ID
|
field.String("course_id"), // 课程ID
|
||||||
field.Enum("need").Values("task", "exam", "all"), // 需要刷的内容:任务点、考试、全部
|
field.Enum("need").Values("task", "exam", "all"), // 需要刷的内容:任务点、考试、全部
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
package course
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
v1 "likedream-goframe/internal/api/v1"
|
||||||
|
"likedream-goframe/internal/application/smartedu/model"
|
||||||
|
"likedream-goframe/internal/application/smartedu/sc"
|
||||||
|
db2 "likedream-goframe/internal/common/db"
|
||||||
|
errors2 "likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/ent"
|
||||||
|
"likedream-goframe/internal/ent/courseentity"
|
||||||
|
"likedream-goframe/internal/ent/sysuser"
|
||||||
|
"likedream-goframe/internal/framework/logger"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SmartEduSCServiceV1 struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取四川智慧教育刷课列表
|
||||||
|
func (s SmartEduSCServiceV1) List(ctx context.Context, req *v1.SmartEduSCListReq) *utils.Response[[]*ent.CourseEntity] {
|
||||||
|
user := utils.GetUserInfoFromCtx(ctx)
|
||||||
|
query := db2.Global.CourseEntity.Query().
|
||||||
|
Where(
|
||||||
|
courseentity.HasSysUserWith(sysuser.ID(user.ID)),
|
||||||
|
courseentity.TypeEQ(courseentity.TypeSmarteduSc),
|
||||||
|
).
|
||||||
|
Order(ent.Desc(courseentity.FieldCreateTime))
|
||||||
|
|
||||||
|
if req.Username != "" {
|
||||||
|
query.Where(courseentity.UsernameContains(req.Username))
|
||||||
|
}
|
||||||
|
if req.Status != "" {
|
||||||
|
query.Where(courseentity.StatusEQ(courseentity.Status(req.Status)))
|
||||||
|
}
|
||||||
|
|
||||||
|
total := query.CountX(ctx)
|
||||||
|
var data []*ent.CourseEntity
|
||||||
|
if total > 0 {
|
||||||
|
query.Limit(req.Size).Offset((req.Page - 1) * req.Size)
|
||||||
|
data = query.AllX(ctx)
|
||||||
|
for _, item := range data {
|
||||||
|
item.Password = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return utils.SuccessWithTotal[[]*ent.CourseEntity](data, total)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryCourses 查询四川智慧教育课程列表
|
||||||
|
func (s SmartEduSCServiceV1) QueryCourses(ctx context.Context, req *v1.SmartEduSCQueryCourseReq) *utils.Response[[]model.Course] {
|
||||||
|
login := sc.NewLogin(req.Username, req.Password)
|
||||||
|
ticket := login.Login(ctx)
|
||||||
|
token := login.OOSLogin(ctx, ticket)
|
||||||
|
client := login.GetClient()
|
||||||
|
|
||||||
|
courseService := sc.NewCourse(token, client)
|
||||||
|
courses := courseService.GetCourseList(ctx)
|
||||||
|
|
||||||
|
return utils.SuccessWithData[[]model.Course](courses)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCourse 添加四川智慧教育刷课任务
|
||||||
|
func (s SmartEduSCServiceV1) AddCourse(ctx context.Context, req *v1.SmartEduSCAddCourseReq) *utils.Response[any] {
|
||||||
|
user := utils.GetUserInfoFromCtx(ctx)
|
||||||
|
|
||||||
|
// 检查是否已存在相同任务
|
||||||
|
exists := db2.Global.CourseEntity.Query().
|
||||||
|
Where(
|
||||||
|
courseentity.HasSysUserWith(sysuser.ID(user.ID)),
|
||||||
|
courseentity.Username(req.Username),
|
||||||
|
courseentity.CourseID(req.CourseID),
|
||||||
|
courseentity.TypeEQ(courseentity.TypeSmarteduSc),
|
||||||
|
).
|
||||||
|
ExistX(ctx)
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
utils.OkOrPanic(errors2.NewBusinessError(400, "该课程已存在刷课任务"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录验证并获取课程信息
|
||||||
|
login := sc.NewLogin(req.Username, req.Password)
|
||||||
|
ticket := login.Login(ctx)
|
||||||
|
token := login.OOSLogin(ctx, ticket)
|
||||||
|
client := login.GetClient()
|
||||||
|
|
||||||
|
courseService := sc.NewCourse(token, client)
|
||||||
|
courseList := courseService.GetCourseList(ctx)
|
||||||
|
|
||||||
|
// 验证课程是否存在
|
||||||
|
var targetCourse *model.Course
|
||||||
|
for _, c := range courseList {
|
||||||
|
if c.Id == req.CourseID {
|
||||||
|
targetCourse = &c
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if targetCourse == nil {
|
||||||
|
utils.OkOrPanic(errors2.NewBusinessError(400, "未找到该课程"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建数据库记录
|
||||||
|
entity := db2.Global.CourseEntity.Create().
|
||||||
|
SetUserID(user.ID).
|
||||||
|
SetUsername(req.Username).
|
||||||
|
SetPassword(req.Password).
|
||||||
|
SetType(courseentity.TypeSmarteduSc).
|
||||||
|
SetCourseName(req.CourseName).
|
||||||
|
SetCourseID(req.CourseID).
|
||||||
|
SetNeed(courseentity.NeedTask).
|
||||||
|
SetViolence(courseentity.ViolenceYes).
|
||||||
|
SetNillableDesc(&req.Desc).
|
||||||
|
SetStatus(courseentity.StatusDoing).
|
||||||
|
SetTaskCount(0).
|
||||||
|
SetSuccessCount(0).
|
||||||
|
SaveX(ctx)
|
||||||
|
|
||||||
|
// 开启协程执行秒刷
|
||||||
|
go s.executeStudy(context.Background(), entity.ID, req.Username, req.Password, req.CourseID)
|
||||||
|
|
||||||
|
return utils.SuccessWithMsg("任务已提交,正在刷课中")
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeStudy 执行刷课(协程中运行)
|
||||||
|
func (s SmartEduSCServiceV1) executeStudy(ctx context.Context, entityID int64, username, password, courseID string) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Logger.Error("刷课执行异常", zap.Any("error", r), zap.Int64("entityID", entityID))
|
||||||
|
db2.Global.CourseEntity.UpdateOneID(entityID).
|
||||||
|
SetStatus(courseentity.StatusFail).
|
||||||
|
SaveX(ctx)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
login := sc.NewLogin(username, password)
|
||||||
|
ticket := login.Login(ctx)
|
||||||
|
token := login.OOSLogin(ctx, ticket)
|
||||||
|
client := login.GetClient()
|
||||||
|
|
||||||
|
courseService := sc.NewCourse(token, client)
|
||||||
|
|
||||||
|
courseList := courseService.GetCourseList(ctx)
|
||||||
|
var targetCourse *model.Course
|
||||||
|
for _, c := range courseList {
|
||||||
|
if c.Id == courseID {
|
||||||
|
targetCourse = &c
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if targetCourse == nil {
|
||||||
|
logger.Logger.Error("未找到课程", zap.String("courseID", courseID))
|
||||||
|
db2.Global.CourseEntity.UpdateOneID(entityID).
|
||||||
|
SetStatus(courseentity.StatusFail).
|
||||||
|
SaveX(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chapterList := courseService.GetChapterList(ctx, courseID)
|
||||||
|
taskCount := 0
|
||||||
|
successCount := 0
|
||||||
|
|
||||||
|
for _, ch := range chapterList {
|
||||||
|
taskCount += len(ch.SubSection)
|
||||||
|
}
|
||||||
|
|
||||||
|
db2.Global.CourseEntity.UpdateOneID(entityID).
|
||||||
|
SetTaskCount(taskCount).
|
||||||
|
SaveX(ctx)
|
||||||
|
|
||||||
|
for _, ch := range chapterList {
|
||||||
|
for _, sec := range ch.SubSection {
|
||||||
|
courseService.Study(ctx, *targetCourse, ch, sec)
|
||||||
|
successCount++
|
||||||
|
db2.Global.CourseEntity.UpdateOneID(entityID).
|
||||||
|
SetSuccessCount(successCount).
|
||||||
|
SaveX(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db2.Global.CourseEntity.UpdateOneID(entityID).
|
||||||
|
SetStatus(courseentity.StatusDone).
|
||||||
|
SaveX(ctx)
|
||||||
|
|
||||||
|
logger.Logger.Info("刷课完成",
|
||||||
|
zap.String("username", username),
|
||||||
|
zap.String("courseID", courseID),
|
||||||
|
zap.Int("taskCount", taskCount),
|
||||||
|
zap.Int("successCount", successCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCourse 删除四川智慧教育刷课任务
|
||||||
|
func (s SmartEduSCServiceV1) DeleteCourse(ctx context.Context, req *v1.SmartEduSCDeleteReq) *utils.Response[any] {
|
||||||
|
user := utils.GetUserInfoFromCtx(ctx)
|
||||||
|
|
||||||
|
course := db2.Global.CourseEntity.Query().
|
||||||
|
Where(
|
||||||
|
courseentity.ID(req.ID),
|
||||||
|
courseentity.HasSysUserWith(sysuser.ID(user.ID)),
|
||||||
|
courseentity.TypeEQ(courseentity.TypeSmarteduSc),
|
||||||
|
).
|
||||||
|
OnlyX(ctx)
|
||||||
|
|
||||||
|
if course == nil {
|
||||||
|
utils.OkOrPanic(errors2.NewBusinessError(400, "任务不存在或无权限操作"))
|
||||||
|
}
|
||||||
|
|
||||||
|
db2.Global.CourseEntity.DeleteOne(course).ExecX(ctx)
|
||||||
|
|
||||||
|
return utils.SuccessWithMsg("删除成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSmartEduSCServiceV1() *SmartEduSCServiceV1 {
|
||||||
|
return &SmartEduSCServiceV1{}
|
||||||
|
}
|
||||||
+34
-32
@@ -15,39 +15,41 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type globalService struct {
|
type globalService struct {
|
||||||
UserLogin *user_login.UserLoginServiceV1
|
UserLogin *user_login.UserLoginServiceV1
|
||||||
UserInfo *user_login.UserInfoServiceV1
|
UserInfo *user_login.UserInfoServiceV1
|
||||||
Role *role.RoleServiceV1
|
Role *role.RoleServiceV1
|
||||||
Menu *menu.MenuServiceV1
|
Menu *menu.MenuServiceV1
|
||||||
Agent *agent.AgentServiceV1
|
Agent *agent.AgentServiceV1
|
||||||
Email *email.EmailServiceV1
|
Email *email.EmailServiceV1
|
||||||
User *user.UserServiceV1
|
User *user.UserServiceV1
|
||||||
CpUser *cpdaily.CpUserServiceV1
|
CpUser *cpdaily.CpUserServiceV1
|
||||||
CpTask *cpdaily.CpTaskServiceV1
|
CpTask *cpdaily.CpTaskServiceV1
|
||||||
CpImage *cpdaily.CpImageServiceV1
|
CpImage *cpdaily.CpImageServiceV1
|
||||||
CpTaskLog *cpdaily.CpTaskLogServiceV1
|
CpTaskLog *cpdaily.CpTaskLogServiceV1
|
||||||
CpLeave *cpdaily.CpLeaveServiceV1
|
CpLeave *cpdaily.CpLeaveServiceV1
|
||||||
Card *card.CardServiceV1
|
Card *card.CardServiceV1
|
||||||
Course *course.CourseServiceV1
|
Course *course.CourseServiceV1
|
||||||
Config *config.ConfigServiceV1
|
SmartEduSC *course.SmartEduSCServiceV1
|
||||||
Balance *balance.BalanceRecordServiceV1
|
Config *config.ConfigServiceV1
|
||||||
|
Balance *balance.BalanceRecordServiceV1
|
||||||
}
|
}
|
||||||
|
|
||||||
var Global = globalService{
|
var Global = globalService{
|
||||||
UserLogin: user_login.NewV1(),
|
UserLogin: user_login.NewV1(),
|
||||||
UserInfo: user_login.NewUserInfoServiceV1(),
|
UserInfo: user_login.NewUserInfoServiceV1(),
|
||||||
Role: role.NewV1(),
|
Role: role.NewV1(),
|
||||||
Menu: menu.NewV1(),
|
Menu: menu.NewV1(),
|
||||||
Agent: agent.NewV1(),
|
Agent: agent.NewV1(),
|
||||||
Email: email.NewV1(),
|
Email: email.NewV1(),
|
||||||
User: user.NewV1(),
|
User: user.NewV1(),
|
||||||
CpUser: cpdaily.NewCpUserServiceV1(),
|
CpUser: cpdaily.NewCpUserServiceV1(),
|
||||||
CpTask: cpdaily.NewCpTaskServiceV1(),
|
CpTask: cpdaily.NewCpTaskServiceV1(),
|
||||||
CpImage: cpdaily.NewCpImageServiceV1(),
|
CpImage: cpdaily.NewCpImageServiceV1(),
|
||||||
CpTaskLog: cpdaily.NewCpTaskLogServiceV1(),
|
CpTaskLog: cpdaily.NewCpTaskLogServiceV1(),
|
||||||
CpLeave: cpdaily.NewCpLeaveServiceV1(),
|
CpLeave: cpdaily.NewCpLeaveServiceV1(),
|
||||||
Card: card.NewCardServiceV1(),
|
Card: card.NewCardServiceV1(),
|
||||||
Course: course.NewCourseServiceV1(),
|
Course: course.NewCourseServiceV1(),
|
||||||
Config: config.NewV1(),
|
SmartEduSC: course.NewSmartEduSCServiceV1(),
|
||||||
Balance: balance.NewBalanceRecordServiceV1(),
|
Config: config.NewV1(),
|
||||||
|
Balance: balance.NewBalanceRecordServiceV1(),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user