Compare commits
132 Commits
v0.0.27-dev
...
v1.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 98eb15f4e4 | |||
| 3a2e679b85 | |||
| 921192a914 | |||
| 107b472cf7 | |||
| 14108f6ba3 | |||
| a22f559889 | |||
| ececfa344d | |||
| 5ba9e38e1a | |||
| 6eb4bc6d8a | |||
| 17e6c0a789 | |||
| f067ed2ad0 | |||
| 7bfc7a58ab | |||
| 1ad0c285f2 | |||
| 498c51c585 | |||
| 6a6716655b | |||
| ddc0949e56 | |||
| a723733562 | |||
| e7307182dd | |||
| 92ccebfa4e | |||
| 30dd8a8dfe | |||
| 788f1f4de7 | |||
| 54d81b5ba8 | |||
| 4912a27348 | |||
| 085820892f | |||
| 391a4c2ccb | |||
| 92363c8185 | |||
| 73b6c01cf9 | |||
| f703344485 | |||
| d9049095e3 | |||
| b2f0f91712 | |||
| ac93a5385e | |||
| 644ceed436 | |||
| 77ea55eda2 | |||
| 0f83cfc9b9 | |||
| 83749fc647 | |||
| bd5f8a9b15 | |||
| 8889be8ca2 | |||
| 6d58222143 | |||
| c3019b741c | |||
| b5b2103372 | |||
| b234c87048 | |||
| f982aa5e0c | |||
| 22823328c0 | |||
| 323e49e375 | |||
| 526e4ac198 | |||
| 7469813e37 | |||
| 8eb4e40b57 | |||
| 311cf18708 | |||
| 1bde0af01d | |||
| 765ec6fb32 | |||
| 5cd8329519 | |||
| 1ea9170d89 | |||
| 29fc69d022 | |||
| 09e7a82882 | |||
| 1fff983d37 | |||
| 836e61b8b0 | |||
| 96044b8a88 | |||
| acb865596e | |||
| 59559f4201 | |||
| 4d516adea8 | |||
| a37368ddcf | |||
| 26dcfaf82d | |||
| e55a8866b1 | |||
| 0702135db2 | |||
| c668a76a99 | |||
| 2a1cb90766 | |||
| ce536eb5bc | |||
| 0244e330fd | |||
| 035bac10cc | |||
| cb808f9c68 | |||
| 22735aeaae | |||
| dd6eeb5dfe | |||
| ac6409f3e2 | |||
| 36a6932056 | |||
| 72dab0e968 | |||
| b716434f62 | |||
| 8ed7dd77e4 | |||
| ae1c9c695e | |||
| 66995c0c78 | |||
| 120e690795 | |||
| bc2bb25578 | |||
| b37fc5cb24 | |||
| 614dd9917f | |||
| 00bc3432bd | |||
| ffec10d1cc | |||
| 3993fa9b32 | |||
| 20b92b7d1c | |||
| 05b9a94d41 | |||
| 49f4475991 | |||
| 76bfdd03dd | |||
| c18f2fdcf7 | |||
| 74c6154c59 | |||
| 3dc9a5e2a4 | |||
| ca30fd89cb | |||
| b56e18b8ce | |||
| b031157f98 | |||
| 7262023382 | |||
| dde5825d30 | |||
| da4bc164e2 | |||
| ae95a871c8 | |||
| 4a29772de0 | |||
| 194065af8d | |||
| a118b75379 | |||
| 09d59162d0 | |||
| 1323bc7aa8 | |||
| 604245add2 | |||
| 90bbc11712 | |||
| 948d118a4f | |||
| 618d6384d9 | |||
| face5ca364 | |||
| d3869f68f8 | |||
| 97ab3a7a1d | |||
| 6cc4582c87 | |||
| 4941d4eb8a | |||
| 11a16a65a0 | |||
| 71f992544e | |||
| 6320d6eb17 | |||
| 6840b69f1a | |||
| 1ae2601209 | |||
| 07444f70df | |||
| 1bc04c74f3 | |||
| dcb5cd8a4e | |||
| 0d7d4c11a0 | |||
| e4d07874eb | |||
| ae9984460f | |||
| e38a1a4480 | |||
| b112f45264 | |||
| 6fad80b5cd | |||
| 1d0e1812c8 | |||
| 40490ffe43 | |||
| 455b6d40de | |||
| 62dcac103f |
+3
-5
@@ -1,18 +1,16 @@
|
|||||||
FROM golang:alpine AS backend-builder
|
FROM golang:alpine AS backend-builder
|
||||||
RUN apk add --no-cache build-base
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV CGO_ENABLED=1
|
|
||||||
RUN go build -ldflags="-s -w" -o bin/server
|
RUN go build -ldflags="-s -w" -o bin/server
|
||||||
|
|
||||||
RUN apk add --no-cache upx
|
RUN apk add --no-cache upx
|
||||||
|
|
||||||
RUN upx --best --lzma bin/server
|
RUN upx --best --lzma bin/server
|
||||||
|
|
||||||
FROM golang:latest
|
FROM alpine:latest
|
||||||
COPY --from=backend-builder /build/bin/server /app/server
|
COPY --from=backend-builder /build/bin/server /app/server
|
||||||
COPY static /app/static
|
|
||||||
COPY static/*.so /app/
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
RUN ls -a
|
RUN ls -a
|
||||||
|
|||||||
+14
-1
@@ -14,7 +14,20 @@ func Run() {
|
|||||||
IDType: &field.TypeInfo{
|
IDType: &field.TypeInfo{
|
||||||
Type: field.TypeInt64,
|
Type: field.TypeInt64,
|
||||||
},
|
},
|
||||||
Features: gen.AllFeatures,
|
Features: []gen.Feature{
|
||||||
|
gen.FeaturePrivacy,
|
||||||
|
gen.FeatureIntercept,
|
||||||
|
gen.FeatureEntQL,
|
||||||
|
gen.FeatureNamedEdges,
|
||||||
|
gen.FeatureBidiEdgeRefs,
|
||||||
|
gen.FeatureSnapshot,
|
||||||
|
gen.FeatureSchemaConfig,
|
||||||
|
gen.FeatureLock,
|
||||||
|
gen.FeatureModifier,
|
||||||
|
gen.FeatureExecQuery,
|
||||||
|
gen.FeatureUpsert,
|
||||||
|
gen.FeatureVersionedMigration,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
utils.OkOrPanic(err)
|
utils.OkOrPanic(err)
|
||||||
|
|||||||
+4
-4
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"likedream-goframe/internal/api"
|
"likedream-goframe/internal/api"
|
||||||
"likedream-goframe/internal/application/filestorage"
|
"likedream-goframe/internal/application/filestorage"
|
||||||
"likedream-goframe/internal/db"
|
db2 "likedream-goframe/internal/common/db"
|
||||||
_ "likedream-goframe/internal/ent/runtime"
|
_ "likedream-goframe/internal/ent/runtime"
|
||||||
"likedream-goframe/internal/framework/redis"
|
"likedream-goframe/internal/framework/redis"
|
||||||
"likedream-goframe/internal/framework/schedule"
|
"likedream-goframe/internal/framework/schedule"
|
||||||
@@ -17,10 +17,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Run() {
|
func Run() {
|
||||||
db.InitDB()
|
db2.InitDB()
|
||||||
// 如果没有数据,进行数据初始化
|
// 如果没有数据,进行数据初始化
|
||||||
if !db.CheckHasData() {
|
if !db2.CheckHasData() {
|
||||||
db.GenerateData()
|
db2.GenerateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(gctx.GetInitCtx())
|
ctx, cancel := context.WithCancel(gctx.GetInitCtx())
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/login"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
const testSmsPhoneNumber = ""
|
||||||
|
handler := login.SmsLoginHandler{}
|
||||||
|
ctx := gctx.GetInitCtx()
|
||||||
|
sendSmsData := handler.SendSms(ctx, testSmsPhoneNumber)
|
||||||
|
// 读取验证码手动输入
|
||||||
|
var code string
|
||||||
|
println("请输入收到的验证码:")
|
||||||
|
_, _ = fmt.Scanln(&code)
|
||||||
|
loginData := handler.LoginByCode(ctx, testSmsPhoneNumber, code, sendSmsData.CookieJar)
|
||||||
|
handler.SetLoginCookie(ctx, sendSmsData.CookieJar, loginData)
|
||||||
|
}
|
||||||
+26
-8
@@ -3,21 +3,24 @@ url = "https://devocr.likedream.xyz"
|
|||||||
|
|
||||||
[database]
|
[database]
|
||||||
type = "mysql"
|
type = "mysql"
|
||||||
dsn = "test_likedream_go:EpXckXmxawDEQGxJ@tcp(database.out:3306)/test_likedream_go?parseTime=true"
|
dsn = "test_likedream_go:EpXckXmxawDEQGxJ@tcp(database.out:3306)/test_likedream_go?parseTime=true&loc=Asia%2FShanghai"
|
||||||
|
dsn2 = "host=database.out user=dev password=JPmKjmpcaG3EYAJZ dbname=dev port=5432 sslmode=disable TimeZone=Asia/Shanghai"
|
||||||
|
# host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai
|
||||||
[server]
|
[server]
|
||||||
port = 8090
|
port = 8090
|
||||||
url = "https://devocr.likedream.xyz"
|
url = "https://devocr.likedream.xyz"
|
||||||
openapiPath= "/api.json"
|
openapiPath= "/api.json"
|
||||||
swaggerPath= "/swagger"
|
swaggerPath= "/swagger"
|
||||||
emailConfirmBaseUrl = "https://dev.likedream.xyz/confirm/"
|
emailConfirmBaseUrl = "http://localhost:3200/confirm/"
|
||||||
|
emailResetPasswordUrl = "http://localhost:3200/reset-password/"
|
||||||
|
balanceMigrateUrl = "http://localhost:3200/balance-migrate/"
|
||||||
[env]
|
[env]
|
||||||
# 可选值: dev, prod
|
# 可选值: dev, prod
|
||||||
mode = "dev"
|
mode = "dev"
|
||||||
|
|
||||||
[ruoliCaptcha]
|
[captcha]
|
||||||
baseUrl = ""
|
baseUrl = "http://captcha-resolver.out"
|
||||||
|
#baseUrl = "http://localhost:8890"
|
||||||
[redis]
|
[redis]
|
||||||
url = "redis://:jhkdjhkjdhsIUTYURTU_pEGmFP@database.out:56379"
|
url = "redis://:jhkdjhkjdhsIUTYURTU_pEGmFP@database.out:56379"
|
||||||
|
|
||||||
@@ -30,11 +33,26 @@ smtpUsername = "demo@likedream.xyz"
|
|||||||
smtpPassword = "baca122..."
|
smtpPassword = "baca122..."
|
||||||
# 发件人邮箱和显示名称
|
# 发件人邮箱和显示名称
|
||||||
fromEmail = "demo@likedream.xyz"
|
fromEmail = "demo@likedream.xyz"
|
||||||
fromName = "今日校园系统"
|
fromName = "若梦系统"
|
||||||
|
|
||||||
|
|
||||||
[s3]
|
[s3]
|
||||||
accessKey = "dzckoE1UlZe8XFItWQDI"
|
accessKey = "dzckoE1UlZe8XFItWQDI"
|
||||||
secretKey = "f8UZdnDohtwZk5HInT1Cw1QCztSGnFuPPqfSSjzu"
|
secretKey = "f8UZdnDohtwZk5HInT1Cw1QCztSGnFuPPqfSSjzu"
|
||||||
baseUrl = "http://s3.out"
|
baseUrl = "http://s3.out"
|
||||||
bucket = "likedream"
|
bucket = "likedream"
|
||||||
|
|
||||||
|
[cpdaily]
|
||||||
|
appVersion = "9.7.2"
|
||||||
|
systemVersion = "iOS 17"
|
||||||
|
calVersion = "firstv"
|
||||||
|
sliderIds = ["1018764144934965", "sdhyxy", "hqdx"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[basePoint]
|
||||||
|
cpdailyTask = "0.5"
|
||||||
|
cpdailyLeave = "4"
|
||||||
|
cpdailyPhone = "0.25"
|
||||||
|
[task]
|
||||||
|
concurrency = 20
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
# 刷课接口文档
|
||||||
|
|
||||||
|
## 接口列表
|
||||||
|
|
||||||
|
### 1. 获取刷课列表
|
||||||
|
|
||||||
|
**接口地址**: `GET /course`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| type | string | 否 | 类型,可选值:`xxt`(学习通) |
|
||||||
|
| username | string | 否 | 账号(模糊搜索) |
|
||||||
|
| status | string | 否 | 状态,可选值:`not_start`(未开始)、`waiting`(等待中)、`doing`(进行中)、`done`(已完成)、`fail`(失败)、`pause`(暂停) |
|
||||||
|
| page | int | 否 | 页码,默认 1 |
|
||||||
|
| size | int | 否 | 每页数量,默认 10 |
|
||||||
|
|
||||||
|
**返回示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"user_id": 1,
|
||||||
|
"username": "学习通账号",
|
||||||
|
"password": "",
|
||||||
|
"type": "xxt",
|
||||||
|
"course_name": "大学英语",
|
||||||
|
"course_id": "123456",
|
||||||
|
"need": "task",
|
||||||
|
"status": "waiting",
|
||||||
|
"task_count": 10,
|
||||||
|
"success_count": 5,
|
||||||
|
"exam_state": "not_start",
|
||||||
|
"violence": "no",
|
||||||
|
"desc": "备注信息",
|
||||||
|
"create_time": "2024-01-01T00:00:00Z",
|
||||||
|
"update_time": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**:
|
||||||
|
|
||||||
|
- `type`: 类型 - `xxt`(学习通)
|
||||||
|
- `need`: 需要刷的内容 - `task`(任务点)、`exam`(考试)、`all`(全部)
|
||||||
|
- `status`: 状态 - `not_start`(未开始)、`waiting`(等待中)、`doing`(进行中)、`done`(已完成)、`fail`(失败)、`pause`(暂停)
|
||||||
|
- `exam_state`: 考试状态 - `not_start`(未开始)、`waiting`(等待中)、`doing`(进行中)、`done`(已完成)、`fail`(失败)
|
||||||
|
- `violence`: 是否暴力刷课 - `no`(否)、`yes`(是)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 查询课程列表
|
||||||
|
|
||||||
|
**接口地址**: `POST /course/query`
|
||||||
|
|
||||||
|
**说明**: 通过学习通账号密码查询当前可选的课程列表,用于在添加刷课前选择课程。
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| username | string | 是 | 学习通账号 |
|
||||||
|
| password | string | 是 | 学习通密码 |
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "学习通账号",
|
||||||
|
"password": "学习通密码"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"cpi": 123456,
|
||||||
|
"key": "234567",
|
||||||
|
"courseId": "345678",
|
||||||
|
"chatId": "456789",
|
||||||
|
"courseTeacher": "张老师",
|
||||||
|
"courseName": "大学英语",
|
||||||
|
"courseImage": "https://example.com/image.jpg",
|
||||||
|
"courseImageBase64": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAA...",
|
||||||
|
"courseDataId": 567890,
|
||||||
|
"ContentID": 678901,
|
||||||
|
"isstart": true,
|
||||||
|
"state": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**:
|
||||||
|
|
||||||
|
- `cpi`: 用户唯一标识
|
||||||
|
- `key`: 课程班级ID(classId)
|
||||||
|
- `courseId`: 课程ID(**添加刷课时需要使用**)
|
||||||
|
- `chatId`: 聊天ID
|
||||||
|
- `courseTeacher`: 授课教师
|
||||||
|
- `courseName`: 课程名称(**添加刷课时需要使用**)
|
||||||
|
- `courseImage`: 课程封面图片URL
|
||||||
|
- `courseImageBase64`: 课程封面图片Base64编码(包含Data URL前缀,可直接用于img标签的src属性)
|
||||||
|
- `courseDataId`: 课程数据ID
|
||||||
|
- `ContentID`: 内容ID
|
||||||
|
- `isstart`: 是否已开课(true:已开课,false:未开课)
|
||||||
|
- `state`: 课程状态(0:正常,1:已结束)
|
||||||
|
|
||||||
|
**注意事项**:
|
||||||
|
|
||||||
|
- `courseImageBase64` 字段已经包含了 Data URL 前缀(如 `data:image/jpeg;base64,`),可以直接设置到 `<img>` 标签的 `src` 属性
|
||||||
|
- 如果图片下载失败,`courseImageBase64` 字段将为空字符串,此时可以使用 `courseImage` URL 作为备用
|
||||||
|
|
||||||
|
**错误示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"message": "登录失败,请检查账号密码",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 添加刷课
|
||||||
|
|
||||||
|
**接口地址**: `POST /course`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| username | string | 是 | 学习通账号 |
|
||||||
|
| password | string | 是 | 学习通密码 |
|
||||||
|
| type | string | 是 | 类型,目前只支持 `xxt`(学习通) |
|
||||||
|
| course_name | string | 是 | 课程名称 |
|
||||||
|
| course_id | string | 是 | 课程ID |
|
||||||
|
| need | string | 是 | 需要刷的内容,可选值:`task`(任务点)、`exam`(考试)、`all`(全部) |
|
||||||
|
| violence | string | 否 | 是否暴力刷课,可选值:`no`(否,默认)、`yes`(是) |
|
||||||
|
| desc | string | 否 | 备注信息 |
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "学习通账号",
|
||||||
|
"password": "学习通密码",
|
||||||
|
"type": "xxt",
|
||||||
|
"course_name": "大学英语",
|
||||||
|
"course_id": "123456",
|
||||||
|
"need": "task",
|
||||||
|
"violence": "no",
|
||||||
|
"desc": "备注信息"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "添加成功",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"message": "该课程已存在刷课任务",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 修改刷课密码
|
||||||
|
|
||||||
|
**接口地址**: `PUT /course/password/{id}`
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| id | int64 | 是 | 刷课任务ID |
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| password | string | 是 | 新密码 |
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"password": "新密码"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "密码修改成功",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"message": "课程不存在或无权限操作",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 重刷课程
|
||||||
|
|
||||||
|
**接口地址**: `POST /course/restart/{id}`
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| id | int64 | 是 | 刷课任务ID |
|
||||||
|
|
||||||
|
**说明**: 只有状态为 `done`(已完成)的课程才能重刷,重刷后会重置课程状态、任务数量、成功数量和考试状态。
|
||||||
|
|
||||||
|
**返回示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "重刷成功",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"message": "只有已完成的课程才能重刷",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. 暂停/恢复刷课
|
||||||
|
|
||||||
|
**接口地址**: `POST /course/pause/{id}`
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| id | int64 | 是 | 刷课任务ID |
|
||||||
|
|
||||||
|
**说明**: 该接口会在暂停和恢复之间切换:
|
||||||
|
- 如果当前状态是 `pause`(暂停),则恢复为 `waiting`(等待中)
|
||||||
|
- 如果当前状态不是暂停,则设置为 `pause`(暂停)
|
||||||
|
|
||||||
|
**返回示例(暂停)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "已暂停刷课",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回示例(恢复)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "已恢复刷课",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 删除刷课
|
||||||
|
|
||||||
|
**接口地址**: `DELETE /course/{id}`
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| id | int64 | 是 | 刷课任务ID |
|
||||||
|
|
||||||
|
**返回示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "删除成功",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"message": "课程不存在或无权限操作",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 枚举值说明
|
||||||
|
|
||||||
|
### 类型 (type)
|
||||||
|
|
||||||
|
| 值 | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| xxt | 学习通 |
|
||||||
|
|
||||||
|
### 需要刷的内容 (need)
|
||||||
|
|
||||||
|
| 值 | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| task | 任务点 |
|
||||||
|
| exam | 考试 |
|
||||||
|
| all | 全部 |
|
||||||
|
|
||||||
|
### 状态 (status)
|
||||||
|
|
||||||
|
| 值 | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| not_start | 未开始 |
|
||||||
|
| waiting | 等待中 |
|
||||||
|
| doing | 进行中 |
|
||||||
|
| done | 已完成 |
|
||||||
|
| fail | 失败 |
|
||||||
|
| pause | 暂停 |
|
||||||
|
|
||||||
|
### 考试状态 (exam_state)
|
||||||
|
|
||||||
|
| 值 | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| not_start | 未开始 |
|
||||||
|
| waiting | 等待中 |
|
||||||
|
| doing | 进行中 |
|
||||||
|
| done | 已完成 |
|
||||||
|
| fail | 失败 |
|
||||||
|
|
||||||
|
### 是否暴力刷课 (violence)
|
||||||
|
|
||||||
|
| 值 | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| no | 否 |
|
||||||
|
| yes | 是 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 通用说明
|
||||||
|
|
||||||
|
### 认证
|
||||||
|
|
||||||
|
所有接口都需要用户认证,需要在请求头中携带认证信息(具体认证方式根据项目配置)。
|
||||||
|
|
||||||
|
### 权限
|
||||||
|
|
||||||
|
用户只能操作自己创建的刷课任务,无法访问或修改其他用户的数据。
|
||||||
|
|
||||||
|
### 错误码
|
||||||
|
|
||||||
|
| 错误码 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| 200 | 成功 |
|
||||||
|
| 400 | 业务错误(如参数错误、权限不足等) |
|
||||||
|
| 401 | 未认证 |
|
||||||
|
| 500 | 服务器错误 |
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
module likedream-goframe
|
module likedream-goframe
|
||||||
|
|
||||||
go 1.24
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
entgo.io/ent v0.14.5
|
entgo.io/ent v0.14.5
|
||||||
@@ -28,10 +28,11 @@ require (
|
|||||||
github.com/wenlng/go-captcha-assets v1.0.7
|
github.com/wenlng/go-captcha-assets v1.0.7
|
||||||
github.com/wenlng/go-captcha/v2 v2.0.4
|
github.com/wenlng/go-captcha/v2 v2.0.4
|
||||||
github.com/wneessen/go-mail v0.6.2
|
github.com/wneessen/go-mail v0.6.2
|
||||||
github.com/yalue/onnxruntime_go v1.9.0
|
|
||||||
go.uber.org/automaxprocs v1.6.0
|
go.uber.org/automaxprocs v1.6.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/crypto v0.39.0
|
golang.org/x/crypto v0.43.0
|
||||||
|
gorm.io/driver/postgres v1.6.0
|
||||||
|
gorm.io/gorm v1.31.1
|
||||||
resty.dev/v3 v3.0.0-beta.3
|
resty.dev/v3 v3.0.0-beta.3
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,6 +70,12 @@ require (
|
|||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
|
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
|
||||||
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
|
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||||
github.com/magiconair/properties v1.8.9 // indirect
|
github.com/magiconair/properties v1.8.9 // indirect
|
||||||
@@ -98,13 +105,13 @@ require (
|
|||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||||
golang.org/x/image v0.29.0 // indirect
|
golang.org/x/image v0.29.0 // indirect
|
||||||
golang.org/x/mod v0.25.0 // indirect
|
golang.org/x/mod v0.28.0 // indirect
|
||||||
golang.org/x/net v0.41.0 // indirect
|
golang.org/x/net v0.45.0 // indirect
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.34.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
golang.org/x/text v0.27.0 // indirect
|
golang.org/x/text v0.30.0 // indirect
|
||||||
golang.org/x/time v0.12.0 // indirect
|
golang.org/x/time v0.12.0 // indirect
|
||||||
golang.org/x/tools v0.34.0 // indirect
|
golang.org/x/tools v0.37.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -111,6 +111,18 @@ github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZY
|
|||||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||||
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
||||||
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
|
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/joomcode/errorx v1.2.0 h1:7Y/fguon+9r6a/75Rv3nrUwS7nXNEcJjLShjCvz00Og=
|
github.com/joomcode/errorx v1.2.0 h1:7Y/fguon+9r6a/75Rv3nrUwS7nXNEcJjLShjCvz00Og=
|
||||||
github.com/joomcode/errorx v1.2.0/go.mod h1:Mbz68VA9hsQLT50iCQQUZ2Z1XYAKYB4EoFkFCTFyiJM=
|
github.com/joomcode/errorx v1.2.0/go.mod h1:Mbz68VA9hsQLT50iCQQUZ2Z1XYAKYB4EoFkFCTFyiJM=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
@@ -134,8 +146,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
|||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||||
@@ -177,6 +189,7 @@ github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqe
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@@ -202,8 +215,6 @@ github.com/wenlng/go-captcha/v2 v2.0.4 h1:5cSUF36ZyA03qeDMjKmeXGpbYJMXEexZIYK3Vg
|
|||||||
github.com/wenlng/go-captcha/v2 v2.0.4/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
|
github.com/wenlng/go-captcha/v2 v2.0.4/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
|
||||||
github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8=
|
github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8=
|
||||||
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
|
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
|
||||||
github.com/yalue/onnxruntime_go v1.9.0 h1:AhgkpBjphJZsHT5karKt93xPkPFNP0Iz6ENUbNAFQU4=
|
|
||||||
github.com/yalue/onnxruntime_go v1.9.0/go.mod h1:b4X26A8pekNb1ACJ58wAXgNKeUCGEAQ9dmACut9Sm/4=
|
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
@@ -236,8 +247,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
|||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
|
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
|
||||||
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
||||||
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
|
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
|
||||||
@@ -246,8 +257,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
@@ -257,8 +268,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -267,8 +278,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -286,8 +297,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -308,8 +319,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -318,8 +329,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
@@ -331,6 +342,10 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
|
resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
|
||||||
resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=
|
resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=
|
||||||
|
|||||||
+14
-1
@@ -2,15 +2,19 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"likedream-goframe/internal/common/cst"
|
||||||
"likedream-goframe/internal/controller/agent"
|
"likedream-goframe/internal/controller/agent"
|
||||||
|
"likedream-goframe/internal/controller/balance"
|
||||||
"likedream-goframe/internal/controller/card"
|
"likedream-goframe/internal/controller/card"
|
||||||
"likedream-goframe/internal/controller/common"
|
"likedream-goframe/internal/controller/common"
|
||||||
|
"likedream-goframe/internal/controller/config"
|
||||||
|
"likedream-goframe/internal/controller/course"
|
||||||
"likedream-goframe/internal/controller/cpdaily"
|
"likedream-goframe/internal/controller/cpdaily"
|
||||||
"likedream-goframe/internal/controller/menu"
|
"likedream-goframe/internal/controller/menu"
|
||||||
|
"likedream-goframe/internal/controller/music"
|
||||||
"likedream-goframe/internal/controller/role"
|
"likedream-goframe/internal/controller/role"
|
||||||
"likedream-goframe/internal/controller/user"
|
"likedream-goframe/internal/controller/user"
|
||||||
"likedream-goframe/internal/controller/user_login"
|
"likedream-goframe/internal/controller/user_login"
|
||||||
"likedream-goframe/internal/cst"
|
|
||||||
"likedream-goframe/internal/middleware"
|
"likedream-goframe/internal/middleware"
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
|
|
||||||
@@ -39,10 +43,12 @@ func StartServer(ctx context.Context) {
|
|||||||
systemGroup.Bind(role.NewV1())
|
systemGroup.Bind(role.NewV1())
|
||||||
systemGroup.Bind(menu.NewV1())
|
systemGroup.Bind(menu.NewV1())
|
||||||
systemGroup.Bind(user.NewUserCtrlV1())
|
systemGroup.Bind(user.NewUserCtrlV1())
|
||||||
|
systemGroup.Bind(config.NewConfigCtrlV1())
|
||||||
//systemGroup.Bind(agent.NewV1())
|
//systemGroup.Bind(agent.NewV1())
|
||||||
// 用户信息路由
|
// 用户信息路由
|
||||||
userGroup := group.Group("/user").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
userGroup := group.Group("/user").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||||
userGroup.Bind(user_login.NewUserInfoV1())
|
userGroup.Bind(user_login.NewUserInfoV1())
|
||||||
|
userGroup.Bind(balance.NewBalanceCtrlV1())
|
||||||
// 代理信息路由
|
// 代理信息路由
|
||||||
agentGroup := group.Group("/agent").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
agentGroup := group.Group("/agent").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||||
agentGroup.Bind(agent.NewV1())
|
agentGroup.Bind(agent.NewV1())
|
||||||
@@ -53,6 +59,13 @@ func StartServer(ctx context.Context) {
|
|||||||
cpdailyGroup.Bind(cpdaily.NewCpTaskCtrlV1())
|
cpdailyGroup.Bind(cpdaily.NewCpTaskCtrlV1())
|
||||||
cpdailyGroup.Bind(cpdaily.NewCpImageCtrlV1())
|
cpdailyGroup.Bind(cpdaily.NewCpImageCtrlV1())
|
||||||
cpdailyGroup.Bind(cpdaily.NewCpTaskLogCtrlV1())
|
cpdailyGroup.Bind(cpdaily.NewCpTaskLogCtrlV1())
|
||||||
|
cpdailyGroup.Bind(cpdaily.NewCpLeaveCtrlV1())
|
||||||
|
// 娱乐
|
||||||
|
musicGroup := group.Group("/music").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||||
|
musicGroup.Bind(music.NewMusicCtrlV1())
|
||||||
|
// 网课
|
||||||
|
courseGroup := group.Group("/course").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||||
|
courseGroup.Bind(course.NewXxtCtrlV1())
|
||||||
})
|
})
|
||||||
|
|
||||||
s.SetPort(port.Int())
|
s.SetPort(port.Int())
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
type BalanceRecordListReq struct {
|
||||||
|
g.Meta `path:"/balance/records" method:"get" tags:"Balance" summary:"获取用户余额变动记录列表"`
|
||||||
|
Type string `json:"type" dc:"变动类型: increase-增加, decrease-减少"`
|
||||||
|
Page int `json:"page" d:"1"`
|
||||||
|
Size int `json:"size" d:"10"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigGetReq struct {
|
||||||
|
g.Meta `path:"/config/{key}" method:"get" tags:"Config" summary:"获取配置"`
|
||||||
|
Key string `json:"key" v:"required#配置键不能为空"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigUpdateReq struct {
|
||||||
|
g.Meta `path:"/config/{key}" method:"put" tags:"Config" summary:"更新配置" permission:"sys:config:edit"`
|
||||||
|
Key string `json:"key" v:"required#配置键不能为空"`
|
||||||
|
Value string `json:"value" v:"required#配置值不能为空"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigListReq struct {
|
||||||
|
g.Meta `path:"/config" method:"get" tags:"Config" summary:"获取配置列表"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Page int `json:"page" d:"1"`
|
||||||
|
Size int `json:"size" d:"10"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigPublicGetReq struct {
|
||||||
|
g.Meta `path:"/config/public/{key}" method:"get" tags:"Config" summary:"获取公开配置(无需认证)"`
|
||||||
|
Key string `json:"key" v:"required#配置键不能为空"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"likedream-goframe/internal/ent/courseentity"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CourseListReq struct {
|
||||||
|
g.Meta `path:"/course" method:"get" tags:"Course" summary:"获取刷课列表"`
|
||||||
|
Type courseentity.Type `json:"type"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Status courseentity.Status `json:"status"`
|
||||||
|
Page int `json:"page" d:"1"`
|
||||||
|
Size int `json:"size" d:"10"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddCourseReq struct {
|
||||||
|
g.Meta `path:"/course" method:"post" tags:"Course" summary:"添加刷课"`
|
||||||
|
Username string `json:"username" v:"required#账号不能为空"`
|
||||||
|
Password string `json:"password" v:"required#密码不能为空"`
|
||||||
|
Type courseentity.Type `json:"type" v:"required#类型不能为空"`
|
||||||
|
CourseName string `json:"course_name" v:"required#课程名称不能为空"`
|
||||||
|
CourseID string `json:"course_id" v:"required#课程ID不能为空"`
|
||||||
|
Need courseentity.Need `json:"need" v:"required#需要刷的内容不能为空"`
|
||||||
|
Violence courseentity.Violence `json:"violence" d:"no"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateCoursePasswordReq struct {
|
||||||
|
g.Meta `path:"/course/password/{id}" method:"put" tags:"Course" summary:"修改刷课密码"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Password string `json:"password" v:"required#密码不能为空"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RestartCourseReq struct {
|
||||||
|
g.Meta `path:"/course/restart/{id}" method:"post" tags:"Course" summary:"重刷课程"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PauseCourseReq struct {
|
||||||
|
g.Meta `path:"/course/pause/{id}" method:"post" tags:"Course" summary:"暂停刷课"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteCourseReq struct {
|
||||||
|
g.Meta `path:"/course/{id}" method:"delete" tags:"Course" summary:"删除刷课"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryCourseReq struct {
|
||||||
|
g.Meta `path:"/course/query" method:"post" tags:"Course" summary:"查询课程列表"`
|
||||||
|
Username string `json:"username" v:"required#账号不能为空"`
|
||||||
|
Password string `json:"password" v:"required#密码不能为空"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
// TaskStatisticsItem 每日任务统计项
|
||||||
|
type TaskStatisticsItem struct {
|
||||||
|
Date string `json:"date"` // 日期 格式:2006-01-02
|
||||||
|
PendingTask int `json:"pendingTask"` // 需要完成的任务数
|
||||||
|
SuccessTask int `json:"successTask"` // 成功执行的任务数
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CpLeaveListReq struct {
|
||||||
|
g.Meta `path:"/leave" method:"get" tags:"CpLeave" summary:"获取销假记录列表"`
|
||||||
|
CpUserId int64 `json:"cpUserId"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Page int `json:"page" d:"1"`
|
||||||
|
Size int `json:"size" d:"10"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetLeaveListFromSystemReq struct {
|
||||||
|
g.Meta `path:"/leave/system/{cpUserId}" method:"get" tags:"CpLeave" summary:"获取今日校园系统中的请假列表"`
|
||||||
|
CpUserId int64 `json:"cpUserId" v:"required#今日校园账号ID不能为空"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CpLeaveReportReq struct {
|
||||||
|
g.Meta `path:"/leave/report" method:"post" tags:"CpLeave" summary:"执行销假"`
|
||||||
|
CpUserId int64 `json:"cpUserId" v:"required#今日校园账号ID不能为空"`
|
||||||
|
LeaveId string `json:"leaveId" v:"required#请假记录ID不能为空"`
|
||||||
|
Title string `json:"title" v:"required#请假记录标题不能为空"`
|
||||||
|
}
|
||||||
@@ -67,3 +67,12 @@ type PauseTaskReq struct {
|
|||||||
g.Meta `path:"/task/pause/{id}" method:"post" tags:"CpTask" summary:"暂停任务"`
|
g.Meta `path:"/task/pause/{id}" method:"post" tags:"CpTask" summary:"暂停任务"`
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FindLocationReq struct {
|
||||||
|
g.Meta `path:"/task/location" method:"get" tags:"CpTask" summary:"查找地址"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetTaskStatisticsReq struct {
|
||||||
|
g.Meta `path:"/task/statistics" method:"get" tags:"CpTask" summary:"获取近七日任务统计数据"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import "github.com/gogf/gf/v2/frame/g"
|
import (
|
||||||
|
"likedream-goframe/internal/framework/captcha"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
type CpSchoolListReq struct {
|
type CpSchoolListReq struct {
|
||||||
g.Meta `path:"/school" method:"get" tags:"CpSchool" summary:"获取cp学校列表"`
|
g.Meta `path:"/school" method:"get" tags:"CpSchool" summary:"获取cp学校列表"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
type CpUserLoginUrlReq struct {
|
||||||
|
g.Meta `path:"/loginUrl" method:"get" tags:"CpUser" summary:"获取登录url" auth:"false"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
type CpUserAddReq struct {
|
type CpUserAddReq struct {
|
||||||
g.Meta `path:"/user" method:"post" tags:"CpUser" summary:"添加cp用户"`
|
g.Meta `path:"/user" method:"post" tags:"CpUser" summary:"添加cp用户"`
|
||||||
Username string `json:"username" v:"required"`
|
Username string `json:"username" v:"required"`
|
||||||
@@ -14,6 +21,24 @@ type CpUserAddReq struct {
|
|||||||
SchoolId string `json:"school_id"`
|
SchoolId string `json:"school_id"`
|
||||||
Email string `json:"email" v:"email|regex:^.*@qq\\.com$#邮箱格式不正确|必须是QQ邮箱"`
|
Email string `json:"email" v:"email|regex:^.*@qq\\.com$#邮箱格式不正确|必须是QQ邮箱"`
|
||||||
}
|
}
|
||||||
|
type LoginGetCode struct {
|
||||||
|
g.Meta `path:"/user/getCode" method:"post" tags:"CpUser" summary:"获取手机号登录验证码"`
|
||||||
|
Mobile string `json:"mobile" v:"required#手机号不能为空"`
|
||||||
|
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||||
|
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddByCode struct {
|
||||||
|
g.Meta `path:"/user/addByCode" method:"post" tags:"CpUser" summary:"手机号登录"`
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||||
|
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||||
|
Mobile string `json:"mobile" v:"required#手机号不能为空"`
|
||||||
|
SchoolId string `json:"school_id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Email string `json:"email" v:"email|regex:^.*@qq\\.com$#邮箱格式不正确|必须是QQ邮箱"`
|
||||||
|
}
|
||||||
|
|
||||||
type CpUserConfirmAddReq struct {
|
type CpUserConfirmAddReq struct {
|
||||||
g.Meta `path:"/user/confirm" method:"post" tags:"CpUser" summary:"多因子确认添加cp用户"`
|
g.Meta `path:"/user/confirm" method:"post" tags:"CpUser" summary:"多因子确认添加cp用户"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type MenuAddReq struct {
|
|||||||
Route string `json:"route"`
|
Route string `json:"route"`
|
||||||
Component string `json:"component"`
|
Component string `json:"component"`
|
||||||
Hidden bool `json:"hidden"`
|
Hidden bool `json:"hidden"`
|
||||||
|
KeepAlive bool `json:"keepAlive"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Order uint `json:"order"`
|
Order uint `json:"order"`
|
||||||
ParentId int64 `json:"parentId"`
|
ParentId int64 `json:"parentId"`
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
type SearchMusicReq struct {
|
||||||
|
g.Meta `path:"/search" method:"get" tags:"Music" summary:"搜索音乐"`
|
||||||
|
Keyword string `json:"keyword"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloadMusicReq struct {
|
||||||
|
g.Meta `path:"/download" method:"get" tags:"Music" summary:"下载音乐"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ type UserMenuRes struct {
|
|||||||
Route string `json:"route"`
|
Route string `json:"route"`
|
||||||
Component string `json:"component"`
|
Component string `json:"component"`
|
||||||
Hidden bool `json:"hidden"`
|
Hidden bool `json:"hidden"`
|
||||||
|
KeepAlive bool `json:"keepAlive"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Children []*UserMenuRes `json:"children"`
|
Children []*UserMenuRes `json:"children"`
|
||||||
Order uint `json:"order"`
|
Order uint `json:"order"`
|
||||||
|
|||||||
@@ -12,22 +12,64 @@ type CaptchaReq struct {
|
|||||||
|
|
||||||
type UserLoginReq struct {
|
type UserLoginReq struct {
|
||||||
g.Meta `path:"/user/login" method:"POST" tags:"UserLogin" summary:"用户登录"`
|
g.Meta `path:"/user/login" method:"POST" tags:"UserLogin" summary:"用户登录"`
|
||||||
Username string `json:"username" v:"required"`
|
Username string `json:"username" v:"required#用户名或密码不能为空"`
|
||||||
Password string `json:"password" v:"required"`
|
Password string `json:"password" v:"required#用户名或密码不能为空"`
|
||||||
CaptchaKey string `json:"captchaKey" v:"required"`
|
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||||
Dots []captcha.ConfirmData `json:"dots" v:"required"`
|
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserRegisterReq struct {
|
type UserRegisterReq struct {
|
||||||
g.Meta `path:"/user/register" method:"POST" tags:"UserLogin" summary:"用户注册"`
|
g.Meta `path:"/user/register" method:"POST" tags:"UserLogin" summary:"用户注册"`
|
||||||
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
|
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
|
||||||
Username string `json:"username" v:"required|length:2,20#用户名不能为空|用户名长度应在2-20之间" `
|
Username string `json:"username" v:"required|length:2,20#用户名不能为空|用户名长度应在2-20之间" `
|
||||||
Password string `json:"password" v:"required|length:6,20#密码不能为空|密码长度应在6-20之间" `
|
Password string `json:"password" v:"required|length:6,20#密码不能为空|密码长度应在6-20之间" `
|
||||||
ConfirmPassword string `json:"confirmPassword" v:"required|same:password#确认密码不能为空|两次输入的密码不一致"`
|
ConfirmPassword string `json:"confirmPassword" v:"required|same:password#确认密码不能为空|两次输入的密码不一致"`
|
||||||
InviteCode string `json:"inviteCode"`
|
InviteCode string `json:"inviteCode"`
|
||||||
|
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||||
|
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserConfirmReq struct {
|
type UserConfirmReq struct {
|
||||||
g.Meta `path:"/user/confirm" method:"POST" tags:"UserLogin" summary:"邮箱二次验证"`
|
g.Meta `path:"/user/confirm" method:"POST" tags:"UserLogin" summary:"邮箱二次验证"`
|
||||||
Token string `json:"token" v:"required"`
|
Token string `json:"token" v:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResendVerificationReq struct {
|
||||||
|
g.Meta `path:"/user/resendVerification" method:"POST" tags:"UserLogin" summary:"重新发送验证邮件"`
|
||||||
|
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
|
||||||
|
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||||
|
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForgotPasswordReq struct {
|
||||||
|
g.Meta `path:"/user/forgotPassword" method:"POST" tags:"UserLogin" summary:"忘记密码-发送重置密码邮件"`
|
||||||
|
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
|
||||||
|
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||||
|
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForgetResetPasswordReq struct {
|
||||||
|
g.Meta `path:"/user/forgetResetPassword" method:"POST" tags:"UserLogin" summary:"忘记密码-重置密码"`
|
||||||
|
Token string `json:"token" v:"required#token不能为空"`
|
||||||
|
NewPassword string `json:"newPassword" v:"required|length:6,20#密码不能为空|密码长度应在6-20之间"`
|
||||||
|
ConfirmPassword string `json:"confirmPassword" v:"required|same:newPassword#确认密码不能为空|两次输入的密码不一致"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetPasswordInfoReq struct {
|
||||||
|
g.Meta `path:"/user/resetPassword/info" method:"POST" tags:"UserLogin" summary:"获取重置密码用户信息"`
|
||||||
|
Token string `json:"token" v:"required#token不能为空"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestBalanceMigrateReq struct {
|
||||||
|
g.Meta `path:"/user/requestBalanceMigrate" method:"POST" tags:"UserLogin" summary:"请求积分转移链接"`
|
||||||
|
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
|
||||||
|
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||||
|
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BalanceMigrateReq struct {
|
||||||
|
g.Meta `path:"/user/balanceMigrate" method:"POST" tags:"UserLogin" summary:"执行积分转移" auth:"false"`
|
||||||
|
Token string `json:"token" v:"required#token不能为空"`
|
||||||
|
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||||
|
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package captcha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
"likedream-goframe/internal/common/cst"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
|
"github.com/samber/oops"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ApiSemiOCRVerification(bytes []byte, shape1, shape2 int64) string {
|
||||||
|
data := Bytes2Base64(bytes)
|
||||||
|
ctx := gctx.GetInitCtx()
|
||||||
|
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/semiOcr"
|
||||||
|
client := httpclient.Cpdaily.Clone(ctx)
|
||||||
|
client.SetDoNotParseResponse(false)
|
||||||
|
post, err := client.R().SetBody(map[string]any{
|
||||||
|
"base64": data,
|
||||||
|
"shape1": shape1,
|
||||||
|
"shape2": shape2,
|
||||||
|
}).Post(baseUrl)
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("SemiOCRVerification", baseUrl).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
result := post.String()
|
||||||
|
return gjson.Get(result, "data").String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiAutoOCRVerification(bytes []byte) string {
|
||||||
|
data := Bytes2Base64(bytes)
|
||||||
|
ctx := gctx.GetInitCtx()
|
||||||
|
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/ocr"
|
||||||
|
client := httpclient.Cpdaily.Clone(ctx)
|
||||||
|
client.SetDoNotParseResponse(false)
|
||||||
|
post, err := client.R().SetBody(map[string]any{
|
||||||
|
"base64": data,
|
||||||
|
}).Post(baseUrl)
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("AutoOCRVerification", baseUrl).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
result := post.String()
|
||||||
|
return gjson.Get(result, "data").String()
|
||||||
|
}
|
||||||
|
func Bytes2Base64(data []byte) string {
|
||||||
|
return base64.StdEncoding.EncodeToString(data)
|
||||||
|
}
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
package classifier
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
_ "image/jpeg"
|
|
||||||
_ "image/png"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
ort "github.com/yalue/onnxruntime_go"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ImageClassifier struct {
|
|
||||||
session *ort.AdvancedSession
|
|
||||||
inputShape []int64
|
|
||||||
classes []string
|
|
||||||
inputTensor *ort.Tensor[float32]
|
|
||||||
outputTensor *ort.Tensor[float32]
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewImageClassifier 创建新的图片分类器
|
|
||||||
func NewImageClassifier(modelPath, classesPath string) (*ImageClassifier, error) {
|
|
||||||
// 初始化ONNX Runtime
|
|
||||||
err := ort.InitializeEnvironment()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("初始化ONNX Runtime失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载类别名称
|
|
||||||
classes, err := loadClasses(classesPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("加载类别失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建session选项
|
|
||||||
options, err := ort.NewSessionOptions()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("创建session选项失败: %v", err)
|
|
||||||
}
|
|
||||||
defer options.Destroy()
|
|
||||||
|
|
||||||
// 创建输入输出tensor
|
|
||||||
inputShape := ort.NewShape(1, 3, 224, 224)
|
|
||||||
outputShape := ort.NewShape(1, int64(len(classes)))
|
|
||||||
|
|
||||||
inputTensor, err := ort.NewEmptyTensor[float32](inputShape)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("创建输入tensor失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outputTensor, err := ort.NewEmptyTensor[float32](outputShape)
|
|
||||||
if err != nil {
|
|
||||||
inputTensor.Destroy()
|
|
||||||
return nil, fmt.Errorf("创建输出tensor失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载模型
|
|
||||||
session, err := ort.NewAdvancedSession(modelPath,
|
|
||||||
[]string{"input"}, []string{"output"},
|
|
||||||
[]ort.ArbitraryTensor{inputTensor}, []ort.ArbitraryTensor{outputTensor},
|
|
||||||
options)
|
|
||||||
if err != nil {
|
|
||||||
inputTensor.Destroy()
|
|
||||||
outputTensor.Destroy()
|
|
||||||
return nil, fmt.Errorf("加载模型失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ImageClassifier{
|
|
||||||
session: session,
|
|
||||||
inputShape: []int64{1, 3, 224, 224},
|
|
||||||
classes: classes,
|
|
||||||
inputTensor: inputTensor,
|
|
||||||
outputTensor: outputTensor,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadClasses 从文件加载类别名称
|
|
||||||
func loadClasses(path string) ([]string, error) {
|
|
||||||
file, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
var classes []string
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
for scanner.Scan() {
|
|
||||||
classes = append(classes, scanner.Text())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return classes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Predict 预测图片类别
|
|
||||||
func (ic *ImageClassifier) Predict(imgPath string) (string, float32, error) {
|
|
||||||
// 加载并预处理图片
|
|
||||||
inputData, err := ic.preprocessImage(imgPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", 0, fmt.Errorf("预处理图片失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置输入数据
|
|
||||||
copy(ic.inputTensor.GetData(), inputData)
|
|
||||||
|
|
||||||
// 运行推理
|
|
||||||
err = ic.session.Run()
|
|
||||||
if err != nil {
|
|
||||||
return "", 0, fmt.Errorf("推理失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取输出数据
|
|
||||||
outputData := ic.outputTensor.GetData()
|
|
||||||
|
|
||||||
// 找到最大概率的类别
|
|
||||||
maxIdx := 0
|
|
||||||
maxProb := outputData[0]
|
|
||||||
for i, prob := range outputData {
|
|
||||||
if prob > maxProb {
|
|
||||||
maxProb = prob
|
|
||||||
maxIdx = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用softmax获取概率
|
|
||||||
expSum := float32(0)
|
|
||||||
for _, v := range outputData {
|
|
||||||
expSum += exp(v)
|
|
||||||
}
|
|
||||||
probability := exp(outputData[maxIdx]) / expSum
|
|
||||||
|
|
||||||
return ic.classes[maxIdx], probability, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PredictFromBytes 从字节数据预测图片类别
|
|
||||||
func (ic *ImageClassifier) PredictFromBytes(imgData []byte) (string, float32, error) {
|
|
||||||
// 预处理图片
|
|
||||||
inputData, err := ic.preprocessImageBytes(imgData)
|
|
||||||
if err != nil {
|
|
||||||
return "", 0, fmt.Errorf("预处理图片失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置输入数据
|
|
||||||
copy(ic.inputTensor.GetData(), inputData)
|
|
||||||
|
|
||||||
// 运行推理
|
|
||||||
err = ic.session.Run()
|
|
||||||
if err != nil {
|
|
||||||
return "", 0, fmt.Errorf("推理失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取输出数据
|
|
||||||
outputData := ic.outputTensor.GetData()
|
|
||||||
|
|
||||||
// 找到最大概率的类别
|
|
||||||
maxIdx := 0
|
|
||||||
maxProb := outputData[0]
|
|
||||||
for i, prob := range outputData {
|
|
||||||
if prob > maxProb {
|
|
||||||
maxProb = prob
|
|
||||||
maxIdx = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用softmax获取概率
|
|
||||||
expSum := float32(0)
|
|
||||||
for _, v := range outputData {
|
|
||||||
expSum += exp(v)
|
|
||||||
}
|
|
||||||
probability := exp(outputData[maxIdx]) / expSum
|
|
||||||
|
|
||||||
return ic.classes[maxIdx], probability, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close 关闭分类器
|
|
||||||
func (ic *ImageClassifier) Close() error {
|
|
||||||
if ic.inputTensor != nil {
|
|
||||||
ic.inputTensor.Destroy()
|
|
||||||
}
|
|
||||||
if ic.outputTensor != nil {
|
|
||||||
ic.outputTensor.Destroy()
|
|
||||||
}
|
|
||||||
if ic.session != nil {
|
|
||||||
ic.session.Destroy()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// exp 计算e的x次方(简单实现)
|
|
||||||
func exp(x float32) float32 {
|
|
||||||
// 使用泰勒级数近似
|
|
||||||
result := float32(1.0)
|
|
||||||
term := float32(1.0)
|
|
||||||
for i := 1; i < 20; i++ {
|
|
||||||
term *= x / float32(i)
|
|
||||||
result += term
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package classifier
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"image"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// preprocessImage 预处理图片文件
|
|
||||||
func (ic *ImageClassifier) preprocessImage(imgPath string) ([]float32, error) {
|
|
||||||
file, err := os.Open(imgPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
img, _, err := image.Decode(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ic.processImage(img)
|
|
||||||
}
|
|
||||||
|
|
||||||
// preprocessImageBytes 预处理图片字节数据
|
|
||||||
func (ic *ImageClassifier) preprocessImageBytes(imgData []byte) ([]float32, error) {
|
|
||||||
img, _, err := image.Decode(bytes.NewReader(imgData))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ic.processImage(img)
|
|
||||||
}
|
|
||||||
|
|
||||||
// processImage 处理图片为模型输入格式
|
|
||||||
func (ic *ImageClassifier) processImage(img image.Image) ([]float32, error) {
|
|
||||||
// 调整图片大小到224x224
|
|
||||||
resized := resizeImage(img, 224, 224)
|
|
||||||
|
|
||||||
// 转换为float32数组,并进行归一化
|
|
||||||
// ImageNet的均值和标准差
|
|
||||||
mean := []float32{0.485, 0.456, 0.406}
|
|
||||||
std := []float32{0.229, 0.224, 0.225}
|
|
||||||
|
|
||||||
data := make([]float32, 3*224*224)
|
|
||||||
|
|
||||||
for y := 0; y < 224; y++ {
|
|
||||||
for x := 0; x < 224; x++ {
|
|
||||||
r, g, b, _ := resized.At(x, y).RGBA()
|
|
||||||
|
|
||||||
// 转换为0-1范围
|
|
||||||
rNorm := float32(r) / 65535.0
|
|
||||||
gNorm := float32(g) / 65535.0
|
|
||||||
bNorm := float32(b) / 65535.0
|
|
||||||
|
|
||||||
// 应用归一化
|
|
||||||
idx := y*224 + x
|
|
||||||
data[idx] = (rNorm - mean[0]) / std[0] // R通道
|
|
||||||
data[224*224+idx] = (gNorm - mean[1]) / std[1] // G通道
|
|
||||||
data[2*224*224+idx] = (bNorm - mean[2]) / std[2] // B通道
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// resizeImage 调整图片大小
|
|
||||||
func resizeImage(img image.Image, width, height int) image.Image {
|
|
||||||
bounds := img.Bounds()
|
|
||||||
srcW := bounds.Dx()
|
|
||||||
srcH := bounds.Dy()
|
|
||||||
|
|
||||||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
|
||||||
|
|
||||||
for y := 0; y < height; y++ {
|
|
||||||
for x := 0; x < width; x++ {
|
|
||||||
// 简单的最近邻插值
|
|
||||||
srcX := x * srcW / width
|
|
||||||
srcY := y * srcH / height
|
|
||||||
|
|
||||||
if srcX >= srcW {
|
|
||||||
srcX = srcW - 1
|
|
||||||
}
|
|
||||||
if srcY >= srcH {
|
|
||||||
srcY = srcH - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
dst.Set(x, y, img.At(srcX, srcY))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// centerCrop 中心裁剪图片
|
|
||||||
func centerCrop(img image.Image, size int) image.Image {
|
|
||||||
bounds := img.Bounds()
|
|
||||||
width := bounds.Dx()
|
|
||||||
height := bounds.Dy()
|
|
||||||
|
|
||||||
var x0, y0 int
|
|
||||||
if width > height {
|
|
||||||
x0 = (width - height) / 2
|
|
||||||
y0 = 0
|
|
||||||
width = height
|
|
||||||
} else {
|
|
||||||
x0 = 0
|
|
||||||
y0 = (height - width) / 2
|
|
||||||
height = width
|
|
||||||
}
|
|
||||||
|
|
||||||
cropped := image.NewRGBA(image.Rect(0, 0, width, height))
|
|
||||||
for y := 0; y < height; y++ {
|
|
||||||
for x := 0; x < width; x++ {
|
|
||||||
cropped.Set(x, y, img.At(x0+x, y0+y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resizeImage(cropped, size, size)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package classifier
|
|
||||||
|
|
||||||
import "likedream-goframe/internal/utils"
|
|
||||||
|
|
||||||
var imageClassifier, _ = NewImageClassifier("static/model.onnx", "static/classes.txt")
|
|
||||||
|
|
||||||
// 图片数据 & 类别名称
|
|
||||||
// return 是否匹配
|
|
||||||
func ClassifyImage(imgData []byte, className string) bool {
|
|
||||||
if imageClassifier == nil {
|
|
||||||
c, err := NewImageClassifier("static/model.onnx", "static/classes.txt")
|
|
||||||
utils.OkOrPanic(err)
|
|
||||||
imageClassifier = c
|
|
||||||
}
|
|
||||||
name, _, _ := imageClassifier.PredictFromBytes(imgData)
|
|
||||||
return className == name
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package cookiejar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"io"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type domainMap map[string]cookieMap
|
||||||
|
|
||||||
|
// AllSavePersistCookieJar 完全无限制的 Cookie Jar
|
||||||
|
// 所有域名的 cookies 都保存在一起,访问任何域名都返回所有 cookies
|
||||||
|
// 不检查域名、路径等限制,适用于需要跨域共享 session 的场景
|
||||||
|
type AllSavePersistCookieJar struct {
|
||||||
|
domainMap domainMap
|
||||||
|
*sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalDomain = "*" // 使用全局域名存储所有 cookies
|
||||||
|
|
||||||
|
func NewAllSavePersistCookieJar() *AllSavePersistCookieJar {
|
||||||
|
return &AllSavePersistCookieJar{
|
||||||
|
domainMap: make(domainMap),
|
||||||
|
RWMutex: &sync.RWMutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AllSavePersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
// 使用全局域名,所有 cookies 保存在一起
|
||||||
|
if _, exists := a.domainMap[globalDomain]; !exists {
|
||||||
|
a.domainMap[globalDomain] = make(cookieMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
if a.domainMap[globalDomain] == nil {
|
||||||
|
a.domainMap[globalDomain] = make(cookieMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找是否存在相同 name 的 cookie(忽略 path 和 domain)
|
||||||
|
existingCookies := a.domainMap[globalDomain][cookie.Name]
|
||||||
|
found := false
|
||||||
|
for i, existing := range existingCookies {
|
||||||
|
if existing.Path == cookie.Path && existing.Domain == cookie.Domain {
|
||||||
|
// 找到完全匹配的 cookie,替换它
|
||||||
|
existingCookies[i] = cookie
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
// 没有找到匹配的,添加新的 cookie
|
||||||
|
a.domainMap[globalDomain][cookie.Name] = append(a.domainMap[globalDomain][cookie.Name], cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AllSavePersistCookieJar) Cookies(u *url.URL) []*http.Cookie {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
// 直接返回全局域名下的所有 cookies,不检查域名和路径
|
||||||
|
if cookiesList, exists := a.domainMap[globalDomain]; exists {
|
||||||
|
result := make([]*http.Cookie, 0, len(cookiesList))
|
||||||
|
|
||||||
|
for _, cookies := range cookiesList {
|
||||||
|
// 如果只有一个 cookie,直接返回
|
||||||
|
if len(cookies) == 1 {
|
||||||
|
result = append(result, cookies[0])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有多个同名 cookie,返回最后设置的那个(列表最后一个)
|
||||||
|
if len(cookies) > 0 {
|
||||||
|
result = append(result, cookies[len(cookies)-1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AllSavePersistCookieJar) Serialize() string {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
json, err := sonic.Marshal(a.domainMap)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
return string(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AllSavePersistCookieJar) Deserialize(content string) {
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
if content == "" {
|
||||||
|
a.domainMap = make(domainMap)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var domainMap domainMap
|
||||||
|
err := sonic.Unmarshal([]byte(content), &domainMap)
|
||||||
|
|
||||||
|
// 如果反序列化失败,尝试从旧格式转换
|
||||||
|
if err != nil {
|
||||||
|
// 旧格式: map[string]map[string]*http.Cookie
|
||||||
|
type oldCookieMap map[string]*http.Cookie
|
||||||
|
type oldDomainMap map[string]oldCookieMap
|
||||||
|
|
||||||
|
var oldMap oldDomainMap
|
||||||
|
oldErr := sonic.Unmarshal([]byte(content), &oldMap)
|
||||||
|
if oldErr != nil {
|
||||||
|
// 两种格式都失败,抛出原始错误
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换旧格式到新格式
|
||||||
|
domainMap = make(map[string]cookieMap)
|
||||||
|
for domain, cookies := range oldMap {
|
||||||
|
domainMap[domain] = make(map[string][]*http.Cookie)
|
||||||
|
for name, cookie := range cookies {
|
||||||
|
domainMap[domain][name] = []*http.Cookie{cookie}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.domainMap = domainMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AllSavePersistCookieJar) SerializeBytes() []byte {
|
||||||
|
a.RLock()
|
||||||
|
defer a.RUnlock()
|
||||||
|
|
||||||
|
ret := make([]byte, 0)
|
||||||
|
retWriter := bytes.NewBuffer(ret)
|
||||||
|
|
||||||
|
err := gob.NewEncoder(retWriter).
|
||||||
|
Encode(a.domainMap)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
return retWriter.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AllSavePersistCookieJar) DeserializeBytes(content []byte) {
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
if len(content) == 0 {
|
||||||
|
a.domainMap = make(domainMap)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
retReader := bytes.NewBuffer(content)
|
||||||
|
|
||||||
|
var domainMap domainMap
|
||||||
|
err := gob.NewDecoder(retReader).
|
||||||
|
Decode(&domainMap)
|
||||||
|
|
||||||
|
// 如果反序列化失败,尝试从旧格式转换
|
||||||
|
if err != nil {
|
||||||
|
// 旧格式: map[string]map[string]*http.Cookie
|
||||||
|
type oldCookieMap map[string]*http.Cookie
|
||||||
|
type oldDomainMap map[string]oldCookieMap
|
||||||
|
|
||||||
|
retReader = bytes.NewBuffer(content)
|
||||||
|
var oldMap oldDomainMap
|
||||||
|
oldErr := gob.NewDecoder(retReader).Decode(&oldMap)
|
||||||
|
if oldErr != nil {
|
||||||
|
// 两种格式都失败,抛出原始错误
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换旧格式到新格式
|
||||||
|
domainMap = make(map[string]cookieMap)
|
||||||
|
for domain, cookies := range oldMap {
|
||||||
|
domainMap[domain] = make(map[string][]*http.Cookie)
|
||||||
|
for name, cookie := range cookies {
|
||||||
|
domainMap[domain][name] = []*http.Cookie{cookie}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.domainMap = domainMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AllSavePersistCookieJar) DeserializeReader(reader io.Reader) {
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
// 先读取所有数据,以便在失败时可以重试
|
||||||
|
data, err := io.ReadAll(reader)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
var domainMap domainMap
|
||||||
|
err = gob.NewDecoder(bytes.NewBuffer(data)).Decode(&domainMap)
|
||||||
|
|
||||||
|
// 如果反序列化失败,尝试从旧格式转换
|
||||||
|
if err != nil {
|
||||||
|
// 旧格式: map[string]map[string]*http.Cookie
|
||||||
|
type oldCookieMap map[string]*http.Cookie
|
||||||
|
type oldDomainMap map[string]oldCookieMap
|
||||||
|
|
||||||
|
var oldMap oldDomainMap
|
||||||
|
oldErr := gob.NewDecoder(bytes.NewBuffer(data)).Decode(&oldMap)
|
||||||
|
if oldErr != nil {
|
||||||
|
// 两种格式都失败,抛出原始错误
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换旧格式到新格式
|
||||||
|
domainMap = make(map[string]cookieMap)
|
||||||
|
for domain, cookies := range oldMap {
|
||||||
|
domainMap[domain] = make(map[string][]*http.Cookie)
|
||||||
|
for name, cookie := range cookies {
|
||||||
|
domainMap[domain][name] = []*http.Cookie{cookie}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.domainMap = domainMap
|
||||||
|
}
|
||||||
@@ -3,17 +3,19 @@ package cookiejar
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
"io"
|
"io"
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
)
|
)
|
||||||
|
|
||||||
type hostMap map[string]cookieMap
|
type hostMap map[string]cookieMap
|
||||||
|
|
||||||
type cookieMap map[string]*http.Cookie
|
type cookieMap map[string][]*http.Cookie
|
||||||
type PersistCookieJar struct {
|
type PersistCookieJar struct {
|
||||||
hostMap hostMap
|
hostMap hostMap
|
||||||
*sync.RWMutex
|
*sync.RWMutex
|
||||||
@@ -33,14 +35,35 @@ func (p *PersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
|
|||||||
host := u.Host
|
host := u.Host
|
||||||
|
|
||||||
if _, exists := p.hostMap[host]; !exists {
|
if _, exists := p.hostMap[host]; !exists {
|
||||||
p.hostMap = make(hostMap)
|
p.hostMap[host] = make(cookieMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cookie := range cookies {
|
for _, cookie := range cookies {
|
||||||
if p.hostMap[host] == nil {
|
if p.hostMap[host] == nil {
|
||||||
p.hostMap[host] = make(cookieMap)
|
p.hostMap[host] = make(cookieMap)
|
||||||
}
|
}
|
||||||
p.hostMap[host][cookie.Name] = cookie
|
|
||||||
|
// 检查并删除全局 cookie 中同名的 cookie
|
||||||
|
if globalCookies, exists := p.hostMap[""]; exists {
|
||||||
|
delete(globalCookies, cookie.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找是否存在相同 name, path 和 domain 的 cookie
|
||||||
|
existingCookies := p.hostMap[host][cookie.Name]
|
||||||
|
found := false
|
||||||
|
for i, existing := range existingCookies {
|
||||||
|
if existing.Path == cookie.Path && existing.Domain == cookie.Domain {
|
||||||
|
// 找到完全匹配的 cookie,替换它
|
||||||
|
existingCookies[i] = cookie
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
// 没有找到匹配的,添加新的 cookie
|
||||||
|
p.hostMap[host][cookie.Name] = append(p.hostMap[host][cookie.Name], cookie)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -50,14 +73,110 @@ func (p *PersistCookieJar) Cookies(u *url.URL) []*http.Cookie {
|
|||||||
defer p.RUnlock()
|
defer p.RUnlock()
|
||||||
|
|
||||||
host := u.Host
|
host := u.Host
|
||||||
if cookies, exists := p.hostMap[host]; exists {
|
requestPath := u.Path
|
||||||
result := make([]*http.Cookie, 0, len(cookies))
|
if requestPath == "" {
|
||||||
for _, cookie := range cookies {
|
requestPath = "/"
|
||||||
result = append(result, cookie)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
result := make([]*http.Cookie, 0)
|
||||||
|
addedNames := make(map[string]bool)
|
||||||
|
|
||||||
|
// 先添加匹配 host 的 cookie
|
||||||
|
if cookiesList, exists := p.hostMap[host]; exists {
|
||||||
|
for cookieName, cookies := range cookiesList {
|
||||||
|
// 如果只有一个 cookie,直接返回
|
||||||
|
if len(cookies) == 1 {
|
||||||
|
result = append(result, cookies[0])
|
||||||
|
addedNames[cookieName] = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有多个同名 cookie,选择最匹配的(path 最长且匹配请求路径的)
|
||||||
|
var bestMatch *http.Cookie
|
||||||
|
bestMatchLen := -1
|
||||||
|
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
cookiePath := cookie.Path
|
||||||
|
if cookiePath == "" {
|
||||||
|
cookiePath = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 path 是否匹配
|
||||||
|
if pathMatch(requestPath, cookiePath) {
|
||||||
|
if len(cookiePath) > bestMatchLen {
|
||||||
|
bestMatch = cookie
|
||||||
|
bestMatchLen = len(cookiePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找到匹配的,添加到结果中
|
||||||
|
if bestMatch != nil {
|
||||||
|
result = append(result, bestMatch)
|
||||||
|
addedNames[cookieName] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再添加没有 host 的全局 cookie(避免重复)
|
||||||
|
if globalCookies, exists := p.hostMap[""]; exists {
|
||||||
|
for cookieName, cookies := range globalCookies {
|
||||||
|
// 如果该名称的 cookie 已经从 host 中添加了,跳过
|
||||||
|
if addedNames[cookieName] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果只有一个 cookie,直接返回
|
||||||
|
if len(cookies) == 1 {
|
||||||
|
result = append(result, cookies[0])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有多个同名 cookie,选择最匹配的
|
||||||
|
var bestMatch *http.Cookie
|
||||||
|
bestMatchLen := -1
|
||||||
|
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
cookiePath := cookie.Path
|
||||||
|
if cookiePath == "" {
|
||||||
|
cookiePath = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if pathMatch(requestPath, cookiePath) {
|
||||||
|
if len(cookiePath) > bestMatchLen {
|
||||||
|
bestMatch = cookie
|
||||||
|
bestMatchLen = len(cookiePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestMatch != nil {
|
||||||
|
result = append(result, bestMatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathMatch 检查请求路径是否匹配 cookie 的 path
|
||||||
|
// 根据 RFC 6265 的规则
|
||||||
|
func pathMatch(requestPath, cookiePath string) bool {
|
||||||
|
if requestPath == cookiePath {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(requestPath, cookiePath) {
|
||||||
|
// cookiePath 以 / 结尾,或者 requestPath 的下一个字符是 /
|
||||||
|
if strings.HasSuffix(cookiePath, "/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(requestPath) > len(cookiePath) && requestPath[len(cookiePath)] == '/' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PersistCookieJar) Serialize() string {
|
func (p *PersistCookieJar) Serialize() string {
|
||||||
@@ -83,7 +202,29 @@ func (p *PersistCookieJar) Deserialize(content string) {
|
|||||||
var hostMap hostMap
|
var hostMap hostMap
|
||||||
err := sonic.Unmarshal([]byte(content), &hostMap)
|
err := sonic.Unmarshal([]byte(content), &hostMap)
|
||||||
|
|
||||||
utils.OkOrPanic(err)
|
// 如果反序列化失败,尝试从旧格式转换
|
||||||
|
if err != nil {
|
||||||
|
// 旧格式: map[string]map[string]*http.Cookie
|
||||||
|
type oldCookieMap map[string]*http.Cookie
|
||||||
|
type oldHostMap map[string]oldCookieMap
|
||||||
|
|
||||||
|
var oldMap oldHostMap
|
||||||
|
oldErr := sonic.Unmarshal([]byte(content), &oldMap)
|
||||||
|
if oldErr != nil {
|
||||||
|
// 两种格式都失败,抛出原始错误
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换旧格式到新格式
|
||||||
|
hostMap = make(map[string]cookieMap)
|
||||||
|
for host, cookies := range oldMap {
|
||||||
|
hostMap[host] = make(map[string][]*http.Cookie)
|
||||||
|
for name, cookie := range cookies {
|
||||||
|
hostMap[host][name] = []*http.Cookie{cookie}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p.hostMap = hostMap
|
p.hostMap = hostMap
|
||||||
}
|
}
|
||||||
@@ -116,7 +257,31 @@ func (p *PersistCookieJar) DeserializeBytes(content []byte) {
|
|||||||
var hostMap hostMap
|
var hostMap hostMap
|
||||||
err := gob.NewDecoder(retReader).
|
err := gob.NewDecoder(retReader).
|
||||||
Decode(&hostMap)
|
Decode(&hostMap)
|
||||||
utils.OkOrPanic(err)
|
|
||||||
|
// 如果反序列化失败,尝试从旧格式转换
|
||||||
|
if err != nil {
|
||||||
|
// 旧格式: map[string]map[string]*http.Cookie
|
||||||
|
type oldCookieMap map[string]*http.Cookie
|
||||||
|
type oldHostMap map[string]oldCookieMap
|
||||||
|
|
||||||
|
retReader = bytes.NewBuffer(content)
|
||||||
|
var oldMap oldHostMap
|
||||||
|
oldErr := gob.NewDecoder(retReader).Decode(&oldMap)
|
||||||
|
if oldErr != nil {
|
||||||
|
// 两种格式都失败,抛出原始错误
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换旧格式到新格式
|
||||||
|
hostMap = make(map[string]cookieMap)
|
||||||
|
for host, cookies := range oldMap {
|
||||||
|
hostMap[host] = make(map[string][]*http.Cookie)
|
||||||
|
for name, cookie := range cookies {
|
||||||
|
hostMap[host][name] = []*http.Cookie{cookie}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p.hostMap = hostMap
|
p.hostMap = hostMap
|
||||||
}
|
}
|
||||||
@@ -125,10 +290,63 @@ func (p *PersistCookieJar) DeserializeReader(reader io.Reader) {
|
|||||||
p.Lock()
|
p.Lock()
|
||||||
defer p.Unlock()
|
defer p.Unlock()
|
||||||
|
|
||||||
var hostMap hostMap
|
// 先读取所有数据,以便在失败时可以重试
|
||||||
err := gob.NewDecoder(reader).
|
data, err := io.ReadAll(reader)
|
||||||
Decode(&hostMap)
|
|
||||||
utils.OkOrPanic(err)
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
var hostMap hostMap
|
||||||
|
err = gob.NewDecoder(bytes.NewBuffer(data)).Decode(&hostMap)
|
||||||
|
|
||||||
|
// 如果反序列化失败,尝试从旧格式转换
|
||||||
|
if err != nil {
|
||||||
|
// 旧格式: map[string]map[string]*http.Cookie
|
||||||
|
type oldCookieMap map[string]*http.Cookie
|
||||||
|
type oldHostMap map[string]oldCookieMap
|
||||||
|
|
||||||
|
var oldMap oldHostMap
|
||||||
|
oldErr := gob.NewDecoder(bytes.NewBuffer(data)).Decode(&oldMap)
|
||||||
|
if oldErr != nil {
|
||||||
|
// 两种格式都失败,抛出原始错误
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换旧格式到新格式
|
||||||
|
hostMap = make(map[string]cookieMap)
|
||||||
|
for host, cookies := range oldMap {
|
||||||
|
hostMap[host] = make(map[string][]*http.Cookie)
|
||||||
|
for name, cookie := range cookies {
|
||||||
|
hostMap[host][name] = []*http.Cookie{cookie}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p.hostMap = hostMap
|
p.hostMap = hostMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveCookieInfo 将所有 host 的 cookie 移除 host 信息,存储为全局 cookie
|
||||||
|
func (p *PersistCookieJar) RemoveCookieInfo() {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
// 确保全局 cookie map 存在
|
||||||
|
if _, exists := p.hostMap[""]; !exists {
|
||||||
|
p.hostMap[""] = make(cookieMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将所有 host 的 cookie 移动到全局
|
||||||
|
for host, hostCookies := range p.hostMap {
|
||||||
|
// 跳过全局 cookie 本身
|
||||||
|
if host == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将该 host 的所有 cookie 移动到全局
|
||||||
|
for cookieName, cookies := range hostCookies {
|
||||||
|
p.hostMap[""][cookieName] = cookies
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除该 host 的 cookie
|
||||||
|
delete(p.hostMap, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -114,3 +114,10 @@ func TestPersistCookieJar_DeserializeReader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeseralizeCookie(t *testing.T) {
|
||||||
|
var str = `{"cdpc.campusphere.net":{"MOD_AUTH_CAS":{"Name":"MOD_AUTH_CAS","Value":"ST-iap:1021988086918169:ST:8d7fe60b-dc7d-4fcd-b26b-5517e3ca1926:20251113162807","Quoted":false,"Path":"/","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":true,"SameSite":0,"Partitioned":false,"Raw":"MOD_AUTH_CAS=ST-iap:1021988086918169:ST:8d7fe60b-dc7d-4fcd-b26b-5517e3ca1926:20251113162807; path=/; Httponly","Unparsed":null},"HWWAFSESID":{"Name":"HWWAFSESID","Value":"88d84731cf506df0bf","Quoted":false,"Path":"/","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":false,"SameSite":0,"Partitioned":false,"Raw":"HWWAFSESID=88d84731cf506df0bf; path=/","Unparsed":null},"HWWAFSESTIME":{"Name":"HWWAFSESTIME","Value":"1763022486728","Quoted":false,"Path":"/","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":false,"SameSite":0,"Partitioned":false,"Raw":"HWWAFSESTIME=1763022486728; path=/","Unparsed":null},"CONVERSATION":{"Name":"CONVERSATION","Value":"iap-1021988086918169-CONV-83bee566-7010-4ef6-ad17-4b9eebcccdf8","Quoted":false,"Path":"/iap","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":true,"SameSite":0,"Partitioned":false,"Raw":"CONVERSATION=iap-1021988086918169-CONV-83bee566-7010-4ef6-ad17-4b9eebcccdf8; Path=/iap; HttpOnly","Unparsed":null},"CASTGC":{"Name":"CASTGC","Value":"iap-1021988086918169-TGT-717aa5db-92cb-46ac-a178-64b630946f43","Quoted":false,"Path":"/iap","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":true,"HttpOnly":true,"SameSite":4,"Partitioned":false,"Raw":"CASTGC=iap-1021988086918169-TGT-717aa5db-92cb-46ac-a178-64b630946f43; Path=/iap; HttpOnly; Secure; SameSite=None","Unparsed":null}}}`
|
||||||
|
jar := NewPersistCookieJar()
|
||||||
|
jar.Deserialize(str)
|
||||||
|
t.Log(jar)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAesDecrypt(t *testing.T) {
|
||||||
|
str := "6XkC1UAk07fK0uTaGPUu77i/+r7j/o1JQ/XygRxee2LMiX5H+w/BOqOB6e7W o9kHDZMb1pqZNN43k1tjl0ephbPLEi8g9C1EqBw0suzxX74j3dUdCf6dd/0W 8KvntNXwju96Vy5B2oD2ZosLmUdB1m0xX5rxAdLJ//cTft87XDWoLM0fuasu fGvR4XXXYZN071x8F03+qqR52fD1UQGgn0uGkSa/nrUho32Sg7DNJzF/JBCk 6/z0lu0XmIPo1SDIVjOtYAmyUDMZGl/ZvSAu2w=="
|
||||||
|
str = strings.ReplaceAll(str, " ", "")
|
||||||
|
decodeString, _ := base64.StdEncoding.DecodeString(str)
|
||||||
|
decrypt := DesDecrypt(decodeString, Cat)
|
||||||
|
t.Log(string(decrypt))
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/container/gvar"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDesDecrypt(t *testing.T) {
|
||||||
|
decodeString, _ := base64.StdEncoding.DecodeString("64JITpWPkKuGMhi1m7Ikxu1ekL6VQOdjmJvaQPI07MWbLETtJsxzkk2bCUVNaWgUULa3wS9LH+ANO5CUBeOXi02CcS/8lN/tYDFYCyuK7dLN+q0fD0gQlUsuHwy/78EkwZa9VrLh4e519vM9AxR2GnK4WGpumeH7TGJjDkLH0AitKfUYQnil9AKZEVgSSkHkg9ko64qj0QJvzmo76zOkMpIb1+1Pn2cO+5soTSl8BCsP/8hOP5HdOMVBCYH++iSK3O5sYz/cwZq/G8aJrqmEFcDHbgTouXyD5kxa4lb72Vw3Ku+eH+3qDwYMYxWrIcAm")
|
||||||
|
decrypt := DesDecrypt(decodeString, Cat)
|
||||||
|
str := "{\"lon\":\"113.045246\",\"lat\":\"28.077659\",\"model\":\"847b5b9c-2d97-41ec-8a5d-e9ac79da090d\",\"appVersion\":\"9.7.2\",\"userId\":\"430181200504150038\",\"systemName\":\"android\",\"deviceId\":\"09E46EC9-57C1-4724-BE53-5879CA80A2A0\",\"cpdailyVersion\":\"9.7.2\",\"systemVersion\":\"iOS 17\"}"
|
||||||
|
encrypt := DesEncrypt([]byte(str), Cat)
|
||||||
|
toString := base64.StdEncoding.EncodeToString(encrypt)
|
||||||
|
t.Log(toString)
|
||||||
|
t.Log(string(decrypt))
|
||||||
|
d2, _ := base64.StdEncoding.DecodeString("86JzX4MBsorI5KasKBHUj6orO1qcPWbo758Ux6G2NUoRuGCoZnE+cJW5slZvsflBrKITPeitF7GTisMNRJxRqlA8cLXAqyo9ZlBPPx0MM3KjILHiA/EvrRrmamo7vi1d/F0ChQc8jLfMgkkcOg4FBmM1H8Yg6KyYdMNN7anIbIdJ+6RPEsakjAIw0r4eawofSh5E0QclDry0v36GK3UA3GDBBfqSub8V5PRL7fQty/R0lx7+mfc2AIXXjUVsv85WQKoyyUwg3siWpQL5S1cbtQNUXClSQP1IAGROnBftvdrib01PR1h7Cp+kvE3HK0+/8l0FoGJ1aijff1t9T9MaVXM/jSUFwkA7tmZIjodrUDhZcCMNm5YLm1ZO4UVBQO118FyOBjSLet0nPc0Wg0/dTO0XZoo9e2bCQwjIQC2G8TdzHDxvDjGEc6hX++nZrN23n19bAgqdvzZt58KftznE9s5t4CEjiHx3ykyidGEGr/VEi/05uOtm8/EsFUVp8+MyVgaTxoaq/4LBu28L59U6+6mZjOFUt49STR5p3UPI3ZJQLe2lmnx9BdqrIHbdXgYNu5oNzrDM8m3z3ns9M2HEGg==")
|
||||||
|
d := AesDecryptForCat(d2)
|
||||||
|
t.Log(d)
|
||||||
|
|
||||||
|
form := map[string]*gvar.Var{
|
||||||
|
"appVersion": gvar.New("9.7.2"),
|
||||||
|
"bodyString": gvar.New("0u7hBd+6O11ZaGsT51FUAVfB72waqtTx5oQQLVEEAuCM29x8lQc41i9/dF6+9wjQNC05jdJTBmHlwFoLD09hfsnQWSyjl/ys124RUQL3r/Um5AwNdKiJE24YQsyQlP3ZamUIFlrNSxmn/DfZ/jR9oZhiVKZSlygA2LNLxtNOyk4rETfqKpZKFOnThbXy5Hf779jUDmPNwn1ZrdWN8UHm6wO+EahrrX60vC02VUHs7r3rMrDWZjBJ57eSgD0QwZabcIb/u9kx1AD8OAxINar3X+N+uAhfrxjHd6v6OVvnc2I9iPLEGNFRKZ94jq64hnATOSH6S/IR/QbDoxebWpGUXm3ALYZeoni09uyhG7IE5NtRw/OmqYtdknrs+c0qyOOBkvxPlqKo+SV4qq7xtrn9RWeZIigiyM8mVtdovN1T/x3DnHxd/AYoSS3/cGkg+2QHzWey/Xf5c4g0EvhaGlE0C7ejKIn5wp0XeHyvAjm4ZVyUhgTN2dZVCQ6ChxXOpLnVDWPNeeZ9vysqVl1ZSkQdCz20gy3p9RfYuArluQqjwIoWQe632Cc/Ep++99V5Tc9iIlLr93PfxFaJJfov3avLKw=="),
|
||||||
|
"calVersion": gvar.New("firstv"),
|
||||||
|
"deviceId": gvar.New("09E46EC9-57C1-4724-BE53-5879CA80A2A0"),
|
||||||
|
"lat": gvar.New("28.077659"),
|
||||||
|
"lon": gvar.New("113.045246"),
|
||||||
|
"model": gvar.New("847b5b9c-2d97-41ec-8a5d-e9ac79da090d"),
|
||||||
|
"systemName": gvar.New("android"),
|
||||||
|
"systemVersion": gvar.New("iOS 17"),
|
||||||
|
"userId": gvar.New("430181200504150038"),
|
||||||
|
"version": gvar.New("first_v4"),
|
||||||
|
}
|
||||||
|
sign := GetSubmitSign(form)
|
||||||
|
t.Log(sign)
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitSignSalt = "hru89rwr87Nthifa" // 请根据实际情况替换为真实盐值
|
||||||
|
|
||||||
|
func GetSubmitSign(form map[string]*gvar.Var) string {
|
||||||
|
// 按照指定顺序构建哈希数据
|
||||||
|
keys := []string{
|
||||||
|
"appVersion",
|
||||||
|
"bodyString",
|
||||||
|
"deviceId",
|
||||||
|
"lat",
|
||||||
|
"lon",
|
||||||
|
"model",
|
||||||
|
"systemName",
|
||||||
|
"systemVersion",
|
||||||
|
"userId",
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, key := range keys {
|
||||||
|
val := ""
|
||||||
|
if v, ok := form[key]; ok && v != nil {
|
||||||
|
val = v.String()
|
||||||
|
}
|
||||||
|
// 这里是 key=UrlEncode(key)&,注意是对 key 进行 url encode
|
||||||
|
sb.WriteString(key)
|
||||||
|
sb.WriteString("=")
|
||||||
|
sb.WriteString(url.QueryEscape(val))
|
||||||
|
sb.WriteString("&")
|
||||||
|
}
|
||||||
|
sb.WriteString(submitSignSalt)
|
||||||
|
|
||||||
|
// 计算MD5
|
||||||
|
hash := md5.Sum([]byte(sb.String()))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MD5 对输入字符串进行 MD5 加密,返回 32 位小写十六进制字符串
|
||||||
|
func MD5(input string) string {
|
||||||
|
hash := md5.Sum([]byte(input))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
@@ -0,0 +1,838 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RSA Constants
|
||||||
|
const (
|
||||||
|
biRadixBits = 16
|
||||||
|
bitsPerDigit = biRadixBits
|
||||||
|
biRadix = 1 << 16 // 65536
|
||||||
|
biHalfRadix = biRadix >> 1
|
||||||
|
biRadixSquared = biRadix * biRadix
|
||||||
|
maxDigitVal = biRadix - 1
|
||||||
|
maxInteger = 9999999999999998
|
||||||
|
dpl10 = 15
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
maxDigits int
|
||||||
|
zeroArray []int
|
||||||
|
bigZero *BigInt
|
||||||
|
bigOne *BigInt
|
||||||
|
lr10 *BigInt
|
||||||
|
highBitMasks = []int{0x0000, 0x8000, 0xC000, 0xE000, 0xF000, 0xF800,
|
||||||
|
0xFC00, 0xFE00, 0xFF00, 0xFF80, 0xFFC0, 0xFFE0,
|
||||||
|
0xFFF0, 0xFFF8, 0xFFFC, 0xFFFE, 0xFFFF}
|
||||||
|
lowBitMasks = []int{0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F,
|
||||||
|
0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF,
|
||||||
|
0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF}
|
||||||
|
hexToChar = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||||
|
'a', 'b', 'c', 'd', 'e', 'f'}
|
||||||
|
hexatrigesimalToChar = []byte{
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||||
|
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
|
||||||
|
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
|
||||||
|
'u', 'v', 'w', 'x', 'y', 'z'}
|
||||||
|
)
|
||||||
|
|
||||||
|
// BigInt represents a big integer
|
||||||
|
type BigInt struct {
|
||||||
|
digits []int
|
||||||
|
isNeg bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBigInt creates a new BigInt
|
||||||
|
func NewBigInt() *BigInt {
|
||||||
|
digits := make([]int, len(zeroArray))
|
||||||
|
copy(digits, zeroArray)
|
||||||
|
return &BigInt{
|
||||||
|
digits: digits,
|
||||||
|
isNeg: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBigIntEmpty creates an empty BigInt
|
||||||
|
func NewBigIntEmpty() *BigInt {
|
||||||
|
return &BigInt{
|
||||||
|
digits: nil,
|
||||||
|
isNeg: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxDigits sets the maximum number of digits
|
||||||
|
func SetMaxDigits(value int) {
|
||||||
|
maxDigits = value
|
||||||
|
zeroArray = make([]int, maxDigits)
|
||||||
|
bigZero = NewBigInt()
|
||||||
|
bigOne = NewBigInt()
|
||||||
|
bigOne.digits[0] = 1
|
||||||
|
lr10 = BiFromNumber(1000000000000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiFromNumber creates a BigInt from a number
|
||||||
|
func BiFromNumber(i int64) *BigInt {
|
||||||
|
result := NewBigInt()
|
||||||
|
result.isNeg = i < 0
|
||||||
|
i = int64(math.Abs(float64(i)))
|
||||||
|
j := 0
|
||||||
|
for i > 0 {
|
||||||
|
result.digits[j] = int(i & maxDigitVal)
|
||||||
|
j++
|
||||||
|
i = i / biRadix
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiFromDecimal creates a BigInt from a decimal string
|
||||||
|
func BiFromDecimal(s string) *BigInt {
|
||||||
|
isNeg := len(s) > 0 && s[0] == '-'
|
||||||
|
i := 0
|
||||||
|
if isNeg {
|
||||||
|
i = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip leading zeros
|
||||||
|
for i < len(s) && s[i] == '0' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(s) {
|
||||||
|
return NewBigInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
digitCount := len(s) - i
|
||||||
|
fgl := digitCount % dpl10
|
||||||
|
if fgl == 0 {
|
||||||
|
fgl = dpl10
|
||||||
|
}
|
||||||
|
|
||||||
|
num := parseNumber(s[i : i+fgl])
|
||||||
|
result := BiFromNumber(num)
|
||||||
|
i += fgl
|
||||||
|
|
||||||
|
for i < len(s) {
|
||||||
|
result = BiAdd(BiMultiply(result, lr10), BiFromNumber(parseNumber(s[i:min(i+dpl10, len(s))])))
|
||||||
|
i += dpl10
|
||||||
|
}
|
||||||
|
|
||||||
|
result.isNeg = isNeg
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiCopy copies a BigInt
|
||||||
|
func BiCopy(bi *BigInt) *BigInt {
|
||||||
|
result := NewBigIntEmpty()
|
||||||
|
result.digits = make([]int, len(bi.digits))
|
||||||
|
copy(result.digits, bi.digits)
|
||||||
|
result.isNeg = bi.isNeg
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReverseStr reverses a string
|
||||||
|
func ReverseStr(s string) string {
|
||||||
|
runes := []rune(s)
|
||||||
|
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
runes[i], runes[j] = runes[j], runes[i]
|
||||||
|
}
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiToString converts BigInt to string with given radix
|
||||||
|
func BiToString(x *BigInt, radix int) string {
|
||||||
|
b := NewBigInt()
|
||||||
|
b.digits[0] = radix
|
||||||
|
qr := BiDivideModulo(x, b)
|
||||||
|
result := string(hexatrigesimalToChar[qr[1].digits[0]])
|
||||||
|
|
||||||
|
for BiCompare(qr[0], bigZero) == 1 {
|
||||||
|
qr = BiDivideModulo(qr[0], b)
|
||||||
|
result += string(hexatrigesimalToChar[qr[1].digits[0]])
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := ""
|
||||||
|
if x.isNeg {
|
||||||
|
prefix = "-"
|
||||||
|
}
|
||||||
|
return prefix + ReverseStr(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiToDecimal converts BigInt to decimal string
|
||||||
|
func BiToDecimal(x *BigInt) string {
|
||||||
|
b := NewBigInt()
|
||||||
|
b.digits[0] = 10
|
||||||
|
qr := BiDivideModulo(x, b)
|
||||||
|
result := fmt.Sprintf("%d", qr[1].digits[0])
|
||||||
|
|
||||||
|
for BiCompare(qr[0], bigZero) == 1 {
|
||||||
|
qr = BiDivideModulo(qr[0], b)
|
||||||
|
result += fmt.Sprintf("%d", qr[1].digits[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := ""
|
||||||
|
if x.isNeg {
|
||||||
|
prefix = "-"
|
||||||
|
}
|
||||||
|
return prefix + ReverseStr(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DigitToHex converts a digit to hex string
|
||||||
|
func DigitToHex(n int) string {
|
||||||
|
mask := 0xf
|
||||||
|
result := ""
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
result += string(hexToChar[n&mask])
|
||||||
|
n >>= 4
|
||||||
|
}
|
||||||
|
return ReverseStr(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiToHex converts BigInt to hex string
|
||||||
|
func BiToHex(x *BigInt) string {
|
||||||
|
result := ""
|
||||||
|
for i := BiHighIndex(x); i > -1; i-- {
|
||||||
|
result += DigitToHex(x.digits[i])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// CharToHex converts a character code to hex value
|
||||||
|
func CharToHex(c byte) int {
|
||||||
|
const (
|
||||||
|
ZERO = 48
|
||||||
|
NINE = ZERO + 9
|
||||||
|
littleA = 97
|
||||||
|
littleZ = littleA + 25
|
||||||
|
bigA = 65
|
||||||
|
bigZ = 65 + 25
|
||||||
|
)
|
||||||
|
|
||||||
|
if c >= ZERO && c <= NINE {
|
||||||
|
return int(c - ZERO)
|
||||||
|
} else if c >= bigA && c <= bigZ {
|
||||||
|
return 10 + int(c-bigA)
|
||||||
|
} else if c >= littleA && c <= littleZ {
|
||||||
|
return 10 + int(c-littleA)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// HexToDigit converts hex string to digit
|
||||||
|
func HexToDigit(s string) int {
|
||||||
|
result := 0
|
||||||
|
sl := min(len(s), 4)
|
||||||
|
for i := 0; i < sl; i++ {
|
||||||
|
result <<= 4
|
||||||
|
result |= CharToHex(s[i])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiFromHex creates BigInt from hex string
|
||||||
|
func BiFromHex(s string) *BigInt {
|
||||||
|
result := NewBigInt()
|
||||||
|
sl := len(s)
|
||||||
|
for i, j := sl, 0; i > 0; i, j = i-4, j+1 {
|
||||||
|
start := max(i-4, 0)
|
||||||
|
length := min(i, 4)
|
||||||
|
if start+length > sl {
|
||||||
|
length = sl - start
|
||||||
|
}
|
||||||
|
result.digits[j] = HexToDigit(s[start : start+length])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiFromString creates BigInt from string with given radix
|
||||||
|
func BiFromString(s string, radix int) *BigInt {
|
||||||
|
isNeg := len(s) > 0 && s[0] == '-'
|
||||||
|
istop := 0
|
||||||
|
if isNeg {
|
||||||
|
istop = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
result := NewBigInt()
|
||||||
|
place := NewBigInt()
|
||||||
|
place.digits[0] = 1
|
||||||
|
|
||||||
|
for i := len(s) - 1; i >= istop; i-- {
|
||||||
|
c := s[i]
|
||||||
|
digit := CharToHex(c)
|
||||||
|
biDigit := BiMultiplyDigit(place, digit)
|
||||||
|
result = BiAdd(result, biDigit)
|
||||||
|
place = BiMultiplyDigit(place, radix)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.isNeg = isNeg
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiAdd adds two BigInts
|
||||||
|
func BiAdd(x, y *BigInt) *BigInt {
|
||||||
|
var result *BigInt
|
||||||
|
|
||||||
|
if x.isNeg != y.isNeg {
|
||||||
|
y.isNeg = !y.isNeg
|
||||||
|
result = BiSubtract(x, y)
|
||||||
|
y.isNeg = !y.isNeg
|
||||||
|
} else {
|
||||||
|
result = NewBigInt()
|
||||||
|
c := 0
|
||||||
|
for i := 0; i < len(x.digits); i++ {
|
||||||
|
n := x.digits[i] + y.digits[i] + c
|
||||||
|
result.digits[i] = n % biRadix
|
||||||
|
if n >= biRadix {
|
||||||
|
c = 1
|
||||||
|
} else {
|
||||||
|
c = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.isNeg = x.isNeg
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiSubtract subtracts two BigInts
|
||||||
|
func BiSubtract(x, y *BigInt) *BigInt {
|
||||||
|
var result *BigInt
|
||||||
|
|
||||||
|
if x.isNeg != y.isNeg {
|
||||||
|
y.isNeg = !y.isNeg
|
||||||
|
result = BiAdd(x, y)
|
||||||
|
y.isNeg = !y.isNeg
|
||||||
|
} else {
|
||||||
|
result = NewBigInt()
|
||||||
|
c := 0
|
||||||
|
|
||||||
|
for i := 0; i < len(x.digits); i++ {
|
||||||
|
n := x.digits[i] - y.digits[i] + c
|
||||||
|
result.digits[i] = n % biRadix
|
||||||
|
if result.digits[i] < 0 {
|
||||||
|
result.digits[i] += biRadix
|
||||||
|
}
|
||||||
|
if n < 0 {
|
||||||
|
c = -1
|
||||||
|
} else {
|
||||||
|
c = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == -1 {
|
||||||
|
c = 0
|
||||||
|
for i := 0; i < len(x.digits); i++ {
|
||||||
|
n := 0 - result.digits[i] + c
|
||||||
|
result.digits[i] = n % biRadix
|
||||||
|
if result.digits[i] < 0 {
|
||||||
|
result.digits[i] += biRadix
|
||||||
|
}
|
||||||
|
if n < 0 {
|
||||||
|
c = -1
|
||||||
|
} else {
|
||||||
|
c = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.isNeg = !x.isNeg
|
||||||
|
} else {
|
||||||
|
result.isNeg = x.isNeg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiHighIndex returns the highest non-zero digit index
|
||||||
|
func BiHighIndex(x *BigInt) int {
|
||||||
|
result := len(x.digits) - 1
|
||||||
|
for result > 0 && x.digits[result] == 0 {
|
||||||
|
result--
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiNumBits returns the number of bits
|
||||||
|
func BiNumBits(x *BigInt) int {
|
||||||
|
n := BiHighIndex(x)
|
||||||
|
d := x.digits[n]
|
||||||
|
m := (n + 1) * bitsPerDigit
|
||||||
|
result := m
|
||||||
|
for result > m-bitsPerDigit {
|
||||||
|
if (d & 0x8000) != 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
d <<= 1
|
||||||
|
result--
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiMultiply multiplies two BigInts
|
||||||
|
func BiMultiply(x, y *BigInt) *BigInt {
|
||||||
|
result := NewBigInt()
|
||||||
|
n := BiHighIndex(x)
|
||||||
|
t := BiHighIndex(y)
|
||||||
|
|
||||||
|
for i := 0; i <= t; i++ {
|
||||||
|
c := 0
|
||||||
|
k := i
|
||||||
|
for j := 0; j <= n; j, k = j+1, k+1 {
|
||||||
|
uv := result.digits[k] + x.digits[j]*y.digits[i] + c
|
||||||
|
result.digits[k] = uv & maxDigitVal
|
||||||
|
c = uv >> biRadixBits
|
||||||
|
}
|
||||||
|
result.digits[i+n+1] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
result.isNeg = x.isNeg != y.isNeg
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiMultiplyDigit multiplies BigInt by a digit
|
||||||
|
func BiMultiplyDigit(x *BigInt, y int) *BigInt {
|
||||||
|
result := NewBigInt()
|
||||||
|
n := BiHighIndex(x)
|
||||||
|
c := 0
|
||||||
|
|
||||||
|
for j := 0; j <= n; j++ {
|
||||||
|
uv := result.digits[j] + x.digits[j]*y + c
|
||||||
|
result.digits[j] = uv & maxDigitVal
|
||||||
|
c = uv >> biRadixBits
|
||||||
|
}
|
||||||
|
result.digits[1+n] = c
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArrayCopy copies array elements
|
||||||
|
func ArrayCopy(src []int, srcStart int, dest []int, destStart int, n int) {
|
||||||
|
m := min(srcStart+n, len(src))
|
||||||
|
for i, j := srcStart, destStart; i < m; i, j = i+1, j+1 {
|
||||||
|
dest[j] = src[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiShiftLeft shifts BigInt left by n bits
|
||||||
|
func BiShiftLeft(x *BigInt, n int) *BigInt {
|
||||||
|
digitCount := n / bitsPerDigit
|
||||||
|
result := NewBigInt()
|
||||||
|
ArrayCopy(x.digits, 0, result.digits, digitCount, len(result.digits)-digitCount)
|
||||||
|
|
||||||
|
bits := n % bitsPerDigit
|
||||||
|
rightBits := bitsPerDigit - bits
|
||||||
|
|
||||||
|
for i, i1 := len(result.digits)-1, len(result.digits)-2; i > 0; i, i1 = i-1, i1-1 {
|
||||||
|
result.digits[i] = ((result.digits[i] << bits) & maxDigitVal) |
|
||||||
|
((result.digits[i1] & highBitMasks[bits]) >> rightBits)
|
||||||
|
}
|
||||||
|
result.digits[0] = ((result.digits[0] << bits) & maxDigitVal)
|
||||||
|
result.isNeg = x.isNeg
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiShiftRight shifts BigInt right by n bits
|
||||||
|
func BiShiftRight(x *BigInt, n int) *BigInt {
|
||||||
|
digitCount := n / bitsPerDigit
|
||||||
|
result := NewBigInt()
|
||||||
|
ArrayCopy(x.digits, digitCount, result.digits, 0, len(x.digits)-digitCount)
|
||||||
|
|
||||||
|
bits := n % bitsPerDigit
|
||||||
|
leftBits := bitsPerDigit - bits
|
||||||
|
|
||||||
|
for i, i1 := 0, 1; i < len(result.digits)-1; i, i1 = i+1, i1+1 {
|
||||||
|
result.digits[i] = (result.digits[i] >> bits) |
|
||||||
|
((result.digits[i1] & lowBitMasks[bits]) << leftBits)
|
||||||
|
}
|
||||||
|
result.digits[len(result.digits)-1] >>= bits
|
||||||
|
result.isNeg = x.isNeg
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiMultiplyByRadixPower multiplies BigInt by radix^n
|
||||||
|
func BiMultiplyByRadixPower(x *BigInt, n int) *BigInt {
|
||||||
|
result := NewBigInt()
|
||||||
|
ArrayCopy(x.digits, 0, result.digits, n, len(result.digits)-n)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiDivideByRadixPower divides BigInt by radix^n
|
||||||
|
func BiDivideByRadixPower(x *BigInt, n int) *BigInt {
|
||||||
|
result := NewBigInt()
|
||||||
|
ArrayCopy(x.digits, n, result.digits, 0, len(result.digits)-n)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiModuloByRadixPower computes BigInt mod radix^n
|
||||||
|
func BiModuloByRadixPower(x *BigInt, n int) *BigInt {
|
||||||
|
result := NewBigInt()
|
||||||
|
ArrayCopy(x.digits, 0, result.digits, 0, n)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiCompare compares two BigInts
|
||||||
|
func BiCompare(x, y *BigInt) int {
|
||||||
|
if x.isNeg != y.isNeg {
|
||||||
|
if x.isNeg {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(x.digits) - 1; i >= 0; i-- {
|
||||||
|
if x.digits[i] != y.digits[i] {
|
||||||
|
if x.isNeg {
|
||||||
|
if x.digits[i] > y.digits[i] {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
if x.digits[i] < y.digits[i] {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiDivideModulo divides two BigInts and returns quotient and remainder
|
||||||
|
func BiDivideModulo(x, y *BigInt) []*BigInt {
|
||||||
|
nb := BiNumBits(x)
|
||||||
|
tb := BiNumBits(y)
|
||||||
|
origYIsNeg := y.isNeg
|
||||||
|
var q, r *BigInt
|
||||||
|
|
||||||
|
if nb < tb {
|
||||||
|
if x.isNeg {
|
||||||
|
q = BiCopy(bigOne)
|
||||||
|
q.isNeg = !y.isNeg
|
||||||
|
x.isNeg = false
|
||||||
|
y.isNeg = false
|
||||||
|
r = BiSubtract(y, x)
|
||||||
|
x.isNeg = true
|
||||||
|
y.isNeg = origYIsNeg
|
||||||
|
} else {
|
||||||
|
q = NewBigInt()
|
||||||
|
r = BiCopy(x)
|
||||||
|
}
|
||||||
|
return []*BigInt{q, r}
|
||||||
|
}
|
||||||
|
|
||||||
|
q = NewBigInt()
|
||||||
|
r = x
|
||||||
|
|
||||||
|
t := int(math.Ceil(float64(tb)/float64(bitsPerDigit))) - 1
|
||||||
|
lambda := 0
|
||||||
|
for y.digits[t] < biHalfRadix {
|
||||||
|
y = BiShiftLeft(y, 1)
|
||||||
|
lambda++
|
||||||
|
tb++
|
||||||
|
t = int(math.Ceil(float64(tb)/float64(bitsPerDigit))) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
r = BiShiftLeft(r, lambda)
|
||||||
|
nb += lambda
|
||||||
|
n := int(math.Ceil(float64(nb)/float64(bitsPerDigit))) - 1
|
||||||
|
|
||||||
|
b := BiMultiplyByRadixPower(y, n-t)
|
||||||
|
for BiCompare(r, b) != -1 {
|
||||||
|
q.digits[n-t]++
|
||||||
|
r = BiSubtract(r, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := n; i > t; i-- {
|
||||||
|
ri := 0
|
||||||
|
if i < len(r.digits) {
|
||||||
|
ri = r.digits[i]
|
||||||
|
}
|
||||||
|
ri1 := 0
|
||||||
|
if i-1 < len(r.digits) {
|
||||||
|
ri1 = r.digits[i-1]
|
||||||
|
}
|
||||||
|
ri2 := 0
|
||||||
|
if i-2 < len(r.digits) {
|
||||||
|
ri2 = r.digits[i-2]
|
||||||
|
}
|
||||||
|
yt := 0
|
||||||
|
if t < len(y.digits) {
|
||||||
|
yt = y.digits[t]
|
||||||
|
}
|
||||||
|
yt1 := 0
|
||||||
|
if t-1 < len(y.digits) {
|
||||||
|
yt1 = y.digits[t-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if ri == yt {
|
||||||
|
q.digits[i-t-1] = maxDigitVal
|
||||||
|
} else {
|
||||||
|
q.digits[i-t-1] = (ri*biRadix + ri1) / yt
|
||||||
|
}
|
||||||
|
|
||||||
|
c1 := q.digits[i-t-1] * ((yt * biRadix) + yt1)
|
||||||
|
c2 := (ri * biRadixSquared) + ((ri1 * biRadix) + ri2)
|
||||||
|
|
||||||
|
for c1 > c2 {
|
||||||
|
q.digits[i-t-1]--
|
||||||
|
c1 = q.digits[i-t-1] * ((yt * biRadix) | yt1)
|
||||||
|
c2 = (ri * biRadix * biRadix) + ((ri1 * biRadix) + ri2)
|
||||||
|
}
|
||||||
|
|
||||||
|
b = BiMultiplyByRadixPower(y, i-t-1)
|
||||||
|
r = BiSubtract(r, BiMultiplyDigit(b, q.digits[i-t-1]))
|
||||||
|
if r.isNeg {
|
||||||
|
r = BiAdd(r, b)
|
||||||
|
q.digits[i-t-1]--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r = BiShiftRight(r, lambda)
|
||||||
|
q.isNeg = x.isNeg != origYIsNeg
|
||||||
|
|
||||||
|
if x.isNeg {
|
||||||
|
if origYIsNeg {
|
||||||
|
q = BiAdd(q, bigOne)
|
||||||
|
} else {
|
||||||
|
q = BiSubtract(q, bigOne)
|
||||||
|
}
|
||||||
|
y = BiShiftRight(y, lambda)
|
||||||
|
r = BiSubtract(y, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.digits[0] == 0 && BiHighIndex(r) == 0 {
|
||||||
|
r.isNeg = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*BigInt{q, r}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiDivide divides two BigInts
|
||||||
|
func BiDivide(x, y *BigInt) *BigInt {
|
||||||
|
return BiDivideModulo(x, y)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiModulo computes x mod y
|
||||||
|
func BiModulo(x, y *BigInt) *BigInt {
|
||||||
|
return BiDivideModulo(x, y)[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiMultiplyMod computes (x * y) mod m
|
||||||
|
func BiMultiplyMod(x, y, m *BigInt) *BigInt {
|
||||||
|
return BiModulo(BiMultiply(x, y), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BiPowMod computes x^y mod m
|
||||||
|
func BiPowMod(x, y, m *BigInt) *BigInt {
|
||||||
|
result := BiCopy(bigOne)
|
||||||
|
a := x
|
||||||
|
k := y
|
||||||
|
|
||||||
|
for {
|
||||||
|
if (k.digits[0] & 1) != 0 {
|
||||||
|
result = BiMultiplyMod(result, a, m)
|
||||||
|
}
|
||||||
|
k = BiShiftRight(k, 1)
|
||||||
|
if k.digits[0] == 0 && BiHighIndex(k) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
a = BiMultiplyMod(a, a, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BarrettMu represents Barrett reduction
|
||||||
|
type BarrettMu struct {
|
||||||
|
modulus *BigInt
|
||||||
|
k int
|
||||||
|
mu *BigInt
|
||||||
|
bkplus1 *BigInt
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBarrettMu creates a new BarrettMu
|
||||||
|
func NewBarrettMu(m *BigInt) *BarrettMu {
|
||||||
|
bm := &BarrettMu{
|
||||||
|
modulus: BiCopy(m),
|
||||||
|
k: BiHighIndex(m) + 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
b2k := NewBigInt()
|
||||||
|
if 2*bm.k < len(b2k.digits) {
|
||||||
|
b2k.digits[2*bm.k] = 1
|
||||||
|
}
|
||||||
|
bm.mu = BiDivide(b2k, bm.modulus)
|
||||||
|
|
||||||
|
bm.bkplus1 = NewBigInt()
|
||||||
|
if bm.k+1 < len(bm.bkplus1.digits) {
|
||||||
|
bm.bkplus1.digits[bm.k+1] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return bm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modulo performs Barrett modulo
|
||||||
|
func (bm *BarrettMu) Modulo(x *BigInt) *BigInt {
|
||||||
|
q1 := BiDivideByRadixPower(x, bm.k-1)
|
||||||
|
q2 := BiMultiply(q1, bm.mu)
|
||||||
|
q3 := BiDivideByRadixPower(q2, bm.k+1)
|
||||||
|
r1 := BiModuloByRadixPower(x, bm.k+1)
|
||||||
|
r2term := BiMultiply(q3, bm.modulus)
|
||||||
|
r2 := BiModuloByRadixPower(r2term, bm.k+1)
|
||||||
|
r := BiSubtract(r1, r2)
|
||||||
|
|
||||||
|
if r.isNeg {
|
||||||
|
r = BiAdd(r, bm.bkplus1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for BiCompare(r, bm.modulus) >= 0 {
|
||||||
|
r = BiSubtract(r, bm.modulus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiplyMod performs modular multiplication
|
||||||
|
func (bm *BarrettMu) MultiplyMod(x, y *BigInt) *BigInt {
|
||||||
|
xy := BiMultiply(x, y)
|
||||||
|
return bm.Modulo(xy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PowMod performs modular exponentiation
|
||||||
|
func (bm *BarrettMu) PowMod(x, y *BigInt) *BigInt {
|
||||||
|
result := NewBigInt()
|
||||||
|
result.digits[0] = 1
|
||||||
|
a := x
|
||||||
|
k := y
|
||||||
|
|
||||||
|
for {
|
||||||
|
if (k.digits[0] & 1) != 0 {
|
||||||
|
result = bm.MultiplyMod(result, a)
|
||||||
|
}
|
||||||
|
k = BiShiftRight(k, 1)
|
||||||
|
if k.digits[0] == 0 && BiHighIndex(k) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
a = bm.MultiplyMod(a, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// NutRSAKeyPair represents an RSA key pair
|
||||||
|
type NutRSAKeyPair struct {
|
||||||
|
e *BigInt
|
||||||
|
d *BigInt
|
||||||
|
m *BigInt
|
||||||
|
chunkSize int
|
||||||
|
radix int
|
||||||
|
barrett *BarrettMu
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNutRSAKeyPair creates a new RSA key pair
|
||||||
|
func NewNutRSAKeyPair(encryptionExponent, decryptionExponent, modulus string) *NutRSAKeyPair {
|
||||||
|
kp := &NutRSAKeyPair{
|
||||||
|
e: BiFromHex(encryptionExponent),
|
||||||
|
d: BiFromHex(decryptionExponent),
|
||||||
|
m: BiFromHex(modulus),
|
||||||
|
radix: 16,
|
||||||
|
}
|
||||||
|
kp.chunkSize = 2 * BiHighIndex(kp.m)
|
||||||
|
kp.barrett = NewBarrettMu(kp.m)
|
||||||
|
return kp
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptedString encrypts a string
|
||||||
|
func (kp *NutRSAKeyPair) EncryptedString(s string) string {
|
||||||
|
a := make([]int, 0)
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
a = append(a, int(s[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding
|
||||||
|
for len(a)%kp.chunkSize != 0 {
|
||||||
|
a = append(a, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := ""
|
||||||
|
for i := 0; i < len(a); i += kp.chunkSize {
|
||||||
|
block := NewBigInt()
|
||||||
|
j := 0
|
||||||
|
for k := i; k < i+kp.chunkSize; j++ {
|
||||||
|
if k < len(a) {
|
||||||
|
block.digits[j] = a[k]
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
if k < len(a) {
|
||||||
|
block.digits[j] += a[k] << 8
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crypt := kp.barrett.PowMod(block, kp.e)
|
||||||
|
var text string
|
||||||
|
if kp.radix == 16 {
|
||||||
|
text = BiToHex(crypt)
|
||||||
|
} else {
|
||||||
|
text = BiToString(crypt, kp.radix)
|
||||||
|
}
|
||||||
|
result += text + " "
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) > 0 {
|
||||||
|
return result[:len(result)-1]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptedString decrypts a string
|
||||||
|
func (kp *NutRSAKeyPair) DecryptedString(s string) string {
|
||||||
|
blocks := strings.Split(s, " ")
|
||||||
|
result := ""
|
||||||
|
|
||||||
|
for i := 0; i < len(blocks); i++ {
|
||||||
|
var bi *BigInt
|
||||||
|
if kp.radix == 16 {
|
||||||
|
bi = BiFromHex(blocks[i])
|
||||||
|
} else {
|
||||||
|
bi = BiFromString(blocks[i], kp.radix)
|
||||||
|
}
|
||||||
|
block := kp.barrett.PowMod(bi, kp.d)
|
||||||
|
|
||||||
|
for j := 0; j <= BiHighIndex(block); j++ {
|
||||||
|
result += string(rune(block.digits[j] & 255))
|
||||||
|
result += string(rune(block.digits[j] >> 8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing null
|
||||||
|
if len(result) > 0 && result[len(result)-1] == 0 {
|
||||||
|
result = result[:len(result)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNumber(s string) int64 {
|
||||||
|
var result int64
|
||||||
|
fmt.Sscanf(s, "%d", &result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SetMaxDigits(130)
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RSAKeyPair RSA密钥对结构体
|
||||||
|
type RSAKeyPair struct {
|
||||||
|
n *big.Int // 模数
|
||||||
|
e *big.Int // 公钥指数
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRSAKeyPair 创建RSA密钥对
|
||||||
|
// exponent: 公钥指数的十六进制字符串,通常是 "10001" (65537)
|
||||||
|
// privateExponent: 私钥指数的十六进制字符串,加密时可以为空
|
||||||
|
// modulus: 模数的十六进制字符串
|
||||||
|
func NewRSAKeyPair(exponent, privateExponent, modulus string) (*RSAKeyPair, error) {
|
||||||
|
// 解析公钥指数
|
||||||
|
e := new(big.Int)
|
||||||
|
if _, ok := e.SetString(exponent, 16); !ok {
|
||||||
|
return nil, fmt.Errorf("invalid exponent: %s", exponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析模数
|
||||||
|
n := new(big.Int)
|
||||||
|
if _, ok := n.SetString(modulus, 16); !ok {
|
||||||
|
return nil, fmt.Errorf("invalid modulus: %s", modulus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RSAKeyPair{
|
||||||
|
n: n,
|
||||||
|
e: e,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRSAKeyPairFromPublicKey 从 X.509 格式的 base64 公钥字符串创建 RSA 密钥对
|
||||||
|
// publicKeyBase64: X.509 DER 格式的 base64 编码公钥字符串
|
||||||
|
func NewRSAKeyPairFromPublicKey(publicKeyBase64 string) (*RSAKeyPair, error) {
|
||||||
|
// 解码 base64
|
||||||
|
derBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode base64: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 X.509 格式的公钥
|
||||||
|
pubKey, err := x509.ParsePKIXPublicKey(derBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为 RSA 公钥
|
||||||
|
rsaPubKey, ok := pubKey.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("not an RSA public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建密钥对
|
||||||
|
return &RSAKeyPair{
|
||||||
|
n: rsaPubKey.N,
|
||||||
|
e: big.NewInt(int64(rsaPubKey.E)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt 使用公钥加密数据,返回十六进制字符串
|
||||||
|
func (k *RSAKeyPair) Encrypt(plaintext string) (string, error) {
|
||||||
|
ciphertext, err := k.EncryptBytes([]byte(plaintext))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// 返回十六进制编码的密文
|
||||||
|
return hex.EncodeToString(ciphertext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptBytes 使用公钥加密字节数据,返回加密后的字节
|
||||||
|
// 使用 PKCS1v15 填充方式,兼容 JavaScript RSA 实现
|
||||||
|
func (k *RSAKeyPair) EncryptBytes(plaintext []byte) ([]byte, error) {
|
||||||
|
// 计算密钥长度(字节)
|
||||||
|
keySize := (k.n.BitLen() + 7) / 8
|
||||||
|
|
||||||
|
// PKCS1v15 填充
|
||||||
|
padded, err := pkcs1v15Pad(plaintext, keySize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将填充后的数据转换为大整数
|
||||||
|
m := new(big.Int).SetBytes(padded)
|
||||||
|
|
||||||
|
// 执行 RSA 加密: c = m^e mod n
|
||||||
|
c := new(big.Int).Exp(m, k.e, k.n)
|
||||||
|
|
||||||
|
// 将结果转换为字节数组,确保长度与密钥长度一致
|
||||||
|
ciphertext := c.Bytes()
|
||||||
|
if len(ciphertext) < keySize {
|
||||||
|
// 如果结果长度不足,在前面填充零
|
||||||
|
result := make([]byte, keySize)
|
||||||
|
copy(result[keySize-len(ciphertext):], ciphertext)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ciphertext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pkcs1v15Pad 实现 PKCS#1 v1.5 填充
|
||||||
|
func pkcs1v15Pad(data []byte, keySize int) ([]byte, error) {
|
||||||
|
if len(data) > keySize-11 {
|
||||||
|
return nil, fmt.Errorf("data too long: %d bytes (max %d)", len(data), keySize-11)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PKCS1v15 格式: 0x00 || 0x02 || PS || 0x00 || data
|
||||||
|
// 其中 PS 是随机非零字节,长度至少为 8 字节
|
||||||
|
padLen := keySize - len(data) - 3
|
||||||
|
|
||||||
|
padded := make([]byte, keySize)
|
||||||
|
padded[0] = 0x00
|
||||||
|
padded[1] = 0x02
|
||||||
|
|
||||||
|
// 生成随机非零字节作为填充
|
||||||
|
ps := make([]byte, padLen)
|
||||||
|
_, err := rand.Read(ps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate random padding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保所有填充字节都是非零
|
||||||
|
for i := 0; i < padLen; i++ {
|
||||||
|
for ps[i] == 0 {
|
||||||
|
rand.Read(ps[i : i+1])
|
||||||
|
}
|
||||||
|
padded[2+i] = ps[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
padded[2+padLen] = 0x00
|
||||||
|
copy(padded[3+padLen:], data)
|
||||||
|
|
||||||
|
return padded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModulus 获取模数
|
||||||
|
func (k *RSAKeyPair) GetModulus() *big.Int {
|
||||||
|
return k.n
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExponent 获取公钥指数
|
||||||
|
func (k *RSAKeyPair) GetExponent() *big.Int {
|
||||||
|
return k.e
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRSAKeyPair(t *testing.T) {
|
||||||
|
// 使用你提供的参数创建 RSA 密钥对
|
||||||
|
exponent := "10001"
|
||||||
|
modulus := "a1155dc73de04614604cbed62cc3b3198a639eb7b7d19cc2dfe3e787fe435f1464beee42a9a3cab6f1d3bfe66cd00317220ee770fd779b24c12c7698e251e875"
|
||||||
|
|
||||||
|
keyPair, err := NewRSAKeyPair(exponent, "", modulus)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("创建 RSA 密钥对失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试加密
|
||||||
|
plaintext := "241703070163"
|
||||||
|
ciphertext, err := keyPair.Encrypt(plaintext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("加密失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("明文: %s", plaintext)
|
||||||
|
t.Logf("密文(十六进制): %s", ciphertext)
|
||||||
|
|
||||||
|
// 验证密文不为空
|
||||||
|
if len(ciphertext) == 0 {
|
||||||
|
t.Error("密文为空")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRSAKeyPair_EncryptBytes(t *testing.T) {
|
||||||
|
exponent := "10001"
|
||||||
|
modulus := "a1155dc73de04614604cbed62cc3b3198a639eb7b7d19cc2dfe3e787fe435f1464beee42a9a3cab6f1d3bfe66cd00317220ee770fd779b24c12c7698e251e875"
|
||||||
|
|
||||||
|
keyPair, err := NewRSAKeyPair(exponent, "", modulus)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("创建 RSA 密钥对失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试字节加密
|
||||||
|
plaintext := []byte("Test data 测试数据")
|
||||||
|
ciphertext, err := keyPair.EncryptBytes(plaintext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("字节加密失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ciphertext) == 0 {
|
||||||
|
t.Error("密文字节为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("密文长度: %d bytes", len(ciphertext))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRSAKeyPairFromPublicKey(t *testing.T) {
|
||||||
|
// 使用你提供的 X.509 格式的 base64 公钥
|
||||||
|
publicKey := "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCpbRy8ZoyQvRPpUDIXycglTwVDcYrLcv7P9HI4E7/TAPZiI3GN0ckTUVVWRvoo/re3uCOOJK+F+Ufh19XiRIdoPYCULsSyEjcLLiyAS09mVOzhAQco84E/lM24T7rRTVID7LIgWPewyN8Nd5vzHET4Q3qTthoL8n82BwC1iXuVQIDAQAB"
|
||||||
|
|
||||||
|
keyPair, err := NewRSAKeyPairFromPublicKey(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("从公钥创建 RSA 密钥对失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试加密
|
||||||
|
plaintext := "241703070163"
|
||||||
|
ciphertext, err := keyPair.Encrypt(plaintext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("加密失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("明文: %s", plaintext)
|
||||||
|
t.Logf("密文(十六进制): %s", ciphertext)
|
||||||
|
|
||||||
|
// 验证密文不为空
|
||||||
|
if len(ciphertext) == 0 {
|
||||||
|
t.Error("密文为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密文长度(1024位密钥应该产生128字节 = 256个十六进制字符)
|
||||||
|
expectedHexLen := (keyPair.GetModulus().BitLen() + 7) / 8 * 2
|
||||||
|
if len(ciphertext) != expectedHexLen {
|
||||||
|
t.Errorf("密文长度不正确: 期望 %d, 实际 %d", expectedHexLen, len(ciphertext))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("密钥长度: %d bits", keyPair.GetModulus().BitLen())
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"likedream-goframe/internal/application/cpdaily/types"
|
"likedream-goframe/internal/application/cpdaily/types"
|
||||||
"likedream-goframe/internal/application/filestorage"
|
"likedream-goframe/internal/application/filestorage"
|
||||||
|
"likedream-goframe/internal/common/db"
|
||||||
"likedream-goframe/internal/common/errors"
|
"likedream-goframe/internal/common/errors"
|
||||||
"likedream-goframe/internal/db"
|
|
||||||
"likedream-goframe/internal/ent"
|
"likedream-goframe/internal/ent"
|
||||||
"likedream-goframe/internal/ent/cpimage"
|
"likedream-goframe/internal/ent/cpimage"
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
@@ -75,7 +75,7 @@ func UploadImageFromStorage(ctx context.Context, host string, imageId uuid.UUID,
|
|||||||
utils.OkOrPanic(errors.NewBusinessError(503, "上传图片失败: "+utils.ReadString(resp.Body)), oops.WithContext(ctx).With("response", utils.ReadString(resp.Body)).Wrap)
|
utils.OkOrPanic(errors.NewBusinessError(503, "上传图片失败: "+utils.ReadString(resp.Body)), oops.WithContext(ctx).With("response", utils.ReadString(resp.Body)).Wrap)
|
||||||
}
|
}
|
||||||
|
|
||||||
previewUrl := GetPreviewUrlAsync(ctx, host, targetImage.Key, taskType, client)
|
previewUrl := GetPreviewUrlAsync(ctx, host, imageUploadPolicy.FileName, taskType, client)
|
||||||
|
|
||||||
return previewUrl
|
return previewUrl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package cpdaily
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"likedream-goframe/internal/common/cst"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"likedream-goframe/internal/cst"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package leave
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"likedream-goframe/internal/application/cpdaily"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/task/leavetype"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/taskhelper"
|
||||||
|
"likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/container/gvar"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/samber/oops"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 销假相关功能
|
||||||
|
*/
|
||||||
|
type CpdailyReportApp struct {
|
||||||
|
client *resty.Client
|
||||||
|
host string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCpdailyReportApp(client *resty.Client, host string) *CpdailyReportApp {
|
||||||
|
|
||||||
|
return &CpdailyReportApp{
|
||||||
|
client: client,
|
||||||
|
host: host,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取请假记录
|
||||||
|
func (c *CpdailyReportApp) GetLeaveList() []leavetype.LeaveListOne {
|
||||||
|
// https://fzfu.campusphere.net/wec-counselor-leave-apps/leave/stu/listOther
|
||||||
|
post, err := c.client.R().SetBody(map[string]int{
|
||||||
|
"pageNumber": 1,
|
||||||
|
"pageSize": 20,
|
||||||
|
"needApproval": 1,
|
||||||
|
}).Post(fmt.Sprintf("%s/wec-counselor-leave-apps/leave/stu/listOther", c.host))
|
||||||
|
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("host", c.host).
|
||||||
|
In("CpdailyReport.GetLeaveList").
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
responseJSON := utils.ReadGjsonResult(post.Body)
|
||||||
|
rows := responseJSON.Get("datas.rows")
|
||||||
|
|
||||||
|
if !rows.Exists() {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "没有获取到待销假信息"), func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("host", c.host).
|
||||||
|
In("CpdailyReport.GetLeaveList").
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var listList []leavetype.LeaveListOne
|
||||||
|
for _, row := range rows.Array() {
|
||||||
|
// 已完成
|
||||||
|
if row.Get("status").String() == "4" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
leaveOne := leavetype.LeaveListOne{
|
||||||
|
Id: row.Get("id").String(),
|
||||||
|
LeaveType: row.Get("leaveType").String(),
|
||||||
|
CreateTime: row.Get("createTime").String(),
|
||||||
|
Reason: row.Get("leaveReason").String(),
|
||||||
|
AceEndTime: row.Get("actEndTime").String(),
|
||||||
|
Status: row.Get("status").String(),
|
||||||
|
}
|
||||||
|
listList = append(listList, leaveOne)
|
||||||
|
}
|
||||||
|
return listList
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CpdailyReportApp) LeaveDetail(leaveId string) gjson.Result {
|
||||||
|
// https://fzfu.campusphere.net/wec-counselor-leave-apps/leave/stu/detailOther
|
||||||
|
post, err := c.client.R().SetBody(map[string]any{
|
||||||
|
"id": leaveId,
|
||||||
|
"needApproval": 1,
|
||||||
|
"uaIsCpadaily": "true",
|
||||||
|
}).Post(fmt.Sprintf("%s/wec-counselor-leave-apps/leave/stu/detailOther", c.host))
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("host", c.host).
|
||||||
|
In("CpdailyReport.LeaveDetail").
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
datas := utils.ReadGjsonResult(post.Body).Get("datas")
|
||||||
|
if !datas.Exists() {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "没有获取到待销假详情信息"), func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("host", c.host).
|
||||||
|
In("CpdailyReport.LeaveDetail").
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return datas
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CpdailyReportApp) FillReportForm(detail gjson.Result) map[string]*gvar.Var {
|
||||||
|
resultForm := make(map[string]*gvar.Var)
|
||||||
|
resultForm["id"] = gvar.New(detail.Get("detail.id").String())
|
||||||
|
resultForm["needApproval"] = gvar.New(1)
|
||||||
|
resultForm["leaveType"] = gvar.New(detail.Get("leaveType.code").Int())
|
||||||
|
resultForm["reportLocation"] = gvar.New(detail.Get("detail.applyLocation.address").String())
|
||||||
|
resultForm["approverWid"] = gvar.New("")
|
||||||
|
resultForm["reportApplyExplain"] = gvar.New(nil)
|
||||||
|
resultForm["reportAttach"] = gvar.New(detail.Get("detail.applyAttach").Array())
|
||||||
|
resultForm["uaIsCpadaily"] = gvar.New(true)
|
||||||
|
return resultForm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CpdailyReportApp) GetLocation(detail gjson.Result) (lon, lat string) {
|
||||||
|
applyLocation := detail.Get("detail.applyLocation")
|
||||||
|
lon = applyLocation.Get("longitude").String()
|
||||||
|
lat = applyLocation.Get("latitude").String()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
func (c *CpdailyReportApp) SubmitReport(ctx context.Context, form map[string]*gvar.Var, lon, lat, username, deviceId string) {
|
||||||
|
cpdailyInfo := cpdaily.GetInfo(ctx)
|
||||||
|
|
||||||
|
extensionForm := map[string]*gvar.Var{
|
||||||
|
"lon": gvar.New(lon),
|
||||||
|
"lat": gvar.New(lat),
|
||||||
|
"model": gvar.New(uuid.New().String()),
|
||||||
|
"appVersion": gvar.New(cpdailyInfo.AppVersion),
|
||||||
|
"systemVersion": gvar.New(cpdailyInfo.SystemVersion),
|
||||||
|
"userId": gvar.New(username),
|
||||||
|
"systemName": gvar.New("android"),
|
||||||
|
"deviceId": gvar.New(deviceId),
|
||||||
|
}
|
||||||
|
|
||||||
|
extensionString := taskhelper.GetCpdailyExtension(ctx, extensionForm)
|
||||||
|
bodyString := taskhelper.GetBodyString(ctx, form)
|
||||||
|
// 构造提交数据
|
||||||
|
submitData := map[string]*gvar.Var{
|
||||||
|
"lon": gvar.New(lon),
|
||||||
|
"version": gvar.New(cryptoutil.EncryptVersion),
|
||||||
|
"calVersion": gvar.New(cpdailyInfo.CalVersion),
|
||||||
|
"deviceId": extensionForm["deviceId"],
|
||||||
|
//"userId": gvar.New(username),
|
||||||
|
"systemName": gvar.New("android"),
|
||||||
|
"bodyString": gvar.New(bodyString),
|
||||||
|
"lat": gvar.New(lat),
|
||||||
|
"systemVersion": gvar.New(cpdailyInfo.SystemVersion),
|
||||||
|
"appVersion": gvar.New(cpdailyInfo.AppVersion),
|
||||||
|
"model": extensionForm["model"],
|
||||||
|
}
|
||||||
|
// https://fzfu.campusphere.net/wec-counselor-leave-apps/leave/report
|
||||||
|
response, e := c.client.R().
|
||||||
|
SetBody(submitData).
|
||||||
|
WithContext(ctx).
|
||||||
|
SetHeader("Cpdaily-Extension", extensionString).
|
||||||
|
Post(fmt.Sprintf("%s/wec-counselor-leave-apps/leave/report", c.host))
|
||||||
|
|
||||||
|
utils.OkOrPanic(e, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("host", c.host).
|
||||||
|
With("username", username).
|
||||||
|
With("deviceId", deviceId).
|
||||||
|
In("CpdailySleepCheckTask.Submit").
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
responseJSON := utils.ReadGjsonResult(response.Body)
|
||||||
|
|
||||||
|
if responseJSON.Get("code").String() != "0" {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, responseJSON.Get("message").String()), oops.WithContext(ctx).Wrap)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"likedream-goframe/internal/application/cookiejar"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/school"
|
||||||
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
"likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/samber/oops"
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 南阳理工学院登录实现 RSA加密 & 验证码必出
|
||||||
|
type CpdailyAhjzuLogin struct {
|
||||||
|
client *resty.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CpdailyAhjzuLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
|
||||||
|
jar := cookiejar.NewPersistCookieJar()
|
||||||
|
|
||||||
|
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
|
|
||||||
|
// 获取合法的登录URL
|
||||||
|
loginUrl := getCasValidLoginUrl(schoolDetail)
|
||||||
|
|
||||||
|
// 请求登录页面并获取真正的登录URL
|
||||||
|
loginPageResponse := getAhjzuLoginPageResponse(ctx, c.client, loginUrl)
|
||||||
|
|
||||||
|
defer loginPageResponse.Body.Close()
|
||||||
|
loginPageBytes, err := io.ReadAll(loginPageResponse.Body)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
|
||||||
|
|
||||||
|
// 解析登录页面
|
||||||
|
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
// 判断登录类型
|
||||||
|
var (
|
||||||
|
allInputs = map[string]string{}
|
||||||
|
fm1Node = loginPage.Find("#credentials").First()
|
||||||
|
)
|
||||||
|
inputs := fm1Node.Find("input")
|
||||||
|
|
||||||
|
for _, input := range inputs.EachIter() {
|
||||||
|
name, exists := input.Attr("name")
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, _ := input.Attr("value")
|
||||||
|
allInputs[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
allInputs["username"] = username
|
||||||
|
|
||||||
|
// 处理密码
|
||||||
|
allInputs["password"] = cryptoutil.MD5(password)
|
||||||
|
|
||||||
|
// 解决常规图片验证码
|
||||||
|
captchaUrl := getAhjzuCaptchaUrl(loginResponseUrl)
|
||||||
|
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||||
|
|
||||||
|
allInputs["validateCode"] = captchaResult
|
||||||
|
|
||||||
|
// 登录提交
|
||||||
|
loginPostResponse, err := c.client.R().
|
||||||
|
SetFormData(allInputs).
|
||||||
|
WithContext(ctx).
|
||||||
|
Post(loginResponseUrl.String())
|
||||||
|
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
WithContext(ctx).
|
||||||
|
With("loginResponseUrl", loginResponseUrl).
|
||||||
|
With("allInputs", allInputs).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
defer loginPostResponse.Body.Close()
|
||||||
|
|
||||||
|
switch loginPostResponse.StatusCode() {
|
||||||
|
case 302:
|
||||||
|
handleCasLoginSuccess(ctx, loginPostResponse, schoolDetail, username, password, c.client)
|
||||||
|
case 200:
|
||||||
|
fallthrough
|
||||||
|
case 401:
|
||||||
|
doc, err := goquery.NewDocumentFromReader(loginPostResponse.Body)
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
WithContext(ctx).
|
||||||
|
With("loginResponseUrl", loginResponseUrl).
|
||||||
|
With("allInputs", allInputs).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
if isCasNeedKickOut(doc) {
|
||||||
|
kickoutResponse := submitCasKickOut(ctx, loginResponseUrl.String(), doc, c.client)
|
||||||
|
handleCasLoginSuccess(ctx, kickoutResponse, schoolDetail, username, password, c.client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message := getCasErrorMessage(doc)
|
||||||
|
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("今日校园登录失败: %s", message)), func(err error) error {
|
||||||
|
return oops.
|
||||||
|
WithContext(ctx).
|
||||||
|
With("loginResponseUrl", loginResponseUrl).
|
||||||
|
With("allInputs", allInputs).
|
||||||
|
With("errorMessage", message).
|
||||||
|
Wrap(errors.NewBusinessError(503, message))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAhjzuLoginPageResponse(ctx context.Context, client *resty.Client, loginUrl string) *resty.Response {
|
||||||
|
res1, err := client.R().Get(loginUrl)
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("loginUrl", loginUrl).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
if res1.StatusCode() != http.StatusFound && res1.StatusCode() != http.StatusMovedPermanently {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("loginUrl", loginUrl).
|
||||||
|
With("statusCode", res1.StatusCode()).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
location := res1.RawResponse.Header.Get("Location")
|
||||||
|
res2, err := client.R().Get(location)
|
||||||
|
if res2.StatusCode() != http.StatusFound && res2.StatusCode() != http.StatusMovedPermanently {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("loginUrl", loginUrl).
|
||||||
|
With("statusCode", res2.StatusCode()).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
location = res2.RawResponse.Header.Get("Location")
|
||||||
|
res3, err := client.R().Get(location)
|
||||||
|
if res3.StatusCode() != http.StatusFound && res3.StatusCode() != http.StatusMovedPermanently {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("loginUrl", loginUrl).
|
||||||
|
With("statusCode", res3.StatusCode()).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
location = res3.RawResponse.Header.Get("Location")
|
||||||
|
res4, err := client.R().Get(location)
|
||||||
|
if res4.StatusCode() != http.StatusFound && res3.StatusCode() != http.StatusMovedPermanently {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("loginUrl", loginUrl).
|
||||||
|
With("statusCode", res4.StatusCode()).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
location = res4.RawResponse.Header.Get("Location")
|
||||||
|
res5, err := client.R().Get(location)
|
||||||
|
if err != nil {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("loginUrl", loginUrl).
|
||||||
|
With("statusCode", res4.StatusCode()).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res5
|
||||||
|
}
|
||||||
|
func getAhjzuCaptchaUrl(url *url.URL) string {
|
||||||
|
return fmt.Sprintf("%s://%s/sso/code/code.jsp", url.Scheme, url.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CpdailyAhjzuLogin) GetCookieJar() http.CookieJar {
|
||||||
|
return c.client.CookieJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CpdailyAhjzuLogin) GetCookieString() string {
|
||||||
|
cookieJar := c.GetCookieJar()
|
||||||
|
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||||
|
return serializableJar.Serialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CpdailyAhjzuLogin) GetClient() *resty.Client {
|
||||||
|
return c.client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAhjzu(ctx context.Context) *CpdailyAhjzuLogin {
|
||||||
|
return &CpdailyAhjzuLogin{
|
||||||
|
client: httpclient.NewCpdailyClient(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,21 @@ import (
|
|||||||
|
|
||||||
func GetCpdailyLoginBySchoolInfo(ctx context.Context, info *school.SchoolDetailInfo) CpdailyLogin {
|
func GetCpdailyLoginBySchoolInfo(ctx context.Context, info *school.SchoolDetailInfo) CpdailyLogin {
|
||||||
switch info.Id {
|
switch info.Id {
|
||||||
|
// 南昌理工学院
|
||||||
|
case "d6e8f2ca-10b2-4599-bdb8-4b33d419658d":
|
||||||
|
return NewNut(ctx)
|
||||||
|
// 安徽建筑大学
|
||||||
|
case "ahjzu":
|
||||||
|
return NewAhjzu(ctx)
|
||||||
|
// 南宁师范大学
|
||||||
|
case "2ba0ea59-a764-4482-bb2b-c924b726b04c":
|
||||||
|
return NewNnnu(ctx)
|
||||||
|
// 长春大学
|
||||||
|
case "ccu":
|
||||||
|
fallthrough
|
||||||
|
// 河北石油职业技术大学
|
||||||
|
case "01e270d1-00e2-4ce9-b0af-5d5fbd462b8a":
|
||||||
|
return NewIap(ctx)
|
||||||
default:
|
default:
|
||||||
if info.IsIap() {
|
if info.IsIap() {
|
||||||
return NewIap(ctx)
|
return NewIap(ctx)
|
||||||
|
|||||||
@@ -9,18 +9,21 @@ import (
|
|||||||
"likedream-goframe/internal/application/cookiejar"
|
"likedream-goframe/internal/application/cookiejar"
|
||||||
"likedream-goframe/internal/application/cpdaily/school"
|
"likedream-goframe/internal/application/cpdaily/school"
|
||||||
"likedream-goframe/internal/application/httpclient"
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
"likedream-goframe/internal/common/cst"
|
||||||
"likedream-goframe/internal/common/errors"
|
"likedream-goframe/internal/common/errors"
|
||||||
"likedream-goframe/internal/cst"
|
|
||||||
"likedream-goframe/internal/framework/redis"
|
"likedream-goframe/internal/framework/redis"
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/avast/retry-go/v4"
|
||||||
"github.com/deatil/go-cryptobin/cryptobin/crypto"
|
"github.com/deatil/go-cryptobin/cryptobin/crypto"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/samber/oops"
|
"github.com/samber/oops"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"resty.dev/v3"
|
"resty.dev/v3"
|
||||||
@@ -37,7 +40,7 @@ type CpdailyCasLogin struct {
|
|||||||
|
|
||||||
func NewCas(ctx context.Context) CpdailyLogin {
|
func NewCas(ctx context.Context) CpdailyLogin {
|
||||||
return &CpdailyCasLogin{
|
return &CpdailyCasLogin{
|
||||||
client: httpclient.Cpdaily.Clone(ctx),
|
client: httpclient.NewCpdailyClient(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +59,7 @@ func (c *CpdailyCasLogin) GetClient() *resty.Client {
|
|||||||
func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
|
func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
|
||||||
jar := cookiejar.NewPersistCookieJar()
|
jar := cookiejar.NewPersistCookieJar()
|
||||||
|
|
||||||
c.client = c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||||
|
|
||||||
// 获取合法的登录URL
|
// 获取合法的登录URL
|
||||||
loginUrl := getCasValidLoginUrl(schoolDetail)
|
loginUrl := getCasValidLoginUrl(schoolDetail)
|
||||||
@@ -76,7 +79,7 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
|||||||
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
|
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
|
||||||
|
|
||||||
// 关闭client的重定向
|
// 关闭client的重定向
|
||||||
c.client = c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
|
|
||||||
// 解析登录页面
|
// 解析登录页面
|
||||||
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
|
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
|
||||||
@@ -155,11 +158,11 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
|||||||
allInputs[name] = value
|
allInputs[name] = value
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
utils.OkOrPanic(oops.With("schoolDetail", schoolDetail).
|
utils.OkOrPanic(errors.NewBusinessError(503, "页面中未找到登录表单"), oops.With("schoolDetail", schoolDetail).
|
||||||
With("username", username).
|
With("username", username).
|
||||||
With("password", password).
|
With("password", password).
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Wrap(errors.NewBusinessError(503, "页面中未找到登录表单")))
|
Wrap)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改部分参数
|
// 修改部分参数
|
||||||
@@ -174,9 +177,9 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
|||||||
if saltValue != "" {
|
if saltValue != "" {
|
||||||
salt = saltValue
|
salt = saltValue
|
||||||
} else {
|
} else {
|
||||||
saltMatches := saltRegexp.FindAllString(string(loginPageBytes), -1)
|
saltMatches := saltRegexp.FindStringSubmatch(string(loginPageBytes))
|
||||||
if len(saltMatches) > 0 {
|
if len(saltMatches) > 1 {
|
||||||
salt = saltMatches[0]
|
salt = saltMatches[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,19 +192,37 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
|||||||
allInputs["password"] = encryptCasPassword(password, salt)
|
allInputs["password"] = encryptCasPassword(password, salt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 声明需要滑块验证码的学校id
|
||||||
|
var SliderCaptchaIds = g.Cfg().MustGet(ctx, cst.ConfigSliderSchoolIds).Strings()
|
||||||
|
// []string{"1018764144934965", "sdhyxy", "hqdx"}
|
||||||
// 判断是否需要验证码
|
// 判断是否需要验证码
|
||||||
if isCasNeedCaptcha(ctx, loginResponseUrl, loginType, username, c.client) {
|
if isCasNeedCaptcha(ctx, loginResponseUrl, loginType, username, c.client) {
|
||||||
// 解决验证码
|
// 如果是滑块验证码
|
||||||
captchaUrl := getCasCaptchaUrl(loginResponseUrl, loginType)
|
if slices.Contains(SliderCaptchaIds, schoolDetail.Id) {
|
||||||
|
e := retry.Do(func() error {
|
||||||
|
return utils.Catch(func() {
|
||||||
|
captchaUrl := getCasSliderCaptchaUrl(loginResponseUrl)
|
||||||
|
verifyUrl := getCasSliderVerifyUrl(loginResponseUrl)
|
||||||
|
ResolveCasSliderCaptcha(ctx, captchaUrl, verifyUrl, c.client)
|
||||||
|
})
|
||||||
|
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||||
|
if e != nil {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "滑块验证码解析失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 解决常规图片验证码
|
||||||
|
captchaUrl := getCasCaptchaUrl(loginResponseUrl, loginType)
|
||||||
|
|
||||||
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||||
|
|
||||||
switch loginType {
|
switch loginType {
|
||||||
case 0:
|
case 0:
|
||||||
allInputs["captchaResponse"] = captchaResult
|
allInputs["captchaResponse"] = captchaResult
|
||||||
default:
|
default:
|
||||||
allInputs["captcha"] = captchaResult
|
allInputs["captcha"] = captchaResult
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loginPostResponse, err := c.client.R().
|
loginPostResponse, err := c.client.R().
|
||||||
@@ -237,6 +258,7 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
|||||||
if isCasNeedKickOut(doc) {
|
if isCasNeedKickOut(doc) {
|
||||||
kickoutResponse := submitCasKickOut(ctx, loginResponseUrl.String(), doc, c.client)
|
kickoutResponse := submitCasKickOut(ctx, loginResponseUrl.String(), doc, c.client)
|
||||||
handleCasLoginSuccess(ctx, kickoutResponse, schoolDetail, username, password, c.client)
|
handleCasLoginSuccess(ctx, kickoutResponse, schoolDetail, username, password, c.client)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
message := getCasErrorMessage(doc)
|
message := getCasErrorMessage(doc)
|
||||||
@@ -269,7 +291,7 @@ func (c *CpdailyCasLogin) ConfirmAdd(ctx context.Context, sc *school.SchoolDetai
|
|||||||
utils.OkOrPanic(err)
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
// 提交确认添加
|
// 提交确认添加
|
||||||
client := httpclient.Cpdaily.Clone(ctx)
|
client := httpclient.NewCpdailyClient()
|
||||||
client.SetCookieJar(cookieJar)
|
client.SetCookieJar(cookieJar)
|
||||||
post, err := client.R().SetFormData(map[string]string{
|
post, err := client.R().SetFormData(map[string]string{
|
||||||
"service": service,
|
"service": service,
|
||||||
@@ -333,7 +355,7 @@ func handleCasLoginSuccess(ctx context.Context, response *resty.Response, school
|
|||||||
jumpUrl, err := response.RawResponse.Location()
|
jumpUrl, err := response.RawResponse.Location()
|
||||||
utils.OkOrPanic(err)
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
client = client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||||
|
|
||||||
verifyResponse, err := client.R().
|
verifyResponse, err := client.R().
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
@@ -369,6 +391,10 @@ func handleCasLoginSuccess(ctx context.Context, response *resty.Response, school
|
|||||||
utils.OkOrPanic(errors.NewBusinessError(201, "多因子验证码认证").WithData(key))
|
utils.OkOrPanic(errors.NewBusinessError(201, "多因子验证码认证").WithData(key))
|
||||||
// 否则应该是登录成功
|
// 否则应该是登录成功
|
||||||
} else if statusCode == 200 || statusCode == 404 {
|
} else if statusCode == 200 || statusCode == 404 {
|
||||||
|
all, _ := io.ReadAll(verifyResponse.Body)
|
||||||
|
if strings.Contains(string(all), "完善资料") || strings.Contains(string(all), "为了你的帐号安全") {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "请手动学工号登录完善信息,修改密码").WithData(schoolInfo.WecloudUrl))
|
||||||
|
}
|
||||||
ActivateSession(ctx, schoolInfo.WecloudUrl, client)
|
ActivateSession(ctx, schoolInfo.WecloudUrl, client)
|
||||||
} else {
|
} else {
|
||||||
utils.OkOrPanic(errors.NewBusinessError(503, "未知错误导致今日校园登录失败"),
|
utils.OkOrPanic(errors.NewBusinessError(503, "未知错误导致今日校园登录失败"),
|
||||||
@@ -398,7 +424,7 @@ func submitCasKickOut(ctx context.Context, requestUrl string, doc *goquery.Docum
|
|||||||
submitForm[name] = value
|
submitForm[name] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
client = client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
|
|
||||||
response, err := client.R().
|
response, err := client.R().
|
||||||
SetFormData(submitForm).
|
SetFormData(submitForm).
|
||||||
@@ -430,11 +456,17 @@ func submitCasKickOut(ctx context.Context, requestUrl string, doc *goquery.Docum
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCasValidLoginUrl(info *school.SchoolDetailInfo) string {
|
func getCasValidLoginUrl(info *school.SchoolDetailInfo) string {
|
||||||
if strings.Contains(info.AmpUrl, "campusphere") {
|
// 特殊学校单独处理
|
||||||
|
// 甘肃农业大学 西安体育学院
|
||||||
|
ids := []string{"gsau", "ddf0639d-394d-48a2-a166-07ed84b7e6f0"}
|
||||||
|
if slices.Contains(ids, info.Id) {
|
||||||
|
return info.IdsUrl
|
||||||
|
}
|
||||||
|
if strings.Contains(info.AmpUrl, "campusphere") || strings.Contains(info.AmpUrl, "wec-portal-mobile") {
|
||||||
return info.AmpUrl
|
return info.AmpUrl
|
||||||
} else if strings.Contains(info.AmpUrl2, "campusphere") {
|
} else if strings.Contains(info.AmpUrl2, "campusphere") || strings.Contains(info.AmpUrl2, "wec-portal-mobile") {
|
||||||
return info.AmpUrl2
|
return info.AmpUrl2
|
||||||
} else if strings.Contains(info.AmpUrl3, "campusphere") {
|
} else if strings.Contains(info.AmpUrl3, "campusphere") || strings.Contains(info.AmpUrl3, "wec-portal-mobile") {
|
||||||
return info.AmpUrl3
|
return info.AmpUrl3
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,3 +560,12 @@ func getCasCaptchaUrl(url *url.URL, loginType int) string {
|
|||||||
return fmt.Sprintf("%s://%s/authserver/getCaptcha.htl", url.Scheme, url.Host)
|
return fmt.Sprintf("%s://%s/authserver/getCaptcha.htl", url.Scheme, url.Host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCasSliderCaptchaUrl(url *url.URL) string {
|
||||||
|
// /authserver/common/openSliderCaptcha.htl?_=1762873725346
|
||||||
|
return fmt.Sprintf("%s://%s/authserver/common/openSliderCaptcha.htl", url.Scheme, url.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCasSliderVerifyUrl(url *url.URL) string {
|
||||||
|
return fmt.Sprintf("%s://%s/authserver/common/verifySliderCaptcha.htl", url.Scheme, url.Host)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"likedream-goframe/internal/application/cookiejar"
|
"likedream-goframe/internal/application/cookiejar"
|
||||||
"likedream-goframe/internal/application/httpclient"
|
"likedream-goframe/internal/application/httpclient"
|
||||||
"likedream-goframe/internal/common/errors"
|
"likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/ent/cpuser"
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -14,7 +15,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ActivateSession(ctx context.Context, host string, client *resty.Client) {
|
func ActivateSession(ctx context.Context, host string, client *resty.Client) {
|
||||||
client = client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
// 处理cookie,干掉path
|
||||||
|
jar := client.CookieJar()
|
||||||
|
realJar := jar.(*cookiejar.PersistCookieJar)
|
||||||
|
realJar.RemoveCookieInfo()
|
||||||
|
client.SetCookieJar(realJar)
|
||||||
|
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||||
|
|
||||||
host = strings.TrimSuffix(host, "/")
|
host = strings.TrimSuffix(host, "/")
|
||||||
|
|
||||||
@@ -27,7 +33,7 @@ func ActivateSession(ctx context.Context, host string, client *resty.Client) {
|
|||||||
|
|
||||||
defer sessionResponse.Body.Close()
|
defer sessionResponse.Body.Close()
|
||||||
|
|
||||||
if !sessionResponse.IsSuccess() {
|
if !sessionResponse.IsSuccess() && sessionResponse.StatusCode() != 502 {
|
||||||
utils.OkOrPanic(errors.NewBusinessError(503, "Session激活失败"),
|
utils.OkOrPanic(errors.NewBusinessError(503, "Session激活失败"),
|
||||||
func(err error) error {
|
func(err error) error {
|
||||||
return oops.
|
return oops.
|
||||||
@@ -78,34 +84,30 @@ func ActivateSession(ctx context.Context, host string, client *resty.Client) {
|
|||||||
activateStep1()
|
activateStep1()
|
||||||
activateStep2()
|
activateStep2()
|
||||||
|
|
||||||
client = client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
}
|
}
|
||||||
|
|
||||||
func CookieEnabled(ctx context.Context, host string, client *resty.Client) bool {
|
func CookieEnabled(ctx context.Context, host string, client *resty.Client, loginType cpuser.LoginType) bool {
|
||||||
client = client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
defer func() {
|
|
||||||
client = client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
|
||||||
}()
|
|
||||||
host = strings.TrimSuffix(host, "/")
|
host = strings.TrimSuffix(host, "/")
|
||||||
sessionResponse, err := client.R().
|
sessionResponse, err := client.R().
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
SetBody(map[string]any{}).
|
SetBody(map[string]any{}).
|
||||||
Post(fmt.Sprintf("%s/wec-counselor-attendance-apps/student/attendance/getStuSignInfosByWeekMonth", host))
|
Post(fmt.Sprintf("%s/wec-counselor-attendance-apps/student/attendance/getStuAttendacesInOneDay", host))
|
||||||
|
|
||||||
utils.OkOrPanic(err)
|
utils.OkOrPanic(err)
|
||||||
defer sessionResponse.Body.Close()
|
needLogin := sessionResponse.StatusCode() == 302
|
||||||
|
if needLogin && loginType == cpuser.LoginTypePhone {
|
||||||
if !sessionResponse.IsSuccess() {
|
utils.OkOrPanic(errors.NewBusinessError(500, "登录信息失效,请重新通过短信验证码登录"))
|
||||||
return sessionResponse.StatusCode() == 405
|
|
||||||
}
|
}
|
||||||
responseContentType := sessionResponse.Header().Get("Content-Type")
|
return !needLogin
|
||||||
return strings.Contains(responseContentType, "json")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetClient(ctx context.Context, cookie string) *resty.Client {
|
func GetClient(ctx context.Context, cookie string) *resty.Client {
|
||||||
cookieJar := cookiejar.NewPersistCookieJar()
|
cookieJar := cookiejar.NewPersistCookieJar()
|
||||||
cookieJar.Deserialize(cookie)
|
cookieJar.Deserialize(cookie)
|
||||||
client := httpclient.Cpdaily.Clone(ctx)
|
// 不使用 Clone,创建全新的独立 client 实例避免状态共享
|
||||||
|
client := httpclient.NewCpdailyClient()
|
||||||
client.SetCookieJar(cookieJar)
|
client.SetCookieJar(cookieJar)
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,25 +5,29 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"likedream-goframe/internal/application/cookiejar"
|
"likedream-goframe/internal/application/cookiejar"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||||
"likedream-goframe/internal/application/cpdaily/school"
|
"likedream-goframe/internal/application/cpdaily/school"
|
||||||
"likedream-goframe/internal/application/httpclient"
|
"likedream-goframe/internal/application/httpclient"
|
||||||
"likedream-goframe/internal/common/errors"
|
"likedream-goframe/internal/common/errors"
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/samber/oops"
|
"github.com/samber/oops"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"resty.dev/v3"
|
"resty.dev/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const iapRsaPubKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCpbRy8ZoyQvRPpUDIXycglTwVDcYrLcv7P9HI4E7/TAPZiI3GN0ckTUVVWRvoo/re3uCOOJK+F+Ufh19XiRIdoPYCULsSyEjcLLiyAS09mVOzhAQco84E/lM24T7rRTVID7LIgWPewyN8Nd5vzHET4Q3qTthoL8n82BwC1iXuVQIDAQAB"
|
||||||
|
|
||||||
type CpdailyIapLogin struct {
|
type CpdailyIapLogin struct {
|
||||||
client *resty.Client
|
client *resty.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIap(ctx context.Context) *CpdailyIapLogin {
|
func NewIap(ctx context.Context) *CpdailyIapLogin {
|
||||||
return &CpdailyIapLogin{
|
return &CpdailyIapLogin{
|
||||||
client: httpclient.Cpdaily.Clone(ctx),
|
client: httpclient.NewCpdailyClient(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,20 +38,39 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
|||||||
loginUrl := getIapValidLoginUrl(schoolDetail)
|
loginUrl := getIapValidLoginUrl(schoolDetail)
|
||||||
|
|
||||||
jar := cookiejar.NewPersistCookieJar()
|
jar := cookiejar.NewPersistCookieJar()
|
||||||
c.client = c.client.SetCookieJar(jar).
|
c.client.SetCookieJar(jar).
|
||||||
SetRedirectPolicy(resty.NoRedirectPolicy())
|
SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
|
|
||||||
|
// 获取基础lt
|
||||||
|
preLoginUrl := loginUrl + "/portal/login"
|
||||||
|
c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||||
|
loginRedirectResponse, err := c.client.R().Get(preLoginUrl)
|
||||||
|
oldLt := ""
|
||||||
|
// 部分学校无法直接访问登录页
|
||||||
|
if err == nil {
|
||||||
|
u := loginRedirectResponse.RawResponse.Request.URL
|
||||||
|
oldLt = strings.Split(u.String(), "=")[1]
|
||||||
|
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
|
} else {
|
||||||
|
preLoginUrl = loginUrl + "/iap/login/mobile.html"
|
||||||
|
c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||||
|
loginRedirectResponse, err = c.client.R().Get(preLoginUrl)
|
||||||
|
}
|
||||||
|
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
ltInfoResponse, err := c.client.R().
|
ltInfoResponse, err := c.client.R().
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
SetBody(map[string]any{}).
|
SetFormData(map[string]string{
|
||||||
|
"lt": oldLt,
|
||||||
|
}).
|
||||||
Post(fmt.Sprintf("%s/iap/security/lt", loginUrl))
|
Post(fmt.Sprintf("%s/iap/security/lt", loginUrl))
|
||||||
|
rsa, _ := cryptoutil.NewRSAKeyPairFromPublicKey(iapRsaPubKey)
|
||||||
|
password, _ = rsa.Encrypt(password)
|
||||||
utils.OkOrPanic(err, func(err error) error {
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
return oops.
|
return oops.
|
||||||
In("CpdailyIapLogin.Login").
|
In("CpdailyIapLogin.Login").
|
||||||
With("schoolDetail", schoolDetail).
|
With("schoolDetail", schoolDetail).
|
||||||
With("username", username).
|
With("username", username).
|
||||||
With("password", password).
|
With("password", "{rsa}"+password).
|
||||||
Wrap(err)
|
Wrap(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -64,8 +87,9 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
|||||||
"rememberMe": "true",
|
"rememberMe": "true",
|
||||||
"dllt": "",
|
"dllt": "",
|
||||||
"mobile": "",
|
"mobile": "",
|
||||||
|
"captcha": "",
|
||||||
"username": username,
|
"username": username,
|
||||||
"password": password,
|
"password": "{rsa}" + password,
|
||||||
}
|
}
|
||||||
|
|
||||||
if isIapNeedCaptcha(ctx, loginUrl, username, c.client) {
|
if isIapNeedCaptcha(ctx, loginUrl, username, c.client) {
|
||||||
@@ -103,7 +127,7 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
|||||||
Wrap(err)
|
Wrap(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
c.client = c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||||
|
|
||||||
finalLoginResponse, err := c.client.R().
|
finalLoginResponse, err := c.client.R().
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
@@ -123,7 +147,7 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
|||||||
|
|
||||||
ActivateSession(ctx, schoolDetail.WecloudUrl, c.client)
|
ActivateSession(ctx, schoolDetail.WecloudUrl, c.client)
|
||||||
|
|
||||||
c.client = c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
default:
|
default:
|
||||||
loginResponseBytes, err := io.ReadAll(loginResponse.Body)
|
loginResponseBytes, err := io.ReadAll(loginResponse.Body)
|
||||||
utils.OkOrPanic(err, func(err error) error {
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
@@ -142,9 +166,18 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
|||||||
case "CAPTCHA_NOTMATCH":
|
case "CAPTCHA_NOTMATCH":
|
||||||
utils.OkOrPanic(errors.NewBusinessError(503, "Iap登录失败, 验证码识别失败"))
|
utils.OkOrPanic(errors.NewBusinessError(503, "Iap登录失败, 验证码识别失败"))
|
||||||
case "FAIL_UPNOTMATCH":
|
case "FAIL_UPNOTMATCH":
|
||||||
utils.OkOrPanic(errors.NewBusinessError(503, "Iap登录失败, 用户名或密码错误"))
|
utils.OkOrPanic(errors.NewBusinessError(503, "Iap登录失败, 用户名或密码错误"), func(err error) error {
|
||||||
|
return oops.With("username", username).
|
||||||
|
With("password", password).
|
||||||
|
With("userId", utils.GetUserIdNotPanic(ctx)).
|
||||||
|
In("Cpdaily.Login").New("Iap登录失败, 用户名或密码错误")
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("Iap登录失败, 状态识别码: %s", resultCode)))
|
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("Iap登录失败, 状态识别码: %s", resultCode)),
|
||||||
|
oops.With("username", username).
|
||||||
|
With("password", password).
|
||||||
|
With("userId", utils.GetUserIdNotPanic(ctx)).
|
||||||
|
In("Cpdaily.Login").Wrap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"likedream-goframe/internal/application/cookiejar"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/school"
|
||||||
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
"likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/samber/oops"
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 南宁师范大学登录实现
|
||||||
|
type CpdailyNnnuLogin struct {
|
||||||
|
client *resty.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CpdailyNnnuLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
|
||||||
|
jar := cookiejar.NewPersistCookieJar()
|
||||||
|
|
||||||
|
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||||
|
|
||||||
|
// 获取合法的登录URL
|
||||||
|
loginUrl := getCasValidLoginUrl(schoolDetail)
|
||||||
|
|
||||||
|
// 请求登录页面并获取真正的登录URL
|
||||||
|
loginPageResponse, err := c.client.R().
|
||||||
|
WithContext(ctx).
|
||||||
|
Get(loginUrl)
|
||||||
|
|
||||||
|
defer loginPageResponse.Body.Close()
|
||||||
|
loginPageBytes, err := io.ReadAll(loginPageResponse.Body)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
|
||||||
|
// 关闭client的重定向
|
||||||
|
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
|
// 解析登录页面
|
||||||
|
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
// 判断登录类型
|
||||||
|
var (
|
||||||
|
allInputs = map[string]string{}
|
||||||
|
form = loginPage.Find("form").First()
|
||||||
|
)
|
||||||
|
inputs := form.Find("input")
|
||||||
|
|
||||||
|
for _, input := range inputs.EachIter() {
|
||||||
|
name, exists := input.Attr("name")
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, _ := input.Attr("value")
|
||||||
|
allInputs[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
allInputs["username"] = username
|
||||||
|
|
||||||
|
// 处理密码
|
||||||
|
allInputs["password"] = cryptoutil.MD5(password)
|
||||||
|
|
||||||
|
// 解决常规图片验证码
|
||||||
|
captchaUrl := getAhjzuCaptchaUrl(loginResponseUrl)
|
||||||
|
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||||
|
|
||||||
|
allInputs["validateCode"] = captchaResult
|
||||||
|
|
||||||
|
// 登录提交
|
||||||
|
loginPostResponse, err := c.client.R().
|
||||||
|
SetFormData(allInputs).
|
||||||
|
WithContext(ctx).
|
||||||
|
Post(loginResponseUrl.String())
|
||||||
|
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
WithContext(ctx).
|
||||||
|
With("loginResponseUrl", loginResponseUrl).
|
||||||
|
With("allInputs", allInputs).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
defer loginPostResponse.Body.Close()
|
||||||
|
|
||||||
|
switch loginPostResponse.StatusCode() {
|
||||||
|
case 302:
|
||||||
|
handleCasLoginSuccess(ctx, loginPostResponse, schoolDetail, username, password, c.client)
|
||||||
|
case 200:
|
||||||
|
fallthrough
|
||||||
|
case 401:
|
||||||
|
doc, err := goquery.NewDocumentFromReader(loginPostResponse.Body)
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
WithContext(ctx).
|
||||||
|
With("loginResponseUrl", loginResponseUrl).
|
||||||
|
With("allInputs", allInputs).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
if isCasNeedKickOut(doc) {
|
||||||
|
kickoutResponse := submitCasKickOut(ctx, loginResponseUrl.String(), doc, c.client)
|
||||||
|
handleCasLoginSuccess(ctx, kickoutResponse, schoolDetail, username, password, c.client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message := getCasErrorMessage(doc)
|
||||||
|
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("今日校园登录失败: %s", message)), func(err error) error {
|
||||||
|
return oops.
|
||||||
|
WithContext(ctx).
|
||||||
|
With("loginResponseUrl", loginResponseUrl).
|
||||||
|
With("allInputs", allInputs).
|
||||||
|
With("errorMessage", message).
|
||||||
|
Wrap(errors.NewBusinessError(503, message))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNuuuCaptchaUrl(url *url.URL) string {
|
||||||
|
return fmt.Sprintf("%s://%s/sso/code/code.jsp", url.Scheme, url.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CpdailyNnnuLogin) GetCookieJar() http.CookieJar {
|
||||||
|
return c.client.CookieJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CpdailyNnnuLogin) GetCookieString() string {
|
||||||
|
cookieJar := c.GetCookieJar()
|
||||||
|
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||||
|
return serializableJar.Serialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CpdailyNnnuLogin) GetClient() *resty.Client {
|
||||||
|
return c.client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNnnu(ctx context.Context) CpdailyLogin {
|
||||||
|
return &CpdailyNnnuLogin{
|
||||||
|
client: httpclient.NewCpdailyClient(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"likedream-goframe/internal/application/cookiejar"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/school"
|
||||||
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
"likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/samber/oops"
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 南阳理工学院登录实现 RSA加密 & 验证码必出
|
||||||
|
type CpdailyNutLogin struct {
|
||||||
|
client *resty.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CpdailyNutLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
|
||||||
|
jar := cookiejar.NewPersistCookieJar()
|
||||||
|
|
||||||
|
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||||
|
|
||||||
|
// 获取合法的登录URL
|
||||||
|
loginUrl := getCasValidLoginUrl(schoolDetail)
|
||||||
|
|
||||||
|
// 请求登录页面并获取真正的登录URL
|
||||||
|
loginPageResponse, err := c.client.R().
|
||||||
|
WithContext(ctx).
|
||||||
|
Get(loginUrl)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
defer loginPageResponse.Body.Close()
|
||||||
|
loginPageBytes, err := io.ReadAll(loginPageResponse.Body)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
|
||||||
|
// 关闭client的重定向
|
||||||
|
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
|
|
||||||
|
// 解析登录页面
|
||||||
|
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
// 判断登录类型
|
||||||
|
var (
|
||||||
|
allInputs = map[string]string{}
|
||||||
|
fm1Node = loginPage.Find("#fm1").First()
|
||||||
|
)
|
||||||
|
inputs := fm1Node.Find("input")
|
||||||
|
|
||||||
|
for _, input := range inputs.EachIter() {
|
||||||
|
name, exists := input.Attr("name")
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, _ := input.Attr("value")
|
||||||
|
allInputs[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
allInputs["username"] = username
|
||||||
|
|
||||||
|
// 处理密码
|
||||||
|
|
||||||
|
type NutPubKeyResponse struct {
|
||||||
|
Modulus string `json:"modulus"`
|
||||||
|
Exponent string `json:"exponent"`
|
||||||
|
}
|
||||||
|
var pubKey NutPubKeyResponse
|
||||||
|
pubKeyResponse, err := c.client.R().Get(getNutPubKey(loginResponseUrl))
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
defer pubKeyResponse.Body.Close()
|
||||||
|
pubKeyBytes, err := io.ReadAll(pubKeyResponse.Body)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
sonic.Unmarshal(pubKeyBytes, &pubKey)
|
||||||
|
rsa := cryptoutil.NewNutRSAKeyPair(pubKey.Exponent, "", pubKey.Modulus)
|
||||||
|
password = rsa.EncryptedString(reverseStringASCII(password))
|
||||||
|
allInputs["password"] = password
|
||||||
|
if isNutNeedCaptcha(ctx, loginResponseUrl, c.client) {
|
||||||
|
// 解决常规图片验证码
|
||||||
|
captchaUrl := getNutCaptchaUrl(loginResponseUrl)
|
||||||
|
|
||||||
|
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||||
|
|
||||||
|
allInputs["authcode"] = captchaResult
|
||||||
|
} else {
|
||||||
|
allInputs["authcode"] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录提交
|
||||||
|
loginPostResponse, err := c.client.R().
|
||||||
|
SetFormData(allInputs).
|
||||||
|
WithContext(ctx).
|
||||||
|
Post(loginResponseUrl.String())
|
||||||
|
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
WithContext(ctx).
|
||||||
|
With("loginResponseUrl", loginResponseUrl).
|
||||||
|
With("allInputs", allInputs).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
defer loginPostResponse.Body.Close()
|
||||||
|
|
||||||
|
switch loginPostResponse.StatusCode() {
|
||||||
|
case 302:
|
||||||
|
handleCasLoginSuccess(ctx, loginPostResponse, schoolDetail, username, password, c.client)
|
||||||
|
case 200:
|
||||||
|
fallthrough
|
||||||
|
case 401:
|
||||||
|
doc, err := goquery.NewDocumentFromReader(loginPostResponse.Body)
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
WithContext(ctx).
|
||||||
|
With("loginResponseUrl", loginResponseUrl).
|
||||||
|
With("allInputs", allInputs).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
if isCasNeedKickOut(doc) {
|
||||||
|
kickoutResponse := submitCasKickOut(ctx, loginResponseUrl.String(), doc, c.client)
|
||||||
|
handleCasLoginSuccess(ctx, kickoutResponse, schoolDetail, username, password, c.client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message := getCasErrorMessage(doc)
|
||||||
|
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("今日校园登录失败: %s", message)), func(err error) error {
|
||||||
|
return oops.
|
||||||
|
WithContext(ctx).
|
||||||
|
With("loginResponseUrl", loginResponseUrl).
|
||||||
|
With("allInputs", allInputs).
|
||||||
|
With("errorMessage", message).
|
||||||
|
Wrap(errors.NewBusinessError(503, message))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func getNutCaptchaUrl(url *url.URL) string {
|
||||||
|
return fmt.Sprintf("%s://%s/cas/kaptcha", url.Scheme, url.Host)
|
||||||
|
}
|
||||||
|
func isNutNeedCaptcha(ctx context.Context, url *url.URL, client *resty.Client) bool {
|
||||||
|
|
||||||
|
requestURL := getNutNeedCaptchaUrl(url)
|
||||||
|
|
||||||
|
captchaResponse, err := client.R().
|
||||||
|
WithContext(ctx).
|
||||||
|
Get(requestURL)
|
||||||
|
|
||||||
|
utils.OkOrPanic(oops.With().
|
||||||
|
With("requestURL", requestURL).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(err))
|
||||||
|
|
||||||
|
defer captchaResponse.Body.Close()
|
||||||
|
|
||||||
|
if captchaResponse.IsError() {
|
||||||
|
utils.OkOrPanic(oops.With().
|
||||||
|
With("requestURL", requestURL).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(errors.NewBusinessError(503, "请求验证码失败")))
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes, err := io.ReadAll(captchaResponse.Body)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
return strings.ToLower(strings.TrimSpace(string(responseBytes))) != "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNutPubKey(url *url.URL) string {
|
||||||
|
return fmt.Sprintf("%s://%s/cas/v2/getPubKey", url.Scheme, url.Host)
|
||||||
|
}
|
||||||
|
func getNutNeedCaptchaUrl(url *url.URL) string {
|
||||||
|
return fmt.Sprintf("%s://%s/cas/v2/getKaptchaStatus", url.Scheme, url.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CpdailyNutLogin) GetCookieJar() http.CookieJar {
|
||||||
|
return c.client.CookieJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CpdailyNutLogin) GetCookieString() string {
|
||||||
|
cookieJar := c.GetCookieJar()
|
||||||
|
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||||
|
return serializableJar.Serialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CpdailyNutLogin) GetClient() *resty.Client {
|
||||||
|
return c.client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNut(ctx context.Context) *CpdailyNutLogin {
|
||||||
|
return &CpdailyNutLogin{
|
||||||
|
client: httpclient.NewCpdailyClient(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func reverseStringASCII(s string) string {
|
||||||
|
bytes := []byte(s)
|
||||||
|
for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
bytes[i], bytes[j] = bytes[j], bytes[i]
|
||||||
|
}
|
||||||
|
return string(bytes)
|
||||||
|
}
|
||||||
@@ -3,70 +3,35 @@ package login
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"likedream-goframe/internal/application/httpclient"
|
"likedream-goframe/internal/application/captcha"
|
||||||
"likedream-goframe/internal/cst"
|
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
|
||||||
"github.com/samber/oops"
|
"github.com/samber/oops"
|
||||||
"github.com/tidwall/gjson"
|
|
||||||
"resty.dev/v3"
|
"resty.dev/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ResolveOcrCaptcha(ctx context.Context, url string, client *resty.Client) string {
|
func ResolveOcrCaptcha(ctx context.Context, url string, client *resty.Client) string {
|
||||||
captchaResponse, err := client.R().
|
byteData := DownCode2Byte(ctx, url, client)
|
||||||
|
return captcha.ApiAutoOCRVerification(byteData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownCode2Byte(ctx context.Context, url string, client *resty.Client) []byte {
|
||||||
|
res, err := client.R().
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Get(url)
|
Get(url)
|
||||||
|
|
||||||
utils.OkOrPanic(err, func(err error) error {
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
return oops.
|
return oops.
|
||||||
With("captchaUrl", url).
|
With("captchaUrl", url).
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Wrap(err)
|
Wrap(err)
|
||||||
})
|
})
|
||||||
|
defer res.Body.Close()
|
||||||
defer func() {
|
all, err := io.ReadAll(res.Body)
|
||||||
_ = captchaResponse.Body.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
ocrUrl := g.Cfg().MustGetWithEnv(ctx, cst.ConfigOcrServerUrl).String()
|
|
||||||
|
|
||||||
ocrResponse, err := httpclient.Common.R().
|
|
||||||
SetMultipartFields(&resty.MultipartField{
|
|
||||||
Name: "file",
|
|
||||||
FileName: "captcha.jpg",
|
|
||||||
ContentType: captchaResponse.Header().Get("Content-Type"),
|
|
||||||
Reader: captchaResponse.Body,
|
|
||||||
}).
|
|
||||||
WithContext(ctx).
|
|
||||||
Post(ocrUrl + "/ocr")
|
|
||||||
|
|
||||||
utils.OkOrPanic(err, func(err error) error {
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
return oops.
|
return oops.
|
||||||
With("captchaUrl", url).
|
With("captchaUrl", url).
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Wrap(err)
|
Wrap(err)
|
||||||
})
|
})
|
||||||
|
return all
|
||||||
defer func() {
|
|
||||||
_ = ocrResponse.Body.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
responseBytes, err := io.ReadAll(ocrResponse.Body)
|
|
||||||
utils.OkOrPanic(err)
|
|
||||||
|
|
||||||
responseJSON := gjson.ParseBytes(responseBytes)
|
|
||||||
|
|
||||||
data := responseJSON.Get("data").String()
|
|
||||||
|
|
||||||
if data == "" {
|
|
||||||
utils.OkOrPanic(oops.
|
|
||||||
In("ResolveOcrCaptcha").
|
|
||||||
With("captchaUrl", url).
|
|
||||||
With("message", responseJSON.Get("message").String()).
|
|
||||||
WithContext(ctx).
|
|
||||||
New("OCR服务返回数据为空"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
"likedream-goframe/internal/common/cst"
|
||||||
|
"likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
|
"github.com/samber/oops"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CasSliderCaptchaReponse struct {
|
||||||
|
BigImage string `json:"bigImage"`
|
||||||
|
SmallImage string `json:"smallImage"`
|
||||||
|
TagWidth int `json:"tagWidth"`
|
||||||
|
YHeight int `json:"yHeight"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CasSliderResult struct {
|
||||||
|
ErrorCode int `json:"errorCode"`
|
||||||
|
ErrorMsg string `json:"errorMsg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveCasSliderCaptcha(ctx context.Context, captchaUrl string, verifyUrl string, client *resty.Client) {
|
||||||
|
res, err := client.R().
|
||||||
|
WithContext(ctx).
|
||||||
|
Get(captchaUrl)
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("captchaUrl", captchaUrl).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
defer res.Body.Close()
|
||||||
|
bytes, _ := io.ReadAll(res.Body)
|
||||||
|
var captcha CasSliderCaptchaReponse
|
||||||
|
sonic.Unmarshal(bytes, &captcha)
|
||||||
|
//width := slider.VerifySliderWidthByBase64(captcha.BigImage, captcha.SmallImage)
|
||||||
|
width := ApiVerifySliderWidthByBase64(captcha.BigImage, captcha.SmallImage)
|
||||||
|
res2, err2 := client.R().
|
||||||
|
WithContext(ctx).
|
||||||
|
SetFormData(map[string]string{
|
||||||
|
"canvasLength": "280",
|
||||||
|
"moveLength": width,
|
||||||
|
}).
|
||||||
|
Post(verifyUrl)
|
||||||
|
utils.OkOrPanic(err2, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("verifyUrl", verifyUrl).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
defer res2.Body.Close()
|
||||||
|
bytes, _ = io.ReadAll(res2.Body)
|
||||||
|
str := string(bytes)
|
||||||
|
if len(str) > 0 {
|
||||||
|
var result CasSliderResult
|
||||||
|
sonic.Unmarshal(bytes, &result)
|
||||||
|
if result.ErrorCode != 1 {
|
||||||
|
utils.OkOrPanic(oops.WithContext(ctx).Wrap(errors.NewBusinessError(500, "滑块解析错误")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiVerifySliderWidthByBase64(bigImage string, smallImage string) string {
|
||||||
|
ctx := gctx.GetInitCtx()
|
||||||
|
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/slider"
|
||||||
|
client := httpclient.Cpdaily.Clone(ctx)
|
||||||
|
client.SetDoNotParseResponse(false)
|
||||||
|
post, err := client.R().SetBody(map[string]any{
|
||||||
|
"big_image": bigImage,
|
||||||
|
"small_image": smallImage,
|
||||||
|
}).Post(baseUrl)
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.
|
||||||
|
With("AutoOCRVerification", baseUrl).
|
||||||
|
WithContext(ctx).
|
||||||
|
Wrap(err)
|
||||||
|
})
|
||||||
|
result := post.String()
|
||||||
|
return gjson.Get(result, "data").String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"likedream-goframe/internal/application/cookiejar"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||||
|
"likedream-goframe/internal/application/cpdaily/school"
|
||||||
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
"likedream-goframe/internal/common/cst"
|
||||||
|
"likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/samber/oops"
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cpdailySmsCommonResponse[T any] struct {
|
||||||
|
ErrCode int `json:"errCode"`
|
||||||
|
ErrMsg string `json:"errMsg"`
|
||||||
|
Data T `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cpdailySmsCommonResponse[T]) EnsureSuccess() {
|
||||||
|
if c.ErrCode != 0 {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, c.ErrMsg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CpdailySmsSendSmsData struct {
|
||||||
|
Countdown int `json:"countdown"`
|
||||||
|
TipMsg string `json:"tipMsg"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
CookieJar *cookiejar.PersistCookieJar `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CpdailySmsLoginByCodeData struct {
|
||||||
|
AuthID string `json:"authId"`
|
||||||
|
AuthStatus string `json:"authStatus"`
|
||||||
|
CompleteUserInfoStatus string `json:"completeUserInfoStatus"`
|
||||||
|
DeviceExceptionMsg string `json:"deviceExceptionMsg"`
|
||||||
|
DeviceStatus string `json:"deviceStatus"`
|
||||||
|
FirstLogin bool `json:"firstLogin"`
|
||||||
|
Mobile string `json:"mobile"`
|
||||||
|
MsgTip string `json:"msgTip"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
OpenID string `json:"openId"`
|
||||||
|
PersonID string `json:"personId"`
|
||||||
|
SessionToken string `json:"sessionToken"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
TenantID string `json:"tenantId"`
|
||||||
|
TenantName string `json:"tenantName"`
|
||||||
|
Tgc string `json:"tgc"`
|
||||||
|
TgcStatus string `json:"tgcStatus"`
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
UserType string `json:"userType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSms(deviceID string) *SmsLoginHandler {
|
||||||
|
return &SmsLoginHandler{
|
||||||
|
deviceID: deviceID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSmsWithoutDeviceID() *SmsLoginHandler {
|
||||||
|
return &SmsLoginHandler{
|
||||||
|
deviceID: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SmsLoginHandler struct {
|
||||||
|
deviceID string
|
||||||
|
client *resty.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SmsLoginHandler) SendSms(ctx context.Context, phone string) *CpdailySmsSendSmsData {
|
||||||
|
jar := cookiejar.NewPersistCookieJar()
|
||||||
|
|
||||||
|
phoneEncrypted := base64.StdEncoding.EncodeToString(cryptoutil.AesEncrypt([]byte(phone)))
|
||||||
|
s.client = httpclient.NewCpdailyClient()
|
||||||
|
req := s.client.
|
||||||
|
SetCookieJar(jar).
|
||||||
|
R().
|
||||||
|
SetContext(utils.NewCpdailySmsContextBuilder().
|
||||||
|
WithDeviceID(s.deviceID).
|
||||||
|
Build(ctx)).
|
||||||
|
SetBody(map[string]string{
|
||||||
|
"a": phoneEncrypted,
|
||||||
|
"b": cryptoutil.EncryptVersion,
|
||||||
|
})
|
||||||
|
|
||||||
|
setSmsRequestHeaders(ctx, req)
|
||||||
|
|
||||||
|
resp, e := req.
|
||||||
|
Post("https://mobile.campushoy.com/app/auth/authentication/mobile/messageCode/v-8222")
|
||||||
|
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||||
|
|
||||||
|
respJson := utils.ReadJson[cpdailySmsCommonResponse[CpdailySmsSendSmsData]](resp.Body)
|
||||||
|
respJson.EnsureSuccess()
|
||||||
|
|
||||||
|
respJson.Data.CookieJar = jar
|
||||||
|
|
||||||
|
return &respJson.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SmsLoginHandler) LoginByCode(ctx context.Context, phone, code string, jar http.CookieJar) *CpdailySmsLoginByCodeData {
|
||||||
|
payload := map[string]any{
|
||||||
|
"d": code,
|
||||||
|
"e": "CPDAILY",
|
||||||
|
"c": phone,
|
||||||
|
"f": "86",
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadString, e := sonic.Marshal(payload)
|
||||||
|
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||||
|
|
||||||
|
encryptedPayload := cryptoutil.AesEncrypt(payloadString)
|
||||||
|
encodedPayload := base64.StdEncoding.EncodeToString(encryptedPayload)
|
||||||
|
|
||||||
|
payload = map[string]any{
|
||||||
|
"a": encodedPayload,
|
||||||
|
"b": cryptoutil.EncryptVersion,
|
||||||
|
}
|
||||||
|
s.client = httpclient.NewCpdailyClient()
|
||||||
|
s.client.SetCookieJar(jar)
|
||||||
|
req := s.client.R().
|
||||||
|
SetBody(payload).
|
||||||
|
SetContext(utils.NewCpdailySmsContextBuilder().
|
||||||
|
WithDeviceID(s.deviceID).
|
||||||
|
Build(ctx))
|
||||||
|
setSmsRequestHeaders(ctx, req)
|
||||||
|
|
||||||
|
resp, e := req.
|
||||||
|
Post("https://mobile.campushoy.com/app/auth/authentication/mobileLogin/v-8222")
|
||||||
|
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||||
|
|
||||||
|
respJson := utils.ReadJson[cpdailySmsCommonResponse[string]](resp.Body)
|
||||||
|
respJson.EnsureSuccess()
|
||||||
|
|
||||||
|
data := decodeData[CpdailySmsLoginByCodeData](respJson.Data)
|
||||||
|
|
||||||
|
return &data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SmsLoginHandler) SetLoginCookie(ctx context.Context, jar http.CookieJar, loginData *CpdailySmsLoginByCodeData) {
|
||||||
|
detail := school.GetSchoolDetail(ctx, loginData.TenantID)
|
||||||
|
|
||||||
|
getCookieUrl := fmt.Sprintf("%s%s", detail.WecloudUrl, "/wec-im-group/group/imUserToken/getImUserToken")
|
||||||
|
|
||||||
|
cookieMap := map[string]any{
|
||||||
|
"AMP2": []map[string]any{
|
||||||
|
{
|
||||||
|
"path": "/",
|
||||||
|
"value": loginData.SessionToken,
|
||||||
|
"domain": "",
|
||||||
|
"name": "sessionToken",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"AMP1": []map[string]any{
|
||||||
|
{
|
||||||
|
"path": "/",
|
||||||
|
"value": loginData.SessionToken,
|
||||||
|
"domain": "",
|
||||||
|
"name": "sessionToken",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ampTokenString, e := sonic.Marshal(cookieMap)
|
||||||
|
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||||
|
|
||||||
|
ampTokenEncrypted := cryptoutil.DesEncrypt(ampTokenString, cryptoutil.Normal)
|
||||||
|
ampToken := base64.StdEncoding.EncodeToString(ampTokenEncrypted)
|
||||||
|
|
||||||
|
// 设置cookie到jar: clientType=cpdaily_student; tenantId=ahnu; standAlone=0; sessionToken=5e55bfa7-d17e-4f5a-a06d-99a649855521;
|
||||||
|
targetSchoolUrl, e := url.Parse(detail.WecloudUrl)
|
||||||
|
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||||
|
targetApiUrl, e := url.Parse("https://api.campushoy.com")
|
||||||
|
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||||
|
|
||||||
|
cookieNameValues := map[string]string{
|
||||||
|
"clientType": "cpdaily_student",
|
||||||
|
"tenantId": detail.Id,
|
||||||
|
"standAlone": "0",
|
||||||
|
"sessionToken": loginData.SessionToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, value := range cookieNameValues {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
|
||||||
|
jar.SetCookies(targetSchoolUrl, []*http.Cookie{cookie})
|
||||||
|
jar.SetCookies(targetApiUrl, []*http.Cookie{cookie})
|
||||||
|
}
|
||||||
|
s.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(50))
|
||||||
|
req := s.client.R().
|
||||||
|
SetHeader("AmpCookies", ampToken).
|
||||||
|
SetContext(ctx)
|
||||||
|
|
||||||
|
ctx = utils.NewCpdailySmsContextBuilder().
|
||||||
|
WithDeviceID(s.deviceID).
|
||||||
|
WithTenantID(detail.Id).
|
||||||
|
WithSessionToken(loginData.SessionToken).
|
||||||
|
Build(ctx)
|
||||||
|
|
||||||
|
setSmsRequestHeaders(ctx, req)
|
||||||
|
|
||||||
|
resp, e := req.
|
||||||
|
Get(getCookieUrl)
|
||||||
|
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||||
|
|
||||||
|
respJson := utils.ReadJson[cpdailySmsCommonResponse[any]](resp.Body)
|
||||||
|
respJson.EnsureSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeData[T any](encodedData string) T {
|
||||||
|
decodedBytes, e := base64.StdEncoding.DecodeString(encodedData)
|
||||||
|
utils.OkOrPanic(e)
|
||||||
|
|
||||||
|
decryptedBytes := cryptoutil.AesDecrypt(decodedBytes)
|
||||||
|
|
||||||
|
result := utils.ReadJson[T](bytes.NewReader(decryptedBytes))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSmsRequestHeaders(ctx context.Context, request *resty.Request) {
|
||||||
|
sessionToken, ok := ctx.Value(cst.CtxCpdailySessionToken).(string)
|
||||||
|
if ok {
|
||||||
|
encryptedSessionToken := cryptoutil.DesEncrypt([]byte(sessionToken), cryptoutil.Normal)
|
||||||
|
encodedSessionToken := base64.StdEncoding.EncodeToString(encryptedSessionToken)
|
||||||
|
request.SetHeader("SessionToken", encodedSessionToken)
|
||||||
|
request.SetHeader("sessionTokenKey", encodedSessionToken)
|
||||||
|
} else {
|
||||||
|
request.SetHeader("SessionToken", sessionToken)
|
||||||
|
request.SetHeader("sessionTokenKey", sessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceId, _ := ctx.Value(cst.CtxCpdailyDeviceID).(string)
|
||||||
|
|
||||||
|
cpdailyInfoDetail := map[string]any{
|
||||||
|
"deviceId": deviceId,
|
||||||
|
"systemName": "android",
|
||||||
|
"userId": "",
|
||||||
|
"appVersion": "9.8.8",
|
||||||
|
"model": "Unknown",
|
||||||
|
"lon": "0",
|
||||||
|
"lat": "0",
|
||||||
|
"cpdailyVersion": "9.8.8",
|
||||||
|
"systemVersion": "11",
|
||||||
|
}
|
||||||
|
|
||||||
|
cpdailyInfoStr, e := sonic.Marshal(cpdailyInfoDetail)
|
||||||
|
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||||
|
|
||||||
|
encryptedCpdailyInfo := cryptoutil.DesEncrypt(cpdailyInfoStr, cryptoutil.Normal)
|
||||||
|
encodedCpdailyInfo := base64.StdEncoding.EncodeToString(encryptedCpdailyInfo)
|
||||||
|
|
||||||
|
request.SetHeaders(map[string]string{
|
||||||
|
"clientType": "cpdaily_student",
|
||||||
|
"deviceType": "1",
|
||||||
|
"CpdailyClientType": "CPDAILY",
|
||||||
|
"CpdailyStandAlone": "0",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Pro Build/RP1A.230805.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36 cpdaily/9.8.8 wisedu/9.8.8",
|
||||||
|
"CpdailyInfo": encodedCpdailyInfo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SmsLoginHandler) GetCookieString() string {
|
||||||
|
cookieJar := s.client.CookieJar()
|
||||||
|
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||||
|
return serializableJar.Serialize()
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"likedream-goframe/internal/application/cookiejar"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testSmsPhoneNumber = ""
|
||||||
|
testSmsCode = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSmsLoginHandler_SendSms(t *testing.T) {
|
||||||
|
handler := SmsLoginHandler{}
|
||||||
|
data := handler.SendSms(context.Background(), testSmsPhoneNumber)
|
||||||
|
|
||||||
|
t.Logf("SendSms Data: %+v", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSmsLoginHandler_LoginByCode(t *testing.T) {
|
||||||
|
handler := SmsLoginHandler{}
|
||||||
|
jar := cookiejar.NewPersistCookieJar()
|
||||||
|
data := handler.LoginByCode(context.Background(), testSmsPhoneNumber, testSmsCode, jar)
|
||||||
|
t.Logf("LoginByCode Data: %+v", data)
|
||||||
|
|
||||||
|
handler.SetLoginCookie(context.Background(), jar, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogin(t *testing.T) {
|
||||||
|
handler := SmsLoginHandler{}
|
||||||
|
ctx := gctx.GetInitCtx()
|
||||||
|
sendSmsData := handler.SendSms(ctx, testSmsPhoneNumber)
|
||||||
|
// 读取验证码手动输入
|
||||||
|
var code string
|
||||||
|
println("请输入收到的验证码:")
|
||||||
|
_, _ = fmt.Scanln(&code)
|
||||||
|
loginData := handler.LoginByCode(ctx, testSmsPhoneNumber, code, sendSmsData.CookieJar)
|
||||||
|
handler.SetLoginCookie(ctx, sendSmsData.CookieJar, loginData)
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package map_info
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
"likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/samber/oops"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SearchAddressLongitudeAndLatitude(ctx context.Context, address string) []MapPoi {
|
||||||
|
client := httpclient.Cpdaily.Clone(ctx).SetDoNotParseResponse(false)
|
||||||
|
// 首先访问获取ak
|
||||||
|
response, err := client.R().Get("https://lbsyun.baidu.com/jsdemo/demo/webgl0_5.htm")
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.WithContext(ctx).With("url", "https://lbsyun.baidu.com/jsdemo/demo/webgl0_5.htm").In("SearchAddressLongitudeAndLatitude.GetAk").Wrap(err)
|
||||||
|
})
|
||||||
|
ak := ParseAkString(response.String())
|
||||||
|
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||||
|
mapResponse, err := client.R().
|
||||||
|
SetQueryParams(map[string]string{
|
||||||
|
"qt": "placesug",
|
||||||
|
"query": address,
|
||||||
|
"callback": "BMapGL._rd._cbk25241",
|
||||||
|
"region": "131",
|
||||||
|
"city_limit": "false",
|
||||||
|
"t": timestamp,
|
||||||
|
"from": "jsapi",
|
||||||
|
"ak": ak,
|
||||||
|
"v": "gl",
|
||||||
|
"timeStamp": timestamp,
|
||||||
|
}).
|
||||||
|
SetHeaders(map[string]string{
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Referer": "https://lbsyun.baidu.com/",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
|
||||||
|
}).
|
||||||
|
Get("https://api.map.baidu.com/")
|
||||||
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
|
return oops.WithContext(ctx).With("address", address).In("SearchAddressLongitudeAndLatitude.QueryMap").Wrap(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从JSONP响应中提取JSON数据
|
||||||
|
str := ParseJsonpToJson(mapResponse.String())
|
||||||
|
var jsonData MapJSONData
|
||||||
|
sonic.Unmarshal([]byte(str), &jsonData)
|
||||||
|
return jsonData.Result.PoiList
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正则提取ak // <script type="text/javascript" src="//api.map.baidu.com/api?type=webgl&v=1.0&ak=E4805d16520de693a3fe707cdc962045"></script>
|
||||||
|
func ParseAkString(data string) string {
|
||||||
|
reg := regexp.MustCompile(`ak=([^"]+)`)
|
||||||
|
matches := reg.FindStringSubmatch(data)
|
||||||
|
if len(matches) < 2 {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(500, "获取ak出错"))
|
||||||
|
}
|
||||||
|
return matches[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从JSONP响应中提取JSON数据
|
||||||
|
// 输入: /**/BMapGL._rd._cbk25241 && BMapGL._rd._cbk25241({...})
|
||||||
|
// 输出: {...}
|
||||||
|
func ParseJsonpToJson(jsonpData string) string {
|
||||||
|
// 匹配 callback(json) 格式,提取括号中的内容
|
||||||
|
reg := regexp.MustCompile(`\((\{.*\})\)`)
|
||||||
|
matches := reg.FindStringSubmatch(jsonpData)
|
||||||
|
if len(matches) < 2 {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(500, "解析JSONP数据出错"))
|
||||||
|
}
|
||||||
|
return matches[1]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package map_info
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMapSearch(t *testing.T) {
|
||||||
|
res := SearchAddressLongitudeAndLatitude(gctx.New(), "河北石油职业技术大学c座")
|
||||||
|
marshal, _ := json.Marshal(res)
|
||||||
|
t.Log(string(marshal))
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package map_info
|
||||||
|
|
||||||
|
type MapJSONData struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Result MapResult `json:"result"`
|
||||||
|
}
|
||||||
|
type MapLocation struct {
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lng float64 `json:"lng"`
|
||||||
|
}
|
||||||
|
type MapPoi struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Location MapLocation `json:"location"`
|
||||||
|
UID string `json:"uid"`
|
||||||
|
Province string `json:"province"`
|
||||||
|
City string `json:"city"`
|
||||||
|
District string `json:"district"`
|
||||||
|
Business string `json:"business"`
|
||||||
|
Cityid string `json:"cityid"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Children []interface{} `json:"children"`
|
||||||
|
Adcode string `json:"adcode"`
|
||||||
|
}
|
||||||
|
type MapResult struct {
|
||||||
|
Q string `json:"q"`
|
||||||
|
PoiList []MapPoi `json:"poi_list"`
|
||||||
|
}
|
||||||
@@ -1,22 +1,27 @@
|
|||||||
package school
|
package school
|
||||||
|
|
||||||
import "C"
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
cryptoutil2 "likedream-goframe/internal/application/cpdaily/cryptoutil"
|
cryptoutil2 "likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||||
"likedream-goframe/internal/application/httpclient"
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
cst2 "likedream-goframe/internal/common/cst"
|
||||||
"likedream-goframe/internal/common/errors"
|
"likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/framework/redis"
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/samber/oops"
|
"github.com/samber/oops"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var lock = sync.RWMutex{}
|
||||||
|
|
||||||
func GetSchoolList(ctx context.Context) []CpdailySchoolBriefInfo {
|
func GetSchoolList(ctx context.Context) []CpdailySchoolBriefInfo {
|
||||||
response, err := httpclient.Cpdaily.R().
|
response, err := httpclient.Cpdaily.Clone(ctx).R().
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Get("https://static.campushoy.com/apicache/tenantListSort")
|
Get("https://static.campushoy.com/apicache/tenantListSort")
|
||||||
|
|
||||||
@@ -52,7 +57,19 @@ func GetSchoolList(ctx context.Context) []CpdailySchoolBriefInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetSchoolDetail(ctx context.Context, tenantId string) SchoolDetailInfo {
|
func GetSchoolDetail(ctx context.Context, tenantId string) SchoolDetailInfo {
|
||||||
response, err := httpclient.Cpdaily.R().Clone(ctx).
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
if !utils.IsDebug() {
|
||||||
|
client := redis.GetClient()
|
||||||
|
redisResult := client.Get(ctx, cst2.RedisSchoolInfo+tenantId)
|
||||||
|
if redisResult.Err() == nil {
|
||||||
|
var ret SchoolDetailInfo
|
||||||
|
err := sonic.Unmarshal([]byte(redisResult.Val()), &ret)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
return parseSpecialSchool(ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response, err := httpclient.Cpdaily.Clone(ctx).R().
|
||||||
SetHeaders(map[string]string{
|
SetHeaders(map[string]string{
|
||||||
"deviceType": "2",
|
"deviceType": "2",
|
||||||
"CpdailyClientType": "CPDAILY",
|
"CpdailyClientType": "CPDAILY",
|
||||||
@@ -89,6 +106,17 @@ func GetSchoolDetail(ctx context.Context, tenantId string) SchoolDetailInfo {
|
|||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Wrap(errors.NewBusinessError(503, "学校ID不存在").Decorate("empty school detail")))
|
Wrap(errors.NewBusinessError(503, "学校ID不存在").Decorate("empty school detail")))
|
||||||
}
|
}
|
||||||
|
if !utils.IsDebug() {
|
||||||
return ret[0]
|
client := redis.GetClient()
|
||||||
|
client.Set(ctx, cst2.RedisSchoolInfo+tenantId, ret[0], time.Duration(cst2.RedisSchoolInfoExpire)*time.Second)
|
||||||
|
}
|
||||||
|
return parseSpecialSchool(ret[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSpecialSchool(s SchoolDetailInfo) SchoolDetailInfo {
|
||||||
|
// 甘肃农业大学
|
||||||
|
if s.Id == "gsau" {
|
||||||
|
s.WecloudUrl = "https://fdm.gsau.edu.cn"
|
||||||
|
}
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package submitcaptcha
|
||||||
|
|
||||||
|
//
|
||||||
|
//import (
|
||||||
|
// "context"
|
||||||
|
// "io"
|
||||||
|
// "likedream-goframe/internal/application/cpdaily/types"
|
||||||
|
// "likedream-goframe/internal/application/recognizer/classifier"
|
||||||
|
// "likedream-goframe/internal/common/errors"
|
||||||
|
// "likedream-goframe/internal/utils"
|
||||||
|
// "strings"
|
||||||
|
//
|
||||||
|
// "github.com/bytedance/sonic"
|
||||||
|
// "resty.dev/v3"
|
||||||
|
//)
|
||||||
|
//
|
||||||
|
//type AIResolver struct {
|
||||||
|
// client *resty.Client
|
||||||
|
// host string
|
||||||
|
// deviceId string
|
||||||
|
// taskType types.TaskType
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func NewAIResolver(client *resty.Client, host, deviceId string, taskType types.TaskType) SubmitCaptchaResolver {
|
||||||
|
// return &AIResolver{
|
||||||
|
// client: client,
|
||||||
|
// host: host,
|
||||||
|
// deviceId: deviceId,
|
||||||
|
// taskType: taskType,
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//func (r *AIResolver) GetValidationInfo(ctx context.Context) ValidationInfo {
|
||||||
|
// // GetValidationInfo 获取验证信息
|
||||||
|
// reqData := map[string]string{
|
||||||
|
// "deviceId": r.deviceId,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// resp, err := r.client.R().
|
||||||
|
// SetBody(reqData).
|
||||||
|
// Post(r.getValidateUrl())
|
||||||
|
// utils.OkOrPanic(err)
|
||||||
|
//
|
||||||
|
// apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||||
|
//
|
||||||
|
// if apiResp.Datas == nil {
|
||||||
|
// message := "获取提交验证码信息失败"
|
||||||
|
// if apiResp.Message != "" {
|
||||||
|
// message = apiResp.Message
|
||||||
|
// }
|
||||||
|
// utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var validationInfo ValidationInfo
|
||||||
|
// err = sonic.Unmarshal(apiResp.Datas, &validationInfo)
|
||||||
|
// utils.OkOrPanic(err)
|
||||||
|
//
|
||||||
|
// return validationInfo
|
||||||
|
//
|
||||||
|
//}
|
||||||
|
//func (r *AIResolver) GetCaptchaTicket(ctx context.Context, info ValidationInfo) string {
|
||||||
|
// captchaInfo := r.createCaptcha(info)
|
||||||
|
// codes := r.getResult(captchaInfo)
|
||||||
|
// return r.submitCaptcha(info, captchaInfo, codes)
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func (r *AIResolver) getResult(data CpSubmitCaptchaForAI) []string {
|
||||||
|
// name := data.Name
|
||||||
|
// infos := data.ImageInfos
|
||||||
|
// var result []string
|
||||||
|
// for _, item := range infos {
|
||||||
|
// url := item.Path
|
||||||
|
// get, err := r.client.R().Get(url)
|
||||||
|
// utils.OkOrPanic(err)
|
||||||
|
// defer get.Body.Close()
|
||||||
|
// bytes, _ := io.ReadAll(get.Body)
|
||||||
|
// //todo 将图片保存方便后面训练
|
||||||
|
// //os.WriteFile(item.Code+".png", bytes, 0666)
|
||||||
|
// if classifier.ClassifyImage(bytes, name) {
|
||||||
|
// result = append(result, item.Code)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return result
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//// getValidateUrl 根据任务类型获取验证URL
|
||||||
|
//func (r *AIResolver) getValidateUrl() string {
|
||||||
|
// baseUrl := strings.TrimSuffix(r.host, "/")
|
||||||
|
//
|
||||||
|
// switch r.taskType {
|
||||||
|
// case types.TaskTypeCollection:
|
||||||
|
// return baseUrl + "/wec-counselor-collector-apps/stu/collector/checkValidation"
|
||||||
|
// case types.TaskTypeSign:
|
||||||
|
// return baseUrl + "/wec-counselor-sign-apps/stu/sign/checkValidation"
|
||||||
|
// case types.TaskTypeSleepCheck:
|
||||||
|
// return baseUrl + "/wec-counselor-attendance-apps/student/attendance/checkValidation"
|
||||||
|
// default:
|
||||||
|
// utils.OkOrPanic(errors.NewBusinessError(400, "不支持的任务类型"))
|
||||||
|
// return ""
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//// createCaptcha 创建验证码
|
||||||
|
//func (r *AIResolver) createCaptcha(info ValidationInfo) CpSubmitCaptchaForAI {
|
||||||
|
// baseUrl := strings.TrimSuffix(r.host, "/")
|
||||||
|
// url := baseUrl + "/captcha-open-api/v1/captcha/create/scenesImage"
|
||||||
|
//
|
||||||
|
// resp, err := r.client.R().
|
||||||
|
// SetHeader("deviceId", r.deviceId).
|
||||||
|
// SetMultipartFormData(map[string]string{
|
||||||
|
// "accountKey": info.AccountKey,
|
||||||
|
// "sceneCode": info.SceneCode,
|
||||||
|
// "tenantId": info.TenantId,
|
||||||
|
// "userId": info.UserId,
|
||||||
|
// }).
|
||||||
|
// Post(url)
|
||||||
|
// utils.OkOrPanic(err)
|
||||||
|
//
|
||||||
|
// apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||||
|
//
|
||||||
|
// if apiResp.Result == nil {
|
||||||
|
// message := "获取提交验证码失败"
|
||||||
|
// if apiResp.Message != "" {
|
||||||
|
// message = apiResp.Message
|
||||||
|
// }
|
||||||
|
// utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var result CpSubmitCaptchaForAI
|
||||||
|
// err = sonic.Unmarshal(apiResp.Result, &result)
|
||||||
|
// utils.OkOrPanic(err)
|
||||||
|
// return result
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//// submitCaptcha 提交验证码
|
||||||
|
//func (r *AIResolver) submitCaptcha(info ValidationInfo, submitCaptcha CpSubmitCaptchaForAI, codes []string) string {
|
||||||
|
// baseUrl := strings.TrimSuffix(r.host, "/")
|
||||||
|
// url := baseUrl + "/captcha-open-api/v1/captcha/validate/scenesImage"
|
||||||
|
//
|
||||||
|
// formData := map[string]string{
|
||||||
|
// "accountKey": info.AccountKey,
|
||||||
|
// "sceneCode": info.SceneCode,
|
||||||
|
// "tenantId": info.TenantId,
|
||||||
|
// "userId": info.UserId,
|
||||||
|
// "scenesImageCode": submitCaptcha.Code,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 创建multipart form request
|
||||||
|
// req := r.client.R().SetHeader("deviceId", r.deviceId)
|
||||||
|
//
|
||||||
|
// // 添加基本表单数据
|
||||||
|
// for key, value := range formData {
|
||||||
|
// req = req.SetMultipartFormData(map[string]string{key: value})
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 添加验证码列表
|
||||||
|
// for _, code := range codes {
|
||||||
|
// req = req.SetMultipartField("scenesImageCodes", "", "", strings.NewReader(code))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// resp, err := req.Post(url)
|
||||||
|
// utils.OkOrPanic(err)
|
||||||
|
//
|
||||||
|
// apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||||
|
//
|
||||||
|
// if apiResp.Code != "200" {
|
||||||
|
// message := "提交验证码失败"
|
||||||
|
// if apiResp.Message != "" {
|
||||||
|
// message = apiResp.Message
|
||||||
|
// }
|
||||||
|
// utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if apiResp.Result == nil {
|
||||||
|
// utils.OkOrPanic(errors.NewBusinessError(503, "今日校园返回空验证码结果"))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var result string
|
||||||
|
// err = sonic.Unmarshal(apiResp.Result, &result)
|
||||||
|
// utils.OkOrPanic(err)
|
||||||
|
//
|
||||||
|
// return result
|
||||||
|
//}
|
||||||
@@ -20,6 +20,15 @@ type CpSubmitCaptcha struct {
|
|||||||
RawResponseJson json.RawMessage `json:"-"`
|
RawResponseJson json.RawMessage `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CpSubmitCaptchaForAI struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ImageInfos []struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
} `json:"imageInfos"`
|
||||||
|
}
|
||||||
|
|
||||||
// ApiResponse 通用API响应结构体
|
// ApiResponse 通用API响应结构体
|
||||||
type ApiResponse struct {
|
type ApiResponse struct {
|
||||||
Code json.Number `json:"code"`
|
Code json.Number `json:"code"`
|
||||||
@@ -33,9 +42,7 @@ type ApiResponse struct {
|
|||||||
type ITStarResponse struct {
|
type ITStarResponse struct {
|
||||||
Code json.Number `json:"code"`
|
Code json.Number `json:"code"`
|
||||||
Msg string `json:"msg"`
|
Msg string `json:"msg"`
|
||||||
Data struct {
|
Data []string `json:"data"`
|
||||||
SuccCode []string `json:"succCode"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubmitCaptchaResolver interface {
|
type SubmitCaptchaResolver interface {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"likedream-goframe/internal/application/cpdaily/types"
|
"likedream-goframe/internal/application/cpdaily/types"
|
||||||
|
"likedream-goframe/internal/common/cst"
|
||||||
"likedream-goframe/internal/common/errors"
|
"likedream-goframe/internal/common/errors"
|
||||||
"likedream-goframe/internal/cst"
|
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"resty.dev/v3"
|
"resty.dev/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RuoliSubmitCaptchaResolver struct {
|
type NormalSubmitCaptchaResolver struct {
|
||||||
client *resty.Client
|
client *resty.Client
|
||||||
host string
|
host string
|
||||||
deviceId string
|
deviceId string
|
||||||
@@ -24,12 +24,12 @@ type RuoliSubmitCaptchaResolver struct {
|
|||||||
itstarUrl string
|
itstarUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ SubmitCaptchaResolver = (*RuoliSubmitCaptchaResolver)(nil)
|
var _ SubmitCaptchaResolver = (*NormalSubmitCaptchaResolver)(nil)
|
||||||
|
|
||||||
// NewRuoliResolver 创建若李验证码解析器
|
// NewNormalResolver 创建若李验证码解析器
|
||||||
func NewRuoliResolver(client *resty.Client, host, deviceId string, taskType types.TaskType) *RuoliSubmitCaptchaResolver {
|
func NewNormalResolver(client *resty.Client, host, deviceId string, taskType types.TaskType) *NormalSubmitCaptchaResolver {
|
||||||
baseUrl := g.Config().MustGetWithEnv(gctx.GetInitCtx(), cst.ConfigRuoliCaptchaBaseUrl).String()
|
baseUrl := g.Config().MustGetWithEnv(gctx.GetInitCtx(), cst.ConfigCaptchaBaseUrl).String() + "/classifier"
|
||||||
return &RuoliSubmitCaptchaResolver{
|
return &NormalSubmitCaptchaResolver{
|
||||||
client: client,
|
client: client,
|
||||||
host: host,
|
host: host,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
@@ -39,12 +39,12 @@ func NewRuoliResolver(client *resty.Client, host, deviceId string, taskType type
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getITStarUrl 获取ITStar服务URL
|
// getITStarUrl 获取ITStar服务URL
|
||||||
func (r *RuoliSubmitCaptchaResolver) getITStarUrl() string {
|
func (r *NormalSubmitCaptchaResolver) getITStarUrl() string {
|
||||||
if r.itstarUrl != "" {
|
if r.itstarUrl != "" {
|
||||||
return r.itstarUrl
|
return r.itstarUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
r.itstarUrl = g.Config().MustGetWithEnv(gctx.GetInitCtx(), cst.ConfigRuoliCaptchaBaseUrl).String()
|
r.itstarUrl = g.Config().MustGetWithEnv(gctx.GetInitCtx(), cst.ConfigCaptchaBaseUrl).String() + "/classifier"
|
||||||
if r.itstarUrl == "" {
|
if r.itstarUrl == "" {
|
||||||
utils.OkOrPanic(errors.NewBusinessError(500, "未配置ITStarUrl"))
|
utils.OkOrPanic(errors.NewBusinessError(500, "未配置ITStarUrl"))
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ func (r *RuoliSubmitCaptchaResolver) getITStarUrl() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getValidateUrl 根据任务类型获取验证URL
|
// getValidateUrl 根据任务类型获取验证URL
|
||||||
func (r *RuoliSubmitCaptchaResolver) getValidateUrl() string {
|
func (r *NormalSubmitCaptchaResolver) getValidateUrl() string {
|
||||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||||
|
|
||||||
switch r.taskType {
|
switch r.taskType {
|
||||||
@@ -70,7 +70,7 @@ func (r *RuoliSubmitCaptchaResolver) getValidateUrl() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetValidationInfo 获取验证信息
|
// GetValidationInfo 获取验证信息
|
||||||
func (r *RuoliSubmitCaptchaResolver) GetValidationInfo(ctx context.Context) ValidationInfo {
|
func (r *NormalSubmitCaptchaResolver) GetValidationInfo(ctx context.Context) ValidationInfo {
|
||||||
reqData := map[string]string{
|
reqData := map[string]string{
|
||||||
"deviceId": r.deviceId,
|
"deviceId": r.deviceId,
|
||||||
}
|
}
|
||||||
@@ -98,14 +98,14 @@ func (r *RuoliSubmitCaptchaResolver) GetValidationInfo(ctx context.Context) Vali
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCaptchaTicket 获取验证码票据
|
// GetCaptchaTicket 获取验证码票据
|
||||||
func (r *RuoliSubmitCaptchaResolver) GetCaptchaTicket(ctx context.Context, info ValidationInfo) string {
|
func (r *NormalSubmitCaptchaResolver) GetCaptchaTicket(ctx context.Context, info ValidationInfo) string {
|
||||||
captchaInfo := r.createCaptcha(info)
|
captchaInfo := r.createCaptcha(info)
|
||||||
codes := r.getResultFromItStar(captchaInfo.RawResponseJson)
|
codes := r.getResultFromItStar(captchaInfo.RawResponseJson)
|
||||||
return r.submitCaptcha(info, captchaInfo, codes)
|
return r.submitCaptcha(info, captchaInfo, codes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createCaptcha 创建验证码
|
// createCaptcha 创建验证码
|
||||||
func (r *RuoliSubmitCaptchaResolver) createCaptcha(info ValidationInfo) CpSubmitCaptcha {
|
func (r *NormalSubmitCaptchaResolver) createCaptcha(info ValidationInfo) CpSubmitCaptcha {
|
||||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||||
url := baseUrl + "/captcha-open-api/v1/captcha/create/scenesImage"
|
url := baseUrl + "/captcha-open-api/v1/captcha/create/scenesImage"
|
||||||
|
|
||||||
@@ -139,16 +139,13 @@ func (r *RuoliSubmitCaptchaResolver) createCaptcha(info ValidationInfo) CpSubmit
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getResultFromItStar 从ITStar获取识别结果
|
// getResultFromItStar 从ITStar获取识别结果
|
||||||
func (r *RuoliSubmitCaptchaResolver) getResultFromItStar(rawResponse json.RawMessage) []string {
|
func (r *NormalSubmitCaptchaResolver) getResultFromItStar(rawResponse json.RawMessage) []string {
|
||||||
// 解析原始响应为map以便添加authKey
|
// 解析原始响应为map以便添加authKey
|
||||||
var responseMap map[string]any
|
var responseMap map[string]any
|
||||||
err := sonic.Unmarshal(rawResponse, &responseMap)
|
err := sonic.Unmarshal(rawResponse, &responseMap)
|
||||||
utils.OkOrPanic(err)
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
responseMap["authKey"] = "ITStarYYDS"
|
url := r.getITStarUrl()
|
||||||
|
|
||||||
itstarUrl := strings.TrimSuffix(r.getITStarUrl(), "/")
|
|
||||||
url := itstarUrl + "/captcha/unLimitValidate"
|
|
||||||
|
|
||||||
resp, err := r.client.R().
|
resp, err := r.client.R().
|
||||||
SetBody(responseMap).
|
SetBody(responseMap).
|
||||||
@@ -161,11 +158,11 @@ func (r *RuoliSubmitCaptchaResolver) getResultFromItStar(rawResponse json.RawMes
|
|||||||
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("验证码识别出错: %s", itstarResp.Msg)))
|
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("验证码识别出错: %s", itstarResp.Msg)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return itstarResp.Data.SuccCode
|
return itstarResp.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
// submitCaptcha 提交验证码
|
// submitCaptcha 提交验证码
|
||||||
func (r *RuoliSubmitCaptchaResolver) submitCaptcha(info ValidationInfo, submitCaptcha CpSubmitCaptcha, codes []string) string {
|
func (r *NormalSubmitCaptchaResolver) submitCaptcha(info ValidationInfo, submitCaptcha CpSubmitCaptcha, codes []string) string {
|
||||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||||
url := baseUrl + "/captcha-open-api/v1/captcha/validate/scenesImage"
|
url := baseUrl + "/captcha-open-api/v1/captcha/validate/scenesImage"
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
func TestRuoliSubmitCaptchaResolver_GetCaptchaTicket(t *testing.T) {
|
func TestRuoliSubmitCaptchaResolver_GetCaptchaTicket(t *testing.T) {
|
||||||
loginClient, detail := testutil.LoginTestUser()
|
loginClient, detail := testutil.LoginTestUser()
|
||||||
|
|
||||||
resolver := NewRuoliResolver(loginClient, detail.WecloudUrl, "abcdefg", types.TaskTypeSleepCheck)
|
resolver := NewNormalResolver(loginClient, detail.WecloudUrl, "abcdefg", types.TaskTypeSleepCheck)
|
||||||
|
|
||||||
info := resolver.GetValidationInfo(context.Background())
|
info := resolver.GetValidationInfo(context.Background())
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,22 @@ package task
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"likedream-goframe/internal/application/cookiejar"
|
|
||||||
"likedream-goframe/internal/application/cpdaily/school"
|
"likedream-goframe/internal/application/cpdaily/school"
|
||||||
"likedream-goframe/internal/application/httpclient"
|
|
||||||
errors2 "likedream-goframe/internal/common/errors"
|
errors2 "likedream-goframe/internal/common/errors"
|
||||||
"likedream-goframe/internal/ent/cptask"
|
"likedream-goframe/internal/ent/cptask"
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
|
|
||||||
|
"resty.dev/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetCpdailyTaskService(ctx context.Context, Type cptask.TaskType, cookie string, schoolId string) CpdailyTask {
|
func GetCpdailyTaskService(ctx context.Context, Type cptask.TaskType, client *resty.Client, schoolDetail school.SchoolDetailInfo) CpdailyTask {
|
||||||
detail := school.GetSchoolDetail(ctx, schoolId)
|
|
||||||
cookieJar := cookiejar.NewPersistCookieJar()
|
|
||||||
cookieJar.Deserialize(cookie)
|
|
||||||
client := httpclient.Cpdaily.Clone(ctx)
|
|
||||||
client.SetCookieJar(cookieJar)
|
|
||||||
switch Type {
|
switch Type {
|
||||||
case cptask.TaskTypeCollection:
|
case cptask.TaskTypeCollection:
|
||||||
return NewCollectionTask(client, detail.WecloudUrl)
|
return NewCollectionTask(client, schoolDetail.WecloudUrl)
|
||||||
case cptask.TaskTypeSign:
|
case cptask.TaskTypeSign:
|
||||||
return NewAutoSignTask(client, detail.WecloudUrl)
|
return NewAutoSignTask(client, schoolDetail.WecloudUrl)
|
||||||
case cptask.TaskTypeSleepCheck:
|
case cptask.TaskTypeSleepCheck:
|
||||||
return NewSleepCheckTask(client, detail.WecloudUrl)
|
return NewSleepCheckTask(client, schoolDetail.WecloudUrl)
|
||||||
default:
|
default:
|
||||||
utils.OkOrPanic(errors2.NewBusinessError(400, "不支持的任务类型"))
|
utils.OkOrPanic(errors2.NewBusinessError(400, "不支持的任务类型"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ func (c *CpdailyCollectionTask) QueryFormByTitle(ctx context.Context, titleGlob
|
|||||||
}
|
}
|
||||||
|
|
||||||
if matchTask == nil {
|
if matchTask == nil {
|
||||||
utils.OkOrPanic(errors.NewBusinessError(404, fmt.Sprintf("未找到表单信息: %s", titleGlob)), func(err error) error {
|
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("未找到表单信息: %s", titleGlob)), func(err error) error {
|
||||||
return oops.With("host", c.host).
|
return oops.With("host", c.host).
|
||||||
In("CpdailyCollectionTask.QueryFormByTitle").
|
In("CpdailyCollectionTask.QueryFormByTitle").
|
||||||
Wrap(err)
|
Wrap(err)
|
||||||
@@ -196,7 +196,7 @@ func (c *CpdailyCollectionTask) QueryFormByTitle(ctx context.Context, titleGlob
|
|||||||
detailDatas := detailJSON["datas"].MapStrVar()
|
detailDatas := detailJSON["datas"].MapStrVar()
|
||||||
|
|
||||||
if len(detailDatas) == 0 {
|
if len(detailDatas) == 0 {
|
||||||
utils.OkOrPanic(errors.NewBusinessError(404, "未找到表单数据"), func(err error) error {
|
utils.OkOrPanic(errors.NewBusinessError(503, "未找到表单数据"), func(err error) error {
|
||||||
return oops.With("host", c.host).
|
return oops.With("host", c.host).
|
||||||
In("CpdailyCollectionTask.QueryFormByTitle").
|
In("CpdailyCollectionTask.QueryFormByTitle").
|
||||||
With("titleGlob", titleGlob).
|
With("titleGlob", titleGlob).
|
||||||
@@ -230,7 +230,7 @@ func (c *CpdailyCollectionTask) QueryFormByTitle(ctx context.Context, titleGlob
|
|||||||
formDatas := formJSON["datas"].MapStrVar()
|
formDatas := formJSON["datas"].MapStrVar()
|
||||||
|
|
||||||
if len(formDatas) == 0 {
|
if len(formDatas) == 0 {
|
||||||
utils.OkOrPanic(errors.NewBusinessError(404, "未找到表单数据"), func(err error) error {
|
utils.OkOrPanic(errors.NewBusinessError(503, "未找到表单数据"), func(err error) error {
|
||||||
return oops.With("host", c.host).
|
return oops.With("host", c.host).
|
||||||
In("CpdailyCollectionTask.QueryFormByTitle").
|
In("CpdailyCollectionTask.QueryFormByTitle").
|
||||||
With("titleGlob", titleGlob).
|
With("titleGlob", titleGlob).
|
||||||
@@ -354,7 +354,7 @@ func (c *CpdailyCollectionTask) ParseFormJson(ctx context.Context, rawJson map[s
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CpdailyCollectionTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var {
|
func (c *CpdailyCollectionTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var {
|
||||||
taskInfo := form["taskInfo"].MapStrVar()
|
taskInfo := form["taskInfo"].MapStrVar()
|
||||||
rows := form["rows"].Array()
|
rows := form["rows"].Array()
|
||||||
filledForm := make([]map[string]*gvar.Var, 0)
|
filledForm := make([]map[string]*gvar.Var, 0)
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ type CpTaskFormItem struct {
|
|||||||
Sort uint `json:"sort,omitempty"`
|
Sort uint `json:"sort,omitempty"`
|
||||||
CacheKey string `json:"cacheKey,omitempty"`
|
CacheKey string `json:"cacheKey,omitempty"`
|
||||||
Show bool `json:"show,omitempty"`
|
Show bool `json:"show,omitempty"`
|
||||||
|
Tip string `json:"tip,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ const (
|
|||||||
ItemTypeMulti
|
ItemTypeMulti
|
||||||
ItemTypeImage
|
ItemTypeImage
|
||||||
ItemTypeAddress
|
ItemTypeAddress
|
||||||
|
ItemTypeLatAndLon
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ type CpdailyTask interface {
|
|||||||
QueryFormByTitle(ctx context.Context, titleGlob string) map[string]*gvar.Var
|
QueryFormByTitle(ctx context.Context, titleGlob string) map[string]*gvar.Var
|
||||||
QueryFormByYearMonth(ctx context.Context, titleGlob string, year, month string) map[string]*gvar.Var
|
QueryFormByYearMonth(ctx context.Context, titleGlob string, year, month string) map[string]*gvar.Var
|
||||||
ParseFormJson(ctx context.Context, rawJson map[string]*gvar.Var) []*formtype.CpTaskFormItem
|
ParseFormJson(ctx context.Context, rawJson map[string]*gvar.Var) []*formtype.CpTaskFormItem
|
||||||
FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var
|
FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var
|
||||||
Submit(ctx context.Context, form map[string]*gvar.Var, username, deviceId string)
|
Submit(ctx context.Context, form map[string]*gvar.Var, username, deviceId string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package leavetype
|
||||||
|
|
||||||
|
type LeaveListOne struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
LeaveType string `json:"leaveType"`
|
||||||
|
CreateTime string `json:"createTime"`
|
||||||
|
AceEndTime string `json:"aceEndTime"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"likedream-goframe/internal/application/cpdaily/types"
|
"likedream-goframe/internal/application/cpdaily/types"
|
||||||
"likedream-goframe/internal/common/errors"
|
"likedream-goframe/internal/common/errors"
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
"math/rand/v2"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -147,8 +146,8 @@ func (c *CpdailyAutoSignTask) QueryFormByTitle(ctx context.Context, titleGlob st
|
|||||||
|
|
||||||
findMatchTitle := func(jsonArr []any) map[string]*gvar.Var {
|
findMatchTitle := func(jsonArr []any) map[string]*gvar.Var {
|
||||||
for _, rawJson := range jsonArr {
|
for _, rawJson := range jsonArr {
|
||||||
item, ok := rawJson.(map[string]*gvar.Var)
|
item := gvar.New(rawJson).MapStrVar()
|
||||||
if !ok {
|
if len(item) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
taskName, exist := item["taskName"]
|
taskName, exist := item["taskName"]
|
||||||
@@ -178,7 +177,7 @@ func (c *CpdailyAutoSignTask) QueryFormByTitle(ctx context.Context, titleGlob st
|
|||||||
if matchTask == nil {
|
if matchTask == nil {
|
||||||
matchTask = findMatchTitle(signedTasks)
|
matchTask = findMatchTitle(signedTasks)
|
||||||
if matchTask == nil {
|
if matchTask == nil {
|
||||||
utils.OkOrPanic(errors.NewBusinessError(404, fmt.Sprintf("任务未找到: %s", titleGlob)), func(err error) error {
|
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("任务未找到: %s", titleGlob)), func(err error) error {
|
||||||
return oops.With("host", c.host).
|
return oops.With("host", c.host).
|
||||||
In("CpdailyAutoSignTask.QueryFormByTitle").
|
In("CpdailyAutoSignTask.QueryFormByTitle").
|
||||||
Wrap(err)
|
Wrap(err)
|
||||||
@@ -210,7 +209,7 @@ func (c *CpdailyAutoSignTask) QueryFormByTitle(ctx context.Context, titleGlob st
|
|||||||
formDatas := detailResponseJSON["datas"].MapStrVar()
|
formDatas := detailResponseJSON["datas"].MapStrVar()
|
||||||
|
|
||||||
if len(formDatas) == 0 {
|
if len(formDatas) == 0 {
|
||||||
utils.OkOrPanic(errors.NewBusinessError(404, "任务数据为空"), func(err error) error {
|
utils.OkOrPanic(errors.NewBusinessError(503, "任务数据为空"), func(err error) error {
|
||||||
return oops.With("host", c.host).
|
return oops.With("host", c.host).
|
||||||
In("CpdailyAutoSignTask.QueryFormByTitle").
|
In("CpdailyAutoSignTask.QueryFormByTitle").
|
||||||
With("titleGlob", titleGlob).
|
With("titleGlob", titleGlob).
|
||||||
@@ -293,7 +292,7 @@ func (c *CpdailyAutoSignTask) QueryFormByYearMonth(ctx context.Context, titleGlo
|
|||||||
if matchTask == nil {
|
if matchTask == nil {
|
||||||
matchTask = findMatchTitle(signedTasks)
|
matchTask = findMatchTitle(signedTasks)
|
||||||
if matchTask == nil {
|
if matchTask == nil {
|
||||||
utils.OkOrPanic(errors.NewBusinessError(404, fmt.Sprintf("任务未找到: %s", titleGlob)), func(err error) error {
|
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("任务未找到: %s", titleGlob)), func(err error) error {
|
||||||
return oops.
|
return oops.
|
||||||
With("host", c.host).
|
With("host", c.host).
|
||||||
With("year", year).
|
With("year", year).
|
||||||
@@ -332,7 +331,7 @@ func (c *CpdailyAutoSignTask) QueryFormByYearMonth(ctx context.Context, titleGlo
|
|||||||
formDatas := detailResponseJSON["datas"].MapStrVar()
|
formDatas := detailResponseJSON["datas"].MapStrVar()
|
||||||
|
|
||||||
if len(formDatas) == 0 {
|
if len(formDatas) == 0 {
|
||||||
utils.OkOrPanic(errors.NewBusinessError(404, "任务数据为空"), func(err error) error {
|
utils.OkOrPanic(errors.NewBusinessError(503, "任务数据为空"), func(err error) error {
|
||||||
return oops.
|
return oops.
|
||||||
With("host", c.host).
|
With("host", c.host).
|
||||||
With("year", year).
|
With("year", year).
|
||||||
@@ -360,42 +359,48 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
|||||||
var list []*formtype.CpTaskFormItem
|
var list []*formtype.CpTaskFormItem
|
||||||
|
|
||||||
// 经纬度
|
// 经纬度
|
||||||
|
//dto := &formtype.CpTaskFormItem{
|
||||||
|
// Title: "经度",
|
||||||
|
// ColName: "lon",
|
||||||
|
// Type: formtype.ItemTypeInput,
|
||||||
|
// Required: true,
|
||||||
|
//}
|
||||||
|
//list = append(list, dto)
|
||||||
|
//
|
||||||
|
//dto = &formtype.CpTaskFormItem{
|
||||||
|
// Title: "纬度",
|
||||||
|
// ColName: "lat",
|
||||||
|
// Type: formtype.ItemTypeInput,
|
||||||
|
// Required: true,
|
||||||
|
//}
|
||||||
|
//list = append(list, dto)
|
||||||
dto := &formtype.CpTaskFormItem{
|
dto := &formtype.CpTaskFormItem{
|
||||||
Title: "经度",
|
Title: "经纬度",
|
||||||
ColName: "lon",
|
ColName: "latAndLon",
|
||||||
Type: formtype.ItemTypeInput,
|
Type: formtype.ItemTypeLatAndLon,
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
list = append(list, dto)
|
|
||||||
|
|
||||||
dto = &formtype.CpTaskFormItem{
|
|
||||||
Title: "纬度",
|
|
||||||
ColName: "lat",
|
|
||||||
Type: formtype.ItemTypeInput,
|
|
||||||
Required: true,
|
Required: true,
|
||||||
|
Tip: "您可以通过地址搜索签到经纬度,如果觉得不够准确可以自行修改",
|
||||||
}
|
}
|
||||||
list = append(list, dto)
|
list = append(list, dto)
|
||||||
|
|
||||||
dto = &formtype.CpTaskFormItem{
|
dto = &formtype.CpTaskFormItem{
|
||||||
Title: "签到具体地址",
|
Title: "签到具体地址",
|
||||||
ColName: "address",
|
ColName: "address",
|
||||||
Type: formtype.ItemTypeAddress,
|
Type: formtype.ItemTypeInput,
|
||||||
Required: true,
|
Required: true,
|
||||||
|
Tip: "这个就是指签到完毕后查看时最底部那个最详细的地址信息哦",
|
||||||
}
|
}
|
||||||
list = append(list, dto)
|
list = append(list, dto)
|
||||||
|
|
||||||
var current uint
|
|
||||||
|
|
||||||
if isPhoto := rows["isPhoto"]; isPhoto != nil && isPhoto.Int() == 1 {
|
if isPhoto := rows["isPhoto"]; isPhoto != nil && isPhoto.Int() == 1 {
|
||||||
dto = &formtype.CpTaskFormItem{
|
dto = &formtype.CpTaskFormItem{
|
||||||
Title: "签到照片",
|
Title: "签到照片",
|
||||||
ColName: "signImg",
|
ColName: "signImg",
|
||||||
Type: formtype.ItemTypeImage,
|
Type: formtype.ItemTypeImage,
|
||||||
Required: true,
|
Required: true,
|
||||||
Number: current,
|
Tip: "您可以上传多张图片,会顺序轮换更新的哦~",
|
||||||
}
|
}
|
||||||
list = append(list, dto)
|
list = append(list, dto)
|
||||||
current++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 是否范围外签到
|
// 是否范围外签到
|
||||||
@@ -404,7 +409,6 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
|||||||
ColName: "isMalposition",
|
ColName: "isMalposition",
|
||||||
Type: formtype.ItemTypeSingle,
|
Type: formtype.ItemTypeSingle,
|
||||||
Required: true,
|
Required: true,
|
||||||
Number: current,
|
|
||||||
Options: []formtype.CpTaskFormItemOption{
|
Options: []formtype.CpTaskFormItemOption{
|
||||||
{
|
{
|
||||||
Content: "是",
|
Content: "是",
|
||||||
@@ -416,9 +420,9 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
|||||||
IsOtherItems: false,
|
IsOtherItems: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Tip: "一般在学校就是选否,如果在外实习、请假之类的才选是",
|
||||||
}
|
}
|
||||||
list = append(list, dto)
|
list = append(list, dto)
|
||||||
current++
|
|
||||||
|
|
||||||
// 范围外签到原因
|
// 范围外签到原因
|
||||||
dto = &formtype.CpTaskFormItem{
|
dto = &formtype.CpTaskFormItem{
|
||||||
@@ -427,16 +431,15 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
|||||||
Type: formtype.ItemTypeInput,
|
Type: formtype.ItemTypeInput,
|
||||||
Required: true,
|
Required: true,
|
||||||
LogicWid: "abnormalReason",
|
LogicWid: "abnormalReason",
|
||||||
Number: current,
|
|
||||||
}
|
}
|
||||||
list = append(list, dto)
|
list = append(list, dto)
|
||||||
current++
|
|
||||||
|
|
||||||
|
current := uint(1)
|
||||||
// 处理额外字段
|
// 处理额外字段
|
||||||
if isNeedExtra := rows["isNeedExtra"]; isNeedExtra != nil && isNeedExtra.Int() == 1 {
|
if isNeedExtra := rows["isNeedExtra"]; isNeedExtra != nil && isNeedExtra.Int() == 1 {
|
||||||
extraField := rows["extraField"].Array()
|
extraField := rows["extraField"].Array()
|
||||||
for i, field := range extraField {
|
for _, field := range extraField {
|
||||||
fieldObj := field.(map[string]*gvar.Var)
|
fieldObj := gvar.New(field).MapStrVar()
|
||||||
title := fieldObj["title"].String()
|
title := fieldObj["title"].String()
|
||||||
isRequired := fieldObj["isRequired"].Bool()
|
isRequired := fieldObj["isRequired"].Bool()
|
||||||
hasOtherItems := fieldObj["hasOtherItems"].Bool()
|
hasOtherItems := fieldObj["hasOtherItems"].Bool()
|
||||||
@@ -445,16 +448,17 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
|||||||
Title: title,
|
Title: title,
|
||||||
ColName: title,
|
ColName: title,
|
||||||
Type: formtype.ItemTypeSingle,
|
Type: formtype.ItemTypeSingle,
|
||||||
Number: uint(i),
|
Number: current,
|
||||||
Required: isRequired,
|
Required: isRequired,
|
||||||
IsOther: hasOtherItems,
|
IsOther: hasOtherItems,
|
||||||
}
|
}
|
||||||
|
current++
|
||||||
|
|
||||||
// 处理选项
|
// 处理选项
|
||||||
extraFieldItems := fieldObj["extraFieldItems"].Array()
|
extraFieldItems := fieldObj["extraFieldItems"].Array()
|
||||||
var options []formtype.CpTaskFormItemOption
|
var options []formtype.CpTaskFormItemOption
|
||||||
for _, item := range extraFieldItems {
|
for _, item := range extraFieldItems {
|
||||||
itemObj := item.(map[string]*gvar.Var)
|
itemObj := gvar.New(item).MapStrVar()
|
||||||
option := formtype.CpTaskFormItemOption{
|
option := formtype.CpTaskFormItemOption{
|
||||||
Content: itemObj["content"].String(),
|
Content: itemObj["content"].String(),
|
||||||
IsOtherItems: itemObj["isOtherItems"].Bool(),
|
IsOtherItems: itemObj["isOtherItems"].Bool(),
|
||||||
@@ -473,7 +477,7 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var {
|
func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var {
|
||||||
var resultForm map[string]*gvar.Var
|
var resultForm map[string]*gvar.Var
|
||||||
|
|
||||||
// 填充必要变量
|
// 填充必要变量
|
||||||
@@ -491,6 +495,11 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
|||||||
lon = v.Content
|
lon = v.Content
|
||||||
case "address":
|
case "address":
|
||||||
address = v.Content
|
address = v.Content
|
||||||
|
case "latAndLon":
|
||||||
|
{
|
||||||
|
res := strings.Split(v.Content, ",")
|
||||||
|
lat, lon = res[1], res[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,36 +523,20 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
|||||||
utils.OkOrPanic(errors.NewBusinessError(400, "签到需要图片,但是当前图片为空"), oops.WithContext(ctx).Wrap)
|
utils.OkOrPanic(errors.NewBusinessError(400, "签到需要图片,但是当前图片为空"), oops.WithContext(ctx).Wrap)
|
||||||
}
|
}
|
||||||
|
|
||||||
imageUuidSeq := strings.SplitSeq(targetParam.Content, ",")
|
if imgId == "" {
|
||||||
|
|
||||||
var imageUuids []string
|
|
||||||
|
|
||||||
for imageUuid := range imageUuidSeq {
|
|
||||||
imageUuid = strings.TrimSpace(imageUuid)
|
|
||||||
if imageUuid == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
imageUuids = append(imageUuids, imageUuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(imageUuids) == 0 {
|
|
||||||
utils.OkOrPanic(errors.NewBusinessError(400, "签到照片未上传"), oops.WithContext(ctx).Wrap)
|
utils.OkOrPanic(errors.NewBusinessError(400, "签到照片未上传"), oops.WithContext(ctx).Wrap)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 随机选择一张照片
|
imageId, e := uuid.Parse(imgId)
|
||||||
imageUuid := imageUuids[rand.IntN(len(imageUuids))]
|
|
||||||
|
|
||||||
imageId, e := uuid.Parse(imageUuid)
|
|
||||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||||
|
|
||||||
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSign, c.client)
|
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSign, c.client)
|
||||||
|
|
||||||
resultForm["signImg"] = gvar.New(previewUrl)
|
resultForm["signPhotoUrl"] = gvar.New(previewUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理验证码
|
// 处理验证码
|
||||||
submitCaptchaResolver := submitcaptcha.NewRuoliResolver(c.client, c.host, deviceId, types.TaskTypeSign)
|
submitCaptchaResolver := submitcaptcha.NewNormalResolver(c.client, c.host, deviceId, types.TaskTypeSign)
|
||||||
captchaInfo := submitCaptchaResolver.GetValidationInfo(ctx)
|
captchaInfo := submitCaptchaResolver.GetValidationInfo(ctx)
|
||||||
|
|
||||||
if captchaInfo.Validation {
|
if captchaInfo.Validation {
|
||||||
@@ -554,7 +547,9 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
|||||||
ticket = submitCaptchaResolver.GetCaptchaTicket(ctx, captchaInfo)
|
ticket = submitCaptchaResolver.GetCaptchaTicket(ctx, captchaInfo)
|
||||||
})
|
})
|
||||||
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||||
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
if e != nil {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||||
|
}
|
||||||
|
|
||||||
resultForm["ticket"] = gvar.New(ticket)
|
resultForm["ticket"] = gvar.New(ticket)
|
||||||
} else {
|
} else {
|
||||||
@@ -577,8 +572,9 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
|||||||
var extraFieldItemValues []map[string]*gvar.Var
|
var extraFieldItemValues []map[string]*gvar.Var
|
||||||
|
|
||||||
for i, field := range extraField {
|
for i, field := range extraField {
|
||||||
|
current := uint(i + 1)
|
||||||
// 检查数字是否在范围内
|
// 检查数字是否在范围内
|
||||||
found := slices.Contains(numbers, uint(i))
|
found := slices.Contains(numbers, current)
|
||||||
if !found {
|
if !found {
|
||||||
utils.OkOrPanic(errors.NewBusinessError(500, "非法的额外信息数据"), oops.WithContext(ctx).Wrap)
|
utils.OkOrPanic(errors.NewBusinessError(500, "非法的额外信息数据"), oops.WithContext(ctx).Wrap)
|
||||||
}
|
}
|
||||||
@@ -586,7 +582,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
|||||||
// 找到对应的参数
|
// 找到对应的参数
|
||||||
var targetParam *formtype.CpTaskFormItem
|
var targetParam *formtype.CpTaskFormItem
|
||||||
for _, v := range paramList {
|
for _, v := range paramList {
|
||||||
if v.Number == uint(i) {
|
if v.Number == current {
|
||||||
targetParam = v
|
targetParam = v
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -596,7 +592,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldObj := field.(map[string]*gvar.Var)
|
fieldObj := gvar.New(field).MapStrVar()
|
||||||
fieldTitle := fieldObj["title"].String()
|
fieldTitle := fieldObj["title"].String()
|
||||||
|
|
||||||
if targetParam.Title != fieldTitle {
|
if targetParam.Title != fieldTitle {
|
||||||
@@ -607,7 +603,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
|||||||
var extraFieldItemValue map[string]*gvar.Var
|
var extraFieldItemValue map[string]*gvar.Var
|
||||||
|
|
||||||
for _, item := range extraFieldItems {
|
for _, item := range extraFieldItems {
|
||||||
itemObj := item.(map[string]*gvar.Var)
|
itemObj := gvar.New(item).MapStrVar()
|
||||||
content := itemObj["content"].String()
|
content := itemObj["content"].String()
|
||||||
|
|
||||||
if content == targetParam.Content {
|
if content == targetParam.Content {
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ import (
|
|||||||
"likedream-goframe/internal/application/cpdaily/task/formtype"
|
"likedream-goframe/internal/application/cpdaily/task/formtype"
|
||||||
"likedream-goframe/internal/application/cpdaily/taskhelper"
|
"likedream-goframe/internal/application/cpdaily/taskhelper"
|
||||||
"likedream-goframe/internal/application/cpdaily/types"
|
"likedream-goframe/internal/application/cpdaily/types"
|
||||||
|
"likedream-goframe/internal/common/cst"
|
||||||
"likedream-goframe/internal/common/errors"
|
"likedream-goframe/internal/common/errors"
|
||||||
"likedream-goframe/internal/cst"
|
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
"math/rand/v2"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -436,19 +435,27 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
|||||||
var list []*formtype.CpTaskFormItem
|
var list []*formtype.CpTaskFormItem
|
||||||
|
|
||||||
// 经纬度
|
// 经纬度
|
||||||
|
//dto := &formtype.CpTaskFormItem{
|
||||||
|
// Title: "经度",
|
||||||
|
// ColName: "lon",
|
||||||
|
// Type: formtype.ItemTypeInput,
|
||||||
|
// Required: true,
|
||||||
|
//}
|
||||||
|
//list = append(list, dto)
|
||||||
|
//
|
||||||
|
//dto = &formtype.CpTaskFormItem{
|
||||||
|
// Title: "纬度",
|
||||||
|
// ColName: "lat",
|
||||||
|
// Type: formtype.ItemTypeInput,
|
||||||
|
// Required: true,
|
||||||
|
//}
|
||||||
|
//list = append(list, dto)
|
||||||
dto := &formtype.CpTaskFormItem{
|
dto := &formtype.CpTaskFormItem{
|
||||||
Title: "经度",
|
Title: "经纬度",
|
||||||
ColName: "lon",
|
ColName: "latAndLon",
|
||||||
Type: formtype.ItemTypeInput,
|
Type: formtype.ItemTypeLatAndLon,
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
list = append(list, dto)
|
|
||||||
|
|
||||||
dto = &formtype.CpTaskFormItem{
|
|
||||||
Title: "纬度",
|
|
||||||
ColName: "lat",
|
|
||||||
Type: formtype.ItemTypeInput,
|
|
||||||
Required: true,
|
Required: true,
|
||||||
|
Tip: "您可以通过地址搜索签到经纬度,如果觉得不够准确可以自行修改",
|
||||||
}
|
}
|
||||||
list = append(list, dto)
|
list = append(list, dto)
|
||||||
dto = &formtype.CpTaskFormItem{
|
dto = &formtype.CpTaskFormItem{
|
||||||
@@ -456,21 +463,20 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
|||||||
ColName: "address",
|
ColName: "address",
|
||||||
Type: formtype.ItemTypeInput,
|
Type: formtype.ItemTypeInput,
|
||||||
Required: true,
|
Required: true,
|
||||||
|
Tip: "这个就是指签到完毕后查看时最底部那个最详细的地址信息哦",
|
||||||
}
|
}
|
||||||
list = append(list, dto)
|
list = append(list, dto)
|
||||||
|
|
||||||
var current uint = 1
|
|
||||||
|
|
||||||
if isPhoto := rows["isPhoto"]; isPhoto != nil && isPhoto.Int() == 1 {
|
if isPhoto := rows["isPhoto"]; isPhoto != nil && isPhoto.Int() == 1 {
|
||||||
dto = &formtype.CpTaskFormItem{
|
dto = &formtype.CpTaskFormItem{
|
||||||
Title: "查寝照片",
|
Title: "查寝照片",
|
||||||
ColName: "signImg",
|
ColName: "signImg",
|
||||||
Type: formtype.ItemTypeImage,
|
Type: formtype.ItemTypeImage,
|
||||||
Required: true,
|
Required: true,
|
||||||
Number: current,
|
Tip: "您可以上传多张图片,会顺序轮换更新的哦~",
|
||||||
}
|
}
|
||||||
list = append(list, dto)
|
list = append(list, dto)
|
||||||
current++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 是否范围外签到
|
// 是否范围外签到
|
||||||
@@ -479,7 +485,6 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
|||||||
ColName: "isMalposition",
|
ColName: "isMalposition",
|
||||||
Type: formtype.ItemTypeSingle,
|
Type: formtype.ItemTypeSingle,
|
||||||
Required: true,
|
Required: true,
|
||||||
Number: current,
|
|
||||||
Options: []formtype.CpTaskFormItemOption{
|
Options: []formtype.CpTaskFormItemOption{
|
||||||
{
|
{
|
||||||
Content: "是",
|
Content: "是",
|
||||||
@@ -491,9 +496,9 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
|||||||
IsOtherItems: false,
|
IsOtherItems: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Tip: "一般在学校就是选否,如果在外实习、请假之类的才选是",
|
||||||
}
|
}
|
||||||
list = append(list, dto)
|
list = append(list, dto)
|
||||||
current++
|
|
||||||
|
|
||||||
for key, item := range list {
|
for key, item := range list {
|
||||||
list[key].Show = item.LogicWid == ""
|
list[key].Show = item.LogicWid == ""
|
||||||
@@ -502,7 +507,7 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var {
|
func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var {
|
||||||
var resultForm map[string]*gvar.Var
|
var resultForm map[string]*gvar.Var
|
||||||
|
|
||||||
// 填充必要变量
|
// 填充必要变量
|
||||||
@@ -520,6 +525,12 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
|||||||
lon = v.Content
|
lon = v.Content
|
||||||
case "address":
|
case "address":
|
||||||
address = v.Content
|
address = v.Content
|
||||||
|
case "latAndLon":
|
||||||
|
{
|
||||||
|
res := strings.Split(v.Content, ",")
|
||||||
|
lat, lon = res[1], res[0]
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,7 +540,8 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
|||||||
resultForm["uaIsCpadaily"] = gvar.New(true)
|
resultForm["uaIsCpadaily"] = gvar.New(true)
|
||||||
|
|
||||||
// 处理验证码
|
// 处理验证码
|
||||||
submitCaptchaResolver := submitcaptcha.NewRuoliResolver(c.client, c.host, deviceId, types.TaskTypeSleepCheck)
|
submitCaptchaResolver := submitcaptcha.NewNormalResolver(c.client, c.host, deviceId, types.TaskTypeSleepCheck)
|
||||||
|
//submitCaptchaResolver := submitcaptcha.NewAIResolver(c.client, c.host, deviceId, types.TaskTypeSleepCheck)
|
||||||
captchaInfo := submitCaptchaResolver.GetValidationInfo(ctx)
|
captchaInfo := submitCaptchaResolver.GetValidationInfo(ctx)
|
||||||
|
|
||||||
if captchaInfo.Validation {
|
if captchaInfo.Validation {
|
||||||
@@ -539,9 +551,10 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
|||||||
return utils.Catch(func() {
|
return utils.Catch(func() {
|
||||||
ticket = submitCaptchaResolver.GetCaptchaTicket(ctx, captchaInfo)
|
ticket = submitCaptchaResolver.GetCaptchaTicket(ctx, captchaInfo)
|
||||||
})
|
})
|
||||||
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
}, retry.Delay(10*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||||
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
if e != nil {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||||
|
}
|
||||||
resultForm["ticket"] = gvar.New(ticket)
|
resultForm["ticket"] = gvar.New(ticket)
|
||||||
} else {
|
} else {
|
||||||
resultForm["ticket"] = gvar.New("")
|
resultForm["ticket"] = gvar.New("")
|
||||||
@@ -560,32 +573,16 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
|||||||
utils.OkOrPanic(errors.NewBusinessError(503, "未找到查寝照片参数,请上传照片"), oops.WithContext(ctx).Wrap)
|
utils.OkOrPanic(errors.NewBusinessError(503, "未找到查寝照片参数,请上传照片"), oops.WithContext(ctx).Wrap)
|
||||||
}
|
}
|
||||||
|
|
||||||
imageUuidSeq := strings.SplitSeq(targetParam.Content, ",")
|
if imgId == "" {
|
||||||
|
|
||||||
var imageUuids []string
|
|
||||||
|
|
||||||
for imageUuid := range imageUuidSeq {
|
|
||||||
imageUuid = strings.TrimSpace(imageUuid)
|
|
||||||
if imageUuid == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
imageUuids = append(imageUuids, imageUuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(imageUuids) == 0 {
|
|
||||||
utils.OkOrPanic(errors.NewBusinessError(503, "未找到查寝照片参数,请上传照片"), oops.WithContext(ctx).Wrap)
|
utils.OkOrPanic(errors.NewBusinessError(503, "未找到查寝照片参数,请上传照片"), oops.WithContext(ctx).Wrap)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 随机选择一张照片
|
imageId, e := uuid.Parse(imgId)
|
||||||
imageUuid := imageUuids[rand.IntN(len(imageUuids))]
|
|
||||||
|
|
||||||
imageId, e := uuid.Parse(imageUuid)
|
|
||||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||||
|
|
||||||
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSleepCheck, c.client)
|
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSleepCheck, c.client)
|
||||||
|
|
||||||
resultForm["signImg"] = gvar.New(previewUrl)
|
resultForm["signPhotoUrl"] = gvar.New(previewUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否存在二维码查寝
|
// 判断是否存在二维码查寝
|
||||||
@@ -605,9 +602,9 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
|||||||
} else {
|
} else {
|
||||||
// 否则,填充查寝其他参数
|
// 否则,填充查寝其他参数
|
||||||
if signInstanceWidVar := rows["signInstanceWid"]; signInstanceWidVar != nil {
|
if signInstanceWidVar := rows["signInstanceWid"]; signInstanceWidVar != nil {
|
||||||
resultForm["signInstanceWid"] = gvar.New(signInstanceWidVar.String())
|
resultForm["SignInstanceWid"] = gvar.New(signInstanceWidVar.String())
|
||||||
} else {
|
} else {
|
||||||
resultForm["signInstanceWid"] = gvar.New("")
|
resultForm["SignInstanceWid"] = gvar.New("")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找 isMalposition 参数
|
// 查找 isMalposition 参数
|
||||||
@@ -660,11 +657,11 @@ func (c *CpdailySleepCheckTask) Submit(ctx context.Context, form map[string]*gva
|
|||||||
|
|
||||||
// 构造提交数据
|
// 构造提交数据
|
||||||
submitData := map[string]*gvar.Var{
|
submitData := map[string]*gvar.Var{
|
||||||
"lon": form["longitude"],
|
"lon": form["longitude"],
|
||||||
"version": gvar.New(cryptoutil.EncryptVersion),
|
"version": gvar.New(cryptoutil.EncryptVersion),
|
||||||
"calVersion": gvar.New(cpdailyInfo.CalVersion),
|
"calVersion": gvar.New(cpdailyInfo.CalVersion),
|
||||||
"deviceId": extensionForm["deviceId"],
|
"deviceId": extensionForm["deviceId"],
|
||||||
"userId": gvar.New(username),
|
//"userId": gvar.New(username),
|
||||||
"systemName": gvar.New("android"),
|
"systemName": gvar.New("android"),
|
||||||
"bodyString": gvar.New(bodyString),
|
"bodyString": gvar.New(bodyString),
|
||||||
"lat": form["latitude"],
|
"lat": form["latitude"],
|
||||||
@@ -674,9 +671,7 @@ func (c *CpdailySleepCheckTask) Submit(ctx context.Context, form map[string]*gva
|
|||||||
}
|
}
|
||||||
|
|
||||||
sign := taskhelper.GetSubmitSign(submitData)
|
sign := taskhelper.GetSubmitSign(submitData)
|
||||||
|
|
||||||
submitData["sign"] = gvar.New(sign)
|
submitData["sign"] = gvar.New(sign)
|
||||||
utils.OkOrPanic(errors.NewBusinessError(503, "临时拦截"), oops.WithContext(ctx).Wrap)
|
|
||||||
response, e := c.client.R().
|
response, e := c.client.R().
|
||||||
SetBody(submitData).
|
SetBody(submitData).
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
@@ -698,3 +693,18 @@ func (c *CpdailySleepCheckTask) Submit(ctx context.Context, form map[string]*gva
|
|||||||
utils.OkOrPanic(errors.NewBusinessError(503, responseJSON.Get("message").String()), oops.WithContext(ctx).Wrap)
|
utils.OkOrPanic(errors.NewBusinessError(503, responseJSON.Get("message").String()), oops.WithContext(ctx).Wrap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CpdailySleepCheckTask) GetYearAndMonth(ctx context.Context) (string, string) {
|
||||||
|
// Get current time in Shanghai timezone
|
||||||
|
timeNow := time.Now().In(cst.TimeZoneShangHai)
|
||||||
|
|
||||||
|
// Check if current day is within first 2 days of the month
|
||||||
|
if timeNow.Day() <= 2 {
|
||||||
|
// Return previous month
|
||||||
|
prevTime := timeNow.AddDate(0, -1, 0)
|
||||||
|
return strconv.Itoa(prevTime.Year()), strconv.Itoa(int(prevTime.Month()))
|
||||||
|
} else {
|
||||||
|
// Return current month
|
||||||
|
return strconv.Itoa(timeNow.Year()), strconv.Itoa(int(timeNow.Month()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
package task
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"likedream-goframe/internal/testutil"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/joomcode/errorx"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCpdailySleepCheckTask_QueryFormByTitle(t *testing.T) {
|
|
||||||
loginClient, schoolDetail := testutil.LoginTestUser()
|
|
||||||
|
|
||||||
task := NewSleepCheckTask(loginClient, schoolDetail.WecloudUrl)
|
|
||||||
tasks := task.QueryFormByTitle(context.Background(), "查寝")
|
|
||||||
|
|
||||||
if len(tasks) == 0 {
|
|
||||||
t.Error("未查询到查寝任务")
|
|
||||||
} else {
|
|
||||||
t.Logf("查询到查寝任务: %v", tasks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCpdailySleepCheckTask_GetRecentTaskTitle(t *testing.T) {
|
|
||||||
loginClient, schoolDetail := testutil.LoginTestUser()
|
|
||||||
|
|
||||||
task := NewSleepCheckTask(loginClient, schoolDetail.WecloudUrl)
|
|
||||||
|
|
||||||
titles := task.GetRecentTaskTitle(context.Background())
|
|
||||||
|
|
||||||
if len(titles) == 0 {
|
|
||||||
t.Error("未查询到查寝任务标题")
|
|
||||||
} else {
|
|
||||||
t.Logf("查询到查寝任务标题: %v", titles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCpdailySleepCheckTask_QueryFormByYearMonth(t *testing.T) {
|
|
||||||
loginClient, schoolDetail := testutil.LoginTestUser()
|
|
||||||
|
|
||||||
task := NewSleepCheckTask(loginClient, schoolDetail.WecloudUrl)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
titleGlob string
|
|
||||||
year string
|
|
||||||
month string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "查询当前月份查寝任务",
|
|
||||||
titleGlob: "查寝",
|
|
||||||
year: "2025",
|
|
||||||
month: "6",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "查询单数字月份查寝任务",
|
|
||||||
titleGlob: "查寝",
|
|
||||||
year: "2025",
|
|
||||||
month: "6", // 将会被格式化为 "06"
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "使用通配符模式匹配",
|
|
||||||
titleGlob: "*查寝*",
|
|
||||||
year: "2025",
|
|
||||||
month: "6",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "查询不存在的任务",
|
|
||||||
titleGlob: "不存在的任务",
|
|
||||||
year: "2025",
|
|
||||||
month: "6",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
oopsError, _ := errorx.ErrorFromPanic(r)
|
|
||||||
if !tt.wantErr {
|
|
||||||
t.Errorf("QueryFormByYearMonth() 意外的panic: %+v", oopsError)
|
|
||||||
} else {
|
|
||||||
t.Logf("预期的错误: %+v", oopsError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
form := task.QueryFormByYearMonth(context.Background(), tt.titleGlob, tt.year, tt.month)
|
|
||||||
|
|
||||||
if tt.wantErr {
|
|
||||||
t.Error("预期应该出现错误,但没有出现")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if form == nil {
|
|
||||||
t.Error("未查询到查寝任务表单")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证返回结果的结构
|
|
||||||
if realTitle := form["realTitle"].String(); realTitle == "" {
|
|
||||||
t.Error("返回结果中缺少 realTitle 或 realTitle 为空")
|
|
||||||
} else {
|
|
||||||
t.Logf("任务真实标题: %s", realTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
if taskInfo := form["taskInfo"].Map(); taskInfo == nil {
|
|
||||||
t.Error("返回结果中缺少 taskInfo 或 taskInfo 为空")
|
|
||||||
} else {
|
|
||||||
t.Logf("任务信息包含字段: %d", len(taskInfo))
|
|
||||||
|
|
||||||
// 验证关键字段是否存在
|
|
||||||
requiredFields := []string{"taskName", "signWid", "signInstanceWid"}
|
|
||||||
for _, field := range requiredFields {
|
|
||||||
if _, exists := taskInfo[field]; !exists {
|
|
||||||
t.Errorf("taskInfo 中缺少必要字段: %s", field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rows := form["rows"].Map(); rows == nil {
|
|
||||||
t.Error("返回结果中缺少 rows 或 rows 为空")
|
|
||||||
} else {
|
|
||||||
t.Logf("表单数据包含字段: %d", len(rows))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("查询到查寝任务表单,标题: %v", form["realTitle"])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"likedream-goframe/internal/testutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetHistoryForm(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
loginClient, schoolDetail := testutil.LoginTestUser()
|
||||||
|
task := NewSleepCheckTask(loginClient, schoolDetail.WecloudUrl)
|
||||||
|
y, m := task.GetYearAndMonth(ctx)
|
||||||
|
month := task.QueryFormByYearMonth(ctx, "晚查寝", y, m)
|
||||||
|
marshal, _ := sonic.Marshal(month)
|
||||||
|
t.Log(string(marshal))
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/gogf/gf/v2/container/gvar"
|
"github.com/gogf/gf/v2/container/gvar"
|
||||||
)
|
)
|
||||||
|
|
||||||
const submitSignSalt = "HashSalt" // 请根据实际情况替换为真实盐值
|
const submitSignSalt = "hru89rwr87Nthifa" // 请根据实际情况替换为真实盐值
|
||||||
|
|
||||||
func GetSubmitSign(form map[string]*gvar.Var) string {
|
func GetSubmitSign(form map[string]*gvar.Var) string {
|
||||||
// 按照指定顺序构建哈希数据
|
// 按照指定顺序构建哈希数据
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package filestorage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"likedream-goframe/internal/common/cst"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"likedream-goframe/internal/cst"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var GlobalS3 *S3
|
var GlobalS3 *S3
|
||||||
|
|||||||
@@ -3,23 +3,58 @@ package httpclient
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"resty.dev/v3"
|
"resty.dev/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var transport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
MinVersion: tls.VersionTLS10,
|
||||||
|
},
|
||||||
|
TLSHandshakeTimeout: 15 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
_ = os.Setenv("GODEBUG", "tlsrsakex=1")
|
||||||
|
}
|
||||||
|
|
||||||
var Cpdaily = resty.New().
|
var Cpdaily = resty.New().
|
||||||
SetCookieJar(nil).
|
SetCookieJar(nil).
|
||||||
SetTimeout(30 * time.Second).
|
SetTimeout(30*time.Second).
|
||||||
SetDoNotParseResponse(true)
|
SetDoNotParseResponse(true).
|
||||||
|
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")
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if !utils.IsDebug() {
|
if !utils.IsDebug() {
|
||||||
Cpdaily.SetDebug(false)
|
Cpdaily.SetDebug(false)
|
||||||
} else {
|
} else {
|
||||||
Cpdaily.SetProxy("http://127.0.0.1:9000").
|
Cpdaily.SetProxy("http://127.0.0.1:9000")
|
||||||
SetTLSClientConfig(&tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCpdailyClient 创建一个全新的独立 client 实例,避免 Clone 可能的浅拷贝问题
|
||||||
|
// 每个实例都是完全独立的,适用于并发场景,不会共享状态
|
||||||
|
func NewCpdailyClient() *resty.Client {
|
||||||
|
client := resty.New().
|
||||||
|
SetCookieJar(nil).
|
||||||
|
SetTimeout(30*time.Second).
|
||||||
|
SetDoNotParseResponse(true).
|
||||||
|
SetTransport(transport).
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMogudingClient() *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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Music = resty.New().
|
||||||
|
SetCookieJar(nil).
|
||||||
|
SetTimeout(3 * time.Minute).
|
||||||
|
SetTLSClientConfig(&tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
})
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"likedream-goframe/internal/application/cookiejar"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Xuexitong = 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewXuexitongClient 创建一个全新的独立 client 实例,避免 Clone 可能的浅拷贝问题
|
||||||
|
// 每个实例都是完全独立的,适用于并发场景,不会共享状态
|
||||||
|
func NewXuexitongClient() *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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AES 密钥
|
||||||
|
Secret = "23DbtQHR2UMbH6mJ"
|
||||||
|
// MD5 签名后缀
|
||||||
|
SignSuffix = "3478cbbc33f84bd00d75d7dfa69e0daa"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encrypt 使用 AES ECB PKCS7 加密并返回十六进制字符串
|
||||||
|
func Encrypt(plaintext string) (string, error) {
|
||||||
|
key := []byte(Secret)
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PKCS7 填充
|
||||||
|
plainBytes := []byte(plaintext)
|
||||||
|
padding := aes.BlockSize - len(plainBytes)%aes.BlockSize
|
||||||
|
padText := make([]byte, len(plainBytes)+padding)
|
||||||
|
copy(padText, plainBytes)
|
||||||
|
for i := len(plainBytes); i < len(padText); i++ {
|
||||||
|
padText[i] = byte(padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECB 模式加密
|
||||||
|
encrypted := make([]byte, len(padText))
|
||||||
|
for i := 0; i < len(padText); i += aes.BlockSize {
|
||||||
|
block.Encrypt(encrypted[i:i+aes.BlockSize], padText[i:i+aes.BlockSize])
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(encrypted), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign2 MD5 签名
|
||||||
|
func Sign2(str string) string {
|
||||||
|
str += SignSuffix
|
||||||
|
hash := md5.Sum([]byte(str))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
// MogudingUser 蘑菇钉用户实体
|
||||||
|
type MogudingUser struct {
|
||||||
|
Username string `json:"username"` // 账号
|
||||||
|
Password string `json:"password"` // 密码
|
||||||
|
UserId string `json:"userId"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
UserType string `json:"userType"`
|
||||||
|
PlanId string `json:"planId"`
|
||||||
|
PlanName string `json:"planName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginDto 登录请求 DTO
|
||||||
|
type LoginDto struct {
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Captcha string `json:"captcha"`
|
||||||
|
LoginType string `json:"loginType"`
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
Device string `json:"device"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
T string `json:"t"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginResponse 登录响应
|
||||||
|
type LoginResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
UserId string `json:"userId"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
UserType string `json:"userType"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlanResponse 获取 PlanId 响应
|
||||||
|
type PlanResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data []struct {
|
||||||
|
PlanId string `json:"planId"`
|
||||||
|
PlanName string `json:"planName"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignRequest 签到请求
|
||||||
|
type SignRequest struct {
|
||||||
|
Country string `json:"country"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Province string `json:"province"`
|
||||||
|
City string `json:"city"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Area string `json:"area"`
|
||||||
|
Latitude string `json:"latitude"`
|
||||||
|
Longitude string `json:"longitude"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
PlanId string `json:"planId"`
|
||||||
|
Type string `json:"type"` // START 或 END
|
||||||
|
Device string `json:"device"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignResponse 签到响应
|
||||||
|
type SignResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data any `json:"data"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package moguding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
"likedream-goframe/internal/application/moguding/cryptoutil"
|
||||||
|
"likedream-goframe/internal/application/moguding/entity"
|
||||||
|
"likedream-goframe/internal/framework/logger"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// API 基础地址
|
||||||
|
ApiBase = "https://api.moguding.net:9000"
|
||||||
|
// 登录接口
|
||||||
|
ApiLogin = ApiBase + "/session/user/v6/login"
|
||||||
|
// 获取实习计划接口
|
||||||
|
ApiGetPlan = ApiBase + "/practice/plan/v3/getPlanByStu"
|
||||||
|
// 打卡签到接口
|
||||||
|
ApiSign = ApiBase + "/attendence/clock/v4/save"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MogudingClient struct {
|
||||||
|
client *resty.Client
|
||||||
|
user *entity.MogudingUser
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMogudingClient 创建蘑菇钉客户端
|
||||||
|
func NewMogudingClient(ctx context.Context) *MogudingClient {
|
||||||
|
client := httpclient.NewMogudingClient()
|
||||||
|
|
||||||
|
return &MogudingClient{
|
||||||
|
client: client,
|
||||||
|
user: &entity.MogudingUser{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login 登录
|
||||||
|
func (c *MogudingClient) Login(ctx context.Context, username, password string) error {
|
||||||
|
if username == "" || password == "" {
|
||||||
|
return fmt.Errorf("账号或密码不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密账号密码
|
||||||
|
encryptedPhone, err := cryptoutil.Encrypt(username)
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Error("加密账号失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedPassword, err := cryptoutil.Encrypt(password)
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Error("加密密码失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密时间戳
|
||||||
|
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||||
|
encryptedTime, err := cryptoutil.Encrypt(timestamp)
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Error("加密时间戳失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造登录请求
|
||||||
|
loginDto := entity.LoginDto{
|
||||||
|
Phone: encryptedPhone,
|
||||||
|
Password: encryptedPassword,
|
||||||
|
T: encryptedTime,
|
||||||
|
LoginType: "android",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送登录请求
|
||||||
|
resp, err := c.client.R().
|
||||||
|
SetBody(loginDto).
|
||||||
|
Post(ApiLogin)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Error("登录请求失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var loginResp entity.LoginResponse
|
||||||
|
if err := json.Unmarshal(resp.Bytes(), &loginResp); err != nil {
|
||||||
|
logger.Logger.Error("解析登录响应失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginResp.Code != 200 {
|
||||||
|
return fmt.Errorf("登录失败:%s", loginResp.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存用户信息
|
||||||
|
c.user.Username = username
|
||||||
|
c.user.Password = password
|
||||||
|
c.user.UserId = loginResp.Data.UserId
|
||||||
|
c.user.Token = loginResp.Data.Token
|
||||||
|
c.user.UserType = loginResp.Data.UserType
|
||||||
|
|
||||||
|
logger.Logger.Info("登录成功",
|
||||||
|
zap.String("userId", c.user.UserId),
|
||||||
|
zap.String("userType", c.user.UserType))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPlanId 获取实习计划 ID
|
||||||
|
func (c *MogudingClient) GetPlanId(ctx context.Context) error {
|
||||||
|
if c.user.Token == "" {
|
||||||
|
return fmt.Errorf("请先登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算签名
|
||||||
|
signStr := c.user.UserId + c.user.UserType
|
||||||
|
sign := cryptoutil.Sign2(signStr)
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
resp, err := c.client.R().
|
||||||
|
SetHeaders(map[string]string{
|
||||||
|
"roleKey": c.user.UserType,
|
||||||
|
"sign": sign,
|
||||||
|
"authorization": c.user.Token,
|
||||||
|
}).
|
||||||
|
SetBody(map[string]string{"state": ""}).
|
||||||
|
Post(ApiGetPlan)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Error("获取 PlanId 请求失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var planResp entity.PlanResponse
|
||||||
|
if err := json.Unmarshal(resp.Bytes(), &planResp); err != nil {
|
||||||
|
logger.Logger.Error("解析 PlanId 响应失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if planResp.Code != 200 {
|
||||||
|
return fmt.Errorf("获取 PlanId 失败:%s", planResp.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(planResp.Data) == 0 {
|
||||||
|
return fmt.Errorf("未找到实习计划")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存第一个计划信息
|
||||||
|
c.user.PlanId = planResp.Data[0].PlanId
|
||||||
|
c.user.PlanName = planResp.Data[0].PlanName
|
||||||
|
|
||||||
|
logger.Logger.Info("获取 PlanId 成功",
|
||||||
|
zap.String("planId", c.user.PlanId),
|
||||||
|
zap.String("planName", c.user.PlanName))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign 打卡签到
|
||||||
|
func (c *MogudingClient) Sign(ctx context.Context, signReq *entity.SignRequest) error {
|
||||||
|
if c.user.Token == "" {
|
||||||
|
return fmt.Errorf("请先登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.user.PlanId == "" {
|
||||||
|
return fmt.Errorf("请先获取实习计划")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
if signReq.PlanId == "" {
|
||||||
|
signReq.PlanId = c.user.PlanId
|
||||||
|
}
|
||||||
|
if signReq.Device == "" {
|
||||||
|
signReq.Device = "Android"
|
||||||
|
}
|
||||||
|
if signReq.Type == "" {
|
||||||
|
signReq.Type = "START"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算签名
|
||||||
|
signStr := signReq.Device + signReq.Type + signReq.PlanId + c.user.UserId + signReq.Address
|
||||||
|
sign := cryptoutil.Sign2(signStr)
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
resp, err := c.client.R().
|
||||||
|
SetHeaders(map[string]string{
|
||||||
|
"roleKey": c.user.UserType,
|
||||||
|
"sign": sign,
|
||||||
|
"authorization": c.user.Token,
|
||||||
|
}).
|
||||||
|
SetBody(signReq).
|
||||||
|
Post(ApiSign)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Error("签到请求失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var signResp entity.SignResponse
|
||||||
|
if err := json.Unmarshal(resp.Bytes(), &signResp); err != nil {
|
||||||
|
logger.Logger.Error("解析签到响应失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if signResp.Code != 200 {
|
||||||
|
return fmt.Errorf("签到失败:%s", signResp.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Logger.Info("签到成功",
|
||||||
|
zap.String("type", signReq.Type),
|
||||||
|
zap.String("address", signReq.Address))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser 获取用户信息
|
||||||
|
func (c *MogudingClient) GetUser() *entity.MogudingUser {
|
||||||
|
return c.user
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUser 设置用户信息(用于恢复登录状态)
|
||||||
|
func (c *MogudingClient) SetUser(user *entity.MogudingUser) {
|
||||||
|
c.user = user
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package moguding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"likedream-goframe/internal/application/moguding/entity"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMogudingLogin(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
client := NewMogudingClient(ctx)
|
||||||
|
|
||||||
|
// 替换为实际的测试账号
|
||||||
|
username := "13307425047"
|
||||||
|
password := "wgz2021322L"
|
||||||
|
|
||||||
|
err := client.Login(ctx, username, password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("登录失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("登录成功,UserId: %s, Token: %s", client.user.UserId, client.user.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMogudingGetPlanId(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
client := NewMogudingClient(ctx)
|
||||||
|
|
||||||
|
// 先登录
|
||||||
|
username := "your_username"
|
||||||
|
password := "your_password"
|
||||||
|
|
||||||
|
err := client.Login(ctx, username, password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("登录失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 PlanId
|
||||||
|
err = client.GetPlanId(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("获取 PlanId 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("获取 PlanId 成功,PlanId: %s, PlanName: %s", client.user.PlanId, client.user.PlanName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMogudingSign(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
client := NewMogudingClient(ctx)
|
||||||
|
|
||||||
|
// 先登录
|
||||||
|
username := "your_username"
|
||||||
|
password := "your_password"
|
||||||
|
|
||||||
|
err := client.Login(ctx, username, password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("登录失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 PlanId
|
||||||
|
err = client.GetPlanId(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("获取 PlanId 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打卡签到
|
||||||
|
signReq := &entity.SignRequest{
|
||||||
|
Country: "中国",
|
||||||
|
Address: "四川省 · 成都市 · 武侯区 · 咕噜咕噜",
|
||||||
|
Province: "四川省",
|
||||||
|
City: "成都市",
|
||||||
|
State: "1",
|
||||||
|
Area: "武侯区",
|
||||||
|
Latitude: "33.100039",
|
||||||
|
Longitude: "101.987848",
|
||||||
|
Description: "测试打卡",
|
||||||
|
Type: "START", // START 表示上班打卡,END 表示下班打卡
|
||||||
|
Device: "Android",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.Sign(ctx, signReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("打卡失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("打卡成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMogudingFullProcess(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
client := NewMogudingClient(ctx)
|
||||||
|
|
||||||
|
// 1. 登录
|
||||||
|
username := "your_username"
|
||||||
|
password := "your_password"
|
||||||
|
|
||||||
|
t.Log("步骤1: 登录")
|
||||||
|
err := client.Login(ctx, username, password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("登录失败: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("登录成功,UserId: %s", client.user.UserId)
|
||||||
|
|
||||||
|
// 2. 获取 PlanId
|
||||||
|
t.Log("步骤2: 获取实习计划")
|
||||||
|
err = client.GetPlanId(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("获取 PlanId 失败: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("获取 PlanId 成功,PlanId: %s, PlanName: %s", client.user.PlanId, client.user.PlanName)
|
||||||
|
|
||||||
|
// 3. 上班打卡
|
||||||
|
t.Log("步骤3: 上班打卡")
|
||||||
|
signInReq := &entity.SignRequest{
|
||||||
|
Country: "中国",
|
||||||
|
Address: "四川省 · 成都市 · 武侯区 · 咕噜咕噜",
|
||||||
|
Province: "四川省",
|
||||||
|
City: "成都市",
|
||||||
|
State: "1",
|
||||||
|
Area: "武侯区",
|
||||||
|
Latitude: "33.100039",
|
||||||
|
Longitude: "101.987848",
|
||||||
|
Description: "上班打卡",
|
||||||
|
Type: "START",
|
||||||
|
Device: "Android",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.Sign(ctx, signInReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("上班打卡失败: %v", err)
|
||||||
|
}
|
||||||
|
t.Log("上班打卡成功")
|
||||||
|
|
||||||
|
// 4. 下班打卡(可选)
|
||||||
|
t.Log("步骤4: 下班打卡")
|
||||||
|
signOutReq := &entity.SignRequest{
|
||||||
|
Country: "中国",
|
||||||
|
Address: "四川省 · 成都市 · 武侯区 · 咕噜咕噜",
|
||||||
|
Province: "四川省",
|
||||||
|
City: "成都市",
|
||||||
|
State: "1",
|
||||||
|
Area: "武侯区",
|
||||||
|
Latitude: "33.100039",
|
||||||
|
Longitude: "101.987848",
|
||||||
|
Description: "下班打卡",
|
||||||
|
Type: "END",
|
||||||
|
Device: "Android",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.Sign(ctx, signOutReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("下班打卡失败: %v", err)
|
||||||
|
}
|
||||||
|
t.Log("下班打卡成功")
|
||||||
|
|
||||||
|
t.Log("完整流程测试通过!")
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package music
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
"likedream-goframe/internal/common/errors"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/samber/oops"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
var MusicDownloader *Downloader
|
||||||
|
|
||||||
|
type Downloader struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Downloader) Search(ctx context.Context, keyword string) (result []MusicSearchResult) {
|
||||||
|
client := httpclient.Music.Clone(ctx)
|
||||||
|
response, err := client.R().Get("https://www.gequbao.net/s/" + url.PathEscape(keyword))
|
||||||
|
if err != nil {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "搜索出错1"), oops.WithContext(ctx).Wrap)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
reader, err := goquery.NewDocumentFromReader(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "搜索出错2"), oops.WithContext(ctx).Wrap)
|
||||||
|
}
|
||||||
|
card := reader.Find(".card")
|
||||||
|
find := card.Find(".card-text > .row:not(:first-child)")
|
||||||
|
find.Each(func(i int, item *goquery.Selection) {
|
||||||
|
selection := item.Find("a")
|
||||||
|
url, _ := selection.Attr("href")
|
||||||
|
name := strings.TrimSpace(selection.Find(".text-primary span").Text())
|
||||||
|
singer := strings.TrimSpace(selection.Find(".text-jade").Text())
|
||||||
|
result = append(result, MusicSearchResult{
|
||||||
|
Name: name,
|
||||||
|
Singer: singer,
|
||||||
|
Path: url,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Downloader) Download(ctx context.Context, url string) string {
|
||||||
|
client := httpclient.Music.Clone(ctx)
|
||||||
|
response, err := client.R().Get("https://www.gequbao.net" + url)
|
||||||
|
if err != nil {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "下载出错1"), oops.WithContext(ctx).Wrap)
|
||||||
|
}
|
||||||
|
html := response.String()
|
||||||
|
reg := regexp.MustCompile("window\\.play_id = '(.*?)';")
|
||||||
|
play_id := reg.FindStringSubmatch(html)[1]
|
||||||
|
post, err := client.R().SetBody(map[string]string{
|
||||||
|
"id": play_id,
|
||||||
|
}).Post("https://www.gequbao.net/api/play-url")
|
||||||
|
if err != nil {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessError(503, "下载出错2"), oops.WithContext(ctx).Wrap)
|
||||||
|
}
|
||||||
|
download_url := gjson.Get(post.String(), "data.url").String()
|
||||||
|
|
||||||
|
// 下载音频文件
|
||||||
|
return download_url
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package music
|
||||||
|
|
||||||
|
type MusicSearchResult struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Singer string `json:"singer"`
|
||||||
|
Path string `json:"url"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/base64"
|
||||||
|
"likedream-goframe/internal/framework/logger"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KEY = "u2oh6Vu^HWe4_AES"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoginEncrypt(str string) (string, error) {
|
||||||
|
key := []byte(KEY)
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Error("aes.NewCipher err:", zap.Error(err))
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
paddedData := pad([]byte(str), block.BlockSize())
|
||||||
|
ciphertext := make([]byte, len(paddedData))
|
||||||
|
mode := cipher.NewCBCEncrypter(block, key)
|
||||||
|
mode.CryptBlocks(ciphertext, paddedData)
|
||||||
|
data := base64.StdEncoding.EncodeToString(ciphertext)
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pad 确保数据长度是块大小的整数倍,以便符合块加密算法的要求
|
||||||
|
func pad(src []byte, blockSize int) []byte {
|
||||||
|
padding := blockSize - len(src)%blockSize
|
||||||
|
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(src, padText...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
// XueXiTCourse 课程所有信息
|
||||||
|
type XueXiTCourseJson struct {
|
||||||
|
Result int `json:"result"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
ChannelList []ChannelItem `json:"channelList"`
|
||||||
|
Mcode string `json:"mcode"`
|
||||||
|
Createcourse int `json:"createcoursed"`
|
||||||
|
TeacherEndCourse int `json:"teacherEndCourse"`
|
||||||
|
ShowEndCourse int `json:"showEndCourse"`
|
||||||
|
HasMore bool `json:"hasMore"`
|
||||||
|
StuEndCourse int `json:"stuEndCourse"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelItem 课程列表
|
||||||
|
type ChannelItem struct {
|
||||||
|
Cfid int `json:"cfid"`
|
||||||
|
Norder int `json:"norder"`
|
||||||
|
CataName string `json:"cataName"`
|
||||||
|
Cataid string `json:"cataid"`
|
||||||
|
Id int `json:"id"`
|
||||||
|
Cpi int `json:"cpi"`
|
||||||
|
Key any `json:"key"`
|
||||||
|
Content struct {
|
||||||
|
Studentcount int `json:"studentcount"`
|
||||||
|
Chatid string `json:"chatid"`
|
||||||
|
IsFiled int `json:"isFiled"`
|
||||||
|
Isthirdaq int `json:"isthirdaq"`
|
||||||
|
Isstart bool `json:"isstart"`
|
||||||
|
Isretire int `json:"isretire"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Course struct {
|
||||||
|
Data []struct {
|
||||||
|
BelongSchoolId string `json:"belongSchoolId"`
|
||||||
|
Coursestate int `json:"coursestate"`
|
||||||
|
Teacherfactor string `json:"teacherfactor"`
|
||||||
|
IsCourseSquare int `json:"isCourseSquare"`
|
||||||
|
Schools string `json:"schools"`
|
||||||
|
CourseSquareUrl string `json:"courseSquareUrl"`
|
||||||
|
Imageurl string `json:"imageurl"`
|
||||||
|
AppInfo string `json:"appInfo"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DefaultShowCatalog int `json:"defaultShowCatalog"`
|
||||||
|
Id int `json:"id"`
|
||||||
|
AppData int `json:"appData"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"course"`
|
||||||
|
Roletype int `json:"roletype"`
|
||||||
|
Id int `json:"id"`
|
||||||
|
State int `json:"state"`
|
||||||
|
Cpi int `json:"cpi"`
|
||||||
|
Bbsid string `json:"bbsid"`
|
||||||
|
IsSquare int `json:"isSquare"`
|
||||||
|
} `json:"content"`
|
||||||
|
Topsign int `json:"topsign"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type XueXiTCourse struct {
|
||||||
|
Cpi int `json:"cpi"` // 用户唯一标识
|
||||||
|
Key string `json:"key"` // classID 在课程API中为key
|
||||||
|
CourseID string `json:"courseId"` // 课程ID
|
||||||
|
ChatID string `json:"chatId"`
|
||||||
|
CourseTeacher string `json:"courseTeacher"` // 课程老师
|
||||||
|
CourseName string `json:"courseName"` // 课程名
|
||||||
|
CourseImage string `json:"courseImage"`
|
||||||
|
// 两个标识 暂时不知道有什么用
|
||||||
|
CourseDataID int `json:"courseDataId"`
|
||||||
|
ContentID int `json:"ContentID"`
|
||||||
|
IsStart bool `json:"isstart"` // 是否开课了,开课了为true,没开课为false
|
||||||
|
State int `json:"state"` // 课程状态,0为正常,1为课程已经结束
|
||||||
|
}
|
||||||
|
|
||||||
|
// XueXiTCourseChapter 课程章节信息
|
||||||
|
type XueXiTCourseChapter struct {
|
||||||
|
ID int `json:"id"` // 章节ID
|
||||||
|
Name string `json:"name"` // 章节名称
|
||||||
|
ParentNodeID int `json:"parentnodeid"` // 父节点ID
|
||||||
|
Layer int `json:"layer"` // 层级
|
||||||
|
Label string `json:"label"` // 标签
|
||||||
|
IndexOrder int `json:"indexOrder"` // 排序索引
|
||||||
|
JobCount int `json:"jobcount"` // 任务点数量
|
||||||
|
Status string `json:"status"` // 状态
|
||||||
|
IsReview int `json:"isReview"` // 是否复习
|
||||||
|
BeginTime int64 `json:"begintime"` // 开始时间
|
||||||
|
EndTime int64 `json:"endtime"` // 结束时间
|
||||||
|
PointTotal int `json:"pointTotal"` // 总任务点
|
||||||
|
PointFinish int `json:"pointFinish"` // 已完成任务点
|
||||||
|
Knowledge []XueXiTCourseChapter `json:"knowledge"` // 子章节列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChapterData 章节数据响应
|
||||||
|
type ChapterData struct {
|
||||||
|
Data []struct {
|
||||||
|
Hideclazz int `json:"hideclazz"`
|
||||||
|
Allowdownload int `json:"allowdownload"`
|
||||||
|
Chatid string `json:"chatid"`
|
||||||
|
Isfiled int `json:"isfiled"`
|
||||||
|
Forbidintoclazz int `json:"forbidintoclazz"`
|
||||||
|
Coursesetting struct {
|
||||||
|
Data []struct {
|
||||||
|
Coursefacecheck int `json:"coursefacecheck"`
|
||||||
|
Hiddencoursecover int `json:"hiddencoursecover"`
|
||||||
|
ID int `json:"id"`
|
||||||
|
Courseid int `json:"courseid"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"coursesetting"`
|
||||||
|
Isstart bool `json:"isstart"`
|
||||||
|
Visiblescore int `json:"visiblescore"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Course struct {
|
||||||
|
Data []struct {
|
||||||
|
App int `json:"app"`
|
||||||
|
Teacherfactor string `json:"teacherfactor"`
|
||||||
|
Jobcount int `json:"jobcount"`
|
||||||
|
Infocontent string `json:"infocontent"`
|
||||||
|
Bulletformat string `json:"bulletformat"`
|
||||||
|
Mappingcourseid int `json:"mappingcourseid"`
|
||||||
|
Imageurl string `json:"imageurl"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ID int `json:"id"`
|
||||||
|
Belongschoolid string `json:"belongschoolid"`
|
||||||
|
Objectid string `json:"objectid"`
|
||||||
|
Knowledge struct {
|
||||||
|
Data []KnowledgeData `json:"data"`
|
||||||
|
} `json:"knowledge"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"course"`
|
||||||
|
ID int `json:"id"`
|
||||||
|
State int `json:"state"`
|
||||||
|
Bbsid string `json:"bbsid"`
|
||||||
|
Classscore int `json:"classscore"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KnowledgeData 章节知识点数据
|
||||||
|
type KnowledgeData struct {
|
||||||
|
Jobcount int `json:"jobcount"`
|
||||||
|
Isreview int `json:"isreview"`
|
||||||
|
Indexorder int `json:"indexorder"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ID int `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Layer int `json:"layer"`
|
||||||
|
Parentnodeid int `json:"parentnodeid"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
// PointType 任务点类型
|
||||||
|
type PointType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PointTypeVideo PointType = "insertvideo"
|
||||||
|
PointTypeDocument PointType = "insertdoc"
|
||||||
|
PointTypeDocument2 PointType = "document"
|
||||||
|
PointTypeBook PointType = "insertbook"
|
||||||
|
PointTypeWork PointType = "insertwork"
|
||||||
|
PointTypeHyperlink PointType = "insertlink"
|
||||||
|
PointTypeLive PointType = "insertlive"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PointVideoDto 视频任务点
|
||||||
|
type PointVideoDto struct {
|
||||||
|
CardIndex int
|
||||||
|
CourseID string
|
||||||
|
ClassID string
|
||||||
|
KnowledgeID int
|
||||||
|
Cpi string
|
||||||
|
ObjectID string
|
||||||
|
// 从SSR视图中获取
|
||||||
|
IsPassed bool
|
||||||
|
FID int
|
||||||
|
DToken string
|
||||||
|
PlayTime int
|
||||||
|
Duration int
|
||||||
|
JobID string
|
||||||
|
OtherInfo string
|
||||||
|
Title string
|
||||||
|
RT float64
|
||||||
|
VideoFaceCaptureEnc string
|
||||||
|
RandomCaptureTime string
|
||||||
|
AttDurationEnc string
|
||||||
|
Enc string
|
||||||
|
Mid string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointDocumentDto 文档查看任务点
|
||||||
|
type PointDocumentDto struct {
|
||||||
|
CardIndex int
|
||||||
|
CourseID string
|
||||||
|
ClassID string
|
||||||
|
KnowledgeID int
|
||||||
|
Cpi string
|
||||||
|
ObjectID string
|
||||||
|
Title string
|
||||||
|
JobID string
|
||||||
|
Jtoken string
|
||||||
|
IsJob bool // 是否是任务点
|
||||||
|
Type string // document / insertbook
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointWorkDto 测验/作业任务点
|
||||||
|
type PointWorkDto struct {
|
||||||
|
CardIndex int
|
||||||
|
CourseID string
|
||||||
|
ClassID string
|
||||||
|
KnowledgeID int
|
||||||
|
Cpi string
|
||||||
|
WorkID string
|
||||||
|
SchoolID string
|
||||||
|
JobID string
|
||||||
|
PUID string
|
||||||
|
KToken string
|
||||||
|
Enc string
|
||||||
|
Title string
|
||||||
|
IsPassed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointHyperlinkDto 外链任务点
|
||||||
|
type PointHyperlinkDto struct {
|
||||||
|
CardIndex int
|
||||||
|
CourseID string
|
||||||
|
ClassID string
|
||||||
|
KnowledgeID int
|
||||||
|
Cpi string
|
||||||
|
ObjectID string
|
||||||
|
Title string
|
||||||
|
JobID string
|
||||||
|
Jtoken string
|
||||||
|
LinkType int // 外链类型
|
||||||
|
IsJob bool // 是否是任务点
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointLiveDto 直播任务点
|
||||||
|
type PointLiveDto struct {
|
||||||
|
CardIndex int
|
||||||
|
CourseID string
|
||||||
|
ClassID string
|
||||||
|
KnowledgeID int
|
||||||
|
Cpi string
|
||||||
|
UserId string // 用户ID
|
||||||
|
Live bool
|
||||||
|
LiveId string // 直播ID
|
||||||
|
Vdoid string
|
||||||
|
Mid string
|
||||||
|
Title string // 直播标题
|
||||||
|
JobID string
|
||||||
|
StreamName string // 直播流名称
|
||||||
|
LiveStatusStr string // 直播状态文字
|
||||||
|
LiveStatusCode int // 直播状态码 0=未开始
|
||||||
|
VideoDuration int // 视频时长
|
||||||
|
Aid string
|
||||||
|
Module string // 类型
|
||||||
|
IsJob bool // 是否为任务点
|
||||||
|
AuthEnc string
|
||||||
|
LiveDragEnc string
|
||||||
|
LiveSetEnc string
|
||||||
|
OtherInfo string
|
||||||
|
Enc string
|
||||||
|
LiveSwDsEnc string
|
||||||
|
VideoCompletePercent float64 // 观看进度
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card 任务点卡片信息
|
||||||
|
type Card struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
CardOrder int `json:"cardorder"`
|
||||||
|
KnowledgeID int `json:"knowledgeid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CardAPIResponse 卡片API响应
|
||||||
|
type CardAPIResponse struct {
|
||||||
|
Data []CardDataItem `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CardDataItem 卡片数据项
|
||||||
|
type CardDataItem struct {
|
||||||
|
ClickCount int `json:"clickcount"`
|
||||||
|
Createtime int64 `json:"createtime"`
|
||||||
|
OpenLock int `json:"openlock"`
|
||||||
|
IndexOrder int `json:"indexorder"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
LastModifyTime int64 `json:"lastmodifytime"`
|
||||||
|
ID int `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Layer int `json:"layer"`
|
||||||
|
Card struct {
|
||||||
|
Data []Card `json:"data"`
|
||||||
|
} `json:"card"`
|
||||||
|
ParentNodeID int `json:"parentnodeid"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IframeAttributes iframe 的属性
|
||||||
|
type IframeAttributes struct {
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
Other map[string]string
|
||||||
|
HasData bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChapterPointStatus 章节任务点状态
|
||||||
|
type ChapterPointStatus map[string]struct {
|
||||||
|
ClickCount int `json:"clickcount"`
|
||||||
|
FinishCount int `json:"finishcount"`
|
||||||
|
TotalCount int `json:"totalcount"`
|
||||||
|
OpenLock int `json:"openlock"`
|
||||||
|
UnFinishCount int `json:"unfinishcount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachmentSetting 附件设置(从页面JS中解析)
|
||||||
|
type AttachmentSetting struct {
|
||||||
|
Attachments []map[string]interface{} `json:"attachments"`
|
||||||
|
Defaults map[string]interface{} `json:"defaults"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChapterPoints 章节所有任务点集合
|
||||||
|
type ChapterPoints struct {
|
||||||
|
Videos []PointVideoDto
|
||||||
|
Documents []PointDocumentDto
|
||||||
|
Works []PointWorkDto
|
||||||
|
Hyperlinks []PointHyperlinkDto
|
||||||
|
Lives []PointLiveDto
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuestionType 题目类型
|
||||||
|
type QuestionType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
QuestionTypeSingleChoice QuestionType = 0 // 单选题
|
||||||
|
QuestionTypeMultipleChoice QuestionType = 1 // 多选题
|
||||||
|
QuestionTypeFillBlank QuestionType = 2 // 填空题
|
||||||
|
QuestionTypeTrueFalse QuestionType = 3 // 判断题
|
||||||
|
QuestionTypeShortAnswer QuestionType = 4 // 简答题
|
||||||
|
)
|
||||||
|
|
||||||
|
// WorkQuestion 作业题目
|
||||||
|
type WorkQuestion struct {
|
||||||
|
Qid string `json:"qid"` // 题目ID
|
||||||
|
Type QuestionType `json:"type"` // 题目类型
|
||||||
|
Text string `json:"text"` // 题目内容
|
||||||
|
Options map[string]string `json:"options"` // 选项 (A: xxx, B: xxx)
|
||||||
|
Answers []string `json:"answers"` // 答案
|
||||||
|
BlankCount int `json:"blankCount"` // 填空题空格数量
|
||||||
|
BlankAnswer map[string]string `json:"blankAnswer"` // 填空题答案 (第1空: xxx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkDto 作业详情
|
||||||
|
type WorkDto struct {
|
||||||
|
Title string `json:"title"` // 作业标题
|
||||||
|
Cpi string `json:"cpi"` // cpi
|
||||||
|
JobId string `json:"jobId"` // 任务ID
|
||||||
|
WorkId string `json:"workId"` // 作业ID
|
||||||
|
ClassId string `json:"classId"` // 班级ID
|
||||||
|
CourseId string `json:"courseId"` // 课程ID
|
||||||
|
KnowledgeId string `json:"knowledgeId"` // 知识点ID
|
||||||
|
UserId string `json:"userId"` // 用户ID
|
||||||
|
FullScore string `json:"fullScore"` // 满分
|
||||||
|
OldWorkId string `json:"oldWorkId"` // 原始作业ID
|
||||||
|
OldSchoolId string `json:"oldSchoolId"` // 原始学校ID
|
||||||
|
Api string `json:"api"` // API值
|
||||||
|
WorkRelationId string `json:"workRelationId"` // 作业关联ID
|
||||||
|
EncWork string `json:"encWork"` // 加密参数
|
||||||
|
WorkAnswerId string `json:"workAnswerId"` // 作业答案ID
|
||||||
|
AnswerId string `json:"answerId"` // 答案ID
|
||||||
|
TotalQuestionNum string `json:"totalQuestionNum"` // 题目总数
|
||||||
|
Questions []WorkQuestion `json:"questions"` // 题目列表
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package xuexitong
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"likedream-goframe/internal/application/cookiejar"
|
||||||
|
"likedream-goframe/internal/application/httpclient"
|
||||||
|
"likedream-goframe/internal/application/xuexitong/cryptoutil"
|
||||||
|
"likedream-goframe/internal/framework/errors"
|
||||||
|
"likedream-goframe/internal/framework/logger"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ApiLoginWeb = "https://passport2.chaoxing.com/fanyalogin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type XuexitongLogin struct {
|
||||||
|
client *resty.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewXuexitongLogin(ctx context.Context) *XuexitongLogin {
|
||||||
|
return &XuexitongLogin{
|
||||||
|
client: httpclient.NewXuexitongClient(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (c *XuexitongLogin) Login(ctx context.Context, username, password string, retry int) (err error) {
|
||||||
|
jar := cookiejar.NewAllSavePersistCookieJar()
|
||||||
|
c.client.SetCookieJar(jar)
|
||||||
|
usernameEncrypted, err := cryptoutil.LoginEncrypt(username)
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Error("LoginEncrypt err:", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
passwordEncrypted, err := cryptoutil.LoginEncrypt(password)
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Error("LoginEncrypt err:", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := c.client.R().SetFormData(map[string]string{
|
||||||
|
"fid": "-1",
|
||||||
|
"uname": usernameEncrypted,
|
||||||
|
"password": passwordEncrypted,
|
||||||
|
"refer": "http%3A%2F%2Fi.mooc.chaoxing.com",
|
||||||
|
"t": "true",
|
||||||
|
"forbidotherlogin": "0",
|
||||||
|
"validate": "",
|
||||||
|
"doubleFactorLogin": "0",
|
||||||
|
"independentId": "0",
|
||||||
|
"independentNameId": "0",
|
||||||
|
}).Post(ApiLoginWeb)
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Error("LoginPost err:", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body := resp.String()
|
||||||
|
//触发风控直接重新登录
|
||||||
|
if strings.Contains(body, "很抱歉,您所浏览的页面暂时不能访问!") && retry > 0 {
|
||||||
|
time.Sleep(time.Duration(retry*5) * time.Second)
|
||||||
|
return c.Login(ctx, username, password, retry-1)
|
||||||
|
}
|
||||||
|
if !gjson.Get(body, "status").Bool() {
|
||||||
|
return errors.NewBusinessErrorWithDetail(401, "登录失败", gjson.Get(body, "msg2").String())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *XuexitongLogin) GetCookieJar() http.CookieJar {
|
||||||
|
return c.client.CookieJar()
|
||||||
|
}
|
||||||
|
func (c *XuexitongLogin) GetCookieString() string {
|
||||||
|
cookieJar := c.GetCookieJar()
|
||||||
|
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||||
|
return serializableJar.Serialize()
|
||||||
|
}
|
||||||
|
func (c *XuexitongLogin) GetClient() *resty.Client {
|
||||||
|
return c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package xuexitong
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"resty.dev/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApiVerifyCodeBytes 获取学习通验证码图片
|
||||||
|
func ApiVerifyCodeBytes(client *resty.Client) ([]byte, error) {
|
||||||
|
urlStr := "https://mooc1-api.chaoxing.com/processVerifyPng.ac?t=" + fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||||
|
|
||||||
|
req := client.R()
|
||||||
|
req.Header.Add("User-Agent", GetUA("mobile"))
|
||||||
|
req.Header.Add("Accept", "*/*")
|
||||||
|
req.Header.Add("Host", "mooc1-api.chaoxing.com")
|
||||||
|
req.Header.Add("Connection", "keep-alive")
|
||||||
|
res, err := req.Get(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
all, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return all, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApiPassVerifyCode 提交学习通验证码
|
||||||
|
func ApiPassVerifyCode(client *resty.Client, code string, retryCount int) bool {
|
||||||
|
if retryCount < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
urlStr := "https://mooc1-api.chaoxing.com/html/processVerify.ac"
|
||||||
|
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||||
|
req := client.R()
|
||||||
|
req.SetMultipartField("ucode", "", "", strings.NewReader(code))
|
||||||
|
req.SetMultipartField("app", "", "", strings.NewReader("0"))
|
||||||
|
req.Header.Add("User-Agent", GetUA("mobile"))
|
||||||
|
req.Header.Add("Accept", "*/*")
|
||||||
|
req.Header.Add("Host", "mooc1-api.chaoxing.com")
|
||||||
|
req.Header.Add("Connection", "keep-alive")
|
||||||
|
res, err := req.Post(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
return ApiPassVerifyCode(client, code, retryCount-1)
|
||||||
|
}
|
||||||
|
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||||
|
return res.StatusCode() == 302
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,170 @@
|
|||||||
|
package xuexitong
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"likedream-goframe/internal/framework/errors"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSchedule(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
xuexitongLogin := NewXuexitongLogin(ctx)
|
||||||
|
err := xuexitongLogin.Login(context.Background(), "13151832241", "yyz@083422", 5)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
client := xuexitongLogin.GetClient()
|
||||||
|
courseService := NewXuexitongCourse(client)
|
||||||
|
|
||||||
|
// 人人必修的人工智能课
|
||||||
|
err = courseService.AutoStudyByCourseID(ctx, "258109701")
|
||||||
|
if err != nil {
|
||||||
|
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "刷课失败", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXuexitongLogin(t *testing.T) {
|
||||||
|
xuexitongLogin := NewXuexitongLogin(context.Background())
|
||||||
|
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPullCourse(t *testing.T) {
|
||||||
|
xuexitongLogin := NewXuexitongLogin(context.Background())
|
||||||
|
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
client := xuexitongLogin.GetClient()
|
||||||
|
courseAction := NewXuexitongCourse(client)
|
||||||
|
list := courseAction.PullCourseList(context.Background())
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
t.Log(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPullChapter(t *testing.T) {
|
||||||
|
xuexitongLogin := NewXuexitongLogin(context.Background())
|
||||||
|
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
client := xuexitongLogin.GetClient()
|
||||||
|
courseAction := NewXuexitongCourse(client)
|
||||||
|
|
||||||
|
// 先获取课程列表
|
||||||
|
courseList := courseAction.PullCourseList(context.Background())
|
||||||
|
if len(courseList) == 0 {
|
||||||
|
t.Fatal("没有找到课程")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取第一个课程的章节
|
||||||
|
chapters := courseAction.PullCourseChapterList(context.Background(), courseList[0])
|
||||||
|
t.Logf("课程 %s 的章节数量: %d", courseList[0].CourseName, len(chapters))
|
||||||
|
|
||||||
|
for _, chapter := range chapters {
|
||||||
|
t.Logf("章节: %s, 任务点数量: %d", chapter.Name, chapter.JobCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchChapterCards(t *testing.T) {
|
||||||
|
xuexitongLogin := NewXuexitongLogin(context.Background())
|
||||||
|
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
client := xuexitongLogin.GetClient()
|
||||||
|
courseAction := NewXuexitongCourse(client)
|
||||||
|
|
||||||
|
// 获取课程列表
|
||||||
|
courseList := courseAction.PullCourseList(context.Background())
|
||||||
|
if len(courseList) == 0 {
|
||||||
|
t.Fatal("没有找到课程")
|
||||||
|
}
|
||||||
|
|
||||||
|
course := courseList[0]
|
||||||
|
|
||||||
|
// 获取章节列表
|
||||||
|
chapters := courseAction.PullCourseChapterList(context.Background(), course)
|
||||||
|
if len(chapters) == 0 {
|
||||||
|
t.Fatal("没有找到章节")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找一个有任务点的章节
|
||||||
|
var targetChapterID int
|
||||||
|
for _, ch := range chapters {
|
||||||
|
if ch.JobCount > 0 {
|
||||||
|
targetChapterID = ch.ID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetChapterID == 0 {
|
||||||
|
t.Fatal("没有找到有任务点的章节")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务点卡片
|
||||||
|
cards, points, err := courseAction.FetchChapterCards(context.Background(), course, targetChapterID)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
t.Logf("卡片数量: %d", len(cards))
|
||||||
|
if points != nil {
|
||||||
|
t.Logf("视频数量: %d, 文档数量: %d, 作业数量: %d, 外链数量: %d, 直播数量: %d",
|
||||||
|
len(points.Videos), len(points.Documents), len(points.Works),
|
||||||
|
len(points.Hyperlinks), len(points.Lives))
|
||||||
|
|
||||||
|
for _, video := range points.Videos {
|
||||||
|
t.Logf("视频任务点: JobID=%s, ObjectID=%s", video.JobID, video.ObjectID)
|
||||||
|
}
|
||||||
|
for _, doc := range points.Documents {
|
||||||
|
t.Logf("文档任务点: JobID=%s, Type=%s", doc.JobID, doc.Type)
|
||||||
|
}
|
||||||
|
for _, work := range points.Works {
|
||||||
|
t.Logf("作业任务点: JobID=%s, WorkID=%s", work.JobID, work.WorkID)
|
||||||
|
}
|
||||||
|
for _, link := range points.Hyperlinks {
|
||||||
|
t.Logf("外链任务点: JobID=%s", link.JobID)
|
||||||
|
}
|
||||||
|
for _, live := range points.Lives {
|
||||||
|
t.Logf("直播任务点: JobID=%s, LiveId=%s, Title=%s", live.JobID, live.LiveId, live.Title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAutoStudyByCourseID 测试通过课程ID刷课(定时任务使用)
|
||||||
|
func TestAutoStudyByCourseID(t *testing.T) {
|
||||||
|
// 登录
|
||||||
|
xuexitongLogin := NewXuexitongLogin(context.Background())
|
||||||
|
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
// 创建课程实例
|
||||||
|
courseAction := NewXuexitongCourse(xuexitongLogin.GetClient())
|
||||||
|
|
||||||
|
// 先获取课程列表,拿到第一个课程的ID
|
||||||
|
courseList := courseAction.PullCourseList(context.Background())
|
||||||
|
if len(courseList) == 0 {
|
||||||
|
t.Fatal("没有找到课程")
|
||||||
|
}
|
||||||
|
|
||||||
|
courseID := courseList[0].CourseID
|
||||||
|
t.Logf("准备刷课程ID: %s", courseID)
|
||||||
|
|
||||||
|
// 一键刷指定课程的所有任务(定时任务就调用这一个方法)
|
||||||
|
err = courseAction.AutoStudyByCourseID(context.Background(), courseID)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
t.Log("指定课程刷课完成")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAutoStudyAllCourses 测试自动刷所有课程
|
||||||
|
func TestAutoStudyAllCourses(t *testing.T) {
|
||||||
|
// 登录
|
||||||
|
xuexitongLogin := NewXuexitongLogin(context.Background())
|
||||||
|
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
// 创建课程实例
|
||||||
|
courseAction := NewXuexitongCourse(xuexitongLogin.GetClient())
|
||||||
|
|
||||||
|
// 一键刷所有课程
|
||||||
|
err = courseAction.AutoStudyAllCourses(context.Background())
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
|
||||||
|
t.Log("所有课程刷课完成")
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package xuexitong
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
crand "crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
APP_VERSION = "6.6.4"
|
||||||
|
DEVICE_VENDOR = "MI10"
|
||||||
|
BUILD = "10831_263"
|
||||||
|
//BUILD = "10903_285"
|
||||||
|
ANDROID_VERSION = "Android 11"
|
||||||
|
)
|
||||||
|
|
||||||
|
var IMEI = TokenHex(16)
|
||||||
|
|
||||||
|
// GetUA 构建并获取 UA
|
||||||
|
func GetUA(uaType string) string {
|
||||||
|
switch uaType {
|
||||||
|
case "mobile":
|
||||||
|
//return strings.Join([]string{
|
||||||
|
// fmt.Sprintf("Dalvik/2.1.0 (Linux; U; %s; %s Build/SKQ1.210216.001)", ANDROID_VERSION, DEVICE_VENDOR),
|
||||||
|
// fmt.Sprintf("(schild:%s)", MobileUASign(DEVICE_VENDOR, "zh_CN", APP_VERSION, BUILD, IMEI)),
|
||||||
|
// fmt.Sprintf("(device:%s)", DEVICE_VENDOR),
|
||||||
|
// "Language/zh_CN",
|
||||||
|
// fmt.Sprintf("com.chaoxing.mobile/ChaoXingStudy_3_%s_android_phone_%s", APP_VERSION, BUILD),
|
||||||
|
// //APP_VERSION,
|
||||||
|
// fmt.Sprintf("(@Kalimdor)_%s", IMEI),
|
||||||
|
//}, " ")
|
||||||
|
|
||||||
|
return strings.Join([]string{
|
||||||
|
fmt.Sprintf("Mozilla/5.0 (Linux; %s; %s Build/OPM1.171019.019; wv)", ANDROID_VERSION, DEVICE_VENDOR),
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36",
|
||||||
|
fmt.Sprintf("(schild:%s)", MobileUASign(DEVICE_VENDOR, "zh_CN", APP_VERSION, BUILD, IMEI)),
|
||||||
|
fmt.Sprintf("(device:%s)", DEVICE_VENDOR),
|
||||||
|
"Language/zh_CN",
|
||||||
|
fmt.Sprintf("com.chaoxing.mobile/ChaoXingStudy_3_%s_android_phone_%s", APP_VERSION, BUILD),
|
||||||
|
fmt.Sprintf("(@Kalimdor)_%s", IMEI),
|
||||||
|
}, " ")
|
||||||
|
case "web":
|
||||||
|
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.35"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenHex 用于生成IMEI
|
||||||
|
func TokenHex(length int) string {
|
||||||
|
b := make([]byte, length)
|
||||||
|
_, err := crand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MobileUASign(model, locale, version, build, imei string) string {
|
||||||
|
// 拼接字符串,和 Python 的 " ".join 相同
|
||||||
|
str := strings.Join([]string{
|
||||||
|
"(schild:ipL$TkeiEmfy1gTXb2XHrdLN0a@7c^vu)",
|
||||||
|
fmt.Sprintf("(device:%s)", model),
|
||||||
|
fmt.Sprintf("Language/%s", locale),
|
||||||
|
fmt.Sprintf("com.chaoxing.mobile/ChaoXingStudy_3_%s_android_phone_%s", version, build),
|
||||||
|
fmt.Sprintf("(@Kalimdor)_%s", imei),
|
||||||
|
}, " ")
|
||||||
|
|
||||||
|
// 计算 md5
|
||||||
|
hash := md5.Sum([]byte(str))
|
||||||
|
|
||||||
|
// 转换为小写 hex 字符串
|
||||||
|
return fmt.Sprintf("%x", hash)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package cst
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfigOcrServerUrl = "ocr.url"
|
||||||
|
ConfigEnvMode = "env.mode"
|
||||||
|
ConfigDatabaseType = "database.type"
|
||||||
|
ConfigDatabaseDsn = "database.dsn"
|
||||||
|
ConfigDatabaseDsn2 = "database.dsn2"
|
||||||
|
ConfigServerPort = "server.port"
|
||||||
|
ConfigEmailConfirmBaseUrl = "server.emailConfirmBaseUrl"
|
||||||
|
ConfigEmailResetPasswordUrl = "server.emailResetPasswordUrl"
|
||||||
|
ConfigBalanceMigrateUrl = "server.balanceMigrateUrl"
|
||||||
|
ConfigAuthorizationKey = "server.authorization"
|
||||||
|
ConfigJwtKey = "server.jwt"
|
||||||
|
ConfigCaptchaBaseUrl = "captcha.baseUrl"
|
||||||
|
ConfigS3AccessKey = "s3.accessKey"
|
||||||
|
ConfigS3SecretKey = "s3.secretKey"
|
||||||
|
ConfigS3BaseUrl = "s3.baseUrl"
|
||||||
|
ConfigS3Bucket = "s3.bucket"
|
||||||
|
ConfigCpdailyAppVersion = "cpdaily.appVersion"
|
||||||
|
ConfigCpdailySystemVersion = "cpdaily.systemVersion"
|
||||||
|
ConfigCpdailyCalVersion = "cpdaily.calVersion"
|
||||||
|
ConfigSliderSchoolIds = "cpdaily.sliderIds"
|
||||||
|
|
||||||
|
ConfigRedisUrl = "redis.url"
|
||||||
|
|
||||||
|
ConfigTaskConcurrency = "task.concurrency"
|
||||||
|
|
||||||
|
// Email配置常量
|
||||||
|
ConfigEmailSMTPHost = "email.smtpHost"
|
||||||
|
ConfigEmailSMTPPort = "email.smtpPort"
|
||||||
|
ConfigEmailSMTPUsername = "email.smtpUsername"
|
||||||
|
ConfigEmailSMTPPassword = "email.smtpPassword"
|
||||||
|
ConfigEmailFromEmail = "email.fromEmail"
|
||||||
|
ConfigEmailFromName = "email.fromName"
|
||||||
|
|
||||||
|
// 基础积分配置
|
||||||
|
ConfigBasePointCpdailyTask = "basePoint.cpdailyTask"
|
||||||
|
ConfigBasePointCpdailyLeave = "basePoint.cpdailyLeave"
|
||||||
|
ConfigBasePointCpdailyPhone = "basePoint.cpdailyPhone"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 应用程序常量
|
||||||
|
AppName = "若梦系统"
|
||||||
|
AppCopyright = "© 2025 若梦系统. 保留所有权利."
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TimeZoneShangHai, _ = time.LoadLocation("Asia/Shanghai")
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package cst
|
||||||
|
|
||||||
|
const (
|
||||||
|
CtxUserInfo = "ctx_user_info"
|
||||||
|
CtxPermissionList = "ctx_permission_list"
|
||||||
|
|
||||||
|
CtxCpdailyDeviceID = "cpdaily_device_id"
|
||||||
|
CtxCpdailySessionToken = "cpdaily_session_token"
|
||||||
|
CtxCpdailyTenantID = "cpdaily_tenant_id"
|
||||||
|
)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package cst
|
||||||
|
|
||||||
|
const (
|
||||||
|
RedisUserInfo = "userinfo:"
|
||||||
|
RedisVerifyCode = "verifycode:"
|
||||||
|
RedisResetPassword = "resetpassword:"
|
||||||
|
RedisBalanceMigrate = "balancemigrate:" // 积分转移token
|
||||||
|
RedisCpUserLogin = "cpuserlogin:"
|
||||||
|
RedisCpUserAuthorize = "cpuser:authorize:"
|
||||||
|
RedisSchoolInfo = "cpuser:schoolinfo:"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
RedisUserInfoExpire = int64(86400)
|
||||||
|
RedisVerifyCodeExpire = int64(86400)
|
||||||
|
RedisResetPasswordExpire = int64(3600) // 重置密码链接1小时有效
|
||||||
|
RedisBalanceMigrateExpire = int64(3600) // 积分转移链接1小时有效
|
||||||
|
RedisCpUserLoginExpire = int64(180)
|
||||||
|
RedisCpUserAuthorizeExpire = int64(1800)
|
||||||
|
RedisSchoolInfoExpire = int64(3600) // 学校信息1小时有效
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package cst
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskCpdaily = "task:cpdaily"
|
||||||
|
TaskEmail = "task:email"
|
||||||
|
TaskResetDaily = "task:reset:daily"
|
||||||
|
TaskXuexitong = "task:xuexitong"
|
||||||
|
TaskXuexitongVio = "task:xuexitong:violence"
|
||||||
|
|
||||||
|
TaskCpdailyId = "cpdaily:task:schedule:%d"
|
||||||
|
|
||||||
|
TaskQueueCpdaily = "queue:cpdaily"
|
||||||
|
TaskQueueEmail = "queue:email"
|
||||||
|
TaskQueueXuexitong = "queue:xuexitong"
|
||||||
|
TaskQueueXuexitongVio = "queue:xuexitong:violence"
|
||||||
|
TaskQueueDefault = "default"
|
||||||
|
)
|
||||||
@@ -2,17 +2,18 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/gogf/gf/v2/os/gctx"
|
"likedream-goframe/internal/common/cst"
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
"likedream-goframe/internal/cst"
|
|
||||||
"likedream-goframe/internal/ent/sysmenu"
|
"likedream-goframe/internal/ent/sysmenu"
|
||||||
"likedream-goframe/internal/ent/sysrole"
|
"likedream-goframe/internal/ent/sysrole"
|
||||||
"likedream-goframe/internal/ent/sysuser"
|
"likedream-goframe/internal/ent/sysuser"
|
||||||
"likedream-goframe/internal/utils"
|
"likedream-goframe/internal/utils"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CheckHasData() bool {
|
func CheckHasData() bool {
|
||||||
count, err := Global.SysUser.Query().Count(context.Background())
|
count, err := Global.Debug().SysUser.Query().Count(context.Background())
|
||||||
utils.OkOrPanic(err, func(err error) error {
|
utils.OkOrPanic(err, func(err error) error {
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
@@ -35,7 +36,7 @@ func GenerateData() {
|
|||||||
// 生成用户
|
// 生成用户
|
||||||
func generateUser() {
|
func generateUser() {
|
||||||
ctx := gctx.New()
|
ctx := gctx.New()
|
||||||
Global.SysUser.Create().
|
Global.Debug().SysUser.Create().
|
||||||
SetUsername("admin").
|
SetUsername("admin").
|
||||||
SetPassword(utils.PasswordBcryptHash("123456")).
|
SetPassword(utils.PasswordBcryptHash("123456")).
|
||||||
SetEmail("admin@qq.com").
|
SetEmail("admin@qq.com").
|
||||||
@@ -49,15 +50,15 @@ func generateUser() {
|
|||||||
// 生成角色
|
// 生成角色
|
||||||
func generateRoles() {
|
func generateRoles() {
|
||||||
ctx := gctx.New()
|
ctx := gctx.New()
|
||||||
Global.SysRole.Create().SetName("管理员").SetLabel(cst.KeyRoleAdmin).SetDescription("超级管理员角色").SetOrder(1).ExecX(ctx)
|
Global.Debug().SysRole.Create().SetName("管理员").SetLabel(cst.KeyRoleAdmin).SetDescription("超级管理员角色").SetOrder(1).ExecX(ctx)
|
||||||
Global.SysRole.Create().SetName("代理").SetLabel(cst.KeyRoleAgent).SetDescription("代理角色").SetOrder(2).ExecX(ctx)
|
Global.Debug().SysRole.Create().SetName("代理").SetLabel(cst.KeyRoleAgent).SetDescription("代理角色").SetOrder(2).ExecX(ctx)
|
||||||
Global.SysRole.Create().SetName("普通用户").SetLabel(cst.KeyRoleUser).SetDescription("普通用户角色").SetOrder(3).ExecX(ctx)
|
Global.Debug().SysRole.Create().SetName("普通用户").SetLabel(cst.KeyRoleUser).SetDescription("普通用户角色").SetOrder(3).ExecX(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成菜单
|
// 生成菜单
|
||||||
func generateMenus() {
|
func generateMenus() {
|
||||||
ctx := gctx.New()
|
ctx := gctx.New()
|
||||||
sysM := Global.SysMenu.Create().
|
sysM := Global.Debug().SysMenu.Create().
|
||||||
SetName("系统管理").
|
SetName("系统管理").
|
||||||
SetLabel("system").
|
SetLabel("system").
|
||||||
SetType(sysmenu.TypeGroup).
|
SetType(sysmenu.TypeGroup).
|
||||||
@@ -65,7 +66,7 @@ func generateMenus() {
|
|||||||
SetRoute("/system").
|
SetRoute("/system").
|
||||||
SetOrder(1).
|
SetOrder(1).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
userM := Global.SysMenu.Create().
|
userM := Global.Debug().SysMenu.Create().
|
||||||
SetName("用户管理").
|
SetName("用户管理").
|
||||||
SetLabel("user").
|
SetLabel("user").
|
||||||
SetType(sysmenu.TypeMenu).
|
SetType(sysmenu.TypeMenu).
|
||||||
@@ -76,7 +77,7 @@ func generateMenus() {
|
|||||||
SetParent(sysM).
|
SetParent(sysM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
// 新增 编辑 删除按钮
|
// 新增 编辑 删除按钮
|
||||||
Global.SysMenu.Create().
|
Global.Debug().SysMenu.Create().
|
||||||
SetName("新增").
|
SetName("新增").
|
||||||
SetLabel("add").
|
SetLabel("add").
|
||||||
SetType(sysmenu.TypeButton).
|
SetType(sysmenu.TypeButton).
|
||||||
@@ -84,7 +85,7 @@ func generateMenus() {
|
|||||||
SetOrder(1).
|
SetOrder(1).
|
||||||
SetParent(userM).
|
SetParent(userM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
Global.SysMenu.Create().
|
Global.Debug().SysMenu.Create().
|
||||||
SetName("编辑").
|
SetName("编辑").
|
||||||
SetLabel("edit").
|
SetLabel("edit").
|
||||||
SetType(sysmenu.TypeButton).
|
SetType(sysmenu.TypeButton).
|
||||||
@@ -93,7 +94,7 @@ func generateMenus() {
|
|||||||
SetParent(userM).
|
SetParent(userM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
// 删除按钮
|
// 删除按钮
|
||||||
Global.SysMenu.Create().
|
Global.Debug().SysMenu.Create().
|
||||||
SetName("删除").
|
SetName("删除").
|
||||||
SetLabel("delete").
|
SetLabel("delete").
|
||||||
SetType(sysmenu.TypeButton).
|
SetType(sysmenu.TypeButton).
|
||||||
@@ -102,7 +103,7 @@ func generateMenus() {
|
|||||||
SetParent(userM).
|
SetParent(userM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
// 列表
|
// 列表
|
||||||
Global.SysMenu.Create().
|
Global.Debug().SysMenu.Create().
|
||||||
SetName("列表").
|
SetName("列表").
|
||||||
SetLabel("list").
|
SetLabel("list").
|
||||||
SetType(sysmenu.TypeButton).
|
SetType(sysmenu.TypeButton).
|
||||||
@@ -110,7 +111,7 @@ func generateMenus() {
|
|||||||
SetOrder(4).
|
SetOrder(4).
|
||||||
SetParent(userM).
|
SetParent(userM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
roleM := Global.SysMenu.Create().
|
roleM := Global.Debug().SysMenu.Create().
|
||||||
SetName("角色管理").
|
SetName("角色管理").
|
||||||
SetLabel("role").
|
SetLabel("role").
|
||||||
SetType(sysmenu.TypeMenu).
|
SetType(sysmenu.TypeMenu).
|
||||||
@@ -121,7 +122,7 @@ func generateMenus() {
|
|||||||
SetParent(sysM).
|
SetParent(sysM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
// 新增 编辑 删除按钮
|
// 新增 编辑 删除按钮
|
||||||
Global.SysMenu.Create().
|
Global.Debug().SysMenu.Create().
|
||||||
SetName("新增").
|
SetName("新增").
|
||||||
SetLabel("add").
|
SetLabel("add").
|
||||||
SetType(sysmenu.TypeButton).
|
SetType(sysmenu.TypeButton).
|
||||||
@@ -129,7 +130,7 @@ func generateMenus() {
|
|||||||
SetOrder(1).
|
SetOrder(1).
|
||||||
SetParent(roleM).
|
SetParent(roleM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
Global.SysMenu.Create().
|
Global.Debug().SysMenu.Create().
|
||||||
SetName("编辑").
|
SetName("编辑").
|
||||||
SetLabel("edit").
|
SetLabel("edit").
|
||||||
SetType(sysmenu.TypeButton).
|
SetType(sysmenu.TypeButton).
|
||||||
@@ -138,7 +139,7 @@ func generateMenus() {
|
|||||||
SetParent(roleM).
|
SetParent(roleM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
// 删除按钮
|
// 删除按钮
|
||||||
Global.SysMenu.Create().
|
Global.Debug().SysMenu.Create().
|
||||||
SetName("删除").
|
SetName("删除").
|
||||||
SetLabel("delete").
|
SetLabel("delete").
|
||||||
SetType(sysmenu.TypeButton).
|
SetType(sysmenu.TypeButton).
|
||||||
@@ -146,7 +147,7 @@ func generateMenus() {
|
|||||||
SetOrder(3).
|
SetOrder(3).
|
||||||
SetParent(roleM).
|
SetParent(roleM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
Global.SysMenu.Create().
|
Global.Debug().SysMenu.Create().
|
||||||
SetName("列表").
|
SetName("列表").
|
||||||
SetLabel("list").
|
SetLabel("list").
|
||||||
SetType(sysmenu.TypeButton).
|
SetType(sysmenu.TypeButton).
|
||||||
@@ -154,7 +155,7 @@ func generateMenus() {
|
|||||||
SetOrder(4).
|
SetOrder(4).
|
||||||
SetParent(roleM).
|
SetParent(roleM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
menuM := Global.SysMenu.Create().
|
menuM := Global.Debug().SysMenu.Create().
|
||||||
SetName("菜单管理").
|
SetName("菜单管理").
|
||||||
SetLabel("menu").
|
SetLabel("menu").
|
||||||
SetType(sysmenu.TypeMenu).
|
SetType(sysmenu.TypeMenu).
|
||||||
@@ -165,7 +166,7 @@ func generateMenus() {
|
|||||||
SetParent(sysM).
|
SetParent(sysM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
// 新增 编辑 删除按钮
|
// 新增 编辑 删除按钮
|
||||||
Global.SysMenu.Create().
|
Global.Debug().SysMenu.Create().
|
||||||
SetName("新增").
|
SetName("新增").
|
||||||
SetLabel("add").
|
SetLabel("add").
|
||||||
SetType(sysmenu.TypeButton).
|
SetType(sysmenu.TypeButton).
|
||||||
@@ -173,7 +174,7 @@ func generateMenus() {
|
|||||||
SetOrder(1).
|
SetOrder(1).
|
||||||
SetParent(menuM).
|
SetParent(menuM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
Global.SysMenu.Create().
|
Global.Debug().SysMenu.Create().
|
||||||
SetName("编辑").
|
SetName("编辑").
|
||||||
SetLabel("edit").
|
SetLabel("edit").
|
||||||
SetType(sysmenu.TypeButton).
|
SetType(sysmenu.TypeButton).
|
||||||
@@ -182,7 +183,7 @@ func generateMenus() {
|
|||||||
SetParent(menuM).
|
SetParent(menuM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
// 删除按钮
|
// 删除按钮
|
||||||
Global.SysMenu.Create().
|
Global.Debug().SysMenu.Create().
|
||||||
SetName("删除").
|
SetName("删除").
|
||||||
SetLabel("delete").
|
SetLabel("delete").
|
||||||
SetType(sysmenu.TypeButton).
|
SetType(sysmenu.TypeButton).
|
||||||
@@ -190,7 +191,7 @@ func generateMenus() {
|
|||||||
SetOrder(3).
|
SetOrder(3).
|
||||||
SetParent(menuM).
|
SetParent(menuM).
|
||||||
SaveX(ctx)
|
SaveX(ctx)
|
||||||
Global.SysMenu.Create().
|
Global.Debug().SysMenu.Create().
|
||||||
SetName("列表").
|
SetName("列表").
|
||||||
SetLabel("list").
|
SetLabel("list").
|
||||||
SetType(sysmenu.TypeButton).
|
SetType(sysmenu.TypeButton).
|
||||||
@@ -203,14 +204,14 @@ func generateMenus() {
|
|||||||
// 关联角色
|
// 关联角色
|
||||||
func relationRoles() {
|
func relationRoles() {
|
||||||
ctx := gctx.New()
|
ctx := gctx.New()
|
||||||
admin := Global.SysUser.Query().Where(sysuser.UsernameEQ("admin")).OnlyX(ctx)
|
admin := Global.Debug().SysUser.Query().Where(sysuser.UsernameEQ("admin")).OnlyX(ctx)
|
||||||
role := Global.SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).AllX(ctx)
|
role := Global.Debug().SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).AllX(ctx)
|
||||||
Global.SysUser.UpdateOne(admin).AddRoles(role...).ExecX(ctx)
|
Global.Debug().SysUser.UpdateOne(admin).AddRoles(role...).ExecX(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关联菜单
|
// 关联菜单
|
||||||
func relationMenus() {
|
func relationMenus() {
|
||||||
ctx := gctx.New()
|
ctx := gctx.New()
|
||||||
adminRole := Global.SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).OnlyX(ctx)
|
adminRole := Global.Debug().SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).OnlyX(ctx)
|
||||||
Global.SysRole.UpdateOne(adminRole).AddMenus(Global.SysMenu.Query().AllX(ctx)...).ExecX(ctx)
|
Global.Debug().SysRole.UpdateOne(adminRole).AddMenus(Global.Debug().SysMenu.Query().AllX(ctx)...).ExecX(ctx)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"likedream-goframe/internal/common/cst"
|
||||||
|
"likedream-goframe/internal/ent"
|
||||||
|
"likedream-goframe/internal/framework/logger"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Global *ent.Client
|
||||||
|
|
||||||
|
var GlobalGorm *gorm.DB
|
||||||
|
|
||||||
|
func InitDB() {
|
||||||
|
dbType, err := g.Cfg().Get(gctx.New(), cst.ConfigDatabaseType)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
dbDsn, err := g.Cfg().Get(gctx.New(), cst.ConfigDatabaseDsn)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
client, err := ent.Open(dbType.String(), dbDsn.String())
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
// 自动迁移
|
||||||
|
if err := client.Schema.Create(context.Background()); err != nil {
|
||||||
|
logger.Logger.Fatal("failed creating schema resources: %v", zap.Error(err))
|
||||||
|
}
|
||||||
|
Global = client
|
||||||
|
dbDsn2, err := g.Cfg().Get(gctx.New(), cst.ConfigDatabaseDsn2)
|
||||||
|
utils.OkOrPanic(err)
|
||||||
|
GlobalGorm, err = gorm.Open(postgres.Open(dbDsn2.String()), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Fatal("连接pg数据库失败: %v", zap.Error(err))
|
||||||
|
}
|
||||||
|
// 可选:配置连接池
|
||||||
|
sqlDB, err := GlobalGorm.DB()
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Fatal("获取数据库实例失败: %v", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置连接池参数
|
||||||
|
sqlDB.SetMaxIdleConns(10) // 设置空闲连接池中连接的最大数量
|
||||||
|
sqlDB.SetMaxOpenConns(100) // 设置打开数据库连接的最大数量
|
||||||
|
sqlDB.SetConnMaxLifetime(0) // 设置连接可复用的最大时间(0表示永久)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBusinessError(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
of, b := Of[*BusinessError](err.(error))
|
||||||
|
t.Log(of, b)
|
||||||
|
t.Log("ok")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
err := NewBusinessError(503, "调试错误")
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
func Of[T error](input error) (T, bool) {
|
||||||
|
var ret T
|
||||||
|
if errors.As(input, &ret) {
|
||||||
|
return ret, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, false
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ func (a AgentCtrlV1) List(ctx context.Context, req *v1.AgentListReq) (res *utils
|
|||||||
// Add 添加下级代理
|
// Add 添加下级代理
|
||||||
func (a AgentCtrlV1) Add(ctx context.Context, req *v1.AgentAddReq) (res *utils.Response[any], err error) {
|
func (a AgentCtrlV1) Add(ctx context.Context, req *v1.AgentAddReq) (res *utils.Response[any], err error) {
|
||||||
defer utils.Pass(&err)
|
defer utils.Pass(&err)
|
||||||
return service.Global.Agent.Add(ctx, req), nil
|
panic("implement me")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ban 禁用代理
|
// Ban 禁用代理
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package balance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
v1 "likedream-goframe/internal/api/v1"
|
||||||
|
"likedream-goframe/internal/ent"
|
||||||
|
"likedream-goframe/internal/service"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BalanceCtrlV1 struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBalanceCtrlV1() IBalanceCtrlV1 {
|
||||||
|
return &BalanceCtrlV1{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b BalanceCtrlV1) List(ctx context.Context, req *v1.BalanceRecordListReq) (res *utils.Response[[]*ent.SysBalanceRecord], err error) {
|
||||||
|
defer utils.Pass(&err)
|
||||||
|
return service.Global.Balance.List(ctx, req), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package balance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
v1 "likedream-goframe/internal/api/v1"
|
||||||
|
"likedream-goframe/internal/ent"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IBalanceCtrlV1 interface {
|
||||||
|
List(ctx context.Context, req *v1.BalanceRecordListReq) (res *utils.Response[[]*ent.SysBalanceRecord], err error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
v1 "likedream-goframe/internal/api/v1"
|
||||||
|
"likedream-goframe/internal/ent"
|
||||||
|
"likedream-goframe/internal/service"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigCtrlV1 struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ConfigCtrlV1) Get(ctx context.Context, req *v1.ConfigGetReq) (res *utils.Response[*ent.SysConfig], err error) {
|
||||||
|
defer utils.Pass(&err)
|
||||||
|
return service.Global.Config.Get(ctx, req), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ConfigCtrlV1) PublicGet(ctx context.Context, req *v1.ConfigPublicGetReq) (res *utils.Response[*ent.SysConfig], err error) {
|
||||||
|
defer utils.Pass(&err)
|
||||||
|
return service.Global.Config.PublicGet(ctx, req), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ConfigCtrlV1) List(ctx context.Context, req *v1.ConfigListReq) (res *utils.Response[[]*ent.SysConfig], err error) {
|
||||||
|
defer utils.Pass(&err)
|
||||||
|
return service.Global.Config.List(ctx, req), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ConfigCtrlV1) Update(ctx context.Context, req *v1.ConfigUpdateReq) (res *utils.Response[any], err error) {
|
||||||
|
defer utils.Pass(&err)
|
||||||
|
return service.Global.Config.Update(ctx, req), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigCtrlV1() IConfigCtrlV1 {
|
||||||
|
return &ConfigCtrlV1{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
v1 "likedream-goframe/internal/api/v1"
|
||||||
|
"likedream-goframe/internal/ent"
|
||||||
|
"likedream-goframe/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IConfigCtrlV1 interface {
|
||||||
|
Get(ctx context.Context, req *v1.ConfigGetReq) (*utils.Response[*ent.SysConfig], error)
|
||||||
|
PublicGet(ctx context.Context, req *v1.ConfigPublicGetReq) (*utils.Response[*ent.SysConfig], error)
|
||||||
|
List(ctx context.Context, req *v1.ConfigListReq) (*utils.Response[[]*ent.SysConfig], error)
|
||||||
|
Update(ctx context.Context, req *v1.ConfigUpdateReq) (*utils.Response[any], error)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user