Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f64b4b5498 | |||
| 84620989ad | |||
| af7fcc4cb4 | |||
| c206a0863e | |||
| b068e178da | |||
| 02737ef743 | |||
| 74a1134cf1 | |||
| b6f777f997 | |||
| 2dc233631e | |||
| 0c60eb5ae4 | |||
| 450c026a6f | |||
| d9d6acee5f | |||
| 164d8956de | |||
| 3f1a08374e | |||
| 334d597f9a |
@@ -2,4 +2,4 @@ ent-new:
|
||||
cd internal;go run -mod=mod entgo.io/ent/cmd/ent new $(Name)
|
||||
|
||||
ent-gen:
|
||||
go run . g
|
||||
go run main.go g
|
||||
+4
-4
@@ -29,10 +29,10 @@ url = "redis://:jhkdjhkjdhsIUTYURTU_pEGmFP@database.out:56379"
|
||||
smtpHost = "smtp-mail.outlook.com"
|
||||
smtpPort = 587
|
||||
# 发件人邮箱和密码(需要使用授权码,不是登录密码)
|
||||
smtpUsername = "demo@likedream.xyz"
|
||||
smtpUsername = "reply@likedream.xyz"
|
||||
smtpPassword = "baca122..."
|
||||
# 发件人邮箱和显示名称
|
||||
fromEmail = "demo@likedream.xyz"
|
||||
fromEmail = "reply@likedream.xyz"
|
||||
fromName = "若梦系统"
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ baseUrl = "http://s3.out"
|
||||
bucket = "likedream"
|
||||
|
||||
[cpdaily]
|
||||
appVersion = "9.7.2"
|
||||
appVersion = "9.8.8"
|
||||
systemVersion = "iOS 17"
|
||||
calVersion = "firstv"
|
||||
sliderIds = ["1018764144934965", "sdhyxy", "hqdx"]
|
||||
@@ -55,4 +55,4 @@ cpdailyTask = "0.5"
|
||||
cpdailyLeave = "4"
|
||||
cpdailyPhone = "0.25"
|
||||
[task]
|
||||
concurrency = 20
|
||||
concurrency = 80
|
||||
@@ -1,403 +0,0 @@
|
||||
# 刷课接口文档
|
||||
|
||||
## 接口列表
|
||||
|
||||
### 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 | 服务器错误 |
|
||||
@@ -0,0 +1,204 @@
|
||||
# 四川智慧教育刷课 API 文档
|
||||
|
||||
## 基础信息
|
||||
|
||||
- 基础路径: `/v1/course`
|
||||
- 需要认证: 是 (Bearer Token)
|
||||
- Content-Type: `application/json`
|
||||
|
||||
---
|
||||
|
||||
## 1. 查询课程列表
|
||||
|
||||
登录四川智慧教育平台,获取用户的课程列表。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
POST /v1/course/smartedu-sc/query
|
||||
```
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| username | string | 是 | 登录账号 |
|
||||
| password | string | 是 | 登录密码 |
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "13800138000",
|
||||
"password": "your_password"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": "course_id_1",
|
||||
"courseName": "课程名称1",
|
||||
"image": "https://xxx.com/image.jpg"
|
||||
},
|
||||
{
|
||||
"id": "course_id_2",
|
||||
"courseName": "课程名称2",
|
||||
"image": "https://xxx.com/image2.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 添加刷课任务
|
||||
|
||||
添加一个新的刷课任务,提交后立即开始秒刷。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
POST /v1/course/smartedu-sc
|
||||
```
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| username | string | 是 | 登录账号 |
|
||||
| password | string | 是 | 登录密码 |
|
||||
| course_name | string | 是 | 课程名称 |
|
||||
| course_id | string | 是 | 课程ID(从查询接口获取) |
|
||||
| desc | string | 否 | 备注说明 |
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "13800138000",
|
||||
"password": "your_password",
|
||||
"course_name": "教师培训课程",
|
||||
"course_id": "abc123",
|
||||
"desc": "2024年培训"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "任务已提交,正在刷课中"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 获取刷课任务列表
|
||||
|
||||
获取当前用户的四川智慧教育刷课任务列表。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
GET /v1/course/smartedu-sc
|
||||
```
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| username | string | 否 | 按账号筛选 |
|
||||
| status | string | 否 | 按状态筛选: not_start/waiting/doing/done/fail/pause |
|
||||
| page | int | 否 | 页码,默认1 |
|
||||
| size | int | 否 | 每页数量,默认10 |
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"createTime": "2024-12-09 10:00:00",
|
||||
"updateTime": "2024-12-09 10:05:00",
|
||||
"username": "13800138000",
|
||||
"type": "smartedu_sc",
|
||||
"course_name": "教师培训课程",
|
||||
"course_id": "abc123",
|
||||
"need": "task",
|
||||
"status": "done",
|
||||
"task_count": 20,
|
||||
"success_count": 20,
|
||||
"violence": "yes",
|
||||
"desc": "2024年培训"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
**状态说明**
|
||||
|
||||
| 状态值 | 说明 |
|
||||
|--------|------|
|
||||
| not_start | 未开始 |
|
||||
| waiting | 等待中 |
|
||||
| doing | 进行中 |
|
||||
| done | 已完成 |
|
||||
| fail | 失败 |
|
||||
| pause | 已暂停 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 删除刷课任务
|
||||
|
||||
删除指定的刷课任务。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
DELETE /v1/course/smartedu-sc/{id}
|
||||
```
|
||||
|
||||
**路径参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | int64 | 是 | 任务ID |
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "删除成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 成功 |
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | 未授权 |
|
||||
| 403 | 无权限 |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. 调用 **查询课程列表** 接口,获取用户在四川智慧教育平台的课程
|
||||
2. 选择需要刷的课程,调用 **添加刷课任务** 接口
|
||||
3. 任务提交后自动开始秒刷,可通过 **获取刷课任务列表** 查看进度
|
||||
4. 刷课完成后状态变为 `done`
|
||||
@@ -66,6 +66,7 @@ func StartServer(ctx context.Context) {
|
||||
// 网课
|
||||
courseGroup := group.Group("/course").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
courseGroup.Bind(course.NewXxtCtrlV1())
|
||||
courseGroup.Bind(course.NewSmartEduSCCtrlV1())
|
||||
})
|
||||
|
||||
s.SetPort(port.Int())
|
||||
|
||||
@@ -24,6 +24,6 @@ type ConfigListReq struct {
|
||||
}
|
||||
|
||||
type ConfigPublicGetReq struct {
|
||||
g.Meta `path:"/config/public/{key}" method:"get" tags:"Config" summary:"获取公开配置(无需认证)"`
|
||||
g.Meta `path:"/config/public/{key}" method:"get" tags:"Config" summary:"获取公开配置(无需认证)" auth:"false"`
|
||||
Key string `json:"key" v:"required#配置键不能为空"`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package v1
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
type CpLeaveListReq struct {
|
||||
@@ -19,8 +20,18 @@ type GetLeaveListFromSystemReq struct {
|
||||
}
|
||||
|
||||
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#请假记录标题不能为空"`
|
||||
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#请假记录标题不能为空"`
|
||||
Longitude string `json:"longitude"` // 经度(可选,不传则使用系统默认)
|
||||
Latitude string `json:"latitude"` // 纬度(可选,不传则使用系统默认)
|
||||
Address string `json:"address"` // 地址(可选,不传则使用系统默认)
|
||||
ImageUrl string `json:"imageUrl"`
|
||||
}
|
||||
|
||||
type CpLeaveUploadReq struct {
|
||||
g.Meta `path:"/leave/upload" method:"post" tags:"CpLeave" summary:"上传销假图片"`
|
||||
File *ghttp.UploadFile `json:"file"`
|
||||
CpUserId int64 `json:"cpUserId"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// PriceConfigGetReq 获取所有费率配置
|
||||
type PriceConfigGetReq struct {
|
||||
g.Meta `path:"/config/price" method:"get" tags:"PriceConfig" summary:"获取所有费率配置"`
|
||||
}
|
||||
|
||||
// PriceConfigUpdateReq 更新费率配置
|
||||
type PriceConfigUpdateReq struct {
|
||||
g.Meta `path:"/config/price" method:"put" tags:"PriceConfig" summary:"更新费率配置" permission:"sys:config:edit"`
|
||||
CpdailyTask string `json:"cpdaily_task"` // 今日校园签到单价
|
||||
CpdailyLeave string `json:"cpdaily_leave"` // 今日校园请假单价
|
||||
CpdailyPhone string `json:"cpdaily_phone"` // 今日校园手机号单价
|
||||
Xxt string `json:"xxt"` // 学习通单价
|
||||
SmarteduSc string `json:"smartedu_sc"` // 四川智慧教育单价
|
||||
}
|
||||
|
||||
// PriceConfigResp 费率配置响应
|
||||
type PriceConfigResp struct {
|
||||
CpdailyTask string `json:"cpdaily_task"`
|
||||
CpdailyLeave string `json:"cpdaily_leave"`
|
||||
CpdailyPhone string `json:"cpdaily_phone"`
|
||||
Xxt string `json:"xxt"`
|
||||
SmarteduSc string `json:"smartedu_sc"`
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// SmartEduSCQueryCourseReq 查询四川智慧教育课程列表
|
||||
type SmartEduSCQueryCourseReq struct {
|
||||
g.Meta `path:"/smartedu-sc/query" method:"post" tags:"SmartEduSC" summary:"查询四川智慧教育课程列表"`
|
||||
Username string `json:"username" v:"required#账号不能为空"`
|
||||
Password string `json:"password" v:"required#密码不能为空"`
|
||||
}
|
||||
|
||||
// SmartEduSCAddCourseReq 添加四川智慧教育刷课任务
|
||||
type SmartEduSCAddCourseReq struct {
|
||||
g.Meta `path:"/smartedu-sc" method:"post" tags:"SmartEduSC" summary:"添加四川智慧教育刷课任务"`
|
||||
Username string `json:"username" v:"required#账号不能为空"`
|
||||
Password string `json:"password" v:"required#密码不能为空"`
|
||||
CourseName string `json:"course_name" v:"required#课程名称不能为空"`
|
||||
CourseID string `json:"course_id" v:"required#课程ID不能为空"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
// SmartEduSCListReq 获取四川智慧教育刷课列表
|
||||
type SmartEduSCListReq struct {
|
||||
g.Meta `path:"/smartedu-sc" method:"get" tags:"SmartEduSC" summary:"获取四川智慧教育刷课列表"`
|
||||
Username string `json:"username"`
|
||||
Status string `json:"status"`
|
||||
Page int `json:"page" d:"1"`
|
||||
Size int `json:"size" d:"10"`
|
||||
}
|
||||
|
||||
// SmartEduSCDeleteReq 删除四川智慧教育刷课任务
|
||||
type SmartEduSCDeleteReq struct {
|
||||
g.Meta `path:"/smartedu-sc/{id}" method:"delete" tags:"SmartEduSC" summary:"删除四川智慧教育刷课任务"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
)
|
||||
|
||||
func TestAesDecrypt(t *testing.T) {
|
||||
str := "6XkC1UAk07fK0uTaGPUu77i/+r7j/o1JQ/XygRxee2LMiX5H+w/BOqOB6e7W o9kHDZMb1pqZNN43k1tjl0ephbPLEi8g9C1EqBw0suzxX74j3dUdCf6dd/0W 8KvntNXwju96Vy5B2oD2ZosLmUdB1m0xX5rxAdLJ//cTft87XDWoLM0fuasu fGvR4XXXYZN071x8F03+qqR52fD1UQGgn0uGkSa/nrUho32Sg7DNJzF/JBCk 6/z0lu0XmIPo1SDIVjOtYAmyUDMZGl/ZvSAu2w=="
|
||||
str := "gTIjjNBTwN0Vdv2o5ZlTZ6gHbFcIA0MV0caRqvv/50tptfoSO+VASLxpQZWy Xa86lY21QtmXhwlvTsfd3XR819O0IUi2FynPOM6s0pDsSndM2JhhqoRNXBTK 4WqfMgt0RCQJ38KP8XtM5O+Ktkj1YHHM/OVEGqOxRcUh2M++JJCheJbSs+aQ 2T3va13/GWJH7PEd8JVEJgBiO3wXLOb9+CvvWuU/Frc9nNNxQL7LrgZTdQAv C9Yt+5/a2n41reOeOs7Bu5C4FFlHQSs71Rom/HtIhL67x1ecIre1rbgK9xxz 1AhTEHY6nhdZmktPGJD5CfxvLVWIeLv09CjKqdSaNodkP4e9YTScNEZ2s8TK y8Xhv8L0r6gllC9B3nla4GRe+lqMvhDPDZx7PFu7kxFspf9yoM4eOpgWfCr9 xbDwxO9cXphiktfWsQiX28W4g2jCFFEazh01WBtpjM9LsLfbBgZA+z3jgfYa AXZuOvFQMJuUnLyOIGPtsaSTwXFDlJwNRgEw+Ctl22JSJbbu1yOoM9S6Ge27 rUVMhbAS5snVbNbLhgAymQLQQVLRwfsMLbtUfLTZuCC8zO8d16eOYw56+DqR QWNG1EXQz3CLeFDs2hFqcNWrAt773qANQ3zLPA65"
|
||||
str = strings.ReplaceAll(str, " ", "")
|
||||
decodeString, _ := base64.StdEncoding.DecodeString(str)
|
||||
decrypt := DesDecrypt(decodeString, Cat)
|
||||
decrypt := AesDecryptForCat(decodeString)
|
||||
t.Log(string(decrypt))
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/google/uuid"
|
||||
"github.com/samber/oops"
|
||||
"resty.dev/v3"
|
||||
@@ -80,11 +81,54 @@ func UploadImageFromStorage(ctx context.Context, host string, imageId uuid.UUID,
|
||||
return previewUrl
|
||||
}
|
||||
|
||||
func UploadImageFromFile(ctx context.Context, host string, file *ghttp.UploadFile, taskType types.TaskType, client *resty.Client) string {
|
||||
|
||||
imageUploadPolicy := GetUploadPolicyAsync(ctx, host, taskType, client)
|
||||
open, _ := file.Open()
|
||||
resp, e := client.R().
|
||||
SetMultipartFields(&resty.MultipartField{
|
||||
Name: "x-obs-acl",
|
||||
Values: []string{"public-read"},
|
||||
}, &resty.MultipartField{
|
||||
Name: "AccessKeyId",
|
||||
Values: []string{imageUploadPolicy.AccessId},
|
||||
}, &resty.MultipartField{
|
||||
Name: "key",
|
||||
Values: []string{imageUploadPolicy.FileName},
|
||||
}, &resty.MultipartField{
|
||||
Name: "signature",
|
||||
Values: []string{imageUploadPolicy.Signature},
|
||||
}, &resty.MultipartField{
|
||||
Name: "policy",
|
||||
Values: []string{imageUploadPolicy.Policy},
|
||||
}, &resty.MultipartField{
|
||||
Name: "content-type",
|
||||
Values: []string{file.Header.Get("Content-Type")},
|
||||
}, &resty.MultipartField{
|
||||
Name: "file",
|
||||
FileName: "file",
|
||||
Reader: open,
|
||||
}).
|
||||
Post(imageUploadPolicy.Host)
|
||||
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
if resp.StatusCode() > 299 {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "上传图片失败: "+utils.ReadString(resp.Body)), oops.WithContext(ctx).With("response", utils.ReadString(resp.Body)).Wrap)
|
||||
}
|
||||
|
||||
previewUrl := GetPreviewUrlAsync(ctx, host, imageUploadPolicy.FileName, taskType, client)
|
||||
|
||||
return previewUrl
|
||||
}
|
||||
|
||||
// GetPreviewUrlAsync 用于获取图片预览地址
|
||||
func GetPreviewUrlAsync(ctx context.Context, host string, fileKey string, taskType types.TaskType, client *resty.Client) string {
|
||||
var apiPath string
|
||||
if taskType == types.TaskTypeSleepCheck {
|
||||
apiPath = "/previewAttachment"
|
||||
} else if taskType == types.TaskTypeLeave {
|
||||
apiPath = "/obs/previewAttachment"
|
||||
} else {
|
||||
apiPath = "/sign/previewAttachment"
|
||||
}
|
||||
@@ -164,6 +208,8 @@ func GetImageUploadUrlPrefix(ctx context.Context, taskType types.TaskType) strin
|
||||
return "/wec-counselor-sign-apps/stu"
|
||||
case types.TaskTypeCollection:
|
||||
return "/wec-counselor-collector-apps/student/collector"
|
||||
case types.TaskTypeLeave:
|
||||
return "/wec-counselor-leave-apps/leave"
|
||||
default:
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "未知的任务类型"), oops.WithContext(ctx).Wrap)
|
||||
panic("unreachable")
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type Attach struct {
|
||||
Type int `json:"type"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
@@ -3,14 +3,19 @@ package leave
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/cpdaily"
|
||||
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||
"likedream-goframe/internal/application/cpdaily/imageuploader"
|
||||
"likedream-goframe/internal/application/cpdaily/leave/model"
|
||||
"likedream-goframe/internal/application/cpdaily/task/leavetype"
|
||||
"likedream-goframe/internal/application/cpdaily/taskhelper"
|
||||
"likedream-goframe/internal/application/cpdaily/types"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/google/uuid"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -66,14 +71,25 @@ func (c *CpdailyReportApp) GetLeaveList() []leavetype.LeaveListOne {
|
||||
if row.Get("status").String() == "4" {
|
||||
continue
|
||||
}
|
||||
id := row.Get("id").String()
|
||||
detail := c.LeaveDetail(id)
|
||||
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(),
|
||||
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(),
|
||||
ChooseAttendance: detail.Get("leaveType.chooseAttendance").Int(),
|
||||
//Longitude: detail.Get("detail.applyLocation.longitude").String(),
|
||||
//Latitude: detail.Get("detail.applyLocation.latitude").String(),
|
||||
//Address: detail.Get("detail.applyLocation.address").String(),
|
||||
|
||||
Longitude: detail.Get("leaveExtends.0.longitude").String(),
|
||||
Latitude: detail.Get("leaveExtends.0.latitude").String(),
|
||||
Address: detail.Get("leaveExtends.0.address").String(),
|
||||
}
|
||||
|
||||
listList = append(listList, leaveOne)
|
||||
}
|
||||
return listList
|
||||
@@ -104,16 +120,33 @@ func (c *CpdailyReportApp) LeaveDetail(leaveId string) gjson.Result {
|
||||
return datas
|
||||
}
|
||||
|
||||
func (c *CpdailyReportApp) FillReportForm(detail gjson.Result) map[string]*gvar.Var {
|
||||
func (c *CpdailyReportApp) UploadImage(ctx context.Context, file *ghttp.UploadFile) string {
|
||||
return imageuploader.UploadImageFromFile(ctx, c.host, file, types.TaskTypeLeave, c.client)
|
||||
}
|
||||
|
||||
func (c *CpdailyReportApp) FillReportForm(detail gjson.Result, req *v1.CpLeaveReportReq) 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["leaveType"] = gvar.New(detail.Get("leaveType.code").String())
|
||||
if req.Address == "" {
|
||||
resultForm["reportLocation"] = gvar.New(detail.Get("leaveExtends.0.address").String())
|
||||
} else {
|
||||
resultForm["reportLocation"] = gvar.New(req.Address)
|
||||
}
|
||||
resultForm["approverWid"] = gvar.New("")
|
||||
resultForm["reportApplyExplain"] = gvar.New(nil)
|
||||
resultForm["reportAttach"] = gvar.New(detail.Get("detail.applyAttach").Array())
|
||||
resultForm["uaIsCpadaily"] = gvar.New(true)
|
||||
if req.ImageUrl != "" {
|
||||
var attach []model.Attach
|
||||
attach = append(attach, model.Attach{
|
||||
Type: 1,
|
||||
Url: req.ImageUrl,
|
||||
})
|
||||
resultForm["reportAttach"] = gvar.New(attach)
|
||||
} else {
|
||||
resultForm["reportAttach"] = gvar.New([]string{})
|
||||
}
|
||||
return resultForm
|
||||
}
|
||||
|
||||
@@ -141,11 +174,11 @@ func (c *CpdailyReportApp) SubmitReport(ctx context.Context, form map[string]*gv
|
||||
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),
|
||||
"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),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,4 +7,9 @@ type LeaveListOne struct {
|
||||
AceEndTime string `json:"aceEndTime"`
|
||||
Reason string `json:"reason"`
|
||||
Status string `json:"status"`
|
||||
// 销假时填充的位置信息
|
||||
Longitude string `json:"longitude"` // 经度
|
||||
Latitude string `json:"latitude"` // 纬度
|
||||
Address string `json:"address"` // 地址
|
||||
ChooseAttendance int64 `json:"chooseAttendance"` // 是否需要图片 1需要 0 不需要
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ const (
|
||||
TaskTypeCollection = TaskType(iota)
|
||||
TaskTypeSign
|
||||
TaskTypeSleepCheck
|
||||
TaskTypeLeave
|
||||
)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/utils"
|
||||
"time"
|
||||
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
var SmartEdu = resty.New().
|
||||
SetCookieJar(cookiejar.NewAllSavePersistCookieJar()).
|
||||
SetRedirectPolicy(resty.FlexibleRedirectPolicy(20)).
|
||||
SetTimeout(30 * time.Second).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
|
||||
func init() {
|
||||
if !utils.IsDebug() {
|
||||
Xuexitong.SetDebug(false)
|
||||
} else {
|
||||
Xuexitong.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
}
|
||||
|
||||
// NewSmartEdu 创建一个全新的独立 client 实例,避免 Clone 可能的浅拷贝问题
|
||||
// 每个实例都是完全独立的,适用于并发场景,不会共享状态
|
||||
func NewSmartEdu() *resty.Client {
|
||||
client := resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30*time.Second).
|
||||
SetDoNotParseResponse(false).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}).
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
|
||||
|
||||
// 根据调试模式设置代理和调试选项
|
||||
if !utils.IsDebug() {
|
||||
client.SetDebug(false)
|
||||
} else {
|
||||
client.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
jar := cookiejar.NewAllSavePersistCookieJar()
|
||||
client.SetCookieJar(jar)
|
||||
return client
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
const (
|
||||
scAESKey = "m1xp9slgfa|su@m2"
|
||||
scAESIV = "pa!st17m@04h5u#0"
|
||||
)
|
||||
|
||||
// EncryptSC AES-CBC加密,使用固定的key和iv
|
||||
func EncryptSC(plaintext string) string {
|
||||
key := []byte(scAESKey)
|
||||
iv := []byte(scAESIV)
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// PKCS7填充
|
||||
blockSize := block.BlockSize()
|
||||
padding := blockSize - len(plaintext)%blockSize
|
||||
padText := make([]byte, padding)
|
||||
for i := range padText {
|
||||
padText[i] = byte(padding)
|
||||
}
|
||||
plaintextBytes := append([]byte(plaintext), padText...)
|
||||
|
||||
// CBC加密
|
||||
ciphertext := make([]byte, len(plaintextBytes))
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(ciphertext, plaintextBytes)
|
||||
|
||||
// Base64编码
|
||||
return base64.StdEncoding.EncodeToString(ciphertext)
|
||||
}
|
||||
|
||||
// DecryptSC AES-CBC解密,使用固定的key和iv
|
||||
func DecryptSC(ciphertext string) string {
|
||||
key := []byte(scAESKey)
|
||||
iv := []byte(scAESIV)
|
||||
|
||||
// Base64解码
|
||||
ciphertextBytes, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// CBC解密
|
||||
blockSize := block.BlockSize()
|
||||
if len(ciphertextBytes)%blockSize != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
plaintext := make([]byte, len(ciphertextBytes))
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(plaintext, ciphertextBytes)
|
||||
|
||||
// 去除PKCS7填充
|
||||
if len(plaintext) == 0 {
|
||||
return ""
|
||||
}
|
||||
padding := int(plaintext[len(plaintext)-1])
|
||||
if padding <= 0 || padding > blockSize || padding > len(plaintext) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(plaintext[:len(plaintext)-padding])
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EncData 实现与JavaScript版本相同的加密功能
|
||||
// clientToken参数用于替代JavaScript中的cookie值
|
||||
func EncData(data interface{}, clientToken string) map[string]string {
|
||||
// 检查数据是否为空
|
||||
if data == nil || data == "" || data == "{}" || data == " " || data == "null" {
|
||||
return map[string]string{
|
||||
"ciphertext": "",
|
||||
}
|
||||
}
|
||||
|
||||
// 获取axes_max
|
||||
axesMax := len(clientToken)
|
||||
if axesMax < 4 {
|
||||
axesMax = 4
|
||||
}
|
||||
|
||||
// 创建axes
|
||||
axes := creatAxes(axesMax)
|
||||
|
||||
// 替换逗号为连字符
|
||||
aesaxes := strings.ReplaceAll(axes, ",", "-")
|
||||
|
||||
// 生成AES密钥
|
||||
aesKey := encryptKey(axes, clientToken)
|
||||
|
||||
// 将数据转换为JSON字符串
|
||||
var dataStr string
|
||||
switch v := data.(type) {
|
||||
case string:
|
||||
dataStr = v
|
||||
default:
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return map[string]string{
|
||||
"ciphertext": "",
|
||||
}
|
||||
}
|
||||
dataStr = string(jsonBytes)
|
||||
}
|
||||
|
||||
// 加密数据
|
||||
encdata := Encrypt(dataStr, aesKey)
|
||||
|
||||
// 构造密文
|
||||
ciphertext := encdata + "," + aesaxes
|
||||
|
||||
return map[string]string{
|
||||
"ciphertext": ciphertext,
|
||||
}
|
||||
}
|
||||
|
||||
// DescData 实现与JavaScript版本相同的解密功能
|
||||
// clientToken参数用于替代JavaScript中的cookie值
|
||||
func DescData(JR map[string]interface{}, clientToken string) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"flag": false,
|
||||
"data": "",
|
||||
}
|
||||
|
||||
if JR == nil {
|
||||
return result
|
||||
}
|
||||
|
||||
// 获取flag
|
||||
flag, ok := JR["flag"].(bool)
|
||||
if !ok {
|
||||
flag = false
|
||||
}
|
||||
|
||||
result["flag"] = flag
|
||||
|
||||
// 如果flag为false,直接返回data
|
||||
if !flag {
|
||||
if data, ok := JR["data"]; ok {
|
||||
result["data"] = data
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 检查key是否存在
|
||||
key, keyExists := JR["key"].(string)
|
||||
if !keyExists || key == "" {
|
||||
if data, ok := JR["data"]; ok {
|
||||
result["data"] = data
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 处理解密
|
||||
axes := key
|
||||
aesaxes := strings.ReplaceAll(axes, "-", ",")
|
||||
aesKey := encryptKey(aesaxes, clientToken)
|
||||
|
||||
// 获取加密数据
|
||||
outdata, ok := JR["data"].(string)
|
||||
if !ok {
|
||||
result["data"] = ""
|
||||
return result
|
||||
}
|
||||
|
||||
// URL解码(与JS版本的decodeURIComponent一致)
|
||||
decodedData, err := url.QueryUnescape(outdata)
|
||||
if err != nil {
|
||||
// 如果URL解码失败,使用原始数据
|
||||
decodedData = outdata
|
||||
}
|
||||
|
||||
// 解密数据
|
||||
decryptedData := Decrypt(decodedData, aesKey)
|
||||
result["data"] = decryptedData
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// encryptKey 根据axes生成加密密钥
|
||||
func encryptKey(axes string, clientToken string) string {
|
||||
aaxes := strings.Split(axes, ",")
|
||||
key := ""
|
||||
|
||||
for _, axis := range aaxes {
|
||||
axisInt := parseInt(axis)
|
||||
// 确保axisInt不会导致越界
|
||||
if axisInt >= 0 && axisInt < len(clientToken) {
|
||||
// 与JavaScript版本一致:substring(aaxes[i], aaxes[i]*1+4)
|
||||
end := axisInt + 4
|
||||
if end <= len(clientToken) {
|
||||
key += clientToken[axisInt:end]
|
||||
} else {
|
||||
// 如果end超出范围,只取到末尾
|
||||
key += clientToken[axisInt:]
|
||||
// 填充到4个字符
|
||||
for len(key)%4 != 0 {
|
||||
key += "A"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果起始位置越界,使用默认值
|
||||
key += "abcd"
|
||||
}
|
||||
}
|
||||
|
||||
// 确保密钥长度至少为16字节,如果超过则截断
|
||||
if len(key) < 16 {
|
||||
// 用'A'填充到16字节
|
||||
for len(key) < 16 {
|
||||
key += "A"
|
||||
}
|
||||
} else if len(key) > 16 {
|
||||
// 截断到16字节
|
||||
key = key[:16]
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
// creatAxes 创建随机axes
|
||||
func creatAxes(max int) string {
|
||||
if max < 4 {
|
||||
max = 4
|
||||
}
|
||||
|
||||
axes := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
// 生成0到max-4之间的随机数
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(int64(max-4)))
|
||||
if err != nil {
|
||||
// 出错时使用默认值
|
||||
axes += "0,"
|
||||
continue
|
||||
}
|
||||
axes += fmt.Sprintf("%d,", n.Int64())
|
||||
}
|
||||
|
||||
// 移除末尾的逗号
|
||||
if len(axes) > 0 {
|
||||
axes = axes[:len(axes)-1]
|
||||
}
|
||||
|
||||
return axes
|
||||
}
|
||||
|
||||
// parseInt 简单的字符串转整数函数
|
||||
func parseInt(s string) int {
|
||||
var result int
|
||||
fmt.Sscanf(s, "%d", &result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Encrypt AES加密函数
|
||||
func Encrypt(data, key string) string {
|
||||
// 确保密钥长度为16字节
|
||||
keyBytes := make([]byte, 16)
|
||||
if len(key) >= 16 {
|
||||
copy(keyBytes, []byte(key)[:16])
|
||||
} else {
|
||||
copy(keyBytes, []byte(key))
|
||||
// 如果密钥不足16字节,用'A'填充
|
||||
for i := len(key); i < 16; i++ {
|
||||
keyBytes[i] = 'A'
|
||||
}
|
||||
}
|
||||
|
||||
// 创建AES密码块
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// PKCS7填充
|
||||
blockSize := block.BlockSize()
|
||||
padding := blockSize - len(data)%blockSize
|
||||
padText := make([]byte, padding)
|
||||
for i := range padText {
|
||||
padText[i] = byte(padding)
|
||||
}
|
||||
|
||||
plaintext := append([]byte(data), padText...)
|
||||
|
||||
// CBC模式加密
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
// 使用固定的IV(与JavaScript版本一致,使用相同的key作为IV)
|
||||
iv := make([]byte, blockSize)
|
||||
copy(iv, keyBytes[:blockSize])
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(ciphertext, plaintext)
|
||||
|
||||
// Base64编码(使用URL编码兼容CryptoJS)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext)
|
||||
}
|
||||
|
||||
// Decrypt AES解密函数
|
||||
func Decrypt(data, key string) string {
|
||||
// Base64解码
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
// 尝试URL解码
|
||||
ciphertext, err = base64.URLEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// 确保密钥长度为16字节
|
||||
keyBytes := make([]byte, 16)
|
||||
if len(key) >= 16 {
|
||||
copy(keyBytes, []byte(key)[:16])
|
||||
} else {
|
||||
copy(keyBytes, []byte(key))
|
||||
// 如果密钥不足16字节,用'A'填充
|
||||
for i := len(key); i < 16; i++ {
|
||||
keyBytes[i] = 'A'
|
||||
}
|
||||
}
|
||||
|
||||
// 创建AES密码块
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// CBC模式解密
|
||||
blockSize := block.BlockSize()
|
||||
if len(ciphertext)%blockSize != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 使用固定的IV(与JavaScript版本一致,使用相同的key作为IV)
|
||||
iv := make([]byte, blockSize)
|
||||
copy(iv, keyBytes[:blockSize])
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
mode.CryptBlocks(plaintext, ciphertext)
|
||||
|
||||
// 去除PKCS7填充
|
||||
if len(plaintext) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
padding := int(plaintext[len(plaintext)-1])
|
||||
// 更宽松的填充检查
|
||||
if padding <= 0 || padding > blockSize || padding > len(plaintext) {
|
||||
// 如果填充不合法,返回原始数据
|
||||
return string(plaintext)
|
||||
}
|
||||
|
||||
// 验证填充是否正确(更宽松的检查)
|
||||
validPadding := true
|
||||
for i := len(plaintext) - padding; i < len(plaintext); i++ {
|
||||
if plaintext[i] != byte(padding) {
|
||||
validPadding = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validPadding {
|
||||
// 填充验证失败,返回尽可能多的有效数据
|
||||
return string(plaintext)
|
||||
}
|
||||
|
||||
// 正常去除填充
|
||||
if len(plaintext) <= padding {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(plaintext[:len(plaintext)-padding])
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
var SAFETOCKEN="Safe-Tocken";//for RSA key cookie
|
||||
var encryptAxes = function(axes){
|
||||
RSAUtils.setMaxDigits(1024);
|
||||
var safeTocken = getCookie(SAFETOCKEN);
|
||||
if(safeTocken=="" || safeTocken ==''){
|
||||
throw new Error("Cannot read Cookie '"+SAFETOCKEN+"' values,System need run RSAData() again!");
|
||||
return ;
|
||||
}
|
||||
var safe_tocken = safeTocken.split(",");
|
||||
var pubexp = safe_tocken[1];
|
||||
var priexp = safe_tocken[0];
|
||||
var modulus = safe_tocken[2];
|
||||
var key = RSAUtils.getKeyPair(safe_tocken[1], safe_tocken[0], safe_tocken[2]);
|
||||
axes = axes.split("").reverse().join("");
|
||||
return RSAUtils.encryptedString(key,axes);
|
||||
}
|
||||
var axesMax = function(){
|
||||
var client_tocken = getCookie(CLIENTTOCKEN);
|
||||
if(client_tocken=="" || client_tocken==''){
|
||||
throw new Error("Cannot read Cookie '"+CLIENTTOCKEN+"' values,System need run RSAData() again!");
|
||||
return ;
|
||||
}
|
||||
return client_tocken.length;
|
||||
}
|
||||
/**
|
||||
* decrypt axes use ras key form http back
|
||||
*/
|
||||
var decryptAxes = function(rsakey){
|
||||
RSAUtils.setMaxDigits(1024);
|
||||
var safeTocken = getCookie(SAFETOCKEN);
|
||||
if(safeTocken=="" || safeTocken ==''){
|
||||
throw new Error("Cannot read Cookie '"+SAFETOCKEN+"' values,System need run RSAData() again!");
|
||||
return ;
|
||||
}
|
||||
var safe_tocken = safeTocken.split(",");
|
||||
var pubexp = safe_tocken[1];
|
||||
var priexp = safe_tocken[0];
|
||||
var modulus = safe_tocken[2];
|
||||
var key = RSAUtils.getKeyPair(safe_tocken[1], safe_tocken[0], safe_tocken[2]);
|
||||
return RSAUtils.decryptedString(key,rsakey);
|
||||
}
|
||||
var CLIENTTOCKEN = "Client-Tocken";
|
||||
/**
|
||||
* Get String key as axes
|
||||
*/
|
||||
var encryptKey = function(axes){
|
||||
//1.get client-tocken
|
||||
//2. realse this axes get this encryptKey
|
||||
var key="";
|
||||
var client_tocken = getCookie(CLIENTTOCKEN);
|
||||
if(client_tocken=="" || client_tocken==''){
|
||||
throw new Error("Cannot read Cookie '"+CLIENTTOCKEN+"' values,System need run RSAData() again!");
|
||||
return ;
|
||||
}
|
||||
var aaxes = axes.split(",");
|
||||
for(var i=0;i<aaxes.length;i++){
|
||||
key += client_tocken.substring(aaxes[i],aaxes[i]*1+4);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
var getCookie = function(name){
|
||||
var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
|
||||
if(arr=document.cookie.match(reg))
|
||||
return decodeURIComponent(arr[2]);
|
||||
else
|
||||
return "";
|
||||
}
|
||||
/**
|
||||
* get a random axes
|
||||
*/
|
||||
var creatAxes = function(max){
|
||||
var axes = "";
|
||||
for(var i=0;i<4;i++ ){
|
||||
axes += parseInt(Math.random()*(max-4),10)+",";
|
||||
}
|
||||
axes = axes.substring(0,axes.length-1);
|
||||
return axes;
|
||||
}
|
||||
/**
|
||||
* just encdata for Ajax
|
||||
*/
|
||||
var EncData = function(data){
|
||||
if(data=="" || data =="{}" || data==" " || data==null || data=="null"){
|
||||
ciphertext="";
|
||||
}else{//存在数据
|
||||
//获取aeskey
|
||||
var axes_max = axesMax();
|
||||
var axes = creatAxes(axes_max);
|
||||
var reg=new RegExp(",","g");
|
||||
var aesaxes = axes.replace(reg,'-');
|
||||
var aesKey = encryptKey(axes);
|
||||
data = JSON.stringify(data);
|
||||
encdata = Encrypt(data,aesKey);
|
||||
ciphertext = encdata+","+aesaxes;
|
||||
}
|
||||
return {'ciphertext':ciphertext};
|
||||
}
|
||||
/**
|
||||
* get data from back http
|
||||
*/
|
||||
var DescData = function(JR){
|
||||
var _data="";
|
||||
if(JR){//data is not empty
|
||||
var flag = JR.flag;
|
||||
if(flag){//成功
|
||||
if(JR.key=="" || JR.key==''){
|
||||
_data = JR.data;
|
||||
}else{//解密数据
|
||||
var axes = JR.key;
|
||||
var reg=new RegExp("-","g");
|
||||
var aesaxes = axes.replace(reg,',');
|
||||
var aesKey = encryptKey(aesaxes);
|
||||
var outdata=decodeURIComponent(JR.data);
|
||||
_data = Decrypt(outdata,aesKey);
|
||||
}
|
||||
}else{
|
||||
_data = JR.data;
|
||||
}
|
||||
}
|
||||
return {'flag':JR.flag,'data':_data};
|
||||
}
|
||||
/**
|
||||
* post data use as AJAX
|
||||
*/
|
||||
var SubData = function(data,URL){
|
||||
var result="";
|
||||
var ciphertext = EncData(data);
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
cache : false,
|
||||
async:false,
|
||||
url: URL,//传入后台的地址/方法
|
||||
data: ciphertext,//参数,这里是一个json语句
|
||||
success: function (RS) {
|
||||
result = DescData(RS);
|
||||
},
|
||||
error: function (err) {
|
||||
throw new Error("Post data was Error!");
|
||||
return ;
|
||||
},
|
||||
fail:function(){
|
||||
throw new Error("Post data was fail,website need NetWork!");
|
||||
return ;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
var Encrypt = function(data,key){
|
||||
var aeskey = CryptoJS.enc.Utf8.parse(key);
|
||||
var aesiv = CryptoJS.enc.Utf8.parse(key);
|
||||
var srcs = CryptoJS.enc.Utf8.parse(data);
|
||||
return CryptoJS.AES.encrypt(srcs, aeskey, { iv: aesiv,mode:CryptoJS.mode.CBC}).toString();
|
||||
}
|
||||
function Decrypt(word,key){
|
||||
var aeskey = CryptoJS.enc.Utf8.parse(key);
|
||||
var aesiv = CryptoJS.enc.Utf8.parse(key);
|
||||
var decrypt = CryptoJS.AES.decrypt(word, aeskey, { iv: aesiv,mode:CryptoJS.mode.CBC});
|
||||
var decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
|
||||
return decryptedStr.toString();
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncdata(t *testing.T) {
|
||||
// 使用与JavaScript版本相同的clientToken
|
||||
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
data := map[string]interface{}{
|
||||
"username": "admin",
|
||||
"password": "123456",
|
||||
}
|
||||
encData := EncData(data, clientToken)
|
||||
fmt.Println("EncData result:", encData)
|
||||
|
||||
// 创建一个模拟的JR对象来测试DescData
|
||||
jr := map[string]interface{}{
|
||||
"flag": true,
|
||||
"key": getKeyFromCiphertext(encData["ciphertext"]),
|
||||
"data": getDataFromCiphertext(encData["ciphertext"]),
|
||||
}
|
||||
|
||||
descResult := DescData(jr, clientToken)
|
||||
fmt.Println("DescData result:", descResult)
|
||||
}
|
||||
|
||||
func TestDecdata(t *testing.T) {
|
||||
// 使用与JavaScript版本相同的clientToken
|
||||
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
data := map[string]interface{}{
|
||||
"flag": true,
|
||||
"data": "YN4tHIF4S3OQgfhTICpFqR425z2sHwCEmlsGdVYt5m6ssyIagtqEvf0hEXdzhc0u",
|
||||
"key": "120-54-61-117",
|
||||
}
|
||||
|
||||
// 调试信息
|
||||
fmt.Println("Input data:", data)
|
||||
|
||||
// 手动测试密钥生成
|
||||
axes := "120-54-61-117"
|
||||
aesaxes := strings.ReplaceAll(axes, "-", ",")
|
||||
aesKey := encryptKey(aesaxes, clientToken)
|
||||
fmt.Println("Generated key:", aesKey)
|
||||
fmt.Println("Key length:", len(aesKey))
|
||||
|
||||
// 直接测试解密
|
||||
encryptedData := "YN4tHIF4S3OQgfhTICpFqR425z2sHwCEmlsGdVYt5m6ssyIagtqEvf0hEXdzhc0u"
|
||||
decrypted := Decrypt(encryptedData, aesKey)
|
||||
fmt.Println("Direct decrypt result:", decrypted)
|
||||
fmt.Printf("Direct decrypt result (bytes): %v\n", []byte(decrypted))
|
||||
|
||||
result := DescData(data, clientToken)
|
||||
fmt.Println("TestDecdata result:", result)
|
||||
|
||||
// 检查结果
|
||||
if result["flag"] != true {
|
||||
t.Errorf("Expected flag to be true, got %v", result["flag"])
|
||||
}
|
||||
}
|
||||
|
||||
// getKeyFromCiphertext 从ciphertext中提取key部分
|
||||
func getKeyFromCiphertext(ciphertext string) string {
|
||||
parts := strings.Split(ciphertext, ",")
|
||||
if len(parts) > 1 {
|
||||
return parts[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getDataFromCiphertext 从ciphertext中提取data部分
|
||||
func getDataFromCiphertext(ciphertext string) string {
|
||||
parts := strings.Split(ciphertext, ",")
|
||||
if len(parts) > 0 {
|
||||
return parts[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 添加一个简单的加密/解密测试
|
||||
func TestEncryptDecrypt(t *testing.T) {
|
||||
originalData := `{"username":"admin","password":"123456"}`
|
||||
key := "abcdabcdabcdabcd" // 16字节密钥
|
||||
|
||||
// 加密
|
||||
encrypted := Encrypt(originalData, key)
|
||||
fmt.Println("Encrypted:", encrypted)
|
||||
|
||||
// 解密
|
||||
decrypted := Decrypt(encrypted, key)
|
||||
fmt.Println("Decrypted:", decrypted)
|
||||
|
||||
if decrypted != originalData {
|
||||
t.Errorf("Decrypt failed. Expected: %s, Got: %s", originalData, decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试特定的axes生成密钥
|
||||
func TestEncryptKey(t *testing.T) {
|
||||
// 使用与JavaScript版本相同的clientToken
|
||||
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
// 测试提供的axes
|
||||
axes := "120,54,61,117"
|
||||
|
||||
key := encryptKey(axes, clientToken)
|
||||
fmt.Println("Test axes:", axes)
|
||||
fmt.Println("Generated key:", key)
|
||||
fmt.Println("Key length:", len(key))
|
||||
|
||||
// 验证每个部分
|
||||
fmt.Println("Client token length:", len(clientToken))
|
||||
|
||||
// 分别计算每个部分
|
||||
parts := strings.Split(axes, ",")
|
||||
for i, part := range parts {
|
||||
axisInt := parseInt(part)
|
||||
fmt.Printf("Part %d: %s -> %d\n", i, part, axisInt)
|
||||
|
||||
if axisInt >= 0 && axisInt < len(clientToken) {
|
||||
end := axisInt + 4
|
||||
if end <= len(clientToken) {
|
||||
fmt.Printf(" Extracted: %s\n", clientToken[axisInt:end])
|
||||
} else {
|
||||
fmt.Printf(" Extracted: %s (partial)\n", clientToken[axisInt:])
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" Out of range, using default\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 测试具体案例
|
||||
func TestSpecificCase(t *testing.T) {
|
||||
// 使用与JavaScript版本相同的clientToken
|
||||
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
// 使用你提供的具体数据
|
||||
axes := "120,54,61,117"
|
||||
key := encryptKey(axes, clientToken)
|
||||
|
||||
fmt.Println("=== Specific Case Test ===")
|
||||
fmt.Printf("Axes: %s\n", axes)
|
||||
fmt.Printf("Generated Key: %s\n", key)
|
||||
fmt.Printf("Key Length: %d\n", len(key))
|
||||
|
||||
// 测试加密
|
||||
testData := "test data"
|
||||
encrypted := Encrypt(testData, key)
|
||||
fmt.Printf("Encrypted: %s\n", encrypted)
|
||||
|
||||
// 测试解密
|
||||
decrypted := Decrypt(encrypted, key)
|
||||
fmt.Printf("Decrypted: %s\n", decrypted)
|
||||
|
||||
if testData != decrypted {
|
||||
fmt.Println("WARNING: Encryption/Decryption mismatch!")
|
||||
} else {
|
||||
fmt.Println("SUCCESS: Encryption/Decryption works correctly!")
|
||||
}
|
||||
}
|
||||
|
||||
// 测试你提供的实际加密数据
|
||||
func TestActualEncryptedData(t *testing.T) {
|
||||
// 使用与JavaScript版本相同的clientToken
|
||||
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
// 你提供的实际加密数据
|
||||
encryptedData := "YN4tHIF4S3OQgfhTICpFqR425z2sHwCEmlsGdVYt5m6ssyIagtqEvf0hEXdzhc0u"
|
||||
|
||||
// 根据axes "120,54,61,117" 生成密钥
|
||||
axes := "120,54,61,117"
|
||||
key := encryptKey(axes, clientToken)
|
||||
|
||||
fmt.Println("=== Actual Encrypted Data Test ===")
|
||||
fmt.Printf("Encrypted Data: %s\n", encryptedData)
|
||||
fmt.Printf("Key: %s\n", key)
|
||||
|
||||
// 尝试解密
|
||||
decrypted := Decrypt(encryptedData, key)
|
||||
fmt.Printf("Decrypted Result: '%s'\n", decrypted)
|
||||
fmt.Printf("Decrypted Length: %d\n", len(decrypted))
|
||||
fmt.Printf("Decrypted Bytes: %v\n", []byte(decrypted))
|
||||
|
||||
// 检查是否是有效的JSON
|
||||
if len(decrypted) > 0 {
|
||||
fmt.Println("Possible JSON data detected")
|
||||
} else {
|
||||
fmt.Println("No data returned from decryption")
|
||||
}
|
||||
}
|
||||
|
||||
// 最终验证测试
|
||||
func TestFinalVerification(t *testing.T) {
|
||||
// 使用与JavaScript版本相同的clientToken
|
||||
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
fmt.Println("=== Final Verification Test ===")
|
||||
|
||||
// 使用你提供的axes生成密钥
|
||||
axes := "120,54,61,117"
|
||||
key := encryptKey(axes, clientToken)
|
||||
fmt.Printf("Generated key for axes '%s': '%s'\n", axes, key)
|
||||
|
||||
// 你提供的加密数据
|
||||
encryptedData := "YN4tHIF4S3OQgfhTICpFqR425z2sHwCEmlsGdVYt5m6ssyIagtqEvf0hEXdzhc0u"
|
||||
|
||||
// 尝试解密
|
||||
decrypted := Decrypt(encryptedData, key)
|
||||
fmt.Printf("Decrypted result: '%s'\n", decrypted)
|
||||
|
||||
// 如果解密结果为空,尝试不同的方法
|
||||
if len(decrypted) == 0 {
|
||||
fmt.Println("Trying alternative decryption approaches...")
|
||||
|
||||
// 尝试不同的填充处理
|
||||
fmt.Println("Testing raw decryption without padding removal...")
|
||||
// 这里我们可以添加更多的调试信息
|
||||
|
||||
// 输出一些调试信息
|
||||
fmt.Printf("Key bytes: %v\n", []byte(key))
|
||||
fmt.Printf("Encrypted data length: %d\n", len(encryptedData))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package model
|
||||
|
||||
type Course struct {
|
||||
Id string `json:"id"`
|
||||
CourseName string `json:"courseName"`
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
type Chapter struct {
|
||||
Id string `json:"id"`
|
||||
ChapterName string `json:"chapterName"`
|
||||
SubSection []SubSection `json:"studySubsections"`
|
||||
}
|
||||
|
||||
type SubSection struct {
|
||||
Id string `json:"id"`
|
||||
SubsectionName string `json:"subsectionName"`
|
||||
VideoTime string `json:"videoTime"`
|
||||
StudyFiles StudyFile `json:"studyFiles"`
|
||||
}
|
||||
|
||||
type StudyFile struct {
|
||||
Id string `json:"id"`
|
||||
FileName string `json:"fileName"`
|
||||
FileSize string `json:"fileSize"`
|
||||
FileTime string `json:"fileTime"`
|
||||
FileType string `json:"fileType"`
|
||||
FilePath string `json:"filePath"`
|
||||
VideoTime string `json:"videoTime"`
|
||||
StudyTime string `json:"studyTime"`
|
||||
State string `json:"state"` // 2是学习中 1是未开始 3是已完成
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package sc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/application/smartedu/model"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
HomeUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/home"
|
||||
CourseDetailUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/studyCourse/getCourseDetails"
|
||||
RecordProcessUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/studyCourseUser/recordProcess"
|
||||
RefresProcessUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/studyCourseUser/refreshProcess"
|
||||
ApiCourseFinishedHour = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/studyCourse/getCourseFinishedHour"
|
||||
)
|
||||
|
||||
type Course struct {
|
||||
Client *resty.Client
|
||||
Token string
|
||||
}
|
||||
|
||||
func NewCourse(token string, client *resty.Client) *Course {
|
||||
return &Course{
|
||||
Token: token,
|
||||
Client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Course) GetCourseList(ctx context.Context) []model.Course {
|
||||
jsUrl := c.GetJsUrl(ctx, HomeUrl)
|
||||
get, err := c.Client.R().Get(jsUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("jsUrl", jsUrl).Wrap)
|
||||
jsonString := ParseJsonString(get.String())
|
||||
returnData := gjson.Get(jsonString, "returnData").Array()
|
||||
var courseList []model.Course
|
||||
for _, one := range returnData {
|
||||
courseList = append(courseList, model.Course{
|
||||
Id: one.Get("id").String(),
|
||||
CourseName: one.Get("courseName").String(),
|
||||
Image: one.Get("studyFiles.filePath").String(),
|
||||
})
|
||||
}
|
||||
return courseList
|
||||
}
|
||||
|
||||
func (c *Course) GetChapterList(ctx context.Context, courseId string) []model.Chapter {
|
||||
get, err := c.Client.R().SetQueryParam("courseId", courseId).SetHeader("X-Token", c.Token).Get(CourseDetailUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("GetChapterList", courseId).Wrap)
|
||||
chapterJson := gjson.Get(get.String(), "returnData.chapters").Array()
|
||||
var chapterList []model.Chapter
|
||||
for _, ch := range chapterJson {
|
||||
chapter := model.Chapter{
|
||||
Id: ch.Get("id").String(),
|
||||
ChapterName: ch.Get("chapterName").String(),
|
||||
}
|
||||
subSectionJson := ch.Get("studySubsections").Array()
|
||||
var subSectionList []model.SubSection
|
||||
for _, sec := range subSectionJson {
|
||||
studyFileJson := sec.Get("studyFiles")
|
||||
studyFile := model.StudyFile{
|
||||
Id: studyFileJson.Get("id").String(),
|
||||
FileName: studyFileJson.Get("fileName").String(),
|
||||
FileSize: studyFileJson.Get("fileSize").String(),
|
||||
FileTime: studyFileJson.Get("fileTime").String(),
|
||||
FileType: studyFileJson.Get("fileType").String(),
|
||||
FilePath: studyFileJson.Get("filePath").String(),
|
||||
VideoTime: studyFileJson.Get("videoTime").String(),
|
||||
State: studyFileJson.Get("state").String(),
|
||||
StudyTime: studyFileJson.Get("studyTime").String(),
|
||||
}
|
||||
subSection := model.SubSection{
|
||||
Id: sec.Get("id").String(),
|
||||
SubsectionName: sec.Get("subsectionName").String(),
|
||||
VideoTime: sec.Get("videoTime").String(),
|
||||
StudyFiles: studyFile,
|
||||
}
|
||||
|
||||
subSectionList = append(subSectionList, subSection)
|
||||
}
|
||||
chapter.SubSection = subSectionList
|
||||
chapterList = append(chapterList, chapter)
|
||||
}
|
||||
|
||||
return chapterList
|
||||
}
|
||||
|
||||
func ParseJsonString(str string) string {
|
||||
// eval("module.exports = /*#__PURE__*/JSON.parse('{...}');\n\n//# sourceURL=...
|
||||
re := regexp.MustCompile(`#__PURE__\*/JSON\.parse\('(.+?)'\);`)
|
||||
matches := re.FindStringSubmatch(str)
|
||||
if len(matches) < 2 {
|
||||
return ""
|
||||
}
|
||||
// 处理转义的引号
|
||||
return strings.ReplaceAll(matches[1], `\"`, `"`)
|
||||
}
|
||||
|
||||
func (c *Course) GetJsUrl(ctx context.Context, url string) string {
|
||||
get, err := c.Client.R().Get(url)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("GetJsUrl", url).Wrap)
|
||||
return ParseJsUrl(get.String())
|
||||
}
|
||||
|
||||
func (c *Course) Study(ctx context.Context, c2 model.Course, ch model.Chapter, sec model.SubSection) {
|
||||
// 如果已完成
|
||||
if sec.StudyFiles.State == "3" {
|
||||
return
|
||||
}
|
||||
|
||||
form := map[string]any{
|
||||
"chapterId": ch.Id,
|
||||
"courseId": c2.Id,
|
||||
"subsectionId": sec.Id,
|
||||
}
|
||||
if sec.StudyFiles.State == "1" {
|
||||
form["state"] = "1"
|
||||
c.Refresh(ctx, c2, ch, sec)
|
||||
form["studyTime"] = 5
|
||||
// 如果state为
|
||||
post, err := c.Client.R().SetHeader("X-Token", c.Token).SetBody(form).Post(RecordProcessUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("Study", form).Wrap)
|
||||
str := post.String()
|
||||
if gjson.Get(str, "returnCode").String() != "200" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "学习失败,"+gjson.Get(str, "returnMessage").String()))
|
||||
}
|
||||
logger.Logger.Info("学习成功", zap.String("课程名", c2.CourseName), zap.String("章节名", ch.ChapterName), zap.String("小节名", sec.SubsectionName))
|
||||
}
|
||||
c.Refresh(ctx, c2, ch, sec)
|
||||
atoi, _ := strconv.Atoi(sec.StudyFiles.VideoTime)
|
||||
form["state"] = "3"
|
||||
form["studyTime"] = atoi
|
||||
// 如果state为
|
||||
post, err := c.Client.R().SetHeader("X-Token", c.Token).SetBody(form).Post(RecordProcessUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("Study", form).Wrap)
|
||||
str := post.String()
|
||||
if gjson.Get(str, "returnCode").String() != "200" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "学习失败,"+gjson.Get(str, "returnMessage").String()))
|
||||
}
|
||||
logger.Logger.Info("学习成功", zap.String("课程名", c2.CourseName), zap.String("章节名", ch.ChapterName), zap.String("小节名", sec.SubsectionName))
|
||||
}
|
||||
|
||||
func (c *Course) Refresh(ctx context.Context, c2 model.Course, ch model.Chapter, sec model.SubSection) {
|
||||
form := map[string]any{
|
||||
"chapterId": ch.Id,
|
||||
"courseId": c2.Id,
|
||||
"subsectionId": sec.Id,
|
||||
"studyTime": 0,
|
||||
}
|
||||
post, err := c.Client.R().SetHeader("X-Token", c.Token).SetBody(form).Post(RefresProcessUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("Refresh", form).Wrap)
|
||||
str := post.String()
|
||||
if gjson.Get(str, "returnCode").String() != "200" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "刷新嫉妒失败,"+gjson.Get(str, "returnMessage").String()))
|
||||
}
|
||||
logger.Logger.Info("刷新成功", zap.String("课程名", c2.CourseName), zap.String("章节名", ch.ChapterName), zap.String("小节名", sec.SubsectionName))
|
||||
}
|
||||
|
||||
func ParseJsUrl(str string) string {
|
||||
// <script defer src="https://cdnbasic.scbdc.edu.cn/%E7%9C%81%E7%BA%A7%E8%B5%84%E6%BA%90/hd/chunk-vendors.5e79f5ce155849.js">
|
||||
re := regexp.MustCompile(`https://[^"'\s]+chunk-vendors[^"'\s]+\.js`)
|
||||
match := re.FindString(str)
|
||||
if match == "" {
|
||||
return ""
|
||||
}
|
||||
return strings.Replace(match, "chunk-vendors", "src_views_home_index_vue", 1)
|
||||
}
|
||||
func (c *Course) GetCourseProcees(ctx context.Context, courseId string) (finished string, total string) {
|
||||
post, err := c.Client.R().SetHeader("X-Token", c.Token).SetBody(map[string]any{
|
||||
"courseIds": []string{courseId},
|
||||
}).Post(ApiCourseFinishedHour)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("GetCourseProcees", courseId).Wrap)
|
||||
str := gjson.Get(post.String(), "returnData.finishedHours").String()
|
||||
split := strings.Split(str, ",")
|
||||
return split[0], split[1]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package sc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/application/smartedu/cryptoutil"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/samber/oops"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
LoginPageUrl = "https://basic.sc.smartedu.cn/ThirdPortalService/user/otherlogin!login.ac?appkey=C56DA16ECBC56FBEEC908DA09E45C72C917A80118F057FA1F0B5BAE41CC9CC9DECD5BDB7133FE17C328C5D37B37CA8E7&pkey=5D79CA42E45C5273DF8532D09E1F158B15E25919CDB958940F84D5E63F5F53A1ECD5BDB7133FE17C328C5D37B37CA8E7¶ms=718F83A5347CBFDB7D1A9065FA090FE949D92330BB9A3351FE0715C5B8A3E86F37916C1004E835C7C7F964E3F301477F7D37F04485FA8707845DAAA23356236ED1D326CF5A5E3C263470516EE9B4A2ED"
|
||||
LoginActionUrl = "https://basic.sc.smartedu.cn/ThirdPortalService/login!login.ac"
|
||||
LoginTicketUrl = "https://basic.sc.smartedu.cn/ThirdPortalService/login!getTicket.ac"
|
||||
LoginOOSUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/sso/oauth/token"
|
||||
)
|
||||
|
||||
type Login struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Client *resty.Client
|
||||
}
|
||||
|
||||
func NewLogin(username, password string) *Login {
|
||||
return &Login{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Client: httpclient.NewSmartEdu(),
|
||||
}
|
||||
}
|
||||
func (l *Login) GetCookieValue(key string) string {
|
||||
cookies := l.Client.CookieJar().(*cookiejar.AllSavePersistCookieJar)
|
||||
for _, cookie := range cookies.Cookies(nil) {
|
||||
if cookie.Name == key {
|
||||
return cookie.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (l *Login) GetClient() *resty.Client {
|
||||
return l.Client
|
||||
}
|
||||
func (l *Login) Login(ctx context.Context) string {
|
||||
form := l.getLoginInput(ctx)
|
||||
form["loginName"] = gvar.New(l.Username)
|
||||
form["password"] = gvar.New(l.Password)
|
||||
token := l.GetCookieValue("Client-Tocken")
|
||||
data := cryptoutil.EncData(form, token)
|
||||
post, err := l.Client.R().SetFormData(data).Post(LoginActionUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginActionUrl", LoginActionUrl).Wrap)
|
||||
descData := cryptoutil.DescData(utils.ReadJson[map[string]interface{}](post.Body), token)
|
||||
result := descData["data"].(string)
|
||||
if !strings.Contains(result, `"Flag":0`) {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "登录失败"))
|
||||
}
|
||||
get, err := l.Client.R().Get(LoginTicketUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginTicketUrl", LoginTicketUrl).Wrap)
|
||||
return get.String()
|
||||
}
|
||||
|
||||
func (l *Login) OOSLogin(ctx context.Context, ticket string) string {
|
||||
post, err := l.Client.R().SetQueryParam("ticket", ticket).Post(LoginOOSUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginOOSUrl", LoginOOSUrl).Wrap)
|
||||
defer post.Body.Close()
|
||||
json := utils.ReadJson[map[string]string](post.Body)
|
||||
if json["returnCode"] != "200" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, json["returnMessage"]))
|
||||
}
|
||||
return json["returnData"]
|
||||
}
|
||||
|
||||
func (l *Login) getLoginInput(ctx context.Context) map[string]*gvar.Var {
|
||||
get, err := l.Client.R().WithContext(ctx).Get(LoginPageUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginPageUrl", LoginPageUrl).Wrap)
|
||||
defer get.Body.Close()
|
||||
reader, err := goquery.NewDocumentFromReader(get.Body)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginPageUrl", LoginPageUrl).Wrap)
|
||||
find := reader.Find("#loginContent input")
|
||||
var inputMap = make(map[string]*gvar.Var)
|
||||
find.Each(func(i int, input *goquery.Selection) {
|
||||
key, exists := input.Attr("name")
|
||||
if exists {
|
||||
val, _ := input.Attr("value")
|
||||
inputMap[key] = gvar.New(val)
|
||||
}
|
||||
})
|
||||
return inputMap
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package sc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
login := NewLogin("13551503012", "XIAOzhi520")
|
||||
login.Login(context.Background())
|
||||
login.GetClient()
|
||||
|
||||
}
|
||||
|
||||
func TestStudy(t *testing.T) {
|
||||
login := NewLogin("13551503012", "XIAOzhi520")
|
||||
//ticket := login.Login(context.Background())
|
||||
//token := login.OOSLogin(context.Background(), ticket)
|
||||
client := login.GetClient()
|
||||
course := NewCourse("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkZXZpY2VUeXBlIjoid2ViIiwibG9naW5UeXBlIjoidXNlcnJlYWxtIiwiZXhwIjoxNzY1MjU3NDI1LCJ1c2VySWQiOiIxOTk4MDI1NjQ1NzU5ODM2MTYwIn0.PL5Roy0t9v0mK-ucK73IZg2qTZfw0YhW1sVxG1SYetU", client)
|
||||
courseList := course.GetCourseList(context.Background())
|
||||
for _, c := range courseList {
|
||||
chapterList := course.GetChapterList(context.Background(), c.Id)
|
||||
for _, ch := range chapterList {
|
||||
for _, sec := range ch.SubSection {
|
||||
course.Study(context.Background(), c, ch, sec)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProcess(t *testing.T) {
|
||||
login := NewLogin("13551503012", "XIAOzhi520")
|
||||
//ticket := login.Login(context.Background())
|
||||
//token := login.OOSLogin(context.Background(), ticket)
|
||||
client := login.GetClient()
|
||||
course := NewCourse("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkZXZpY2VUeXBlIjoid2ViIiwibG9naW5UeXBlIjoidXNlcnJlYWxtIiwiZXhwIjoxNzY1MjU3NDI1LCJ1c2VySWQiOiIxOTk4MDI1NjQ1NzU5ODM2MTYwIn0.PL5Roy0t9v0mK-ucK73IZg2qTZfw0YhW1sVxG1SYetU", client)
|
||||
courseList := course.GetCourseList(context.Background())
|
||||
for _, c := range courseList {
|
||||
finished, total := course.GetCourseProcees(context.Background(), c.Id)
|
||||
t.Log(finished, total)
|
||||
}
|
||||
}
|
||||
@@ -44,9 +44,20 @@ const (
|
||||
VideoEncKey = "d_yHJ!$pdA~5"
|
||||
)
|
||||
|
||||
// ErrTimeRestriction 时间限制错误,表示当前时间不在允许执行的范围内
|
||||
var ErrTimeRestriction = errors2.NewBusinessError(499, "当前时间不在允许执行的范围内")
|
||||
|
||||
// TimeCheckFunc 时间检查回调函数类型,返回 true 表示可以继续执行
|
||||
type TimeCheckFunc func() bool
|
||||
|
||||
// TaskCompleteCallback 任务点完成回调函数类型
|
||||
type TaskCompleteCallback func()
|
||||
|
||||
type XuexitongCourse struct {
|
||||
client *resty.Client
|
||||
userID string
|
||||
client *resty.Client
|
||||
userID string
|
||||
timeCheckFunc TimeCheckFunc // 时间检查回调,每个任务点完成后调用
|
||||
taskCompleteCallback TaskCompleteCallback // 任务点完成回调
|
||||
}
|
||||
|
||||
func NewXuexitongCourse(client *resty.Client) *XuexitongCourse {
|
||||
@@ -55,6 +66,31 @@ func NewXuexitongCourse(client *resty.Client) *XuexitongCourse {
|
||||
}
|
||||
}
|
||||
|
||||
// SetTimeCheckFunc 设置时间检查回调函数
|
||||
func (c *XuexitongCourse) SetTimeCheckFunc(fn TimeCheckFunc) {
|
||||
c.timeCheckFunc = fn
|
||||
}
|
||||
|
||||
// SetTaskCompleteCallback 设置任务点完成回调函数
|
||||
func (c *XuexitongCourse) SetTaskCompleteCallback(fn TaskCompleteCallback) {
|
||||
c.taskCompleteCallback = fn
|
||||
}
|
||||
|
||||
// onTaskComplete 任务点完成时调用
|
||||
func (c *XuexitongCourse) onTaskComplete() {
|
||||
if c.taskCompleteCallback != nil {
|
||||
c.taskCompleteCallback()
|
||||
}
|
||||
}
|
||||
|
||||
// checkTimeRestriction 检查时间限制,如果设置了回调且返回 false,则返回时间限制错误
|
||||
func (c *XuexitongCourse) checkTimeRestriction() error {
|
||||
if c.timeCheckFunc != nil && !c.timeCheckFunc() {
|
||||
return ErrTimeRestriction
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *XuexitongCourse) SetUserID(userID string) {
|
||||
c.userID = userID
|
||||
}
|
||||
@@ -118,7 +154,13 @@ func (c *XuexitongCourse) PullCourseList(ctx context.Context) []entity.XueXiTCou
|
||||
courseID = strings.Split(strings.Split(sqUrl, "courseId=")[1], "&")[0]
|
||||
}
|
||||
if strings.Contains(sqUrl, "userId=") {
|
||||
c.userID = strings.Split(sqUrl, "userId=")[1]
|
||||
userIDPart := strings.Split(sqUrl, "userId=")[1]
|
||||
// 处理 userId 后面可能还有其他参数的情况
|
||||
if idx := strings.Index(userIDPart, "&"); idx != -1 {
|
||||
c.userID = userIDPart[:idx]
|
||||
} else {
|
||||
c.userID = userIDPart
|
||||
}
|
||||
}
|
||||
|
||||
course := entity.XueXiTCourse{
|
||||
@@ -295,6 +337,13 @@ func (c *XuexitongCourse) FetchChapterPointStatus(ctx context.Context, course en
|
||||
"courseid": {strconv.Itoa(courseID)},
|
||||
}
|
||||
|
||||
logger.Logger.Debug("FetchChapterPointStatus 请求参数",
|
||||
zap.String("userID", c.userID),
|
||||
zap.Int("classID", classID),
|
||||
zap.Int("courseID", courseID),
|
||||
zap.Int("cpi", course.Cpi),
|
||||
zap.Int("nodesCount", len(nodes)))
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
@@ -312,12 +361,24 @@ func (c *XuexitongCourse) FetchChapterPointStatus(ctx context.Context, course en
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Logger.Debug("FetchChapterPointStatus 响应", zap.String("body", body))
|
||||
|
||||
var status entity.ChapterPointStatus
|
||||
err = json.Unmarshal([]byte(body), &status)
|
||||
if err != nil {
|
||||
logger.Logger.Error("FetchChapterPointStatus 解析失败", zap.Error(err), zap.String("body", body))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新章节的 PointTotal 和 PointFinish 字段
|
||||
for i := range chapters {
|
||||
chapterIDStr := strconv.Itoa(chapters[i].ID)
|
||||
if pointData, exists := status[chapterIDStr]; exists {
|
||||
chapters[i].PointTotal = pointData.UnFinishCount + pointData.FinishCount
|
||||
chapters[i].PointFinish = pointData.FinishCount
|
||||
}
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
@@ -412,6 +473,8 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
|
||||
continue
|
||||
}
|
||||
pointType, ok := iframe.Other["module"]
|
||||
// 调试日志:打印每个 iframe 的 module 类型
|
||||
logger.Logger.Debug("解析到任务点", zap.String("module", pointType), zap.Bool("hasModule", ok), zap.Any("data", iframe.Data))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
@@ -465,7 +528,7 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
|
||||
Type: "insertbook",
|
||||
})
|
||||
|
||||
case "insertwork":
|
||||
case "work", "insertwork":
|
||||
workID, _ := iframe.Data["workid"].(string)
|
||||
schoolID, _ := iframe.Data["schoolid"].(string)
|
||||
jobID, _ := iframe.Data["_jobid"].(string)
|
||||
@@ -486,7 +549,7 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
|
||||
JobID: jobID,
|
||||
})
|
||||
|
||||
case "insertlink":
|
||||
case "hyperlink", "insertlink":
|
||||
jobID, _ := iframe.Data["_jobid"].(string)
|
||||
if jobID == "" {
|
||||
continue
|
||||
@@ -989,6 +1052,7 @@ func (c *XuexitongCourse) AutoPlayVideo(ctx context.Context, course entity.XueXi
|
||||
if err := json.Unmarshal([]byte(playReport), &result); err == nil {
|
||||
if isPassed, ok := result["isPassed"].(bool); ok && isPassed {
|
||||
logger.Logger.Info("视频播放完成", zap.String("jobID", p.JobID))
|
||||
c.onTaskComplete() // 任务点完成回调
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1005,6 +1069,7 @@ func (c *XuexitongCourse) AutoPlayVideo(ctx context.Context, course entity.XueXi
|
||||
if err := json.Unmarshal([]byte(playReport), &result); err == nil {
|
||||
if isPassed, ok := result["isPassed"].(bool); ok && isPassed {
|
||||
logger.Logger.Info("视频播放完成", zap.String("jobID", p.JobID))
|
||||
c.onTaskComplete() // 任务点完成回调
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1035,6 +1100,7 @@ func (c *XuexitongCourse) AutoReadDocument(ctx context.Context, course entity.Xu
|
||||
if err := json.Unmarshal([]byte(report), &result); err == nil {
|
||||
if status, ok := result["status"].(bool); ok && status {
|
||||
logger.Logger.Info("文档刷课完成", zap.String("jobID", p.JobID), zap.String("title", p.Title))
|
||||
c.onTaskComplete() // 任务点完成回调
|
||||
return nil
|
||||
}
|
||||
if msg, ok := result["msg"].(string); ok {
|
||||
@@ -1129,32 +1195,30 @@ func (c *XuexitongCourse) AutoStudyCourse(ctx context.Context, course entity.Xue
|
||||
|
||||
logger.Logger.Info(fmt.Sprintf("课程 %s 共有 %d 个章节", course.CourseName, len(chapters)))
|
||||
|
||||
// 获取章节任务点状态
|
||||
status, err := c.FetchChapterPointStatus(ctx, course, chapters)
|
||||
// 获取章节任务点状态(会自动更新 chapters 中每个章节的 PointTotal 和 PointFinish)
|
||||
_, err := c.FetchChapterPointStatus(ctx, course, chapters)
|
||||
if err != nil {
|
||||
logger.Logger.Warn("获取章节状态失败", zap.Error(err))
|
||||
} else {
|
||||
// 更新章节状态
|
||||
for i := range chapters {
|
||||
if pointData, exists := status[strconv.Itoa(chapters[i].ID)]; exists {
|
||||
if pointData.UnFinishCount != 0 && pointData.TotalCount == 0 {
|
||||
chapters[i].PointTotal = pointData.UnFinishCount
|
||||
} else {
|
||||
chapters[i].PointTotal = pointData.TotalCount
|
||||
}
|
||||
chapters[i].PointFinish = pointData.FinishCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历所有章节
|
||||
for i, chapter := range chapters {
|
||||
// 检查时间限制
|
||||
if err := c.checkTimeRestriction(); err != nil {
|
||||
logger.Logger.Info("时间限制,暂停刷课", zap.String("course", course.CourseName))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Logger.Info(fmt.Sprintf("章节 [%d/%d]: %s (任务点: %d, 已完成: %d)",
|
||||
i+1, len(chapters), chapter.Name, chapter.PointTotal, chapter.PointFinish))
|
||||
|
||||
if chapter.JobCount > 0 || chapter.PointTotal > chapter.PointFinish {
|
||||
err := c.AutoStudyChapter(ctx, course, chapter)
|
||||
if err != nil {
|
||||
// 如果是时间限制错误,向上传递
|
||||
if err == ErrTimeRestriction {
|
||||
return err
|
||||
}
|
||||
logger.Logger.Error(fmt.Sprintf("刷章节失败: %s", chapter.Name), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
@@ -1267,6 +1331,11 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
|
||||
|
||||
// 刷视频
|
||||
for i, video := range points.Videos {
|
||||
// 检查时间限制
|
||||
if err := c.checkTimeRestriction(); err != nil {
|
||||
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
|
||||
return err
|
||||
}
|
||||
logger.Logger.Info(fmt.Sprintf(" 视频任务点 [%d/%d]: %s", i+1, len(points.Videos), video.Title))
|
||||
if err := c.AutoPlayVideo(ctx, course, &points.Videos[i]); err != nil {
|
||||
logger.Logger.Error("视频刷课失败", zap.Error(err), zap.String("title", video.Title))
|
||||
@@ -1276,6 +1345,11 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
|
||||
|
||||
// 刷文档
|
||||
for i, doc := range points.Documents {
|
||||
// 检查时间限制
|
||||
if err := c.checkTimeRestriction(); err != nil {
|
||||
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
|
||||
return err
|
||||
}
|
||||
logger.Logger.Info(fmt.Sprintf(" 文档任务点 [%d/%d]: %s", i+1, len(points.Documents), doc.Title))
|
||||
if err := c.AutoReadDocument(ctx, course, &points.Documents[i]); err != nil {
|
||||
logger.Logger.Error("文档刷课失败", zap.Error(err), zap.String("title", doc.Title))
|
||||
@@ -1285,6 +1359,11 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
|
||||
|
||||
// 刷作业
|
||||
for i, work := range points.Works {
|
||||
// 检查时间限制
|
||||
if err := c.checkTimeRestriction(); err != nil {
|
||||
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
|
||||
return err
|
||||
}
|
||||
logger.Logger.Info(fmt.Sprintf(" 作业任务点 [%d/%d]: %s", i+1, len(points.Works), work.Title))
|
||||
if err := c.AutoCompleteWork(ctx, course, &points.Works[i]); err != nil {
|
||||
logger.Logger.Error("作业刷课失败", zap.Error(err), zap.String("title", work.Title))
|
||||
@@ -1294,6 +1373,11 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
|
||||
|
||||
// 刷外链
|
||||
for i, link := range points.Hyperlinks {
|
||||
// 检查时间限制
|
||||
if err := c.checkTimeRestriction(); err != nil {
|
||||
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
|
||||
return err
|
||||
}
|
||||
logger.Logger.Info(fmt.Sprintf(" 外链任务点 [%d/%d]: %s", i+1, len(points.Hyperlinks), link.Title))
|
||||
if err := c.AutoCompleteHyperlink(ctx, course, &points.Hyperlinks[i]); err != nil {
|
||||
logger.Logger.Error("外链刷课失败", zap.Error(err), zap.String("title", link.Title))
|
||||
@@ -1303,6 +1387,11 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
|
||||
|
||||
// 刷直播
|
||||
for i, live := range points.Lives {
|
||||
// 检查时间限制
|
||||
if err := c.checkTimeRestriction(); err != nil {
|
||||
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
|
||||
return err
|
||||
}
|
||||
logger.Logger.Info(fmt.Sprintf(" 直播任务点 [%d/%d]: %s", i+1, len(points.Lives), live.Title))
|
||||
if err := c.AutoWatchLive(ctx, course, &points.Lives[i]); err != nil {
|
||||
logger.Logger.Error("直播刷课失败", zap.Error(err), zap.String("title", live.Title))
|
||||
@@ -1446,6 +1535,7 @@ func (c *XuexitongCourse) AutoCompleteHyperlink(ctx context.Context, course enti
|
||||
if err := json.Unmarshal([]byte(report), &result); err == nil {
|
||||
if status, ok := result["status"].(bool); ok && status {
|
||||
logger.Logger.Info("外链刷课完成", zap.String("jobID", p.JobID), zap.String("title", p.Title))
|
||||
c.onTaskComplete() // 任务点完成回调
|
||||
return nil
|
||||
}
|
||||
if msg, ok := result["msg"].(string); ok {
|
||||
@@ -1537,6 +1627,7 @@ func (c *XuexitongCourse) AutoWatchLive(ctx context.Context, course entity.XueXi
|
||||
|
||||
if p.VideoCompletePercent >= 90 {
|
||||
logger.Logger.Info("直播刷课完成", zap.String("title", p.Title))
|
||||
c.onTaskComplete() // 任务点完成回调
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package cst
|
||||
|
||||
const (
|
||||
BasePointCpdaily = 1
|
||||
)
|
||||
@@ -40,6 +40,15 @@ const (
|
||||
ConfigBasePointCpdailyTask = "basePoint.cpdailyTask"
|
||||
ConfigBasePointCpdailyLeave = "basePoint.cpdailyLeave"
|
||||
ConfigBasePointCpdailyPhone = "basePoint.cpdailyPhone"
|
||||
|
||||
// 系统配置
|
||||
ConfigSystemAllowRegister = "system.allowRegister" // 是否允许注册
|
||||
|
||||
ConfigRedisCpdailyTask = "price.cpdaily_task"
|
||||
ConfigRedisCpdailyLeave = "price.cpdaily_leave"
|
||||
ConfigRedisCpdailyPhone = "price.cpdaily_phone"
|
||||
ConfigRedisXxt = "price.xxt"
|
||||
ConfigRedisSmarteduSc = "price.smartedu_sc"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -31,6 +31,18 @@ func (c ConfigCtrlV1) Update(ctx context.Context, req *v1.ConfigUpdateReq) (res
|
||||
return service.Global.Config.Update(ctx, req), nil
|
||||
}
|
||||
|
||||
// GetPriceConfig 获取费率配置
|
||||
func (c ConfigCtrlV1) GetPriceConfig(ctx context.Context, req *v1.PriceConfigGetReq) (res *utils.Response[*v1.PriceConfigResp], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.Config.GetPriceConfig(ctx, req), nil
|
||||
}
|
||||
|
||||
// UpdatePriceConfig 更新费率配置
|
||||
func (c ConfigCtrlV1) UpdatePriceConfig(ctx context.Context, req *v1.PriceConfigUpdateReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.Config.UpdatePriceConfig(ctx, req), nil
|
||||
}
|
||||
|
||||
func NewConfigCtrlV1() IConfigCtrlV1 {
|
||||
return &ConfigCtrlV1{}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,6 @@ type IConfigCtrlV1 interface {
|
||||
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)
|
||||
GetPriceConfig(ctx context.Context, req *v1.PriceConfigGetReq) (*utils.Response[*v1.PriceConfigResp], error)
|
||||
UpdatePriceConfig(ctx context.Context, req *v1.PriceConfigUpdateReq) (*utils.Response[any], error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package course
|
||||
|
||||
import (
|
||||
"context"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/smartedu/model"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/utils"
|
||||
)
|
||||
|
||||
type ISmartEduSCCtrlV1 interface {
|
||||
List(ctx context.Context, req *v1.SmartEduSCListReq) (*utils.Response[[]*ent.CourseEntity], error)
|
||||
QueryCourses(ctx context.Context, req *v1.SmartEduSCQueryCourseReq) (*utils.Response[[]model.Course], error)
|
||||
AddCourse(ctx context.Context, req *v1.SmartEduSCAddCourseReq) (*utils.Response[any], error)
|
||||
DeleteCourse(ctx context.Context, req *v1.SmartEduSCDeleteReq) (*utils.Response[any], error)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package course
|
||||
|
||||
import (
|
||||
"context"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/smartedu/model"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/service"
|
||||
"likedream-goframe/internal/utils"
|
||||
)
|
||||
|
||||
type SmartEduSCCtrlV1 struct {
|
||||
}
|
||||
|
||||
// List 获取四川智慧教育刷课列表
|
||||
func (c SmartEduSCCtrlV1) List(ctx context.Context, req *v1.SmartEduSCListReq) (res *utils.Response[[]*ent.CourseEntity], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.SmartEduSC.List(ctx, req), nil
|
||||
}
|
||||
|
||||
// QueryCourses 查询四川智慧教育课程列表
|
||||
func (c SmartEduSCCtrlV1) QueryCourses(ctx context.Context, req *v1.SmartEduSCQueryCourseReq) (res *utils.Response[[]model.Course], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.SmartEduSC.QueryCourses(ctx, req), nil
|
||||
}
|
||||
|
||||
// AddCourse 添加四川智慧教育刷课任务
|
||||
func (c SmartEduSCCtrlV1) AddCourse(ctx context.Context, req *v1.SmartEduSCAddCourseReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.SmartEduSC.AddCourse(ctx, req), nil
|
||||
}
|
||||
|
||||
// DeleteCourse 删除四川智慧教育刷课任务
|
||||
func (c SmartEduSCCtrlV1) DeleteCourse(ctx context.Context, req *v1.SmartEduSCDeleteReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.SmartEduSC.DeleteCourse(ctx, req), nil
|
||||
}
|
||||
|
||||
func NewSmartEduSCCtrlV1() ISmartEduSCCtrlV1 {
|
||||
return &SmartEduSCCtrlV1{}
|
||||
}
|
||||
@@ -26,6 +26,11 @@ func (c CpLeaveCtrlV1) Report(ctx context.Context, req *v1.CpLeaveReportReq) (re
|
||||
return service.Global.CpLeave.Report(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c CpLeaveCtrlV1) Upload(ctx context.Context, req *v1.CpLeaveUploadReq) (res *utils.Response[string], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.CpLeave.Upload(ctx, req), nil
|
||||
}
|
||||
|
||||
func NewCpLeaveCtrlV1() ICpLeaveCtrlV1 {
|
||||
return &CpLeaveCtrlV1{}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ type ICpLeaveCtrlV1 interface {
|
||||
GetLeaveListFromSystem(ctx context.Context, req *v1.GetLeaveListFromSystemReq) (*utils.Response[any], error)
|
||||
List(ctx context.Context, req *v1.CpLeaveListReq) (*utils.Response[[]*ent.CpLeave], error)
|
||||
Report(ctx context.Context, req *v1.CpLeaveReportReq) (*utils.Response[any], error)
|
||||
Upload(ctx context.Context, req *v1.CpLeaveUploadReq) (*utils.Response[string], error)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/framework/captcha"
|
||||
"likedream-goframe/internal/framework/errors"
|
||||
"likedream-goframe/internal/framework/redis"
|
||||
"likedream-goframe/internal/service"
|
||||
"likedream-goframe/internal/utils"
|
||||
@@ -64,7 +63,6 @@ func (c *UserLoginV1) Register(ctx context.Context, req *v1.UserRegisterReq) (re
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
utils.OkOrPanic(errors.NewBusinessError(400, "注册功能暂时关闭,请等待重新开放"))
|
||||
return service.Global.UserLogin.Register(ctx, req), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -47,9 +47,9 @@ type CourseEntity struct {
|
||||
// Violence holds the value of the "violence" field.
|
||||
Violence courseentity.Violence `json:"violence,omitempty"`
|
||||
// Nodes holds the value of the "nodes" field.
|
||||
Nodes string `json:"nodes,omitempty"`
|
||||
Nodes string `json:"-"`
|
||||
// CompletedNodes holds the value of the "completed_nodes" field.
|
||||
CompletedNodes string `json:"completed_nodes,omitempty"`
|
||||
CompletedNodes string `json:"-"`
|
||||
// Desc holds the value of the "desc" field.
|
||||
Desc string `json:"desc,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
@@ -294,11 +294,9 @@ func (_m *CourseEntity) String() string {
|
||||
builder.WriteString("violence=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.Violence))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("nodes=")
|
||||
builder.WriteString(_m.Nodes)
|
||||
builder.WriteString("nodes=<sensitive>")
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("completed_nodes=")
|
||||
builder.WriteString(_m.CompletedNodes)
|
||||
builder.WriteString("completed_nodes=<sensitive>")
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("desc=")
|
||||
builder.WriteString(_m.Desc)
|
||||
|
||||
@@ -117,7 +117,8 @@ type Type string
|
||||
|
||||
// Type values.
|
||||
const (
|
||||
TypeXxt Type = "xxt"
|
||||
TypeXxt Type = "xxt"
|
||||
TypeSmarteduSc Type = "smartedu_sc"
|
||||
)
|
||||
|
||||
func (_type Type) String() string {
|
||||
@@ -127,7 +128,7 @@ func (_type Type) String() string {
|
||||
// TypeValidator is a validator for the "type" field enum values. It is called by the builders before save.
|
||||
func TypeValidator(_type Type) error {
|
||||
switch _type {
|
||||
case TypeXxt:
|
||||
case TypeXxt, TypeSmarteduSc:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("courseentity: invalid enum value for type field: %q", _type)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ var (
|
||||
{Name: "update_time", Type: field.TypeString, SchemaType: map[string]string{"mysql": "datetime", "postgres": "timestamp", "sqlite3": "datetime"}},
|
||||
{Name: "username", Type: field.TypeString},
|
||||
{Name: "password", Type: field.TypeString},
|
||||
{Name: "type", Type: field.TypeEnum, Enums: []string{"xxt"}},
|
||||
{Name: "type", Type: field.TypeEnum, Enums: []string{"xxt", "smartedu_sc"}},
|
||||
{Name: "course_name", Type: field.TypeString},
|
||||
{Name: "course_id", Type: field.TypeString},
|
||||
{Name: "need", Type: field.TypeEnum, Enums: []string{"task", "exam", "all"}},
|
||||
|
||||
@@ -20,7 +20,7 @@ func (CourseEntity) Fields() []ent.Field {
|
||||
field.Int64("user_id"),
|
||||
field.String("username"), // 账号
|
||||
field.String("password").Sensitive(), //密码
|
||||
field.Enum("type").Values("xxt"), // 类型:目前只有学习通
|
||||
field.Enum("type").Values("xxt", "smartedu_sc"), // 类型:学习通、四川智慧教育
|
||||
field.String("course_name"), // 课程名称
|
||||
field.String("course_id"), // 课程ID
|
||||
field.Enum("need").Values("task", "exam", "all"), // 需要刷的内容:任务点、考试、全部
|
||||
@@ -29,8 +29,8 @@ func (CourseEntity) Fields() []ent.Field {
|
||||
field.Int("success_count").Default(0), // 成功数量
|
||||
field.Enum("exam_state").Values("not_start", "waiting", "doing", "done", "fail").Default("not_start"), // 考试状态
|
||||
field.Enum("violence").Values("no", "yes").Default("no"), // 是否暴力刷课
|
||||
field.Text("nodes").Optional(), // 所有节点信息(JSON格式)
|
||||
field.Text("completed_nodes").Optional(), // 已完成的节点ID列表(JSON格式)
|
||||
field.Text("nodes").Optional().Sensitive(), // 所有节点信息(JSON格式)
|
||||
field.Text("completed_nodes").Optional().Sensitive(), // 已完成的节点ID列表(JSON格式)
|
||||
field.String("desc").Optional(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
// ENT数据库文件,请勿编辑。
|
||||
|
||||
package sysannouncement
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"likedream-goframe/internal/framework/ent2"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||
)
|
||||
|
||||
const (
|
||||
// Label holds the string label denoting the sysannouncement type in the database.
|
||||
Label = "sys_announcement"
|
||||
// FieldID holds the string denoting the id field in the database.
|
||||
FieldID = "id"
|
||||
// FieldCreateTime holds the string denoting the create_time field in the database.
|
||||
FieldCreateTime = "create_time"
|
||||
// FieldUpdateTime holds the string denoting the update_time field in the database.
|
||||
FieldUpdateTime = "update_time"
|
||||
// FieldTitle holds the string denoting the title field in the database.
|
||||
FieldTitle = "title"
|
||||
// FieldContent holds the string denoting the content field in the database.
|
||||
FieldContent = "content"
|
||||
// FieldStatus holds the string denoting the status field in the database.
|
||||
FieldStatus = "status"
|
||||
// FieldPinned holds the string denoting the pinned field in the database.
|
||||
FieldPinned = "pinned"
|
||||
// FieldOrder holds the string denoting the order field in the database.
|
||||
FieldOrder = "order"
|
||||
// FieldCreatorID holds the string denoting the creator_id field in the database.
|
||||
FieldCreatorID = "creator_id"
|
||||
// EdgeCreator holds the string denoting the creator edge name in mutations.
|
||||
EdgeCreator = "creator"
|
||||
// Table holds the table name of the sysannouncement in the database.
|
||||
Table = "sys_announcement"
|
||||
// CreatorTable is the table that holds the creator relation/edge.
|
||||
CreatorTable = "sys_announcement"
|
||||
// CreatorInverseTable is the table name for the SysUser entity.
|
||||
// It exists in this package in order to avoid circular dependency with the "sysuser" package.
|
||||
CreatorInverseTable = "sys_user"
|
||||
// CreatorColumn is the table column denoting the creator relation/edge.
|
||||
CreatorColumn = "creator_id"
|
||||
)
|
||||
|
||||
// Columns holds all SQL columns for sysannouncement fields.
|
||||
var Columns = []string{
|
||||
FieldID,
|
||||
FieldCreateTime,
|
||||
FieldUpdateTime,
|
||||
FieldTitle,
|
||||
FieldContent,
|
||||
FieldStatus,
|
||||
FieldPinned,
|
||||
FieldOrder,
|
||||
FieldCreatorID,
|
||||
}
|
||||
|
||||
// ValidColumn reports if the column name is valid (part of the table columns).
|
||||
func ValidColumn(column string) bool {
|
||||
for i := range Columns {
|
||||
if column == Columns[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Note that the variables below are initialized by the runtime
|
||||
// package on the initialization of the application. Therefore,
|
||||
// it should be imported in the main as follows:
|
||||
//
|
||||
// import _ "likedream-goframe/internal/ent/runtime"
|
||||
var (
|
||||
Hooks [1]ent.Hook
|
||||
// DefaultCreateTime holds the default value on creation for the "create_time" field.
|
||||
DefaultCreateTime func() *ent2.CustomTime
|
||||
// DefaultUpdateTime holds the default value on creation for the "update_time" field.
|
||||
DefaultUpdateTime func() *ent2.CustomTime
|
||||
// DefaultPinned holds the default value on creation for the "pinned" field.
|
||||
DefaultPinned bool
|
||||
// DefaultOrder holds the default value on creation for the "order" field.
|
||||
DefaultOrder uint
|
||||
)
|
||||
|
||||
// Status defines the type for the "status" enum field.
|
||||
type Status string
|
||||
|
||||
// StatusDraft is the default value of the Status enum.
|
||||
const DefaultStatus = StatusDraft
|
||||
|
||||
// Status values.
|
||||
const (
|
||||
StatusDraft Status = "draft"
|
||||
StatusPublished Status = "published"
|
||||
StatusArchived Status = "archived"
|
||||
)
|
||||
|
||||
func (s Status) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// StatusValidator is a validator for the "status" field enum values. It is called by the builders before save.
|
||||
func StatusValidator(s Status) error {
|
||||
switch s {
|
||||
case StatusDraft, StatusPublished, StatusArchived:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("sysannouncement: invalid enum value for status field: %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
// OrderOption defines the ordering options for the SysAnnouncement queries.
|
||||
type OrderOption func(*sql.Selector)
|
||||
|
||||
// ByID orders the results by the id field.
|
||||
func ByID(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldID, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByCreateTime orders the results by the create_time field.
|
||||
func ByCreateTime(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldCreateTime, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByUpdateTime orders the results by the update_time field.
|
||||
func ByUpdateTime(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldUpdateTime, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByTitle orders the results by the title field.
|
||||
func ByTitle(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldTitle, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByContent orders the results by the content field.
|
||||
func ByContent(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldContent, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByStatus orders the results by the status field.
|
||||
func ByStatus(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldStatus, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByPinned orders the results by the pinned field.
|
||||
func ByPinned(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldPinned, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByOrder orders the results by the order field.
|
||||
func ByOrder(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldOrder, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByCreatorID orders the results by the creator_id field.
|
||||
func ByCreatorID(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldCreatorID, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByCreatorField orders the results by creator field.
|
||||
func ByCreatorField(field string, opts ...sql.OrderTermOption) OrderOption {
|
||||
return func(s *sql.Selector) {
|
||||
sqlgraph.OrderByNeighborTerms(s, newCreatorStep(), sql.OrderByField(field, opts...))
|
||||
}
|
||||
}
|
||||
func newCreatorStep() *sqlgraph.Step {
|
||||
return sqlgraph.NewStep(
|
||||
sqlgraph.From(Table, FieldID),
|
||||
sqlgraph.To(CreatorInverseTable, FieldID),
|
||||
sqlgraph.Edge(sqlgraph.M2O, true, CreatorTable, CreatorColumn),
|
||||
)
|
||||
}
|
||||
@@ -1,496 +0,0 @@
|
||||
// ENT数据库文件,请勿编辑。
|
||||
|
||||
package sysannouncement
|
||||
|
||||
import (
|
||||
"likedream-goframe/internal/ent/internal"
|
||||
"likedream-goframe/internal/ent/predicate"
|
||||
"likedream-goframe/internal/framework/ent2"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||
)
|
||||
|
||||
// ID filters vertices based on their ID field.
|
||||
func ID(id int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldID, id))
|
||||
}
|
||||
|
||||
// IDEQ applies the EQ predicate on the ID field.
|
||||
func IDEQ(id int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldID, id))
|
||||
}
|
||||
|
||||
// IDNEQ applies the NEQ predicate on the ID field.
|
||||
func IDNEQ(id int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNEQ(FieldID, id))
|
||||
}
|
||||
|
||||
// IDIn applies the In predicate on the ID field.
|
||||
func IDIn(ids ...int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldIn(FieldID, ids...))
|
||||
}
|
||||
|
||||
// IDNotIn applies the NotIn predicate on the ID field.
|
||||
func IDNotIn(ids ...int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNotIn(FieldID, ids...))
|
||||
}
|
||||
|
||||
// IDGT applies the GT predicate on the ID field.
|
||||
func IDGT(id int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldGT(FieldID, id))
|
||||
}
|
||||
|
||||
// IDGTE applies the GTE predicate on the ID field.
|
||||
func IDGTE(id int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldGTE(FieldID, id))
|
||||
}
|
||||
|
||||
// IDLT applies the LT predicate on the ID field.
|
||||
func IDLT(id int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldLT(FieldID, id))
|
||||
}
|
||||
|
||||
// IDLTE applies the LTE predicate on the ID field.
|
||||
func IDLTE(id int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldLTE(FieldID, id))
|
||||
}
|
||||
|
||||
// CreateTime applies equality check predicate on the "create_time" field. It's identical to CreateTimeEQ.
|
||||
func CreateTime(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldCreateTime, v))
|
||||
}
|
||||
|
||||
// UpdateTime applies equality check predicate on the "update_time" field. It's identical to UpdateTimeEQ.
|
||||
func UpdateTime(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldUpdateTime, v))
|
||||
}
|
||||
|
||||
// Title applies equality check predicate on the "title" field. It's identical to TitleEQ.
|
||||
func Title(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldTitle, v))
|
||||
}
|
||||
|
||||
// Content applies equality check predicate on the "content" field. It's identical to ContentEQ.
|
||||
func Content(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldContent, v))
|
||||
}
|
||||
|
||||
// Pinned applies equality check predicate on the "pinned" field. It's identical to PinnedEQ.
|
||||
func Pinned(v bool) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldPinned, v))
|
||||
}
|
||||
|
||||
// Order applies equality check predicate on the "order" field. It's identical to OrderEQ.
|
||||
func Order(v uint) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldOrder, v))
|
||||
}
|
||||
|
||||
// CreatorID applies equality check predicate on the "creator_id" field. It's identical to CreatorIDEQ.
|
||||
func CreatorID(v int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldCreatorID, v))
|
||||
}
|
||||
|
||||
// CreateTimeEQ applies the EQ predicate on the "create_time" field.
|
||||
func CreateTimeEQ(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldCreateTime, v))
|
||||
}
|
||||
|
||||
// CreateTimeNEQ applies the NEQ predicate on the "create_time" field.
|
||||
func CreateTimeNEQ(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNEQ(FieldCreateTime, v))
|
||||
}
|
||||
|
||||
// CreateTimeIn applies the In predicate on the "create_time" field.
|
||||
func CreateTimeIn(vs ...*ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldIn(FieldCreateTime, vs...))
|
||||
}
|
||||
|
||||
// CreateTimeNotIn applies the NotIn predicate on the "create_time" field.
|
||||
func CreateTimeNotIn(vs ...*ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNotIn(FieldCreateTime, vs...))
|
||||
}
|
||||
|
||||
// CreateTimeGT applies the GT predicate on the "create_time" field.
|
||||
func CreateTimeGT(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldGT(FieldCreateTime, v))
|
||||
}
|
||||
|
||||
// CreateTimeGTE applies the GTE predicate on the "create_time" field.
|
||||
func CreateTimeGTE(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldGTE(FieldCreateTime, v))
|
||||
}
|
||||
|
||||
// CreateTimeLT applies the LT predicate on the "create_time" field.
|
||||
func CreateTimeLT(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldLT(FieldCreateTime, v))
|
||||
}
|
||||
|
||||
// CreateTimeLTE applies the LTE predicate on the "create_time" field.
|
||||
func CreateTimeLTE(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldLTE(FieldCreateTime, v))
|
||||
}
|
||||
|
||||
// CreateTimeContains applies the Contains predicate on the "create_time" field.
|
||||
func CreateTimeContains(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
vc := v.String()
|
||||
return predicate.SysAnnouncement(sql.FieldContains(FieldCreateTime, vc))
|
||||
}
|
||||
|
||||
// CreateTimeHasPrefix applies the HasPrefix predicate on the "create_time" field.
|
||||
func CreateTimeHasPrefix(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
vc := v.String()
|
||||
return predicate.SysAnnouncement(sql.FieldHasPrefix(FieldCreateTime, vc))
|
||||
}
|
||||
|
||||
// CreateTimeHasSuffix applies the HasSuffix predicate on the "create_time" field.
|
||||
func CreateTimeHasSuffix(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
vc := v.String()
|
||||
return predicate.SysAnnouncement(sql.FieldHasSuffix(FieldCreateTime, vc))
|
||||
}
|
||||
|
||||
// CreateTimeEqualFold applies the EqualFold predicate on the "create_time" field.
|
||||
func CreateTimeEqualFold(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
vc := v.String()
|
||||
return predicate.SysAnnouncement(sql.FieldEqualFold(FieldCreateTime, vc))
|
||||
}
|
||||
|
||||
// CreateTimeContainsFold applies the ContainsFold predicate on the "create_time" field.
|
||||
func CreateTimeContainsFold(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
vc := v.String()
|
||||
return predicate.SysAnnouncement(sql.FieldContainsFold(FieldCreateTime, vc))
|
||||
}
|
||||
|
||||
// UpdateTimeEQ applies the EQ predicate on the "update_time" field.
|
||||
func UpdateTimeEQ(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldUpdateTime, v))
|
||||
}
|
||||
|
||||
// UpdateTimeNEQ applies the NEQ predicate on the "update_time" field.
|
||||
func UpdateTimeNEQ(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNEQ(FieldUpdateTime, v))
|
||||
}
|
||||
|
||||
// UpdateTimeIn applies the In predicate on the "update_time" field.
|
||||
func UpdateTimeIn(vs ...*ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldIn(FieldUpdateTime, vs...))
|
||||
}
|
||||
|
||||
// UpdateTimeNotIn applies the NotIn predicate on the "update_time" field.
|
||||
func UpdateTimeNotIn(vs ...*ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNotIn(FieldUpdateTime, vs...))
|
||||
}
|
||||
|
||||
// UpdateTimeGT applies the GT predicate on the "update_time" field.
|
||||
func UpdateTimeGT(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldGT(FieldUpdateTime, v))
|
||||
}
|
||||
|
||||
// UpdateTimeGTE applies the GTE predicate on the "update_time" field.
|
||||
func UpdateTimeGTE(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldGTE(FieldUpdateTime, v))
|
||||
}
|
||||
|
||||
// UpdateTimeLT applies the LT predicate on the "update_time" field.
|
||||
func UpdateTimeLT(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldLT(FieldUpdateTime, v))
|
||||
}
|
||||
|
||||
// UpdateTimeLTE applies the LTE predicate on the "update_time" field.
|
||||
func UpdateTimeLTE(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldLTE(FieldUpdateTime, v))
|
||||
}
|
||||
|
||||
// UpdateTimeContains applies the Contains predicate on the "update_time" field.
|
||||
func UpdateTimeContains(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
vc := v.String()
|
||||
return predicate.SysAnnouncement(sql.FieldContains(FieldUpdateTime, vc))
|
||||
}
|
||||
|
||||
// UpdateTimeHasPrefix applies the HasPrefix predicate on the "update_time" field.
|
||||
func UpdateTimeHasPrefix(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
vc := v.String()
|
||||
return predicate.SysAnnouncement(sql.FieldHasPrefix(FieldUpdateTime, vc))
|
||||
}
|
||||
|
||||
// UpdateTimeHasSuffix applies the HasSuffix predicate on the "update_time" field.
|
||||
func UpdateTimeHasSuffix(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
vc := v.String()
|
||||
return predicate.SysAnnouncement(sql.FieldHasSuffix(FieldUpdateTime, vc))
|
||||
}
|
||||
|
||||
// UpdateTimeEqualFold applies the EqualFold predicate on the "update_time" field.
|
||||
func UpdateTimeEqualFold(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
vc := v.String()
|
||||
return predicate.SysAnnouncement(sql.FieldEqualFold(FieldUpdateTime, vc))
|
||||
}
|
||||
|
||||
// UpdateTimeContainsFold applies the ContainsFold predicate on the "update_time" field.
|
||||
func UpdateTimeContainsFold(v *ent2.CustomTime) predicate.SysAnnouncement {
|
||||
vc := v.String()
|
||||
return predicate.SysAnnouncement(sql.FieldContainsFold(FieldUpdateTime, vc))
|
||||
}
|
||||
|
||||
// TitleEQ applies the EQ predicate on the "title" field.
|
||||
func TitleEQ(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldTitle, v))
|
||||
}
|
||||
|
||||
// TitleNEQ applies the NEQ predicate on the "title" field.
|
||||
func TitleNEQ(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNEQ(FieldTitle, v))
|
||||
}
|
||||
|
||||
// TitleIn applies the In predicate on the "title" field.
|
||||
func TitleIn(vs ...string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldIn(FieldTitle, vs...))
|
||||
}
|
||||
|
||||
// TitleNotIn applies the NotIn predicate on the "title" field.
|
||||
func TitleNotIn(vs ...string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNotIn(FieldTitle, vs...))
|
||||
}
|
||||
|
||||
// TitleGT applies the GT predicate on the "title" field.
|
||||
func TitleGT(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldGT(FieldTitle, v))
|
||||
}
|
||||
|
||||
// TitleGTE applies the GTE predicate on the "title" field.
|
||||
func TitleGTE(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldGTE(FieldTitle, v))
|
||||
}
|
||||
|
||||
// TitleLT applies the LT predicate on the "title" field.
|
||||
func TitleLT(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldLT(FieldTitle, v))
|
||||
}
|
||||
|
||||
// TitleLTE applies the LTE predicate on the "title" field.
|
||||
func TitleLTE(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldLTE(FieldTitle, v))
|
||||
}
|
||||
|
||||
// TitleContains applies the Contains predicate on the "title" field.
|
||||
func TitleContains(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldContains(FieldTitle, v))
|
||||
}
|
||||
|
||||
// TitleHasPrefix applies the HasPrefix predicate on the "title" field.
|
||||
func TitleHasPrefix(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldHasPrefix(FieldTitle, v))
|
||||
}
|
||||
|
||||
// TitleHasSuffix applies the HasSuffix predicate on the "title" field.
|
||||
func TitleHasSuffix(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldHasSuffix(FieldTitle, v))
|
||||
}
|
||||
|
||||
// TitleEqualFold applies the EqualFold predicate on the "title" field.
|
||||
func TitleEqualFold(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEqualFold(FieldTitle, v))
|
||||
}
|
||||
|
||||
// TitleContainsFold applies the ContainsFold predicate on the "title" field.
|
||||
func TitleContainsFold(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldContainsFold(FieldTitle, v))
|
||||
}
|
||||
|
||||
// ContentEQ applies the EQ predicate on the "content" field.
|
||||
func ContentEQ(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldContent, v))
|
||||
}
|
||||
|
||||
// ContentNEQ applies the NEQ predicate on the "content" field.
|
||||
func ContentNEQ(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNEQ(FieldContent, v))
|
||||
}
|
||||
|
||||
// ContentIn applies the In predicate on the "content" field.
|
||||
func ContentIn(vs ...string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldIn(FieldContent, vs...))
|
||||
}
|
||||
|
||||
// ContentNotIn applies the NotIn predicate on the "content" field.
|
||||
func ContentNotIn(vs ...string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNotIn(FieldContent, vs...))
|
||||
}
|
||||
|
||||
// ContentGT applies the GT predicate on the "content" field.
|
||||
func ContentGT(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldGT(FieldContent, v))
|
||||
}
|
||||
|
||||
// ContentGTE applies the GTE predicate on the "content" field.
|
||||
func ContentGTE(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldGTE(FieldContent, v))
|
||||
}
|
||||
|
||||
// ContentLT applies the LT predicate on the "content" field.
|
||||
func ContentLT(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldLT(FieldContent, v))
|
||||
}
|
||||
|
||||
// ContentLTE applies the LTE predicate on the "content" field.
|
||||
func ContentLTE(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldLTE(FieldContent, v))
|
||||
}
|
||||
|
||||
// ContentContains applies the Contains predicate on the "content" field.
|
||||
func ContentContains(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldContains(FieldContent, v))
|
||||
}
|
||||
|
||||
// ContentHasPrefix applies the HasPrefix predicate on the "content" field.
|
||||
func ContentHasPrefix(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldHasPrefix(FieldContent, v))
|
||||
}
|
||||
|
||||
// ContentHasSuffix applies the HasSuffix predicate on the "content" field.
|
||||
func ContentHasSuffix(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldHasSuffix(FieldContent, v))
|
||||
}
|
||||
|
||||
// ContentEqualFold applies the EqualFold predicate on the "content" field.
|
||||
func ContentEqualFold(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEqualFold(FieldContent, v))
|
||||
}
|
||||
|
||||
// ContentContainsFold applies the ContainsFold predicate on the "content" field.
|
||||
func ContentContainsFold(v string) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldContainsFold(FieldContent, v))
|
||||
}
|
||||
|
||||
// StatusEQ applies the EQ predicate on the "status" field.
|
||||
func StatusEQ(v Status) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldStatus, v))
|
||||
}
|
||||
|
||||
// StatusNEQ applies the NEQ predicate on the "status" field.
|
||||
func StatusNEQ(v Status) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNEQ(FieldStatus, v))
|
||||
}
|
||||
|
||||
// StatusIn applies the In predicate on the "status" field.
|
||||
func StatusIn(vs ...Status) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldIn(FieldStatus, vs...))
|
||||
}
|
||||
|
||||
// StatusNotIn applies the NotIn predicate on the "status" field.
|
||||
func StatusNotIn(vs ...Status) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNotIn(FieldStatus, vs...))
|
||||
}
|
||||
|
||||
// PinnedEQ applies the EQ predicate on the "pinned" field.
|
||||
func PinnedEQ(v bool) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldPinned, v))
|
||||
}
|
||||
|
||||
// PinnedNEQ applies the NEQ predicate on the "pinned" field.
|
||||
func PinnedNEQ(v bool) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNEQ(FieldPinned, v))
|
||||
}
|
||||
|
||||
// OrderEQ applies the EQ predicate on the "order" field.
|
||||
func OrderEQ(v uint) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldOrder, v))
|
||||
}
|
||||
|
||||
// OrderNEQ applies the NEQ predicate on the "order" field.
|
||||
func OrderNEQ(v uint) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNEQ(FieldOrder, v))
|
||||
}
|
||||
|
||||
// OrderIn applies the In predicate on the "order" field.
|
||||
func OrderIn(vs ...uint) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldIn(FieldOrder, vs...))
|
||||
}
|
||||
|
||||
// OrderNotIn applies the NotIn predicate on the "order" field.
|
||||
func OrderNotIn(vs ...uint) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNotIn(FieldOrder, vs...))
|
||||
}
|
||||
|
||||
// OrderGT applies the GT predicate on the "order" field.
|
||||
func OrderGT(v uint) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldGT(FieldOrder, v))
|
||||
}
|
||||
|
||||
// OrderGTE applies the GTE predicate on the "order" field.
|
||||
func OrderGTE(v uint) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldGTE(FieldOrder, v))
|
||||
}
|
||||
|
||||
// OrderLT applies the LT predicate on the "order" field.
|
||||
func OrderLT(v uint) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldLT(FieldOrder, v))
|
||||
}
|
||||
|
||||
// OrderLTE applies the LTE predicate on the "order" field.
|
||||
func OrderLTE(v uint) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldLTE(FieldOrder, v))
|
||||
}
|
||||
|
||||
// CreatorIDEQ applies the EQ predicate on the "creator_id" field.
|
||||
func CreatorIDEQ(v int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldEQ(FieldCreatorID, v))
|
||||
}
|
||||
|
||||
// CreatorIDNEQ applies the NEQ predicate on the "creator_id" field.
|
||||
func CreatorIDNEQ(v int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNEQ(FieldCreatorID, v))
|
||||
}
|
||||
|
||||
// CreatorIDIn applies the In predicate on the "creator_id" field.
|
||||
func CreatorIDIn(vs ...int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldIn(FieldCreatorID, vs...))
|
||||
}
|
||||
|
||||
// CreatorIDNotIn applies the NotIn predicate on the "creator_id" field.
|
||||
func CreatorIDNotIn(vs ...int64) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.FieldNotIn(FieldCreatorID, vs...))
|
||||
}
|
||||
|
||||
// HasCreator applies the HasEdge predicate on the "creator" edge.
|
||||
func HasCreator() predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(func(s *sql.Selector) {
|
||||
step := sqlgraph.NewStep(
|
||||
sqlgraph.From(Table, FieldID),
|
||||
sqlgraph.Edge(sqlgraph.M2O, true, CreatorTable, CreatorColumn),
|
||||
)
|
||||
schemaConfig := internal.SchemaConfigFromContext(s.Context())
|
||||
step.To.Schema = schemaConfig.SysUser
|
||||
step.Edge.Schema = schemaConfig.SysAnnouncement
|
||||
sqlgraph.HasNeighbors(s, step)
|
||||
})
|
||||
}
|
||||
|
||||
// HasCreatorWith applies the HasEdge predicate on the "creator" edge with a given conditions (other predicates).
|
||||
func HasCreatorWith(preds ...predicate.SysUser) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(func(s *sql.Selector) {
|
||||
step := newCreatorStep()
|
||||
schemaConfig := internal.SchemaConfigFromContext(s.Context())
|
||||
step.To.Schema = schemaConfig.SysUser
|
||||
step.Edge.Schema = schemaConfig.SysAnnouncement
|
||||
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
|
||||
for _, p := range preds {
|
||||
p(s)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// And groups predicates with the AND operator between them.
|
||||
func And(predicates ...predicate.SysAnnouncement) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.AndPredicates(predicates...))
|
||||
}
|
||||
|
||||
// Or groups predicates with the OR operator between them.
|
||||
func Or(predicates ...predicate.SysAnnouncement) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.OrPredicates(predicates...))
|
||||
}
|
||||
|
||||
// Not applies the not operator on the given predicate.
|
||||
func Not(p predicate.SysAnnouncement) predicate.SysAnnouncement {
|
||||
return predicate.SysAnnouncement(sql.NotPredicates(p))
|
||||
}
|
||||
@@ -1,18 +1,32 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var Logger *zap.Logger
|
||||
|
||||
func init() {
|
||||
logger, err := zap.NewProduction()
|
||||
// Create a production config and customize the time encoder
|
||||
config := zap.NewProductionConfig()
|
||||
config.EncoderConfig.EncodeTime = BeijingTimeEncoder
|
||||
|
||||
// Build the logger with the custom config
|
||||
logger, err := config.Build()
|
||||
if err != nil {
|
||||
log.Fatalf("cannot init zap logger: %+v", err)
|
||||
}
|
||||
|
||||
Logger = logger
|
||||
}
|
||||
|
||||
// BeijingTimeEncoder formats time in Beijing timezone
|
||||
func BeijingTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
// Set timezone to Beijing (UTC+8)
|
||||
beijingLoc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
enc.AppendString(t.In(beijingLoc).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
@@ -40,19 +40,15 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
e := gob.NewDecoder(bytes.NewReader(task.Payload())).Decode(&taskPayload)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).With("payload", task.Payload(), "type", task.Type()).In("CpdailyTask.DecodePayload").Wrap)
|
||||
|
||||
// 查询指定的任务和关联今日校园用户
|
||||
targetTask, e := db.Global.CpTask.Query().Where(cptask.ID(taskPayload.CpTaskID)).
|
||||
WithCpUser().
|
||||
WithSysUser().
|
||||
First(ctx)
|
||||
if ent.IsNotFound(e) {
|
||||
utils.OkOrPanic(oops.WithContext(ctx).With("task_id", taskPayload.CpTaskID).In("CpdailyTask.ProcessTask").Wrap(e))
|
||||
}
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).With("task_id", taskPayload.CpTaskID).In("CpdailyTask.ProcessTask").Wrap)
|
||||
|
||||
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusDoing).ExecX(ctx)
|
||||
|
||||
// 先注册错误处理,确保任何阶段的错误都能被捕获并重置状态
|
||||
var targetTask *ent.CpTask
|
||||
defer utils.HandleError(func(err error) {
|
||||
// 如果 targetTask 还没查询到,只能重置状态为 not_start,让任务重新入队
|
||||
if targetTask == nil {
|
||||
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusNotStart).Exec(ctx)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 执行失败应该更新下次执行时间
|
||||
executeTime := utils.GetCpdailyNextExecuteTime(targetTask)
|
||||
e := db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetNextExecuteTime(executeTime).SetTaskStatus(cptask.TaskStatusFail).Exec(ctx)
|
||||
@@ -90,6 +86,15 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
panic(err)
|
||||
})
|
||||
|
||||
// 查询指定的任务和关联今日校园用户
|
||||
targetTask, e = db.Global.CpTask.Query().Where(cptask.ID(taskPayload.CpTaskID)).
|
||||
WithCpUser().
|
||||
WithSysUser().
|
||||
First(ctx)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).With("task_id", taskPayload.CpTaskID).In("CpdailyTask.ProcessTask").Wrap)
|
||||
|
||||
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusDoing).ExecX(ctx)
|
||||
|
||||
// 查询学校详情
|
||||
schoolDetail := school.GetSchoolDetail(ctx, targetTask.Edges.CpUser.SchoolID)
|
||||
cookie := targetTask.Edges.CpUser.Cookie
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/framework/model/email"
|
||||
"likedream-goframe/internal/utils"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/samber/oops"
|
||||
@@ -19,11 +20,18 @@ type EmailTaskHandler struct {
|
||||
|
||||
func (h *EmailTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task) (err error) {
|
||||
defer utils.Pass(&err)
|
||||
|
||||
var payload email.SendPayload
|
||||
|
||||
e := gob.NewDecoder(bytes.NewReader(task.Payload())).Decode(&payload)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
logger.Logger.Info("开始处理邮件发送", zap.String("邮箱", payload.To))
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// 打印日志
|
||||
logger.Logger.Error("邮件发送失败", zap.String("邮箱", payload.To), zap.Error(r.(error)))
|
||||
// 重新抛出相同的 panic
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
|
||||
message := mail.NewMsg()
|
||||
|
||||
@@ -37,7 +45,10 @@ func (h *EmailTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task) (e
|
||||
|
||||
message.SetBodyString(mail.TypeTextHTML, payload.Body)
|
||||
|
||||
client, e := mail.NewClient(payload.SMTPHost, mail.WithPort(payload.SMTPPort), mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover),
|
||||
client, e := mail.NewClient(payload.SMTPHost, mail.WithPort(payload.SMTPPort),
|
||||
mail.WithSMTPAuth(mail.SMTPAuthLogin), // Outlook 使用 LOGIN 认证
|
||||
mail.WithTLSPortPolicy(mail.TLSMandatory),
|
||||
mail.WithTimeout(30*time.Second), // 连接超时 30 秒
|
||||
mail.WithUsername(payload.SMTPUsername), mail.WithPassword(payload.SMTPPassword))
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
|
||||
@@ -2,21 +2,85 @@ package schedule
|
||||
|
||||
import (
|
||||
"context"
|
||||
cst2 "likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/db"
|
||||
"likedream-goframe/internal/ent/cptask"
|
||||
"likedream-goframe/internal/framework/queue"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
func Init(ctx context.Context) {
|
||||
queue.InitClient(ctx)
|
||||
initServer(ctx)
|
||||
initInspector(ctx)
|
||||
cleanupStuckTasks(ctx) // 清理卡住的任务
|
||||
initScanner()
|
||||
initPeriodicTasks(ctx)
|
||||
}
|
||||
|
||||
// cleanupStuckTasks 清理服务重启时卡在 queue 或 doing 状态的任务
|
||||
// 检查 asynq 队列中是否有 archived 的任务,如果有则删除并重置数据库状态
|
||||
func cleanupStuckTasks(ctx context.Context) {
|
||||
// 获取所有状态为 queue 或 doing 的任务
|
||||
stuckTasks, err := db.Global.CpTask.Query().
|
||||
Where(
|
||||
cptask.Or(
|
||||
cptask.TaskStatusEQ(cptask.TaskStatusQueue),
|
||||
cptask.TaskStatusEQ(cptask.TaskStatusDoing),
|
||||
),
|
||||
).
|
||||
All(ctx)
|
||||
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "查询卡住的任务失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(stuckTasks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 archived 队列中的任务数量
|
||||
archivedTasks, err := globalInspector.ListArchivedTasks(cst2.TaskQueueCpdaily, asynq.PageSize(10000))
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "获取 archived 任务列表失败: %v", err)
|
||||
}
|
||||
|
||||
// 删除所有 archived 的任务,让它们可以重新入队
|
||||
for _, task := range archivedTasks {
|
||||
if err := globalInspector.DeleteTask(cst2.TaskQueueCpdaily, task.ID); err != nil {
|
||||
g.Log().Debugf(ctx, "删除 archived 任务失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(archivedTasks) > 0 {
|
||||
g.Log().Infof(ctx, "已清理 %d 个 archived 状态的 asynq 任务", len(archivedTasks))
|
||||
}
|
||||
|
||||
// 重置数据库中卡住的任务状态
|
||||
affected, err := db.Global.CpTask.Update().
|
||||
Where(
|
||||
cptask.Or(
|
||||
cptask.TaskStatusEQ(cptask.TaskStatusQueue),
|
||||
),
|
||||
).
|
||||
SetTaskStatus(cptask.TaskStatusNotStart).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "重置卡住的任务失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if affected > 0 {
|
||||
g.Log().Infof(ctx, "已重置 %d 个卡住的任务状态为 not_start", affected)
|
||||
}
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
globalScanner.StopScan()
|
||||
g.Log().Info(gctx.GetInitCtx(), "task scanner stopped")
|
||||
@@ -30,7 +94,10 @@ func Stop() {
|
||||
g.Log().Info(gctx.GetInitCtx(), "asynq client closed")
|
||||
|
||||
globalServer.Shutdown()
|
||||
g.Log().Info(gctx.GetInitCtx(), "asynq server stopped")
|
||||
g.Log().Info(gctx.GetInitCtx(), "asynq main server stopped")
|
||||
|
||||
auxiliaryServer.Shutdown()
|
||||
g.Log().Info(gctx.GetInitCtx(), "asynq auxiliary server stopped")
|
||||
|
||||
e = globalInspector.Close()
|
||||
utils.OkOrPanic(e)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
cst2 "likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/db"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/ent/courseentity"
|
||||
"likedream-goframe/internal/ent/cptask"
|
||||
"likedream-goframe/internal/framework/queue"
|
||||
"likedream-goframe/internal/utils"
|
||||
@@ -26,6 +27,20 @@ import (
|
||||
var globalScanner *TaskScanner
|
||||
|
||||
func initScanner() {
|
||||
ctx := gctx.GetInitCtx()
|
||||
|
||||
// 应用启动时,将所有 StatusDoing(执行中)状态的学习通任务重置为 StatusWaiting
|
||||
// 这样可以确保应用重启后,之前正在执行的任务能够继续执行
|
||||
count, err := db.Global.CourseEntity.Update().
|
||||
Where(courseentity.StatusEQ(courseentity.StatusDoing)).
|
||||
SetStatus(courseentity.StatusWaiting).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "重置执行中的学习通任务失败: %v", err)
|
||||
} else if count > 0 {
|
||||
g.Log().Infof(ctx, "应用启动时重置了 %d 个执行中的学习通任务为等待状态", count)
|
||||
}
|
||||
|
||||
globalScanner = NewTaskScanner()
|
||||
go globalScanner.StartScan()
|
||||
}
|
||||
@@ -65,6 +80,7 @@ func (ts *TaskScanner) StartScan() {
|
||||
}
|
||||
|
||||
ts.scanCpdailyTask()
|
||||
ts.scanXuexitongTask()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,12 +137,16 @@ func (ts *TaskScanner) scanCpdailyTask() {
|
||||
year, month, day := currentTime.Date()
|
||||
execTime := time.Date(year, month, day, *task.ExecuteHour, *task.ExecuteMinute, 0, 0, cst2.TimeZoneShangHai)
|
||||
|
||||
// 添加随机延迟(0-30秒),分散同一时间点的任务执行,避免瞬间占满所有 worker
|
||||
randomDelay := time.Duration(task.ID%30) * time.Second
|
||||
execTime = execTime.Add(randomDelay)
|
||||
|
||||
e = utils.Catch(func() {
|
||||
queue.AddTask(ts.ctx, asynq.NewTask(cst2.TaskCpdaily, payloadWriter.Bytes()),
|
||||
asynq.Unique(5*time.Minute),
|
||||
asynq.Unique(2*time.Minute),
|
||||
asynq.MaxRetry(4),
|
||||
asynq.ProcessAt(execTime),
|
||||
asynq.Timeout(10*time.Minute),
|
||||
asynq.Timeout(5*time.Minute),
|
||||
asynq.Queue(cst2.TaskQueueCpdaily),
|
||||
)
|
||||
// 更新未开始的任务状态为队列中
|
||||
@@ -134,7 +154,8 @@ func (ts *TaskScanner) scanCpdailyTask() {
|
||||
})
|
||||
if e != nil {
|
||||
if errors.Is(e, asynq.ErrDuplicateTask) {
|
||||
// 任务已在队列中,不更新下次执行时间,等任务执行完成后再处理
|
||||
// 任务已在队列中,确保数据库状态也是 queue,然后跳过
|
||||
db.Global.CpTask.Update().Where(cptask.ID(task.ID)).SetTaskStatus(cptask.TaskStatusQueue).Exec(ts.ctx)
|
||||
g.Log().Debugf(ts.ctx, "任务ID %d 已在队列中,跳过本次扫描", task.ID)
|
||||
failedIDS = append(failedIDS, task.ID)
|
||||
continue
|
||||
@@ -186,3 +207,76 @@ func (ts *TaskScanner) scanCpdailyTask() {
|
||||
utils.LogError(ts.ctx, "重置过期任务下次执行时间失败", oops.With("task_id", task.ID, "next_execute_at", nextExecuteAt).Wrap(e))
|
||||
}
|
||||
}
|
||||
|
||||
// scanXuexitongTask 扫描学习通刷课任务
|
||||
func (ts *TaskScanner) scanXuexitongTask() {
|
||||
// 查询所有状态为not_start或waiting的课程
|
||||
allCourses := db.Global.CourseEntity.Query().
|
||||
Where(
|
||||
courseentity.Or(
|
||||
courseentity.StatusEQ(courseentity.StatusNotStart),
|
||||
courseentity.StatusEQ(courseentity.StatusWaiting),
|
||||
),
|
||||
).
|
||||
AllX(ts.ctx)
|
||||
|
||||
if len(allCourses) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 按暴力模式区分任务
|
||||
for _, course := range allCourses {
|
||||
// 检查是否暂停
|
||||
if course.Status == courseentity.StatusPause {
|
||||
continue
|
||||
}
|
||||
|
||||
payload := &XuexitongTaskPayload{
|
||||
CourseID: course.ID,
|
||||
}
|
||||
|
||||
payloadWriter := bytes.NewBuffer(nil)
|
||||
|
||||
e := gob.NewEncoder(payloadWriter).Encode(payload)
|
||||
utils.OkOrPanic(e, oops.WithContext(ts.ctx).With("payload", payload).In("XuexitongTaskScanner.scanXuexitongTask").Wrap)
|
||||
|
||||
// 根据violence字段选择队列和任务类型
|
||||
var queueName string
|
||||
var taskType string
|
||||
|
||||
if course.Violence == courseentity.ViolenceYes {
|
||||
queueName = cst2.TaskQueueXuexitongVio
|
||||
taskType = cst2.TaskXuexitongVio
|
||||
} else {
|
||||
queueName = cst2.TaskQueueXuexitong
|
||||
taskType = cst2.TaskXuexitong
|
||||
}
|
||||
|
||||
e = utils.Catch(func() {
|
||||
queue.AddTask(ts.ctx, asynq.NewTask(taskType, payloadWriter.Bytes()),
|
||||
//asynq.Unique(30*time.Minute), // 30分钟内去重
|
||||
asynq.MaxRetry(3),
|
||||
asynq.Timeout(2*time.Hour), // 刷课可能需要较长时间
|
||||
asynq.Queue(queueName),
|
||||
)
|
||||
// 更新状态为等待中
|
||||
db.Global.CourseEntity.Update().
|
||||
Where(courseentity.ID(course.ID), courseentity.StatusEQ(courseentity.StatusNotStart)).
|
||||
SetStatus(courseentity.StatusWaiting).
|
||||
Exec(ts.ctx)
|
||||
})
|
||||
if e != nil {
|
||||
if errors.Is(e, asynq.ErrDuplicateTask) {
|
||||
// 任务已在队列中,跳过
|
||||
g.Log().Debugf(ts.ctx, "学习通课程ID %d 已在队列中,跳过本次扫描", course.ID)
|
||||
continue
|
||||
} else if errors.Is(e, context.Canceled) {
|
||||
return
|
||||
} else {
|
||||
utils.LogError(ts.ctx, "扫描学习通任务时,入列失败", oops.With("course_id", course.ID).Wrap(e))
|
||||
}
|
||||
} else {
|
||||
g.Log().Debugf(ts.ctx, "学习通课程 %s (ID: %d) 已加入队列: %s", course.CourseName, course.ID, queueName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,19 @@ import (
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
var globalServer *asynq.Server
|
||||
var (
|
||||
// 主任务服务器:处理 cpdaily、xuexitong 等业务任务
|
||||
globalServer *asynq.Server
|
||||
// 辅助任务服务器:处理 email、reset 等轻量级任务
|
||||
auxiliaryServer *asynq.Server
|
||||
)
|
||||
|
||||
func initServer(ctx context.Context) {
|
||||
cfg := g.Cfg()
|
||||
|
||||
taskConcurrency := cfg.MustGetWithEnv(ctx, cst2.ConfigTaskConcurrency).Int()
|
||||
|
||||
// 主服务器:专门处理业务任务,使用大部分 worker
|
||||
globalServer = asynq.NewServerFromRedisClient(redis.GetClient(), asynq.Config{
|
||||
Concurrency: taskConcurrency,
|
||||
BaseContext: gctx.GetInitCtx,
|
||||
@@ -28,24 +34,45 @@ func initServer(ctx context.Context) {
|
||||
},
|
||||
DelayedTaskCheckInterval: 2 * time.Second,
|
||||
ErrorHandler: taskErrorHandler{},
|
||||
// 只处理业务队列,使用严格优先级模式
|
||||
StrictPriority: true,
|
||||
Queues: map[string]int{
|
||||
cst2.TaskQueueEmail: 2,
|
||||
cst2.TaskQueueCpdaily: 10,
|
||||
cst2.TaskQueueXuexitong: 5,
|
||||
cst2.TaskQueueCpdaily: 10, // 最高优先级
|
||||
cst2.TaskQueueXuexitongVio: 8,
|
||||
cst2.TaskQueueDefault: 1,
|
||||
cst2.TaskQueueXuexitong: 5,
|
||||
},
|
||||
})
|
||||
|
||||
mux := asynq.NewServeMux()
|
||||
// 辅助服务器:专门处理邮件等轻量级任务,独立的 worker 池
|
||||
auxiliaryServer = asynq.NewServerFromRedisClient(redis.GetClient(), asynq.Config{
|
||||
Concurrency: 5, // 邮件任务不需要太多并发
|
||||
BaseContext: gctx.GetInitCtx,
|
||||
TaskCheckInterval: 10 * time.Second,
|
||||
RetryDelayFunc: func(n int, _ error, _ *asynq.Task) time.Duration {
|
||||
return time.Duration(n) * 2 * time.Second
|
||||
},
|
||||
DelayedTaskCheckInterval: 2 * time.Second,
|
||||
Queues: map[string]int{
|
||||
cst2.TaskQueueEmail: 2,
|
||||
cst2.TaskQueueDefault: 1,
|
||||
},
|
||||
})
|
||||
|
||||
mux.Handle(cst2.TaskCpdaily, &CpdailyTaskHandler{})
|
||||
mux.Handle(cst2.TaskEmail, &EmailTaskHandler{})
|
||||
mux.Handle(cst2.TaskResetDaily, &ResetTaskHandler{})
|
||||
mux.Handle(cst2.TaskXuexitong, &XuexitongTaskHandler{})
|
||||
mux.Handle(cst2.TaskXuexitongVio, &XuexitongTaskHandler{})
|
||||
// 主服务器的任务处理器
|
||||
mainMux := asynq.NewServeMux()
|
||||
mainMux.Handle(cst2.TaskCpdaily, &CpdailyTaskHandler{})
|
||||
mainMux.Handle(cst2.TaskXuexitong, &XuexitongTaskHandler{})
|
||||
mainMux.Handle(cst2.TaskXuexitongVio, &XuexitongTaskHandler{})
|
||||
|
||||
e := globalServer.Start(mux)
|
||||
// 辅助服务器的任务处理器
|
||||
auxMux := asynq.NewServeMux()
|
||||
auxMux.Handle(cst2.TaskEmail, &EmailTaskHandler{})
|
||||
auxMux.Handle(cst2.TaskResetDaily, &ResetTaskHandler{})
|
||||
|
||||
e := globalServer.Start(mainMux)
|
||||
utils.OkOrPanic(e)
|
||||
|
||||
e = auxiliaryServer.Start(auxMux)
|
||||
utils.OkOrPanic(e)
|
||||
}
|
||||
|
||||
|
||||
@@ -97,14 +97,20 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
|
||||
// 创建课程服务
|
||||
courseService := xuexitong.NewXuexitongCourse(client)
|
||||
|
||||
// 执行刷课
|
||||
err = courseService.AutoStudyByCourseID(ctx, targetCourse.CourseID)
|
||||
if err != nil {
|
||||
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "刷课失败", err))
|
||||
}
|
||||
// 非暴力模式下,设置时间检查回调,每个任务点完成后都会检查时间
|
||||
if targetCourse.Violence == courseentity.ViolenceNo {
|
||||
courseService.SetTimeCheckFunc(func() bool {
|
||||
currentTime := time.Now()
|
||||
currentHour := currentTime.Hour()
|
||||
currentMinute := currentTime.Minute()
|
||||
|
||||
// 刷课完成后,统计任务情况
|
||||
g.Log().Infof(ctx, "刷课完成,正在统计任务情况...")
|
||||
startTime := 8*60 + 30 // 8:30
|
||||
endTime := 17*60 + 30 // 17:30
|
||||
currentMinutes := currentHour*60 + currentMinute
|
||||
|
||||
return currentMinutes >= startTime && currentMinutes < endTime
|
||||
})
|
||||
}
|
||||
|
||||
// 先获取课程列表找到目标课程
|
||||
courseList := courseService.PullCourseList(ctx)
|
||||
@@ -116,23 +122,81 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
|
||||
}
|
||||
}
|
||||
|
||||
totalTasks := 0
|
||||
if course != nil {
|
||||
// 获取章节列表
|
||||
chapters := courseService.PullCourseChapterList(ctx, *course)
|
||||
if course == nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(404, "未找到课程"))
|
||||
}
|
||||
|
||||
// 统计总任务数(累加所有章节的JobCount)
|
||||
// 开始刷课前,先获取并更新任务点统计
|
||||
g.Log().Infof(ctx, "课程 %s 正在获取任务点统计...", targetCourse.CourseName)
|
||||
|
||||
chapters := courseService.PullCourseChapterList(ctx, *course)
|
||||
// FetchChapterPointStatus 会更新 chapters 中每个章节的 PointTotal 和 PointFinish
|
||||
_, statusErr := courseService.FetchChapterPointStatus(ctx, *course, chapters)
|
||||
if statusErr != nil {
|
||||
g.Log().Warningf(ctx, "获取章节任务点状态失败: %v", statusErr)
|
||||
} else {
|
||||
// 通过累加每个章节的 PointTotal 和 PointFinish 计算总数
|
||||
var totalPointCount, finishedPointCount int
|
||||
for _, chapter := range chapters {
|
||||
totalTasks += chapter.JobCount
|
||||
totalPointCount += chapter.PointTotal
|
||||
finishedPointCount += chapter.PointFinish
|
||||
}
|
||||
g.Log().Infof(ctx, "课程 %s 任务点统计: 总数 %d, 已完成 %d", targetCourse.CourseName, totalPointCount, finishedPointCount)
|
||||
|
||||
// 更新数据库
|
||||
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).
|
||||
SetTaskCount(totalPointCount).
|
||||
SetSuccessCount(finishedPointCount).
|
||||
ExecX(ctx)
|
||||
}
|
||||
|
||||
// 设置任务点完成回调,每完成一个任务点更新 success_count
|
||||
courseService.SetTaskCompleteCallback(func() {
|
||||
// 增加 success_count
|
||||
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).
|
||||
AddSuccessCount(1).
|
||||
ExecX(ctx)
|
||||
})
|
||||
|
||||
// 执行刷课
|
||||
err = courseService.AutoStudyByCourseID(ctx, targetCourse.CourseID)
|
||||
if err != nil {
|
||||
// 如果是时间限制导致的停止,不算失败
|
||||
if err == xuexitong.ErrTimeRestriction {
|
||||
g.Log().Infof(ctx, "课程ID %d 因时间限制暂停刷课,等待下次执行", targetCourse.ID)
|
||||
// 更新状态为等待中,等待下次定时任务
|
||||
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).SetStatus(courseentity.StatusWaiting).ExecX(ctx)
|
||||
return nil
|
||||
}
|
||||
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "刷课失败", err))
|
||||
}
|
||||
|
||||
// 刷课完成后,重新统计任务情况
|
||||
g.Log().Infof(ctx, "刷课完成,正在重新统计任务情况...")
|
||||
|
||||
// 重新获取章节列表和任务点状态
|
||||
chaptersAfter := courseService.PullCourseChapterList(ctx, *course)
|
||||
// FetchChapterPointStatus 会更新 chapters 中每个章节的 PointTotal 和 PointFinish
|
||||
_, statusErrAfter := courseService.FetchChapterPointStatus(ctx, *course, chaptersAfter)
|
||||
|
||||
var totalPointCountAfter, finishedPointCountAfter int
|
||||
if statusErrAfter != nil {
|
||||
g.Log().Warningf(ctx, "获取章节任务点状态失败: %v", statusErrAfter)
|
||||
} else {
|
||||
// 通过累加每个章节的 PointTotal 和 PointFinish 计算总数
|
||||
for _, chapter := range chaptersAfter {
|
||||
totalPointCountAfter += chapter.PointTotal
|
||||
finishedPointCountAfter += chapter.PointFinish
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "课程 %s 统计完成: 总任务点 %d", targetCourse.CourseName, totalTasks)
|
||||
g.Log().Infof(ctx, "课程 %s 统计完成: 总任务点 %d, 已完成 %d", targetCourse.CourseName, totalPointCountAfter, finishedPointCountAfter)
|
||||
|
||||
// 更新数据库(暂时不统计完成数,只统计总数)
|
||||
// 更新数据库
|
||||
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).
|
||||
SetStatus(courseentity.StatusDone).
|
||||
SetTaskCount(totalTasks).
|
||||
SetTaskCount(totalPointCountAfter).
|
||||
SetSuccessCount(finishedPointCountAfter).
|
||||
ExecX(ctx)
|
||||
|
||||
g.Log().Infof(ctx, "学习通刷课完成 - 课程: %s", targetCourse.CourseName)
|
||||
|
||||
@@ -36,7 +36,7 @@ func (s *ConfigServiceV1) Get(ctx context.Context, req *v1.ConfigGetReq) *utils.
|
||||
// PublicGet 获取公开配置(无需认证)
|
||||
func (s *ConfigServiceV1) PublicGet(ctx context.Context, req *v1.ConfigPublicGetReq) *utils.Response[*ent.SysConfig] {
|
||||
// 只允许获取特定的公开配置
|
||||
allowedKeys := []string{"system.announcement"}
|
||||
allowedKeys := []string{"system.announcement", "system.allowRegister"}
|
||||
|
||||
allowed := false
|
||||
for _, key := range allowedKeys {
|
||||
@@ -112,3 +112,80 @@ func (s *ConfigServiceV1) Update(ctx context.Context, req *v1.ConfigUpdateReq) *
|
||||
|
||||
return utils.SuccessWithMsg("配置更新成功")
|
||||
}
|
||||
|
||||
// 费率配置键常量
|
||||
const (
|
||||
PriceKeyCpdailyTask = "price.cpdaily_task"
|
||||
PriceKeyCpdailyLeave = "price.cpdaily_leave"
|
||||
PriceKeyCpdailyPhone = "price.cpdaily_phone"
|
||||
PriceKeyXxt = "price.xxt"
|
||||
PriceKeySmarteduSc = "price.smartedu_sc"
|
||||
)
|
||||
|
||||
// GetPriceConfig 获取费率配置
|
||||
func (s *ConfigServiceV1) GetPriceConfig(ctx context.Context, req *v1.PriceConfigGetReq) *utils.Response[*v1.PriceConfigResp] {
|
||||
resp := &v1.PriceConfigResp{
|
||||
CpdailyTask: s.getConfigValue(ctx, PriceKeyCpdailyTask, "0.5"),
|
||||
CpdailyLeave: s.getConfigValue(ctx, PriceKeyCpdailyLeave, "4"),
|
||||
CpdailyPhone: s.getConfigValue(ctx, PriceKeyCpdailyPhone, "0.25"),
|
||||
Xxt: s.getConfigValue(ctx, PriceKeyXxt, "0"),
|
||||
SmarteduSc: s.getConfigValue(ctx, PriceKeySmarteduSc, "0"),
|
||||
}
|
||||
return utils.SuccessWithData[*v1.PriceConfigResp](resp)
|
||||
}
|
||||
|
||||
// getConfigValue 获取配置值,不存在则返回默认值
|
||||
func (s *ConfigServiceV1) getConfigValue(ctx context.Context, key, defaultValue string) string {
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ(key)).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return config.Value
|
||||
}
|
||||
|
||||
// UpdatePriceConfig 更新费率配置
|
||||
func (s *ConfigServiceV1) UpdatePriceConfig(ctx context.Context, req *v1.PriceConfigUpdateReq) *utils.Response[any] {
|
||||
// 更新各项费率配置
|
||||
if req.CpdailyTask != "" {
|
||||
s.upsertConfig(ctx, PriceKeyCpdailyTask, req.CpdailyTask, "今日校园签到单价")
|
||||
}
|
||||
if req.CpdailyLeave != "" {
|
||||
s.upsertConfig(ctx, PriceKeyCpdailyLeave, req.CpdailyLeave, "今日校园请假单价")
|
||||
}
|
||||
if req.CpdailyPhone != "" {
|
||||
s.upsertConfig(ctx, PriceKeyCpdailyPhone, req.CpdailyPhone, "今日校园手机号单价")
|
||||
}
|
||||
if req.Xxt != "" {
|
||||
s.upsertConfig(ctx, PriceKeyXxt, req.Xxt, "学习通单价")
|
||||
}
|
||||
if req.SmarteduSc != "" {
|
||||
s.upsertConfig(ctx, PriceKeySmarteduSc, req.SmarteduSc, "四川智慧教育单价")
|
||||
}
|
||||
return utils.SuccessWithMsg("费率配置更新成功")
|
||||
}
|
||||
|
||||
// upsertConfig 更新或创建配置
|
||||
func (s *ConfigServiceV1) upsertConfig(ctx context.Context, key, value, description string) {
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ(key)).
|
||||
Only(ctx)
|
||||
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
db.Global.SysConfig.Create().
|
||||
SetKey(key).
|
||||
SetValue(value).
|
||||
SetDescription(description).
|
||||
SetType("number").
|
||||
SaveX(ctx)
|
||||
return
|
||||
}
|
||||
utils.OkOrPanic(err)
|
||||
}
|
||||
|
||||
db.Global.SysConfig.UpdateOne(config).
|
||||
SetValue(value).
|
||||
ExecX(ctx)
|
||||
}
|
||||
|
||||
@@ -6,16 +6,18 @@ import (
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/xuexitong"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
db2 "likedream-goframe/internal/common/db"
|
||||
errors2 "likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/ent/courseentity"
|
||||
"likedream-goframe/internal/ent/sysconfig"
|
||||
"likedream-goframe/internal/ent/sysuser"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/service/balance"
|
||||
"likedream-goframe/internal/utils"
|
||||
"strconv"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
@@ -91,109 +93,15 @@ func (s CourseServiceV1) AddCourse(ctx context.Context, req *v1.AddCourseReq) *u
|
||||
if targetCourse == nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "未找到该课程"))
|
||||
}
|
||||
chapters := courseService.PullCourseChapterList(ctx, *targetCourse)
|
||||
|
||||
// 定义节点信息结构
|
||||
type NodeInfo struct {
|
||||
ChapterID string `json:"chapter_id"`
|
||||
ChapterName string `json:"chapter_name"`
|
||||
CardID string `json:"card_id"`
|
||||
CardTitle string `json:"card_title"`
|
||||
CardType string `json:"card_type"`
|
||||
basePoint, err := decimal.NewFromString(db2.Global.SysConfig.Query().Where(sysconfig.KeyEQ(cst.ConfigRedisXxt)).OnlyX(ctx).Value)
|
||||
utils.OkOrPanic(err)
|
||||
if user.Edges.Agent != nil {
|
||||
basePoint = basePoint.Mul(user.Edges.Agent.Rate)
|
||||
}
|
||||
// 先扣款
|
||||
balance.BalanceService.ChangeUserBalance(ctx, user.ID, basePoint.Neg(), "学习通刷课")
|
||||
|
||||
var allNodes []NodeInfo
|
||||
var completedNodes []string
|
||||
|
||||
// 递归获取所有章节的任务点
|
||||
var collectNodes func(chapters []entity.XueXiTCourseChapter)
|
||||
collectNodes = func(chapters []entity.XueXiTCourseChapter) {
|
||||
for _, chapter := range chapters {
|
||||
// 如果有任务点,获取卡片信息
|
||||
if chapter.JobCount > 0 {
|
||||
cards, points, err := courseService.FetchChapterCards(ctx, *targetCourse, chapter.ID)
|
||||
if err != nil {
|
||||
logger.Logger.Warn("获取章节任务点失败", zap.String("chapter", chapter.Name), zap.Error(err))
|
||||
} else if points != nil {
|
||||
chapterIDStr := strconv.Itoa(chapter.ID)
|
||||
// 统计视频任务点
|
||||
for _, video := range points.Videos {
|
||||
nodeInfo := NodeInfo{
|
||||
ChapterID: chapterIDStr,
|
||||
ChapterName: chapter.Name,
|
||||
CardID: video.JobID,
|
||||
CardTitle: video.Title,
|
||||
CardType: "video",
|
||||
}
|
||||
allNodes = append(allNodes, nodeInfo)
|
||||
if video.IsPassed {
|
||||
completedNodes = append(completedNodes, chapterIDStr+":"+video.JobID)
|
||||
}
|
||||
}
|
||||
// 统计文档任务点
|
||||
for _, doc := range points.Documents {
|
||||
nodeInfo := NodeInfo{
|
||||
ChapterID: chapterIDStr,
|
||||
ChapterName: chapter.Name,
|
||||
CardID: doc.JobID,
|
||||
CardTitle: doc.Title,
|
||||
CardType: "document",
|
||||
}
|
||||
allNodes = append(allNodes, nodeInfo)
|
||||
}
|
||||
// 统计作业任务点
|
||||
for _, work := range points.Works {
|
||||
nodeInfo := NodeInfo{
|
||||
ChapterID: chapterIDStr,
|
||||
ChapterName: chapter.Name,
|
||||
CardID: work.JobID,
|
||||
CardTitle: work.Title,
|
||||
CardType: "work",
|
||||
}
|
||||
allNodes = append(allNodes, nodeInfo)
|
||||
if work.IsPassed {
|
||||
completedNodes = append(completedNodes, chapterIDStr+":"+work.JobID)
|
||||
}
|
||||
}
|
||||
// 统计外链任务点
|
||||
for _, link := range points.Hyperlinks {
|
||||
nodeInfo := NodeInfo{
|
||||
ChapterID: chapterIDStr,
|
||||
ChapterName: chapter.Name,
|
||||
CardID: link.JobID,
|
||||
CardTitle: link.Title,
|
||||
CardType: "hyperlink",
|
||||
}
|
||||
allNodes = append(allNodes, nodeInfo)
|
||||
}
|
||||
// 统计直播任务点
|
||||
for _, live := range points.Lives {
|
||||
nodeInfo := NodeInfo{
|
||||
ChapterID: chapterIDStr,
|
||||
ChapterName: chapter.Name,
|
||||
CardID: live.JobID,
|
||||
CardTitle: live.Title,
|
||||
CardType: "live",
|
||||
}
|
||||
allNodes = append(allNodes, nodeInfo)
|
||||
}
|
||||
_ = cards // cards 暂时不使用
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理子章节
|
||||
if len(chapter.Knowledge) > 0 {
|
||||
collectNodes(chapter.Knowledge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectNodes(chapters)
|
||||
|
||||
// 将节点信息序列化为JSON
|
||||
nodesJSON, _ := sonic.MarshalString(allNodes)
|
||||
completedNodesJSON, _ := sonic.MarshalString(completedNodes)
|
||||
|
||||
// 添加课程时不获取任务点统计,由定时任务处理
|
||||
db2.Global.CourseEntity.Create().
|
||||
SetUserID(user.ID).
|
||||
SetUsername(req.Username).
|
||||
@@ -205,10 +113,8 @@ func (s CourseServiceV1) AddCourse(ctx context.Context, req *v1.AddCourseReq) *u
|
||||
SetViolence(req.Violence).
|
||||
SetNillableDesc(&req.Desc).
|
||||
SetStatus(courseentity.StatusNotStart).
|
||||
SetTaskCount(len(allNodes)).
|
||||
SetSuccessCount(len(completedNodes)).
|
||||
SetNodes(nodesJSON).
|
||||
SetCompletedNodes(completedNodesJSON).
|
||||
SetTaskCount(0).
|
||||
SetSuccessCount(0).
|
||||
SaveX(ctx)
|
||||
|
||||
return utils.SuccessWithMsg("添加成功")
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
package course
|
||||
|
||||
import (
|
||||
"context"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/smartedu/model"
|
||||
"likedream-goframe/internal/application/smartedu/sc"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
db2 "likedream-goframe/internal/common/db"
|
||||
errors2 "likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/ent/courseentity"
|
||||
"likedream-goframe/internal/ent/sysconfig"
|
||||
"likedream-goframe/internal/ent/sysuser"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/service/balance"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type SmartEduSCServiceV1 struct {
|
||||
}
|
||||
|
||||
// List 获取四川智慧教育刷课列表
|
||||
func (s SmartEduSCServiceV1) List(ctx context.Context, req *v1.SmartEduSCListReq) *utils.Response[[]*ent.CourseEntity] {
|
||||
user := utils.GetUserInfoFromCtx(ctx)
|
||||
query := db2.Global.CourseEntity.Query().
|
||||
Where(
|
||||
courseentity.HasSysUserWith(sysuser.ID(user.ID)),
|
||||
courseentity.TypeEQ(courseentity.TypeSmarteduSc),
|
||||
).
|
||||
Order(ent.Desc(courseentity.FieldCreateTime))
|
||||
|
||||
if req.Username != "" {
|
||||
query.Where(courseentity.UsernameContains(req.Username))
|
||||
}
|
||||
if req.Status != "" {
|
||||
query.Where(courseentity.StatusEQ(courseentity.Status(req.Status)))
|
||||
}
|
||||
|
||||
total := query.CountX(ctx)
|
||||
var data []*ent.CourseEntity
|
||||
if total > 0 {
|
||||
query.Limit(req.Size).Offset((req.Page - 1) * req.Size)
|
||||
data = query.AllX(ctx)
|
||||
for _, item := range data {
|
||||
item.Password = ""
|
||||
}
|
||||
}
|
||||
return utils.SuccessWithTotal[[]*ent.CourseEntity](data, total)
|
||||
}
|
||||
|
||||
// QueryCourses 查询四川智慧教育课程列表
|
||||
func (s SmartEduSCServiceV1) QueryCourses(ctx context.Context, req *v1.SmartEduSCQueryCourseReq) *utils.Response[[]model.Course] {
|
||||
login := sc.NewLogin(req.Username, req.Password)
|
||||
ticket := login.Login(ctx)
|
||||
token := login.OOSLogin(ctx, ticket)
|
||||
client := login.GetClient()
|
||||
|
||||
courseService := sc.NewCourse(token, client)
|
||||
courses := courseService.GetCourseList(ctx)
|
||||
|
||||
return utils.SuccessWithData[[]model.Course](courses)
|
||||
}
|
||||
|
||||
// AddCourse 添加四川智慧教育刷课任务
|
||||
func (s SmartEduSCServiceV1) AddCourse(ctx context.Context, req *v1.SmartEduSCAddCourseReq) *utils.Response[any] {
|
||||
user := utils.GetUserInfoFromCtx(ctx)
|
||||
|
||||
// 检查是否已存在相同任务
|
||||
exists := db2.Global.CourseEntity.Query().
|
||||
Where(
|
||||
courseentity.HasSysUserWith(sysuser.ID(user.ID)),
|
||||
courseentity.Username(req.Username),
|
||||
courseentity.CourseID(req.CourseID),
|
||||
courseentity.TypeEQ(courseentity.TypeSmarteduSc),
|
||||
).
|
||||
ExistX(ctx)
|
||||
|
||||
if exists {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "该课程已存在刷课任务"))
|
||||
}
|
||||
|
||||
// 登录验证并获取课程信息
|
||||
login := sc.NewLogin(req.Username, req.Password)
|
||||
ticket := login.Login(ctx)
|
||||
token := login.OOSLogin(ctx, ticket)
|
||||
client := login.GetClient()
|
||||
|
||||
courseService := sc.NewCourse(token, client)
|
||||
courseList := courseService.GetCourseList(ctx)
|
||||
|
||||
// 验证课程是否存在
|
||||
var targetCourse *model.Course
|
||||
for _, c := range courseList {
|
||||
if c.Id == req.CourseID {
|
||||
targetCourse = &c
|
||||
break
|
||||
}
|
||||
}
|
||||
if targetCourse == nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "未找到该课程"))
|
||||
}
|
||||
basePoint, err := decimal.NewFromString(db2.Global.SysConfig.Query().Where(sysconfig.KeyEQ(cst.ConfigRedisSmarteduSc)).OnlyX(ctx).Value)
|
||||
utils.OkOrPanic(err)
|
||||
if user.Edges.Agent != nil {
|
||||
basePoint = basePoint.Mul(user.Edges.Agent.Rate)
|
||||
}
|
||||
// 先扣款
|
||||
balance.BalanceService.ChangeUserBalance(ctx, user.ID, basePoint.Neg(), "四川中小学教师刷课")
|
||||
|
||||
// 创建数据库记录
|
||||
entity := db2.Global.CourseEntity.Create().
|
||||
SetUserID(user.ID).
|
||||
SetUsername(req.Username).
|
||||
SetPassword(req.Password).
|
||||
SetType(courseentity.TypeSmarteduSc).
|
||||
SetCourseName(req.CourseName).
|
||||
SetCourseID(req.CourseID).
|
||||
SetNeed(courseentity.NeedTask).
|
||||
SetViolence(courseentity.ViolenceYes).
|
||||
SetNillableDesc(&req.Desc).
|
||||
SetStatus(courseentity.StatusDoing).
|
||||
SetTaskCount(0).
|
||||
SetSuccessCount(0).
|
||||
SaveX(ctx)
|
||||
|
||||
// 开启协程执行秒刷
|
||||
go s.executeStudy(context.Background(), entity.ID, req.Username, req.Password, req.CourseID)
|
||||
|
||||
return utils.SuccessWithMsg("任务已提交,正在刷课中")
|
||||
}
|
||||
|
||||
// executeStudy 执行刷课(协程中运行)
|
||||
func (s SmartEduSCServiceV1) executeStudy(ctx context.Context, entityID int64, username, password, courseID string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Logger.Error("刷课执行异常", zap.Any("error", r), zap.Int64("entityID", entityID))
|
||||
db2.Global.CourseEntity.UpdateOneID(entityID).
|
||||
SetStatus(courseentity.StatusFail).
|
||||
SaveX(ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
login := sc.NewLogin(username, password)
|
||||
ticket := login.Login(ctx)
|
||||
token := login.OOSLogin(ctx, ticket)
|
||||
client := login.GetClient()
|
||||
|
||||
courseService := sc.NewCourse(token, client)
|
||||
|
||||
courseList := courseService.GetCourseList(ctx)
|
||||
var targetCourse *model.Course
|
||||
for _, c := range courseList {
|
||||
if c.Id == courseID {
|
||||
targetCourse = &c
|
||||
break
|
||||
}
|
||||
}
|
||||
if targetCourse == nil {
|
||||
logger.Logger.Error("未找到课程", zap.String("courseID", courseID))
|
||||
db2.Global.CourseEntity.UpdateOneID(entityID).
|
||||
SetStatus(courseentity.StatusFail).
|
||||
SaveX(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
chapterList := courseService.GetChapterList(ctx, courseID)
|
||||
taskCount := 0
|
||||
successCount := 0
|
||||
|
||||
for _, ch := range chapterList {
|
||||
taskCount += len(ch.SubSection)
|
||||
}
|
||||
|
||||
db2.Global.CourseEntity.UpdateOneID(entityID).
|
||||
SetTaskCount(taskCount).
|
||||
SaveX(ctx)
|
||||
|
||||
for _, ch := range chapterList {
|
||||
for _, sec := range ch.SubSection {
|
||||
courseService.Study(ctx, *targetCourse, ch, sec)
|
||||
successCount++
|
||||
db2.Global.CourseEntity.UpdateOneID(entityID).
|
||||
SetSuccessCount(successCount).
|
||||
SaveX(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
db2.Global.CourseEntity.UpdateOneID(entityID).
|
||||
SetStatus(courseentity.StatusDone).
|
||||
SaveX(ctx)
|
||||
|
||||
logger.Logger.Info("刷课完成",
|
||||
zap.String("username", username),
|
||||
zap.String("courseID", courseID),
|
||||
zap.Int("taskCount", taskCount),
|
||||
zap.Int("successCount", successCount))
|
||||
}
|
||||
|
||||
// DeleteCourse 删除四川智慧教育刷课任务
|
||||
func (s SmartEduSCServiceV1) DeleteCourse(ctx context.Context, req *v1.SmartEduSCDeleteReq) *utils.Response[any] {
|
||||
user := utils.GetUserInfoFromCtx(ctx)
|
||||
|
||||
course := db2.Global.CourseEntity.Query().
|
||||
Where(
|
||||
courseentity.ID(req.ID),
|
||||
courseentity.HasSysUserWith(sysuser.ID(user.ID)),
|
||||
courseentity.TypeEQ(courseentity.TypeSmarteduSc),
|
||||
).
|
||||
OnlyX(ctx)
|
||||
|
||||
if course == nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "任务不存在或无权限操作"))
|
||||
}
|
||||
|
||||
db2.Global.CourseEntity.DeleteOne(course).ExecX(ctx)
|
||||
|
||||
return utils.SuccessWithMsg("删除成功")
|
||||
}
|
||||
|
||||
func NewSmartEduSCServiceV1() *SmartEduSCServiceV1 {
|
||||
return &SmartEduSCServiceV1{}
|
||||
}
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/ent/cpleave"
|
||||
"likedream-goframe/internal/ent/cpuser"
|
||||
"likedream-goframe/internal/ent/sysconfig"
|
||||
"likedream-goframe/internal/ent/sysuser"
|
||||
"likedream-goframe/internal/service/balance"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
@@ -110,17 +110,9 @@ func (v CpLeaveServiceV1) Report(ctx context.Context, req *v1.CpLeaveReportReq)
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "非法的今日校园账号"))
|
||||
}
|
||||
|
||||
// 检查是否已经存在该销假记录
|
||||
existLeave, _ := db2.Global.CpLeave.Query().
|
||||
Where(cpleave.LeaveIDEQ(req.LeaveId), cpleave.CpUserIDEQ(req.CpUserId)).
|
||||
First(ctx)
|
||||
|
||||
if existLeave != nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, fmt.Sprintf("该请假记录已处理,状态:%s", existLeave.Status)))
|
||||
}
|
||||
|
||||
// 计算扣款金额
|
||||
basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst2.ConfigBasePointCpdailyLeave).String())
|
||||
//basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst2.ConfigBasePointCpdailyLeave).String())
|
||||
basePoint, err := decimal.NewFromString(db2.Global.SysConfig.Query().Where(sysconfig.KeyEQ(cst2.ConfigRedisCpdailyLeave)).OnlyX(ctx).Value)
|
||||
utils.OkOrPanic(err)
|
||||
if user.Edges.Agent != nil {
|
||||
basePoint = basePoint.Mul(user.Edges.Agent.Rate)
|
||||
@@ -176,13 +168,10 @@ func (v CpLeaveServiceV1) Report(ctx context.Context, req *v1.CpLeaveReportReq)
|
||||
detail := reportApp.LeaveDetail(req.LeaveId)
|
||||
|
||||
// 填充表单
|
||||
form := reportApp.FillReportForm(detail)
|
||||
|
||||
// 获取位置信息
|
||||
lon, lat := reportApp.GetLocation(detail)
|
||||
form := reportApp.FillReportForm(detail, req)
|
||||
|
||||
// 提交销假
|
||||
reportApp.SubmitReport(ctx, form, lon, lat, cpUser.Username, cpUser.MobileCode)
|
||||
reportApp.SubmitReport(ctx, form, req.Longitude, req.Latitude, cpUser.Username, cpUser.MobileCode)
|
||||
|
||||
// 如果执行成功,更新记录状态
|
||||
db2.Global.CpLeave.UpdateOne(leaveRecord).
|
||||
@@ -193,6 +182,30 @@ func (v CpLeaveServiceV1) Report(ctx context.Context, req *v1.CpLeaveReportReq)
|
||||
return utils.SuccessWithMsg("销假成功")
|
||||
}
|
||||
|
||||
// Upload 上传图片并返回预览地址
|
||||
func (v CpLeaveServiceV1) Upload(ctx context.Context, req *v1.CpLeaveUploadReq) *utils.Response[string] {
|
||||
user := utils.GetUserInfoFromCtx(ctx)
|
||||
// 检测是否属于自己的cpuser
|
||||
cpUser := db2.Global.CpUser.Query().Where(cpuser.ID(req.CpUserId), cpuser.HasSysuserWith(sysuser.ID(user.ID))).WithSysuser().WithSchool().OnlyX(ctx)
|
||||
if cpUser.Edges.Sysuser == nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "非法的今日校园账号"))
|
||||
}
|
||||
schoolDetail := school.GetSchoolDetail(ctx, cpUser.SchoolID)
|
||||
client := login.GetClient(ctx, cpUser.Cookie)
|
||||
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, client, cpUser.LoginType) {
|
||||
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
|
||||
loginExecutor.Login(ctx, &schoolDetail, cpUser.Username, cpUser.Password)
|
||||
cookie := loginExecutor.GetCookieString()
|
||||
db2.Global.CpUser.UpdateOne(cpUser).SetCookie(cookie).ExecX(ctx)
|
||||
client = login.GetClient(ctx, cookie)
|
||||
}
|
||||
// 创建销假服务
|
||||
reportApp := leave.NewCpdailyReportApp(client, schoolDetail.WecloudUrl)
|
||||
|
||||
imageUrl := reportApp.UploadImage(ctx, req.File)
|
||||
return utils.SuccessWithData(imageUrl)
|
||||
}
|
||||
|
||||
func NewCpLeaveServiceV1() *CpLeaveServiceV1 {
|
||||
return &CpLeaveServiceV1{}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/ent/cptask"
|
||||
"likedream-goframe/internal/ent/cpuser"
|
||||
"likedream-goframe/internal/ent/sysconfig"
|
||||
"likedream-goframe/internal/ent/sysuser"
|
||||
"likedream-goframe/internal/framework/queue"
|
||||
"likedream-goframe/internal/framework/schedule"
|
||||
@@ -180,7 +181,7 @@ func (v CpTaskServiceV1) PostExecute(ctx context.Context, req *v1.PostCpTaskExec
|
||||
}
|
||||
|
||||
//扣积分
|
||||
basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst2.ConfigBasePointCpdailyTask).String())
|
||||
basePoint, err := decimal.NewFromString(db2.Global.SysConfig.Query().Where(sysconfig.KeyEQ(cst2.ConfigRedisCpdailyTask)).OnlyX(ctx).Value)
|
||||
utils.OkOrPanic(err)
|
||||
if user.Edges.Agent != nil {
|
||||
basePoint = basePoint.Mul(user.Edges.Agent.Rate)
|
||||
@@ -265,7 +266,7 @@ func (v CpTaskServiceV1) ExecuteNow(ctx context.Context, req *v1.ExecuteNowReq)
|
||||
queue.AddTask(ctx, asynq.NewTask(cst2.TaskCpdaily, payloadWriter.Bytes()),
|
||||
asynq.Unique(2*time.Minute),
|
||||
asynq.MaxRetry(0),
|
||||
asynq.Timeout(10*time.Minute),
|
||||
asynq.Timeout(5*time.Minute),
|
||||
asynq.Deadline(time.Now().Add(10*time.Minute)),
|
||||
asynq.Queue(cst2.TaskQueueCpdaily),
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/ent/cpschool"
|
||||
"likedream-goframe/internal/ent/cpuser"
|
||||
"likedream-goframe/internal/ent/sysconfig"
|
||||
"likedream-goframe/internal/ent/sysuser"
|
||||
"likedream-goframe/internal/framework/redis"
|
||||
"likedream-goframe/internal/service/balance"
|
||||
@@ -19,7 +20,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/samber/oops"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
@@ -182,7 +182,8 @@ func (v CpUserServiceV1) AuthorizeRandom(ctx context.Context, req *v1.CpUserAuth
|
||||
func (v CpUserServiceV1) LoginGetCode(ctx context.Context, req *v1.LoginGetCode) *utils.Response[any] {
|
||||
user := utils.GetUserInfoFromCtx(ctx)
|
||||
// 扣费
|
||||
basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst.ConfigBasePointCpdailyPhone).String())
|
||||
//basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst.ConfigBasePointCpdailyPhone).String())
|
||||
basePoint, err := decimal.NewFromString(db.Global.SysConfig.Query().Where(sysconfig.KeyEQ(cst.ConfigRedisCpdailyPhone)).OnlyX(ctx).Value)
|
||||
utils.OkOrPanic(err)
|
||||
if user.Edges.Agent != nil {
|
||||
basePoint = basePoint.Mul(user.Edges.Agent.Rate)
|
||||
@@ -203,7 +204,8 @@ func (v CpUserServiceV1) AddByCode(ctx context.Context, req *v1.AddByCode) *util
|
||||
data := loginExecutor.LoginByCode(context.Background(), req.Mobile, req.Code, jar)
|
||||
loginExecutor.SetLoginCookie(context.Background(), jar, data)
|
||||
// 扣费
|
||||
basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst.ConfigBasePointCpdailyPhone).String())
|
||||
//basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst.ConfigBasePointCpdailyPhone).String())
|
||||
basePoint, err := decimal.NewFromString(db.Global.SysConfig.Query().Where(sysconfig.KeyEQ(cst.ConfigRedisCpdailyPhone)).OnlyX(ctx).Value)
|
||||
utils.OkOrPanic(err)
|
||||
if user.Edges.Agent != nil {
|
||||
basePoint = basePoint.Mul(user.Edges.Agent.Rate)
|
||||
|
||||
@@ -143,8 +143,8 @@ func (s *EmailServiceV1) sendEmail(ctx context.Context, to, subject, body string
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
task := queue.AddTask(ctx, asynq.NewTask(cst2.TaskEmail, payload.Bytes()), asynq.Queue(cst2.TaskQueueEmail),
|
||||
asynq.MaxRetry(3),
|
||||
asynq.Timeout(10*time.Minute),
|
||||
asynq.MaxRetry(1),
|
||||
asynq.Timeout(5*time.Minute),
|
||||
)
|
||||
logger.Logger.Info("添加任务: ", zap.String("task_id", task.ID), zap.String("邮箱", to))
|
||||
return nil
|
||||
|
||||
+34
-32
@@ -15,39 +15,41 @@ import (
|
||||
)
|
||||
|
||||
type globalService struct {
|
||||
UserLogin *user_login.UserLoginServiceV1
|
||||
UserInfo *user_login.UserInfoServiceV1
|
||||
Role *role.RoleServiceV1
|
||||
Menu *menu.MenuServiceV1
|
||||
Agent *agent.AgentServiceV1
|
||||
Email *email.EmailServiceV1
|
||||
User *user.UserServiceV1
|
||||
CpUser *cpdaily.CpUserServiceV1
|
||||
CpTask *cpdaily.CpTaskServiceV1
|
||||
CpImage *cpdaily.CpImageServiceV1
|
||||
CpTaskLog *cpdaily.CpTaskLogServiceV1
|
||||
CpLeave *cpdaily.CpLeaveServiceV1
|
||||
Card *card.CardServiceV1
|
||||
Course *course.CourseServiceV1
|
||||
Config *config.ConfigServiceV1
|
||||
Balance *balance.BalanceRecordServiceV1
|
||||
UserLogin *user_login.UserLoginServiceV1
|
||||
UserInfo *user_login.UserInfoServiceV1
|
||||
Role *role.RoleServiceV1
|
||||
Menu *menu.MenuServiceV1
|
||||
Agent *agent.AgentServiceV1
|
||||
Email *email.EmailServiceV1
|
||||
User *user.UserServiceV1
|
||||
CpUser *cpdaily.CpUserServiceV1
|
||||
CpTask *cpdaily.CpTaskServiceV1
|
||||
CpImage *cpdaily.CpImageServiceV1
|
||||
CpTaskLog *cpdaily.CpTaskLogServiceV1
|
||||
CpLeave *cpdaily.CpLeaveServiceV1
|
||||
Card *card.CardServiceV1
|
||||
Course *course.CourseServiceV1
|
||||
SmartEduSC *course.SmartEduSCServiceV1
|
||||
Config *config.ConfigServiceV1
|
||||
Balance *balance.BalanceRecordServiceV1
|
||||
}
|
||||
|
||||
var Global = globalService{
|
||||
UserLogin: user_login.NewV1(),
|
||||
UserInfo: user_login.NewUserInfoServiceV1(),
|
||||
Role: role.NewV1(),
|
||||
Menu: menu.NewV1(),
|
||||
Agent: agent.NewV1(),
|
||||
Email: email.NewV1(),
|
||||
User: user.NewV1(),
|
||||
CpUser: cpdaily.NewCpUserServiceV1(),
|
||||
CpTask: cpdaily.NewCpTaskServiceV1(),
|
||||
CpImage: cpdaily.NewCpImageServiceV1(),
|
||||
CpTaskLog: cpdaily.NewCpTaskLogServiceV1(),
|
||||
CpLeave: cpdaily.NewCpLeaveServiceV1(),
|
||||
Card: card.NewCardServiceV1(),
|
||||
Course: course.NewCourseServiceV1(),
|
||||
Config: config.NewV1(),
|
||||
Balance: balance.NewBalanceRecordServiceV1(),
|
||||
UserLogin: user_login.NewV1(),
|
||||
UserInfo: user_login.NewUserInfoServiceV1(),
|
||||
Role: role.NewV1(),
|
||||
Menu: menu.NewV1(),
|
||||
Agent: agent.NewV1(),
|
||||
Email: email.NewV1(),
|
||||
User: user.NewV1(),
|
||||
CpUser: cpdaily.NewCpUserServiceV1(),
|
||||
CpTask: cpdaily.NewCpTaskServiceV1(),
|
||||
CpImage: cpdaily.NewCpImageServiceV1(),
|
||||
CpTaskLog: cpdaily.NewCpTaskLogServiceV1(),
|
||||
CpLeave: cpdaily.NewCpLeaveServiceV1(),
|
||||
Card: card.NewCardServiceV1(),
|
||||
Course: course.NewCourseServiceV1(),
|
||||
SmartEduSC: course.NewSmartEduSCServiceV1(),
|
||||
Config: config.NewV1(),
|
||||
Balance: balance.NewBalanceRecordServiceV1(),
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"likedream-goframe/internal/api/v1"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
cst2 "likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/db"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/ent/sysconfig"
|
||||
"likedream-goframe/internal/ent/sysrole"
|
||||
"likedream-goframe/internal/ent/sysuser"
|
||||
"likedream-goframe/internal/framework/redis"
|
||||
@@ -47,6 +48,12 @@ func (s *UserLoginServiceV1) Login(ctx context.Context, req *v1.UserLoginReq) *u
|
||||
}
|
||||
|
||||
func (s *UserLoginServiceV1) Register(ctx context.Context, req *v1.UserRegisterReq) *utils.Response[any] {
|
||||
// 检查是否允许注册
|
||||
allowRegister := s.IsRegistrationAllowed(ctx)
|
||||
if !allowRegister {
|
||||
return utils.FailParam("系统当前不允许注册")
|
||||
}
|
||||
|
||||
//邀请码查询
|
||||
var parent *ent.SysUser
|
||||
if req.InviteCode != "" {
|
||||
@@ -255,3 +262,18 @@ func (s *UserLoginServiceV1) GetResetPasswordInfo(ctx context.Context, req *v1.R
|
||||
Email: user.Email,
|
||||
})
|
||||
}
|
||||
|
||||
// IsRegistrationAllowed 检查是否允许注册
|
||||
func (s *UserLoginServiceV1) IsRegistrationAllowed(ctx context.Context) bool {
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ(cst2.ConfigSystemAllowRegister)).
|
||||
Only(ctx)
|
||||
|
||||
// 如果配置不存在或查询出错,默认允许注册
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查配置值,"true" 或 "1" 表示允许注册
|
||||
return config.Value == "true" || config.Value == "1"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user