Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55fc94c37f | |||
| a590f58c6c | |||
| 0959ca117c | |||
| 3641fb1782 | |||
| d0e7652bf6 | |||
| 07e2438f93 | |||
| d1b954ab8e | |||
| 24da8bde3a | |||
| 16692177d4 | |||
| 5f81a7ba5e | |||
| 58fb282fe8 | |||
| f64b4b5498 | |||
| 84620989ad | |||
| af7fcc4cb4 | |||
| c206a0863e | |||
| b068e178da | |||
| 02737ef743 | |||
| 74a1134cf1 | |||
| b6f777f997 | |||
| 2dc233631e | |||
| 0c60eb5ae4 | |||
| 450c026a6f | |||
| d9d6acee5f | |||
| 164d8956de | |||
| 3f1a08374e | |||
| 334d597f9a | |||
| 98eb15f4e4 | |||
| 3a2e679b85 | |||
| 921192a914 | |||
| 107b472cf7 | |||
| 14108f6ba3 |
@@ -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
|
||||
+2
-2
@@ -50,8 +50,8 @@ func Run() {
|
||||
g.Log().Info(ctx, "server is going to shut down...")
|
||||
api.StopServer()
|
||||
g.Log().Info(ctx, "api server stopped")
|
||||
//schedule.Stop()
|
||||
//g.Log().Info(ctx, "schedule stopped")
|
||||
schedule.Stop()
|
||||
g.Log().Info(ctx, "schedule stopped")
|
||||
redis.Shutdown()
|
||||
g.Log().Info(ctx, "redis client closed")
|
||||
os.Exit(0)
|
||||
|
||||
+7
-5
@@ -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"]
|
||||
@@ -51,6 +51,8 @@ sliderIds = ["1018764144934965", "sdhyxy", "hqdx"]
|
||||
|
||||
|
||||
[basePoint]
|
||||
cpdaily = "0.5"
|
||||
cpdailyTask = "0.5"
|
||||
cpdailyLeave = "4"
|
||||
cpdailyPhone = "0.25"
|
||||
[task]
|
||||
concurrency = 20
|
||||
concurrency = 80
|
||||
@@ -0,0 +1,289 @@
|
||||
# AI 答题配置接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
AI 答题功能用于在刷课过程中自动完成作业任务点。系统支持多种 AI 服务商,管理员可以通过配置接口设置 AI 参数。
|
||||
|
||||
核心代码位于 `internal/application/quesbank` 包,提供通用的题库答题能力,可被不同平台(学习通、智慧教育等)复用。
|
||||
|
||||
## 接口列表
|
||||
|
||||
### 1. 获取支持的 AI 类型列表
|
||||
|
||||
获取系统支持的所有 AI 类型及其说明。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
GET /v1/system/ai-config/types
|
||||
```
|
||||
|
||||
**响应**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"types": [
|
||||
{
|
||||
"type": "chatglm",
|
||||
"name": "智谱AI (ChatGLM)",
|
||||
"description": "智谱AI大模型,支持glm-4等模型",
|
||||
"needUrl": false
|
||||
},
|
||||
{
|
||||
"type": "xinghuo",
|
||||
"name": "讯飞星火",
|
||||
"description": "讯飞星火认知大模型",
|
||||
"needUrl": false
|
||||
},
|
||||
{
|
||||
"type": "tongyi",
|
||||
"name": "通义千问",
|
||||
"description": "阿里云通义千问大模型",
|
||||
"needUrl": false
|
||||
},
|
||||
{
|
||||
"type": "doubao",
|
||||
"name": "豆包",
|
||||
"description": "字节跳动豆包大模型",
|
||||
"needUrl": false
|
||||
},
|
||||
{
|
||||
"type": "openai",
|
||||
"name": "OpenAI",
|
||||
"description": "OpenAI GPT系列模型",
|
||||
"needUrl": false
|
||||
},
|
||||
{
|
||||
"type": "other",
|
||||
"name": "其他 (兼容OpenAI格式)",
|
||||
"description": "支持任何兼容OpenAI API格式的服务",
|
||||
"needUrl": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 获取当前 AI 配置
|
||||
|
||||
获取当前系统配置的 AI 参数(API密钥会脱敏显示)。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
GET /v1/system/ai-config
|
||||
```
|
||||
|
||||
**响应**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"type": "chatglm",
|
||||
"url": "",
|
||||
"model": "glm-4",
|
||||
"apiKey": "sk-1****************************xyz"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| type | string | AI类型,可选值见类型列表 |
|
||||
| url | string | API地址,仅 `other` 类型需要 |
|
||||
| model | string | 模型名称 |
|
||||
| apiKey | string | API密钥(脱敏显示) |
|
||||
|
||||
---
|
||||
|
||||
### 3. 更新 AI 配置
|
||||
|
||||
更新系统的 AI 配置参数。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
PUT /v1/system/ai-config
|
||||
```
|
||||
|
||||
**权限**
|
||||
|
||||
需要 `sys:ai:edit` 权限
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "chatglm",
|
||||
"url": "",
|
||||
"model": "glm-4",
|
||||
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| type | string | 是 | AI类型,可选值:`chatglm`, `xinghuo`, `tongyi`, `doubao`, `openai`, `other` |
|
||||
| url | string | 否 | API地址,当 type 为 `other` 时必填 |
|
||||
| model | string | 否 | 模型名称,不填则使用默认模型 |
|
||||
| apiKey | string | 是 | API密钥 |
|
||||
|
||||
**各 AI 类型默认模型**
|
||||
|
||||
| AI类型 | 默认模型 |
|
||||
|--------|----------|
|
||||
| chatglm | glm-4 |
|
||||
| xinghuo | generalv3.5 |
|
||||
| tongyi | qwen-plus |
|
||||
| doubao | 需指定 |
|
||||
| openai | gpt-3.5-turbo |
|
||||
| other | 需指定 |
|
||||
|
||||
**响应**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "AI配置更新成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 测试 AI 配置
|
||||
|
||||
测试 AI 配置是否可用,发送测试消息验证连接。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
POST /v1/system/ai-config/test
|
||||
```
|
||||
|
||||
**权限**
|
||||
|
||||
需要 `sys:ai:edit` 权限
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "chatglm",
|
||||
"url": "",
|
||||
"model": "glm-4",
|
||||
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**
|
||||
|
||||
成功:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"success": true,
|
||||
"message": "AI连接测试成功,响应: 测试成功..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
失败:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"success": false,
|
||||
"message": "AI连接测试失败: failed to execute request: ..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AI 服务商配置指南
|
||||
|
||||
### 智谱AI (ChatGLM)
|
||||
|
||||
1. 访问 [智谱AI开放平台](https://open.bigmodel.cn/)
|
||||
2. 注册并创建 API Key
|
||||
3. 配置参数:
|
||||
- type: `chatglm`
|
||||
- model: `glm-4` 或 `glm-3-turbo`
|
||||
- apiKey: 你的 API Key
|
||||
|
||||
### 讯飞星火
|
||||
|
||||
1. 访问 [讯飞开放平台](https://www.xfyun.cn/)
|
||||
2. 创建应用并获取 API Key
|
||||
3. 配置参数:
|
||||
- type: `xinghuo`
|
||||
- model: `generalv3.5` 或其他版本
|
||||
- apiKey: 你的 API Key
|
||||
|
||||
### 通义千问
|
||||
|
||||
1. 访问 [阿里云百炼](https://dashscope.aliyun.com/)
|
||||
2. 开通服务并获取 API Key
|
||||
3. 配置参数:
|
||||
- type: `tongyi`
|
||||
- model: `qwen-plus` 或 `qwen-turbo`
|
||||
- apiKey: 你的 API Key
|
||||
|
||||
### 豆包
|
||||
|
||||
1. 访问 [火山引擎](https://www.volcengine.com/)
|
||||
2. 开通豆包大模型服务
|
||||
3. 配置参数:
|
||||
- type: `doubao`
|
||||
- model: 你的模型端点 ID
|
||||
- apiKey: 你的 API Key
|
||||
|
||||
### OpenAI
|
||||
|
||||
1. 访问 [OpenAI Platform](https://platform.openai.com/)
|
||||
2. 创建 API Key
|
||||
3. 配置参数:
|
||||
- type: `openai`
|
||||
- model: `gpt-3.5-turbo` 或 `gpt-4`
|
||||
- apiKey: 你的 API Key
|
||||
|
||||
### 其他兼容服务
|
||||
|
||||
支持任何兼容 OpenAI API 格式的服务(如 DeepSeek、Moonshot 等)。
|
||||
|
||||
配置参数:
|
||||
- type: `other`
|
||||
- url: API 地址(如 `https://api.deepseek.com/v1/chat/completions`)
|
||||
- model: 模型名称
|
||||
- apiKey: 你的 API Key
|
||||
|
||||
---
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. 配置 AI 后,系统在执行刷课任务时会自动使用 AI 完成作业任务点
|
||||
2. 作业答案默认仅保存不提交,需要用户手动确认后提交
|
||||
3. 如果未配置 AI,作业任务点将被跳过
|
||||
4. 建议先使用测试接口验证配置是否正确
|
||||
|
||||
## 错误码
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 400 | 参数错误或配置不完整 |
|
||||
| 403 | 无权限操作 |
|
||||
| 500 | 服务器内部错误 |
|
||||
@@ -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`
|
||||
@@ -31,6 +31,7 @@ require (
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/net v0.45.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
resty.dev/v3 v3.0.0-beta.3
|
||||
@@ -106,7 +107,6 @@ require (
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/image v0.29.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
|
||||
@@ -44,6 +44,7 @@ func StartServer(ctx context.Context) {
|
||||
systemGroup.Bind(menu.NewV1())
|
||||
systemGroup.Bind(user.NewUserCtrlV1())
|
||||
systemGroup.Bind(config.NewConfigCtrlV1())
|
||||
systemGroup.Bind(config.NewAIConfigCtrlV1())
|
||||
//systemGroup.Bind(agent.NewV1())
|
||||
// 用户信息路由
|
||||
userGroup := group.Group("/user").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
@@ -59,12 +60,14 @@ func StartServer(ctx context.Context) {
|
||||
cpdailyGroup.Bind(cpdaily.NewCpTaskCtrlV1())
|
||||
cpdailyGroup.Bind(cpdaily.NewCpImageCtrlV1())
|
||||
cpdailyGroup.Bind(cpdaily.NewCpTaskLogCtrlV1())
|
||||
cpdailyGroup.Bind(cpdaily.NewCpLeaveCtrlV1())
|
||||
// 娱乐
|
||||
musicGroup := group.Group("/music").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
musicGroup.Bind(music.NewMusicCtrlV1())
|
||||
// 网课
|
||||
courseGroup := group.Group("/course").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
courseGroup.Bind(course.NewXxtCtrlV1())
|
||||
courseGroup.Bind(course.NewSmartEduSCCtrlV1())
|
||||
})
|
||||
|
||||
s.SetPort(port.Int())
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// AIConfigGetReq 获取AI配置请求
|
||||
type AIConfigGetReq struct {
|
||||
g.Meta `path:"/ai-config" method:"get" tags:"AIConfig" summary:"获取AI配置"`
|
||||
}
|
||||
|
||||
// AIConfigGetResp AI配置响应
|
||||
type AIConfigGetResp struct {
|
||||
Type string `json:"type"` // AI类型: chatglm, xinghuo, tongyi, doubao, openai, other
|
||||
URL string `json:"url"` // API地址(other类型必填)
|
||||
Model string `json:"model"` // 模型名称
|
||||
APIKey string `json:"apiKey"` // API密钥(返回时脱敏)
|
||||
}
|
||||
|
||||
// AIConfigUpdateReq 更新AI配置请求
|
||||
type AIConfigUpdateReq struct {
|
||||
g.Meta `path:"/ai-config" method:"put" tags:"AIConfig" summary:"更新AI配置" permission:"sys:ai:edit"`
|
||||
Type string `json:"type" v:"required|in:chatglm,xinghuo,tongyi,doubao,openai,other#AI类型不能为空|AI类型不合法"`
|
||||
URL string `json:"url"` // API地址(other类型必填)
|
||||
Model string `json:"model"` // 模型名称
|
||||
APIKey string `json:"apiKey" v:"required#API密钥不能为空"` // API密钥
|
||||
}
|
||||
|
||||
// AIConfigTestReq 测试AI配置请求
|
||||
type AIConfigTestReq struct {
|
||||
g.Meta `path:"/ai-config/test" method:"post" tags:"AIConfig" summary:"测试AI配置" permission:"sys:ai:edit"`
|
||||
Type string `json:"type" v:"required|in:chatglm,xinghuo,tongyi,doubao,openai,other#AI类型不能为空|AI类型不合法"`
|
||||
URL string `json:"url"`
|
||||
Model string `json:"model"`
|
||||
APIKey string `json:"apiKey" v:"required#API密钥不能为空"`
|
||||
}
|
||||
|
||||
// AIConfigTestResp 测试AI配置响应
|
||||
type AIConfigTestResp struct {
|
||||
Success bool `json:"success"` // 是否成功
|
||||
Message string `json:"message"` // 测试结果消息
|
||||
}
|
||||
|
||||
// AITypeListReq 获取支持的AI类型列表请求
|
||||
type AITypeListReq struct {
|
||||
g.Meta `path:"/ai-config/types" method:"get" tags:"AIConfig" summary:"获取支持的AI类型列表"`
|
||||
}
|
||||
|
||||
// AITypeItem AI类型项
|
||||
type AITypeItem struct {
|
||||
Type string `json:"type"` // 类型标识
|
||||
Name string `json:"name"` // 显示名称
|
||||
Description string `json:"description"` // 描述
|
||||
NeedURL bool `json:"needUrl"` // 是否需要自定义URL
|
||||
}
|
||||
|
||||
// AITypeListResp AI类型列表响应
|
||||
type AITypeListResp struct {
|
||||
Types []AITypeItem `json:"types"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -30,10 +30,13 @@ type LoginGetCode struct {
|
||||
|
||||
type AddByCode struct {
|
||||
g.Meta `path:"/user/addByCode" method:"post" tags:"CpUser" summary:"手机号登录"`
|
||||
Id int64 `json:"id"`
|
||||
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||
Mobile string `json:"mobile" v:"required#手机号不能为空"`
|
||||
SchoolId string `json:"school_id"`
|
||||
Code string `json:"code"`
|
||||
Email string `json:"email" v:"email|regex:^.*@qq\\.com$#邮箱格式不正确|必须是QQ邮箱"`
|
||||
}
|
||||
|
||||
type CpUserConfirmAddReq struct {
|
||||
|
||||
@@ -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"
|
||||
@@ -62,13 +67,29 @@ func (c *CpdailyReportApp) GetLeaveList() []leavetype.LeaveListOne {
|
||||
|
||||
var listList []leavetype.LeaveListOne
|
||||
for _, row := range rows.Array() {
|
||||
leaveOne := leavetype.LeaveListOne{
|
||||
Id: row.Get("id").String(),
|
||||
LeaveType: row.Get("leaveType").String(),
|
||||
CreateTime: row.Get("createTime").String(),
|
||||
Reason: row.Get("reason").String(),
|
||||
AceEndTime: row.Get("aceEndTime").String(),
|
||||
// 已完成
|
||||
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(),
|
||||
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
|
||||
@@ -99,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
|
||||
}
|
||||
|
||||
@@ -136,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
@@ -6,6 +6,7 @@ import (
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/ent/cpuser"
|
||||
"likedream-goframe/internal/utils"
|
||||
"strings"
|
||||
|
||||
@@ -86,7 +87,7 @@ func ActivateSession(ctx context.Context, host string, client *resty.Client) {
|
||||
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
}
|
||||
|
||||
func CookieEnabled(ctx context.Context, host string, client *resty.Client) bool {
|
||||
func CookieEnabled(ctx context.Context, host string, client *resty.Client, loginType cpuser.LoginType) bool {
|
||||
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
host = strings.TrimSuffix(host, "/")
|
||||
sessionResponse, err := client.R().
|
||||
@@ -95,7 +96,11 @@ func CookieEnabled(ctx context.Context, host string, client *resty.Client) bool
|
||||
Post(fmt.Sprintf("%s/wec-counselor-attendance-apps/student/attendance/getStuAttendacesInOneDay", host))
|
||||
|
||||
utils.OkOrPanic(err)
|
||||
return sessionResponse.StatusCode() != 302
|
||||
needLogin := sessionResponse.StatusCode() == 302
|
||||
if needLogin && loginType == cpuser.LoginTypePhone {
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, "登录信息失效,请重新通过短信验证码登录"))
|
||||
}
|
||||
return !needLogin
|
||||
}
|
||||
|
||||
func GetClient(ctx context.Context, cookie string) *resty.Client {
|
||||
|
||||
@@ -75,15 +75,15 @@ func NewSmsWithoutDeviceID() *SmsLoginHandler {
|
||||
|
||||
type SmsLoginHandler struct {
|
||||
deviceID string
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func (s *SmsLoginHandler) SendSms(ctx context.Context, phone string) *CpdailySmsSendSmsData {
|
||||
jar := cookiejar.NewPersistCookieJar()
|
||||
|
||||
phoneEncrypted := base64.StdEncoding.EncodeToString(cryptoutil.AesEncrypt([]byte(phone)))
|
||||
|
||||
req := httpclient.
|
||||
NewCpdailyClient().
|
||||
s.client = httpclient.NewCpdailyClient()
|
||||
req := s.client.
|
||||
SetCookieJar(jar).
|
||||
R().
|
||||
SetContext(utils.NewCpdailySmsContextBuilder().
|
||||
@@ -126,8 +126,9 @@ func (s *SmsLoginHandler) LoginByCode(ctx context.Context, phone, code string, j
|
||||
"a": encodedPayload,
|
||||
"b": cryptoutil.EncryptVersion,
|
||||
}
|
||||
|
||||
req := httpclient.NewCpdailyClient().SetCookieJar(jar).R().
|
||||
s.client = httpclient.NewCpdailyClient()
|
||||
s.client.SetCookieJar(jar)
|
||||
req := s.client.R().
|
||||
SetBody(payload).
|
||||
SetContext(utils.NewCpdailySmsContextBuilder().
|
||||
WithDeviceID(s.deviceID).
|
||||
@@ -198,10 +199,8 @@ func (s *SmsLoginHandler) SetLoginCookie(ctx context.Context, jar http.CookieJar
|
||||
jar.SetCookies(targetSchoolUrl, []*http.Cookie{cookie})
|
||||
jar.SetCookies(targetApiUrl, []*http.Cookie{cookie})
|
||||
}
|
||||
|
||||
req := httpclient.NewCpdailyClient().
|
||||
SetRedirectPolicy(resty.FlexibleRedirectPolicy(50)).
|
||||
SetCookieJar(jar).R().
|
||||
s.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(50))
|
||||
req := s.client.R().
|
||||
SetHeader("AmpCookies", ampToken).
|
||||
SetContext(ctx)
|
||||
|
||||
@@ -273,3 +272,9 @@ func setSmsRequestHeaders(ctx context.Context, request *resty.Request) {
|
||||
"CpdailyInfo": encodedCpdailyInfo,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SmsLoginHandler) GetCookieString() string {
|
||||
cookieJar := s.client.CookieJar()
|
||||
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||
return serializableJar.Serialize()
|
||||
}
|
||||
|
||||
@@ -6,4 +6,10 @@ type LeaveListOne struct {
|
||||
CreateTime string `json:"createTime"`
|
||||
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 不需要
|
||||
}
|
||||
|
||||
@@ -693,3 +693,18 @@ func (c *CpdailySleepCheckTask) Submit(ctx context.Context, form map[string]*gva
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, responseJSON.Get("message").String()), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CpdailySleepCheckTask) GetYearAndMonth(ctx context.Context) (string, string) {
|
||||
// Get current time in Shanghai timezone
|
||||
timeNow := time.Now().In(cst.TimeZoneShangHai)
|
||||
|
||||
// Check if current day is within first 2 days of the month
|
||||
if timeNow.Day() <= 2 {
|
||||
// Return previous month
|
||||
prevTime := timeNow.AddDate(0, -1, 0)
|
||||
return strconv.Itoa(prevTime.Year()), strconv.Itoa(int(prevTime.Month()))
|
||||
} else {
|
||||
// Return current month
|
||||
return strconv.Itoa(timeNow.Year()), strconv.Itoa(int(timeNow.Month()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/testutil"
|
||||
"testing"
|
||||
|
||||
"github.com/joomcode/errorx"
|
||||
)
|
||||
|
||||
func TestCpdailySleepCheckTask_QueryFormByTitle(t *testing.T) {
|
||||
loginClient, schoolDetail := testutil.LoginTestUser()
|
||||
|
||||
task := NewSleepCheckTask(loginClient, schoolDetail.WecloudUrl)
|
||||
tasks := task.QueryFormByTitle(context.Background(), "查寝")
|
||||
|
||||
if len(tasks) == 0 {
|
||||
t.Error("未查询到查寝任务")
|
||||
} else {
|
||||
t.Logf("查询到查寝任务: %v", tasks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCpdailySleepCheckTask_GetRecentTaskTitle(t *testing.T) {
|
||||
loginClient, schoolDetail := testutil.LoginTestUser()
|
||||
|
||||
task := NewSleepCheckTask(loginClient, schoolDetail.WecloudUrl)
|
||||
|
||||
titles := task.GetRecentTaskTitle(context.Background())
|
||||
|
||||
if len(titles) == 0 {
|
||||
t.Error("未查询到查寝任务标题")
|
||||
} else {
|
||||
t.Logf("查询到查寝任务标题: %v", titles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCpdailySleepCheckTask_QueryFormByYearMonth(t *testing.T) {
|
||||
loginClient, schoolDetail := testutil.LoginTestUser()
|
||||
|
||||
task := NewSleepCheckTask(loginClient, schoolDetail.WecloudUrl)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
titleGlob string
|
||||
year string
|
||||
month string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "查询当前月份查寝任务",
|
||||
titleGlob: "查寝",
|
||||
year: "2025",
|
||||
month: "6",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "查询单数字月份查寝任务",
|
||||
titleGlob: "查寝",
|
||||
year: "2025",
|
||||
month: "6", // 将会被格式化为 "06"
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "使用通配符模式匹配",
|
||||
titleGlob: "*查寝*",
|
||||
year: "2025",
|
||||
month: "6",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "查询不存在的任务",
|
||||
titleGlob: "不存在的任务",
|
||||
year: "2025",
|
||||
month: "6",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
oopsError, _ := errorx.ErrorFromPanic(r)
|
||||
if !tt.wantErr {
|
||||
t.Errorf("QueryFormByYearMonth() 意外的panic: %+v", oopsError)
|
||||
} else {
|
||||
t.Logf("预期的错误: %+v", oopsError)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
form := task.QueryFormByYearMonth(context.Background(), tt.titleGlob, tt.year, tt.month)
|
||||
|
||||
if tt.wantErr {
|
||||
t.Error("预期应该出现错误,但没有出现")
|
||||
return
|
||||
}
|
||||
|
||||
if form == nil {
|
||||
t.Error("未查询到查寝任务表单")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证返回结果的结构
|
||||
if realTitle := form["realTitle"].String(); realTitle == "" {
|
||||
t.Error("返回结果中缺少 realTitle 或 realTitle 为空")
|
||||
} else {
|
||||
t.Logf("任务真实标题: %s", realTitle)
|
||||
}
|
||||
|
||||
if taskInfo := form["taskInfo"].Map(); taskInfo == nil {
|
||||
t.Error("返回结果中缺少 taskInfo 或 taskInfo 为空")
|
||||
} else {
|
||||
t.Logf("任务信息包含字段: %d", len(taskInfo))
|
||||
|
||||
// 验证关键字段是否存在
|
||||
requiredFields := []string{"taskName", "signWid", "signInstanceWid"}
|
||||
for _, field := range requiredFields {
|
||||
if _, exists := taskInfo[field]; !exists {
|
||||
t.Errorf("taskInfo 中缺少必要字段: %s", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rows := form["rows"].Map(); rows == nil {
|
||||
t.Error("返回结果中缺少 rows 或 rows 为空")
|
||||
} else {
|
||||
t.Logf("表单数据包含字段: %d", len(rows))
|
||||
}
|
||||
|
||||
t.Logf("查询到查寝任务表单,标题: %v", form["realTitle"])
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/testutil"
|
||||
"testing"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
func TestGetHistoryForm(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
loginClient, schoolDetail := testutil.LoginTestUser()
|
||||
task := NewSleepCheckTask(loginClient, schoolDetail.WecloudUrl)
|
||||
y, m := task.GetYearAndMonth(ctx)
|
||||
month := task.QueryFormByYearMonth(ctx, "晚查寝", y, m)
|
||||
marshal, _ := sonic.Marshal(month)
|
||||
t.Log(string(marshal))
|
||||
}
|
||||
@@ -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,32 @@
|
||||
package quesbank
|
||||
|
||||
// AIType AI类型
|
||||
type AIType string
|
||||
|
||||
const (
|
||||
AITypeChatGLM AIType = "chatglm" // 智谱AI
|
||||
AITypeXingHuo AIType = "xinghuo" // 讯飞星火
|
||||
AITypeTongYi AIType = "tongyi" // 通义千问
|
||||
AITypeDouBao AIType = "doubao" // 豆包
|
||||
AITypeOpenAI AIType = "openai" // OpenAI
|
||||
AITypeOther AIType = "other" // 其他兼容OpenAI格式的API
|
||||
)
|
||||
|
||||
// AIConfig AI配置
|
||||
type AIConfig struct {
|
||||
Type AIType `json:"type"` // AI类型
|
||||
URL string `json:"url"` // API地址(Other类型必填)
|
||||
Model string `json:"model"` // 模型名称
|
||||
APIKey string `json:"apiKey"` // API密钥
|
||||
}
|
||||
|
||||
// AIChatMessage 聊天消息
|
||||
type AIChatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// AIChatMessages 聊天消息列表
|
||||
type AIChatMessages struct {
|
||||
Messages []AIChatMessage `json:"messages"`
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package quesbank
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AIAnswerService AI答题服务
|
||||
type AIAnswerService struct {
|
||||
config AIConfig
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewAIAnswerService 创建AI答题服务
|
||||
func NewAIAnswerService(config AIConfig) *AIAnswerService {
|
||||
return &AIAnswerService{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAnswer 获取AI答案
|
||||
func (s *AIAnswerService) GetAnswer(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
switch s.config.Type {
|
||||
case AITypeChatGLM:
|
||||
return s.chatGLMRequest(ctx, messages)
|
||||
case AITypeXingHuo:
|
||||
return s.xingHuoRequest(ctx, messages)
|
||||
case AITypeTongYi:
|
||||
return s.tongYiRequest(ctx, messages)
|
||||
case AITypeDouBao:
|
||||
return s.douBaoRequest(ctx, messages)
|
||||
case AITypeOpenAI:
|
||||
return s.openAIRequest(ctx, messages)
|
||||
case AITypeOther:
|
||||
return s.otherRequest(ctx, messages)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported AI type: %s", s.config.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// chatGLMRequest 智谱AI请求
|
||||
func (s *AIAnswerService) chatGLMRequest(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
url := "https://open.bigmodel.cn/api/paas/v4/chat/completions"
|
||||
model := s.config.Model
|
||||
if model == "" {
|
||||
model = "glm-4"
|
||||
}
|
||||
return s.doOpenAIStyleRequest(ctx, url, model, messages)
|
||||
}
|
||||
|
||||
// xingHuoRequest 讯飞星火请求
|
||||
func (s *AIAnswerService) xingHuoRequest(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
url := "https://spark-api-open.xf-yun.com/v1/chat/completions"
|
||||
model := s.config.Model
|
||||
if model == "" {
|
||||
model = "generalv3.5"
|
||||
}
|
||||
return s.doOpenAIStyleRequest(ctx, url, model, messages)
|
||||
}
|
||||
|
||||
// tongYiRequest 通义千问请求
|
||||
func (s *AIAnswerService) tongYiRequest(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
url := "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
|
||||
model := s.config.Model
|
||||
if model == "" {
|
||||
model = "qwen-plus"
|
||||
}
|
||||
return s.doOpenAIStyleRequest(ctx, url, model, messages)
|
||||
}
|
||||
|
||||
// douBaoRequest 豆包请求
|
||||
func (s *AIAnswerService) douBaoRequest(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
url := "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
|
||||
return s.doOpenAIStyleRequest(ctx, url, s.config.Model, messages)
|
||||
}
|
||||
|
||||
// openAIRequest OpenAI请求
|
||||
func (s *AIAnswerService) openAIRequest(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
url := "https://api.openai.com/v1/chat/completions"
|
||||
model := s.config.Model
|
||||
if model == "" {
|
||||
model = "gpt-3.5-turbo"
|
||||
}
|
||||
return s.doOpenAIStyleRequest(ctx, url, model, messages)
|
||||
}
|
||||
|
||||
// otherRequest 其他兼容OpenAI格式的API请求
|
||||
func (s *AIAnswerService) otherRequest(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
if s.config.URL == "" {
|
||||
return "", errors.New("API URL is required for 'other' AI type")
|
||||
}
|
||||
return s.doOpenAIStyleRequest(ctx, s.config.URL, s.config.Model, messages)
|
||||
}
|
||||
|
||||
// doOpenAIStyleRequest 执行OpenAI风格的API请求
|
||||
func (s *AIAnswerService) doOpenAIStyleRequest(ctx context.Context, url, model string, messages AIChatMessages) (string, error) {
|
||||
requestBody := map[string]interface{}{
|
||||
"model": model,
|
||||
"messages": messages.Messages,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for retry := 0; retry < 3; retry++ {
|
||||
result, err := s.doHTTPRequest(ctx, url, jsonData)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
lastErr = err
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return "", lastErr
|
||||
}
|
||||
|
||||
// doHTTPRequest 执行HTTP请求
|
||||
func (s *AIAnswerService) doHTTPRequest(ctx context.Context, url string, jsonData []byte) (string, error) {
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+s.config.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %v", err)
|
||||
}
|
||||
|
||||
var responseMap map[string]interface{}
|
||||
if err := json.Unmarshal(body, &responseMap); err != nil {
|
||||
return "", fmt.Errorf("failed to parse response: %v, body: %s", err, string(body))
|
||||
}
|
||||
|
||||
choices, ok := responseMap["choices"].([]interface{})
|
||||
if !ok || len(choices) == 0 {
|
||||
return "", fmt.Errorf("AI response not found, response: %s", string(body))
|
||||
}
|
||||
|
||||
message, ok := choices[0].(map[string]interface{})["message"].(map[string]interface{})
|
||||
if !ok {
|
||||
return "", errors.New("failed to parse message from response")
|
||||
}
|
||||
|
||||
content, ok := message["content"].(string)
|
||||
if !ok {
|
||||
return "", errors.New("content field missing in response")
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package quesbank
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseAIAnswer 解析AI答案
|
||||
func ParseAIAnswer(aiResponse string) ([]string, error) {
|
||||
aiResponse = strings.TrimSpace(aiResponse)
|
||||
|
||||
// 尝试直接解析JSON数组
|
||||
var answers []string
|
||||
if err := json.Unmarshal([]byte(aiResponse), &answers); err == nil {
|
||||
return answers, nil
|
||||
}
|
||||
|
||||
// 尝试从响应中提取JSON数组
|
||||
start := strings.Index(aiResponse, "[")
|
||||
end := strings.LastIndex(aiResponse, "]")
|
||||
if start != -1 && end != -1 && end > start {
|
||||
jsonStr := aiResponse[start : end+1]
|
||||
if err := json.Unmarshal([]byte(jsonStr), &answers); err == nil {
|
||||
return answers, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 如果都失败,返回原始响应作为单个答案
|
||||
return []string{aiResponse}, nil
|
||||
}
|
||||
|
||||
// MatchAnswerToOption 将AI答案匹配到选项
|
||||
func MatchAnswerToOption(answer string, options map[string]string) string {
|
||||
// 精确匹配
|
||||
for key, val := range options {
|
||||
if val == answer || strings.TrimSpace(val) == strings.TrimSpace(answer) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
// 模糊匹配(包含关系)
|
||||
for key, val := range options {
|
||||
if strings.Contains(val, answer) || strings.Contains(answer, val) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
// 相似度匹配
|
||||
bestKey := ""
|
||||
bestScore := 0.0
|
||||
for key, val := range options {
|
||||
score := similarity(answer, val)
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
bestKey = key
|
||||
}
|
||||
}
|
||||
if bestScore >= 0.5 {
|
||||
return bestKey
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// similarity 计算字符串相似度(简单实现)
|
||||
func similarity(s1, s2 string) float64 {
|
||||
if s1 == s2 {
|
||||
return 1.0
|
||||
}
|
||||
if len(s1) == 0 || len(s2) == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// 使用最长公共子序列计算相似度
|
||||
lcs := longestCommonSubsequence(s1, s2)
|
||||
return float64(lcs) / float64(max(len(s1), len(s2)))
|
||||
}
|
||||
|
||||
// longestCommonSubsequence 计算最长公共子序列长度
|
||||
func longestCommonSubsequence(s1, s2 string) int {
|
||||
m, n := len(s1), len(s2)
|
||||
dp := make([][]int, m+1)
|
||||
for i := range dp {
|
||||
dp[i] = make([]int, n+1)
|
||||
}
|
||||
|
||||
for i := 1; i <= m; i++ {
|
||||
for j := 1; j <= n; j++ {
|
||||
if s1[i-1] == s2[j-1] {
|
||||
dp[i][j] = dp[i-1][j-1] + 1
|
||||
} else {
|
||||
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[m][n]
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package quesbank
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BuildQuestionPrompt 构建题目提示词
|
||||
func BuildQuestionPrompt(question Question) AIChatMessages {
|
||||
var messages []AIChatMessage
|
||||
|
||||
switch question.Type {
|
||||
case QuestionTypeSingleChoice:
|
||||
messages = buildSingleChoicePrompt(question)
|
||||
case QuestionTypeMultipleChoice:
|
||||
messages = buildMultipleChoicePrompt(question)
|
||||
case QuestionTypeTrueFalse:
|
||||
messages = buildTrueFalsePrompt(question)
|
||||
case QuestionTypeFillBlank:
|
||||
messages = buildFillBlankPrompt(question)
|
||||
case QuestionTypeShortAnswer:
|
||||
messages = buildShortAnswerPrompt(question)
|
||||
default:
|
||||
messages = buildDefaultPrompt(question)
|
||||
}
|
||||
|
||||
return AIChatMessages{Messages: messages}
|
||||
}
|
||||
|
||||
// buildSingleChoicePrompt 构建单选题提示词
|
||||
func buildSingleChoicePrompt(q Question) []AIChatMessage {
|
||||
context := buildQuestionContext(q)
|
||||
return []AIChatMessage{
|
||||
{Role: "user", Content: "接下来你只需要回答选项对应内容即可...格式:[\"选项1\"]"},
|
||||
{Role: "user", Content: "就算你不知道选什么也随机选...无需回答任何解释!!!"},
|
||||
{Role: "user", Content: singleChoiceExample()},
|
||||
{Role: "user", Content: fmt.Sprintf("题目类型:单选题\n题目内容:%s", context)},
|
||||
}
|
||||
}
|
||||
|
||||
// buildMultipleChoicePrompt 构建多选题提示词
|
||||
func buildMultipleChoicePrompt(q Question) []AIChatMessage {
|
||||
context := buildQuestionContext(q)
|
||||
return []AIChatMessage{
|
||||
{Role: "user", Content: "接下来你只需要回答选项对应内容即可...格式:[\"选项1\",\"选项2\"]"},
|
||||
{Role: "user", Content: "就算你不知道选什么也随机选...无需回答任何解释!!!"},
|
||||
{Role: "user", Content: multipleChoiceExample()},
|
||||
{Role: "user", Content: fmt.Sprintf("题目类型:多选题\n题目内容:%s", context)},
|
||||
}
|
||||
}
|
||||
|
||||
// buildTrueFalsePrompt 构建判断题提示词
|
||||
func buildTrueFalsePrompt(q Question) []AIChatMessage {
|
||||
context := buildQuestionContext(q)
|
||||
return []AIChatMessage{
|
||||
{Role: "user", Content: "接下来你只需要回答\"正确\"或者\"错误\"即可...格式:[\"正确\"]"},
|
||||
{Role: "user", Content: "就算你不知道选什么也随机选...无需回答任何解释!!!"},
|
||||
{Role: "user", Content: trueFalseExample()},
|
||||
{Role: "user", Content: fmt.Sprintf("题目类型:判断题\n题目内容:%s", context)},
|
||||
}
|
||||
}
|
||||
|
||||
// buildFillBlankPrompt 构建填空题提示词
|
||||
func buildFillBlankPrompt(q Question) []AIChatMessage {
|
||||
return []AIChatMessage{
|
||||
{Role: "user", Content: "其中,\"( )\"相关字样的地方是你需要填写答案的地方...格式:[\"答案1\",\"答案2\"]"},
|
||||
{Role: "user", Content: "就算你不知道填什么也随机填...无需回答任何解释!!!"},
|
||||
{Role: "user", Content: fillBlankExample()},
|
||||
{Role: "user", Content: fmt.Sprintf("题目类型:填空题\n题目内容:%s", q.Text)},
|
||||
}
|
||||
}
|
||||
|
||||
// buildShortAnswerPrompt 构建简答题提示词
|
||||
func buildShortAnswerPrompt(q Question) []AIChatMessage {
|
||||
return []AIChatMessage{
|
||||
{Role: "user", Content: "这是一个简答题...格式:[\"答案\"],注意不要拆分答案!!!"},
|
||||
{Role: "user", Content: "就算你不知道答什么也随机答...无需回答任何解释!!!"},
|
||||
{Role: "user", Content: shortAnswerExample()},
|
||||
{Role: "user", Content: fmt.Sprintf("题目类型:简答题\n题目内容:%s", q.Text)},
|
||||
}
|
||||
}
|
||||
|
||||
// buildDefaultPrompt 构建默认提示词
|
||||
func buildDefaultPrompt(q Question) []AIChatMessage {
|
||||
context := buildQuestionContext(q)
|
||||
return []AIChatMessage{
|
||||
{Role: "user", Content: "请回答以下题目,格式:[\"答案\"]"},
|
||||
{Role: "user", Content: context},
|
||||
}
|
||||
}
|
||||
|
||||
// buildQuestionContext 构建题目上下文
|
||||
func buildQuestionContext(q Question) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(q.Text)
|
||||
sb.WriteString("\n")
|
||||
for key, val := range q.Options {
|
||||
sb.WriteString(fmt.Sprintf("%s. %s\n", key, val))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func singleChoiceExample() string {
|
||||
return `比如:
|
||||
题目类型:单选题
|
||||
题目内容:新中国是什么时候成立的
|
||||
A. 1949年10月5日
|
||||
B. 1949年10月1日
|
||||
C. 1949年09月1日
|
||||
D. 2002年10月1日
|
||||
|
||||
那么你应该回答选项B的内容:"["1949年10月1日"]"`
|
||||
}
|
||||
|
||||
func multipleChoiceExample() string {
|
||||
return `比如:
|
||||
题目类型:多选题
|
||||
题目内容:马克思关于资本积累的学说是剩余价值理论的重要组成部分...
|
||||
A. 资本主义扩大再生产的源泉
|
||||
B. 资本有机构成呈现不断降低趋势的根本原因
|
||||
C. 社会财富占有两极分化的重要原因
|
||||
D. 资本主义社会失业现象产生的根源
|
||||
|
||||
那么你应该回答选项A、C、D的内容:"["资本主义扩大再生产的源泉","社会财富占有两极分化的重要原因","资本主义社会失业现象产生的根源"]"`
|
||||
}
|
||||
|
||||
func trueFalseExample() string {
|
||||
return `比如:
|
||||
题目类型:判断题
|
||||
题目内容:新中国是1949年10月1日成立的吗?
|
||||
A. 正确
|
||||
B. 错误
|
||||
|
||||
那么你应该回答选项A的内容:"["正确"]"`
|
||||
}
|
||||
|
||||
func fillBlankExample() string {
|
||||
return `比如:
|
||||
题目类型:填空题
|
||||
题目内容:新中国成立于( )年。
|
||||
|
||||
那么你应该回答:"["1949"]"`
|
||||
}
|
||||
|
||||
func shortAnswerExample() string {
|
||||
return `比如:
|
||||
题目类型:简答题
|
||||
题目内容:请简述新中国成立的历史意义
|
||||
|
||||
那么你应该回答:"["新中国的成立标志着中国人民从此站起来了,结束了半殖民地半封建社会的历史..."]"`
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package quesbank
|
||||
@@ -0,0 +1,23 @@
|
||||
package quesbank
|
||||
|
||||
// QuestionType 题目类型
|
||||
type QuestionType int
|
||||
|
||||
const (
|
||||
QuestionTypeSingleChoice QuestionType = 0 // 单选题
|
||||
QuestionTypeMultipleChoice QuestionType = 1 // 多选题
|
||||
QuestionTypeFillBlank QuestionType = 2 // 填空题
|
||||
QuestionTypeTrueFalse QuestionType = 3 // 判断题
|
||||
QuestionTypeShortAnswer QuestionType = 4 // 简答题
|
||||
)
|
||||
|
||||
// Question 通用题目结构
|
||||
type Question struct {
|
||||
Qid string `json:"qid"` // 题目ID
|
||||
Type QuestionType `json:"type"` // 题目类型
|
||||
Text string `json:"text"` // 题目内容
|
||||
Options map[string]string `json:"options"` // 选项 (A: xxx, B: xxx)
|
||||
Answers []string `json:"answers"` // 答案
|
||||
BlankCount int `json:"blankCount"` // 填空题空格数量
|
||||
BlankAnswer map[string]string `json:"blankAnswer"` // 填空题答案
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
const (
|
||||
scAESKey = "m1xp9slgfa|su@m2"
|
||||
scAESIV = "pa!st17m@04h5u#0"
|
||||
)
|
||||
|
||||
// EncryptSC AES-CBC加密,使用固定的key和iv
|
||||
func EncryptSC(plaintext string) string {
|
||||
key := []byte(scAESKey)
|
||||
iv := []byte(scAESIV)
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// PKCS7填充
|
||||
blockSize := block.BlockSize()
|
||||
padding := blockSize - len(plaintext)%blockSize
|
||||
padText := make([]byte, padding)
|
||||
for i := range padText {
|
||||
padText[i] = byte(padding)
|
||||
}
|
||||
plaintextBytes := append([]byte(plaintext), padText...)
|
||||
|
||||
// CBC加密
|
||||
ciphertext := make([]byte, len(plaintextBytes))
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(ciphertext, plaintextBytes)
|
||||
|
||||
// Base64编码
|
||||
return base64.StdEncoding.EncodeToString(ciphertext)
|
||||
}
|
||||
|
||||
// DecryptSC AES-CBC解密,使用固定的key和iv
|
||||
func DecryptSC(ciphertext string) string {
|
||||
key := []byte(scAESKey)
|
||||
iv := []byte(scAESIV)
|
||||
|
||||
// Base64解码
|
||||
ciphertextBytes, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// CBC解密
|
||||
blockSize := block.BlockSize()
|
||||
if len(ciphertextBytes)%blockSize != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
plaintext := make([]byte, len(ciphertextBytes))
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(plaintext, ciphertextBytes)
|
||||
|
||||
// 去除PKCS7填充
|
||||
if len(plaintext) == 0 {
|
||||
return ""
|
||||
}
|
||||
padding := int(plaintext[len(plaintext)-1])
|
||||
if padding <= 0 || padding > blockSize || padding > len(plaintext) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(plaintext[:len(plaintext)-padding])
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EncData 实现与JavaScript版本相同的加密功能
|
||||
// clientToken参数用于替代JavaScript中的cookie值
|
||||
func EncData(data interface{}, clientToken string) map[string]string {
|
||||
// 检查数据是否为空
|
||||
if data == nil || data == "" || data == "{}" || data == " " || data == "null" {
|
||||
return map[string]string{
|
||||
"ciphertext": "",
|
||||
}
|
||||
}
|
||||
|
||||
// 获取axes_max
|
||||
axesMax := len(clientToken)
|
||||
if axesMax < 4 {
|
||||
axesMax = 4
|
||||
}
|
||||
|
||||
// 创建axes
|
||||
axes := creatAxes(axesMax)
|
||||
|
||||
// 替换逗号为连字符
|
||||
aesaxes := strings.ReplaceAll(axes, ",", "-")
|
||||
|
||||
// 生成AES密钥
|
||||
aesKey := encryptKey(axes, clientToken)
|
||||
|
||||
// 将数据转换为JSON字符串
|
||||
var dataStr string
|
||||
switch v := data.(type) {
|
||||
case string:
|
||||
dataStr = v
|
||||
default:
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return map[string]string{
|
||||
"ciphertext": "",
|
||||
}
|
||||
}
|
||||
dataStr = string(jsonBytes)
|
||||
}
|
||||
|
||||
// 加密数据
|
||||
encdata := Encrypt(dataStr, aesKey)
|
||||
|
||||
// 构造密文
|
||||
ciphertext := encdata + "," + aesaxes
|
||||
|
||||
return map[string]string{
|
||||
"ciphertext": ciphertext,
|
||||
}
|
||||
}
|
||||
|
||||
// DescData 实现与JavaScript版本相同的解密功能
|
||||
// clientToken参数用于替代JavaScript中的cookie值
|
||||
func DescData(JR map[string]interface{}, clientToken string) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"flag": false,
|
||||
"data": "",
|
||||
}
|
||||
|
||||
if JR == nil {
|
||||
return result
|
||||
}
|
||||
|
||||
// 获取flag
|
||||
flag, ok := JR["flag"].(bool)
|
||||
if !ok {
|
||||
flag = false
|
||||
}
|
||||
|
||||
result["flag"] = flag
|
||||
|
||||
// 如果flag为false,直接返回data
|
||||
if !flag {
|
||||
if data, ok := JR["data"]; ok {
|
||||
result["data"] = data
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 检查key是否存在
|
||||
key, keyExists := JR["key"].(string)
|
||||
if !keyExists || key == "" {
|
||||
if data, ok := JR["data"]; ok {
|
||||
result["data"] = data
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 处理解密
|
||||
axes := key
|
||||
aesaxes := strings.ReplaceAll(axes, "-", ",")
|
||||
aesKey := encryptKey(aesaxes, clientToken)
|
||||
|
||||
// 获取加密数据
|
||||
outdata, ok := JR["data"].(string)
|
||||
if !ok {
|
||||
result["data"] = ""
|
||||
return result
|
||||
}
|
||||
|
||||
// URL解码(与JS版本的decodeURIComponent一致)
|
||||
decodedData, err := url.QueryUnescape(outdata)
|
||||
if err != nil {
|
||||
// 如果URL解码失败,使用原始数据
|
||||
decodedData = outdata
|
||||
}
|
||||
|
||||
// 解密数据
|
||||
decryptedData := Decrypt(decodedData, aesKey)
|
||||
result["data"] = decryptedData
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// encryptKey 根据axes生成加密密钥
|
||||
func encryptKey(axes string, clientToken string) string {
|
||||
aaxes := strings.Split(axes, ",")
|
||||
key := ""
|
||||
|
||||
for _, axis := range aaxes {
|
||||
axisInt := parseInt(axis)
|
||||
// 确保axisInt不会导致越界
|
||||
if axisInt >= 0 && axisInt < len(clientToken) {
|
||||
// 与JavaScript版本一致:substring(aaxes[i], aaxes[i]*1+4)
|
||||
end := axisInt + 4
|
||||
if end <= len(clientToken) {
|
||||
key += clientToken[axisInt:end]
|
||||
} else {
|
||||
// 如果end超出范围,只取到末尾
|
||||
key += clientToken[axisInt:]
|
||||
// 填充到4个字符
|
||||
for len(key)%4 != 0 {
|
||||
key += "A"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果起始位置越界,使用默认值
|
||||
key += "abcd"
|
||||
}
|
||||
}
|
||||
|
||||
// 确保密钥长度至少为16字节,如果超过则截断
|
||||
if len(key) < 16 {
|
||||
// 用'A'填充到16字节
|
||||
for len(key) < 16 {
|
||||
key += "A"
|
||||
}
|
||||
} else if len(key) > 16 {
|
||||
// 截断到16字节
|
||||
key = key[:16]
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
// creatAxes 创建随机axes
|
||||
func creatAxes(max int) string {
|
||||
if max < 4 {
|
||||
max = 4
|
||||
}
|
||||
|
||||
axes := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
// 生成0到max-4之间的随机数
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(int64(max-4)))
|
||||
if err != nil {
|
||||
// 出错时使用默认值
|
||||
axes += "0,"
|
||||
continue
|
||||
}
|
||||
axes += fmt.Sprintf("%d,", n.Int64())
|
||||
}
|
||||
|
||||
// 移除末尾的逗号
|
||||
if len(axes) > 0 {
|
||||
axes = axes[:len(axes)-1]
|
||||
}
|
||||
|
||||
return axes
|
||||
}
|
||||
|
||||
// parseInt 简单的字符串转整数函数
|
||||
func parseInt(s string) int {
|
||||
var result int
|
||||
fmt.Sscanf(s, "%d", &result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Encrypt AES加密函数
|
||||
func Encrypt(data, key string) string {
|
||||
// 确保密钥长度为16字节
|
||||
keyBytes := make([]byte, 16)
|
||||
if len(key) >= 16 {
|
||||
copy(keyBytes, []byte(key)[:16])
|
||||
} else {
|
||||
copy(keyBytes, []byte(key))
|
||||
// 如果密钥不足16字节,用'A'填充
|
||||
for i := len(key); i < 16; i++ {
|
||||
keyBytes[i] = 'A'
|
||||
}
|
||||
}
|
||||
|
||||
// 创建AES密码块
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// PKCS7填充
|
||||
blockSize := block.BlockSize()
|
||||
padding := blockSize - len(data)%blockSize
|
||||
padText := make([]byte, padding)
|
||||
for i := range padText {
|
||||
padText[i] = byte(padding)
|
||||
}
|
||||
|
||||
plaintext := append([]byte(data), padText...)
|
||||
|
||||
// CBC模式加密
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
// 使用固定的IV(与JavaScript版本一致,使用相同的key作为IV)
|
||||
iv := make([]byte, blockSize)
|
||||
copy(iv, keyBytes[:blockSize])
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(ciphertext, plaintext)
|
||||
|
||||
// Base64编码(使用URL编码兼容CryptoJS)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext)
|
||||
}
|
||||
|
||||
// Decrypt AES解密函数
|
||||
func Decrypt(data, key string) string {
|
||||
// Base64解码
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
// 尝试URL解码
|
||||
ciphertext, err = base64.URLEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// 确保密钥长度为16字节
|
||||
keyBytes := make([]byte, 16)
|
||||
if len(key) >= 16 {
|
||||
copy(keyBytes, []byte(key)[:16])
|
||||
} else {
|
||||
copy(keyBytes, []byte(key))
|
||||
// 如果密钥不足16字节,用'A'填充
|
||||
for i := len(key); i < 16; i++ {
|
||||
keyBytes[i] = 'A'
|
||||
}
|
||||
}
|
||||
|
||||
// 创建AES密码块
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// CBC模式解密
|
||||
blockSize := block.BlockSize()
|
||||
if len(ciphertext)%blockSize != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 使用固定的IV(与JavaScript版本一致,使用相同的key作为IV)
|
||||
iv := make([]byte, blockSize)
|
||||
copy(iv, keyBytes[:blockSize])
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
mode.CryptBlocks(plaintext, ciphertext)
|
||||
|
||||
// 去除PKCS7填充
|
||||
if len(plaintext) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
padding := int(plaintext[len(plaintext)-1])
|
||||
// 更宽松的填充检查
|
||||
if padding <= 0 || padding > blockSize || padding > len(plaintext) {
|
||||
// 如果填充不合法,返回原始数据
|
||||
return string(plaintext)
|
||||
}
|
||||
|
||||
// 验证填充是否正确(更宽松的检查)
|
||||
validPadding := true
|
||||
for i := len(plaintext) - padding; i < len(plaintext); i++ {
|
||||
if plaintext[i] != byte(padding) {
|
||||
validPadding = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validPadding {
|
||||
// 填充验证失败,返回尽可能多的有效数据
|
||||
return string(plaintext)
|
||||
}
|
||||
|
||||
// 正常去除填充
|
||||
if len(plaintext) <= padding {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(plaintext[:len(plaintext)-padding])
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
var SAFETOCKEN="Safe-Tocken";//for RSA key cookie
|
||||
var encryptAxes = function(axes){
|
||||
RSAUtils.setMaxDigits(1024);
|
||||
var safeTocken = getCookie(SAFETOCKEN);
|
||||
if(safeTocken=="" || safeTocken ==''){
|
||||
throw new Error("Cannot read Cookie '"+SAFETOCKEN+"' values,System need run RSAData() again!");
|
||||
return ;
|
||||
}
|
||||
var safe_tocken = safeTocken.split(",");
|
||||
var pubexp = safe_tocken[1];
|
||||
var priexp = safe_tocken[0];
|
||||
var modulus = safe_tocken[2];
|
||||
var key = RSAUtils.getKeyPair(safe_tocken[1], safe_tocken[0], safe_tocken[2]);
|
||||
axes = axes.split("").reverse().join("");
|
||||
return RSAUtils.encryptedString(key,axes);
|
||||
}
|
||||
var axesMax = function(){
|
||||
var client_tocken = getCookie(CLIENTTOCKEN);
|
||||
if(client_tocken=="" || client_tocken==''){
|
||||
throw new Error("Cannot read Cookie '"+CLIENTTOCKEN+"' values,System need run RSAData() again!");
|
||||
return ;
|
||||
}
|
||||
return client_tocken.length;
|
||||
}
|
||||
/**
|
||||
* decrypt axes use ras key form http back
|
||||
*/
|
||||
var decryptAxes = function(rsakey){
|
||||
RSAUtils.setMaxDigits(1024);
|
||||
var safeTocken = getCookie(SAFETOCKEN);
|
||||
if(safeTocken=="" || safeTocken ==''){
|
||||
throw new Error("Cannot read Cookie '"+SAFETOCKEN+"' values,System need run RSAData() again!");
|
||||
return ;
|
||||
}
|
||||
var safe_tocken = safeTocken.split(",");
|
||||
var pubexp = safe_tocken[1];
|
||||
var priexp = safe_tocken[0];
|
||||
var modulus = safe_tocken[2];
|
||||
var key = RSAUtils.getKeyPair(safe_tocken[1], safe_tocken[0], safe_tocken[2]);
|
||||
return RSAUtils.decryptedString(key,rsakey);
|
||||
}
|
||||
var CLIENTTOCKEN = "Client-Tocken";
|
||||
/**
|
||||
* Get String key as axes
|
||||
*/
|
||||
var encryptKey = function(axes){
|
||||
//1.get client-tocken
|
||||
//2. realse this axes get this encryptKey
|
||||
var key="";
|
||||
var client_tocken = getCookie(CLIENTTOCKEN);
|
||||
if(client_tocken=="" || client_tocken==''){
|
||||
throw new Error("Cannot read Cookie '"+CLIENTTOCKEN+"' values,System need run RSAData() again!");
|
||||
return ;
|
||||
}
|
||||
var aaxes = axes.split(",");
|
||||
for(var i=0;i<aaxes.length;i++){
|
||||
key += client_tocken.substring(aaxes[i],aaxes[i]*1+4);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
var getCookie = function(name){
|
||||
var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
|
||||
if(arr=document.cookie.match(reg))
|
||||
return decodeURIComponent(arr[2]);
|
||||
else
|
||||
return "";
|
||||
}
|
||||
/**
|
||||
* get a random axes
|
||||
*/
|
||||
var creatAxes = function(max){
|
||||
var axes = "";
|
||||
for(var i=0;i<4;i++ ){
|
||||
axes += parseInt(Math.random()*(max-4),10)+",";
|
||||
}
|
||||
axes = axes.substring(0,axes.length-1);
|
||||
return axes;
|
||||
}
|
||||
/**
|
||||
* just encdata for Ajax
|
||||
*/
|
||||
var EncData = function(data){
|
||||
if(data=="" || data =="{}" || data==" " || data==null || data=="null"){
|
||||
ciphertext="";
|
||||
}else{//存在数据
|
||||
//获取aeskey
|
||||
var axes_max = axesMax();
|
||||
var axes = creatAxes(axes_max);
|
||||
var reg=new RegExp(",","g");
|
||||
var aesaxes = axes.replace(reg,'-');
|
||||
var aesKey = encryptKey(axes);
|
||||
data = JSON.stringify(data);
|
||||
encdata = Encrypt(data,aesKey);
|
||||
ciphertext = encdata+","+aesaxes;
|
||||
}
|
||||
return {'ciphertext':ciphertext};
|
||||
}
|
||||
/**
|
||||
* get data from back http
|
||||
*/
|
||||
var DescData = function(JR){
|
||||
var _data="";
|
||||
if(JR){//data is not empty
|
||||
var flag = JR.flag;
|
||||
if(flag){//成功
|
||||
if(JR.key=="" || JR.key==''){
|
||||
_data = JR.data;
|
||||
}else{//解密数据
|
||||
var axes = JR.key;
|
||||
var reg=new RegExp("-","g");
|
||||
var aesaxes = axes.replace(reg,',');
|
||||
var aesKey = encryptKey(aesaxes);
|
||||
var outdata=decodeURIComponent(JR.data);
|
||||
_data = Decrypt(outdata,aesKey);
|
||||
}
|
||||
}else{
|
||||
_data = JR.data;
|
||||
}
|
||||
}
|
||||
return {'flag':JR.flag,'data':_data};
|
||||
}
|
||||
/**
|
||||
* post data use as AJAX
|
||||
*/
|
||||
var SubData = function(data,URL){
|
||||
var result="";
|
||||
var ciphertext = EncData(data);
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
cache : false,
|
||||
async:false,
|
||||
url: URL,//传入后台的地址/方法
|
||||
data: ciphertext,//参数,这里是一个json语句
|
||||
success: function (RS) {
|
||||
result = DescData(RS);
|
||||
},
|
||||
error: function (err) {
|
||||
throw new Error("Post data was Error!");
|
||||
return ;
|
||||
},
|
||||
fail:function(){
|
||||
throw new Error("Post data was fail,website need NetWork!");
|
||||
return ;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
var Encrypt = function(data,key){
|
||||
var aeskey = CryptoJS.enc.Utf8.parse(key);
|
||||
var aesiv = CryptoJS.enc.Utf8.parse(key);
|
||||
var srcs = CryptoJS.enc.Utf8.parse(data);
|
||||
return CryptoJS.AES.encrypt(srcs, aeskey, { iv: aesiv,mode:CryptoJS.mode.CBC}).toString();
|
||||
}
|
||||
function Decrypt(word,key){
|
||||
var aeskey = CryptoJS.enc.Utf8.parse(key);
|
||||
var aesiv = CryptoJS.enc.Utf8.parse(key);
|
||||
var decrypt = CryptoJS.AES.decrypt(word, aeskey, { iv: aesiv,mode:CryptoJS.mode.CBC});
|
||||
var decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
|
||||
return decryptedStr.toString();
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncdata(t *testing.T) {
|
||||
// 使用与JavaScript版本相同的clientToken
|
||||
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
data := map[string]interface{}{
|
||||
"username": "admin",
|
||||
"password": "123456",
|
||||
}
|
||||
encData := EncData(data, clientToken)
|
||||
fmt.Println("EncData result:", encData)
|
||||
|
||||
// 创建一个模拟的JR对象来测试DescData
|
||||
jr := map[string]interface{}{
|
||||
"flag": true,
|
||||
"key": getKeyFromCiphertext(encData["ciphertext"]),
|
||||
"data": getDataFromCiphertext(encData["ciphertext"]),
|
||||
}
|
||||
|
||||
descResult := DescData(jr, clientToken)
|
||||
fmt.Println("DescData result:", descResult)
|
||||
}
|
||||
|
||||
func TestDecdata(t *testing.T) {
|
||||
// 使用与JavaScript版本相同的clientToken
|
||||
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
data := map[string]interface{}{
|
||||
"flag": true,
|
||||
"data": "YN4tHIF4S3OQgfhTICpFqR425z2sHwCEmlsGdVYt5m6ssyIagtqEvf0hEXdzhc0u",
|
||||
"key": "120-54-61-117",
|
||||
}
|
||||
|
||||
// 调试信息
|
||||
fmt.Println("Input data:", data)
|
||||
|
||||
// 手动测试密钥生成
|
||||
axes := "120-54-61-117"
|
||||
aesaxes := strings.ReplaceAll(axes, "-", ",")
|
||||
aesKey := encryptKey(aesaxes, clientToken)
|
||||
fmt.Println("Generated key:", aesKey)
|
||||
fmt.Println("Key length:", len(aesKey))
|
||||
|
||||
// 直接测试解密
|
||||
encryptedData := "YN4tHIF4S3OQgfhTICpFqR425z2sHwCEmlsGdVYt5m6ssyIagtqEvf0hEXdzhc0u"
|
||||
decrypted := Decrypt(encryptedData, aesKey)
|
||||
fmt.Println("Direct decrypt result:", decrypted)
|
||||
fmt.Printf("Direct decrypt result (bytes): %v\n", []byte(decrypted))
|
||||
|
||||
result := DescData(data, clientToken)
|
||||
fmt.Println("TestDecdata result:", result)
|
||||
|
||||
// 检查结果
|
||||
if result["flag"] != true {
|
||||
t.Errorf("Expected flag to be true, got %v", result["flag"])
|
||||
}
|
||||
}
|
||||
|
||||
// getKeyFromCiphertext 从ciphertext中提取key部分
|
||||
func getKeyFromCiphertext(ciphertext string) string {
|
||||
parts := strings.Split(ciphertext, ",")
|
||||
if len(parts) > 1 {
|
||||
return parts[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getDataFromCiphertext 从ciphertext中提取data部分
|
||||
func getDataFromCiphertext(ciphertext string) string {
|
||||
parts := strings.Split(ciphertext, ",")
|
||||
if len(parts) > 0 {
|
||||
return parts[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 添加一个简单的加密/解密测试
|
||||
func TestEncryptDecrypt(t *testing.T) {
|
||||
originalData := `{"username":"admin","password":"123456"}`
|
||||
key := "abcdabcdabcdabcd" // 16字节密钥
|
||||
|
||||
// 加密
|
||||
encrypted := Encrypt(originalData, key)
|
||||
fmt.Println("Encrypted:", encrypted)
|
||||
|
||||
// 解密
|
||||
decrypted := Decrypt(encrypted, key)
|
||||
fmt.Println("Decrypted:", decrypted)
|
||||
|
||||
if decrypted != originalData {
|
||||
t.Errorf("Decrypt failed. Expected: %s, Got: %s", originalData, decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试特定的axes生成密钥
|
||||
func TestEncryptKey(t *testing.T) {
|
||||
// 使用与JavaScript版本相同的clientToken
|
||||
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
// 测试提供的axes
|
||||
axes := "120,54,61,117"
|
||||
|
||||
key := encryptKey(axes, clientToken)
|
||||
fmt.Println("Test axes:", axes)
|
||||
fmt.Println("Generated key:", key)
|
||||
fmt.Println("Key length:", len(key))
|
||||
|
||||
// 验证每个部分
|
||||
fmt.Println("Client token length:", len(clientToken))
|
||||
|
||||
// 分别计算每个部分
|
||||
parts := strings.Split(axes, ",")
|
||||
for i, part := range parts {
|
||||
axisInt := parseInt(part)
|
||||
fmt.Printf("Part %d: %s -> %d\n", i, part, axisInt)
|
||||
|
||||
if axisInt >= 0 && axisInt < len(clientToken) {
|
||||
end := axisInt + 4
|
||||
if end <= len(clientToken) {
|
||||
fmt.Printf(" Extracted: %s\n", clientToken[axisInt:end])
|
||||
} else {
|
||||
fmt.Printf(" Extracted: %s (partial)\n", clientToken[axisInt:])
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" Out of range, using default\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 测试具体案例
|
||||
func TestSpecificCase(t *testing.T) {
|
||||
// 使用与JavaScript版本相同的clientToken
|
||||
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
// 使用你提供的具体数据
|
||||
axes := "120,54,61,117"
|
||||
key := encryptKey(axes, clientToken)
|
||||
|
||||
fmt.Println("=== Specific Case Test ===")
|
||||
fmt.Printf("Axes: %s\n", axes)
|
||||
fmt.Printf("Generated Key: %s\n", key)
|
||||
fmt.Printf("Key Length: %d\n", len(key))
|
||||
|
||||
// 测试加密
|
||||
testData := "test data"
|
||||
encrypted := Encrypt(testData, key)
|
||||
fmt.Printf("Encrypted: %s\n", encrypted)
|
||||
|
||||
// 测试解密
|
||||
decrypted := Decrypt(encrypted, key)
|
||||
fmt.Printf("Decrypted: %s\n", decrypted)
|
||||
|
||||
if testData != decrypted {
|
||||
fmt.Println("WARNING: Encryption/Decryption mismatch!")
|
||||
} else {
|
||||
fmt.Println("SUCCESS: Encryption/Decryption works correctly!")
|
||||
}
|
||||
}
|
||||
|
||||
// 测试你提供的实际加密数据
|
||||
func TestActualEncryptedData(t *testing.T) {
|
||||
// 使用与JavaScript版本相同的clientToken
|
||||
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
// 你提供的实际加密数据
|
||||
encryptedData := "YN4tHIF4S3OQgfhTICpFqR425z2sHwCEmlsGdVYt5m6ssyIagtqEvf0hEXdzhc0u"
|
||||
|
||||
// 根据axes "120,54,61,117" 生成密钥
|
||||
axes := "120,54,61,117"
|
||||
key := encryptKey(axes, clientToken)
|
||||
|
||||
fmt.Println("=== Actual Encrypted Data Test ===")
|
||||
fmt.Printf("Encrypted Data: %s\n", encryptedData)
|
||||
fmt.Printf("Key: %s\n", key)
|
||||
|
||||
// 尝试解密
|
||||
decrypted := Decrypt(encryptedData, key)
|
||||
fmt.Printf("Decrypted Result: '%s'\n", decrypted)
|
||||
fmt.Printf("Decrypted Length: %d\n", len(decrypted))
|
||||
fmt.Printf("Decrypted Bytes: %v\n", []byte(decrypted))
|
||||
|
||||
// 检查是否是有效的JSON
|
||||
if len(decrypted) > 0 {
|
||||
fmt.Println("Possible JSON data detected")
|
||||
} else {
|
||||
fmt.Println("No data returned from decryption")
|
||||
}
|
||||
}
|
||||
|
||||
// 最终验证测试
|
||||
func TestFinalVerification(t *testing.T) {
|
||||
// 使用与JavaScript版本相同的clientToken
|
||||
clientToken := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
fmt.Println("=== Final Verification Test ===")
|
||||
|
||||
// 使用你提供的axes生成密钥
|
||||
axes := "120,54,61,117"
|
||||
key := encryptKey(axes, clientToken)
|
||||
fmt.Printf("Generated key for axes '%s': '%s'\n", axes, key)
|
||||
|
||||
// 你提供的加密数据
|
||||
encryptedData := "YN4tHIF4S3OQgfhTICpFqR425z2sHwCEmlsGdVYt5m6ssyIagtqEvf0hEXdzhc0u"
|
||||
|
||||
// 尝试解密
|
||||
decrypted := Decrypt(encryptedData, key)
|
||||
fmt.Printf("Decrypted result: '%s'\n", decrypted)
|
||||
|
||||
// 如果解密结果为空,尝试不同的方法
|
||||
if len(decrypted) == 0 {
|
||||
fmt.Println("Trying alternative decryption approaches...")
|
||||
|
||||
// 尝试不同的填充处理
|
||||
fmt.Println("Testing raw decryption without padding removal...")
|
||||
// 这里我们可以添加更多的调试信息
|
||||
|
||||
// 输出一些调试信息
|
||||
fmt.Printf("Key bytes: %v\n", []byte(key))
|
||||
fmt.Printf("Encrypted data length: %d\n", len(encryptedData))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package model
|
||||
|
||||
type Course struct {
|
||||
Id string `json:"id"`
|
||||
CourseName string `json:"courseName"`
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
type Chapter struct {
|
||||
Id string `json:"id"`
|
||||
ChapterName string `json:"chapterName"`
|
||||
SubSection []SubSection `json:"studySubsections"`
|
||||
}
|
||||
|
||||
type SubSection struct {
|
||||
Id string `json:"id"`
|
||||
SubsectionName string `json:"subsectionName"`
|
||||
VideoTime string `json:"videoTime"`
|
||||
StudyFiles StudyFile `json:"studyFiles"`
|
||||
}
|
||||
|
||||
type StudyFile struct {
|
||||
Id string `json:"id"`
|
||||
FileName string `json:"fileName"`
|
||||
FileSize string `json:"fileSize"`
|
||||
FileTime string `json:"fileTime"`
|
||||
FileType string `json:"fileType"`
|
||||
FilePath string `json:"filePath"`
|
||||
VideoTime string `json:"videoTime"`
|
||||
StudyTime string `json:"studyTime"`
|
||||
State string `json:"state"` // 2是学习中 1是未开始 3是已完成
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package sc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/application/smartedu/model"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
HomeUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/home"
|
||||
CourseDetailUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/studyCourse/getCourseDetails"
|
||||
RecordProcessUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/studyCourseUser/recordProcess"
|
||||
RefresProcessUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/studyCourseUser/refreshProcess"
|
||||
ApiCourseFinishedHour = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/studyCourse/getCourseFinishedHour"
|
||||
)
|
||||
|
||||
type Course struct {
|
||||
Client *resty.Client
|
||||
Token string
|
||||
}
|
||||
|
||||
func NewCourse(token string, client *resty.Client) *Course {
|
||||
return &Course{
|
||||
Token: token,
|
||||
Client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Course) GetCourseList(ctx context.Context) []model.Course {
|
||||
jsUrl := c.GetJsUrl(ctx, HomeUrl)
|
||||
get, err := c.Client.R().Get(jsUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("jsUrl", jsUrl).Wrap)
|
||||
jsonString := ParseJsonString(get.String())
|
||||
returnData := gjson.Get(jsonString, "returnData").Array()
|
||||
var courseList []model.Course
|
||||
for _, one := range returnData {
|
||||
courseList = append(courseList, model.Course{
|
||||
Id: one.Get("id").String(),
|
||||
CourseName: one.Get("courseName").String(),
|
||||
Image: one.Get("studyFiles.filePath").String(),
|
||||
})
|
||||
}
|
||||
return courseList
|
||||
}
|
||||
|
||||
func (c *Course) GetChapterList(ctx context.Context, courseId string) []model.Chapter {
|
||||
get, err := c.Client.R().SetQueryParam("courseId", courseId).SetHeader("X-Token", c.Token).Get(CourseDetailUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("GetChapterList", courseId).Wrap)
|
||||
chapterJson := gjson.Get(get.String(), "returnData.chapters").Array()
|
||||
var chapterList []model.Chapter
|
||||
for _, ch := range chapterJson {
|
||||
chapter := model.Chapter{
|
||||
Id: ch.Get("id").String(),
|
||||
ChapterName: ch.Get("chapterName").String(),
|
||||
}
|
||||
subSectionJson := ch.Get("studySubsections").Array()
|
||||
var subSectionList []model.SubSection
|
||||
for _, sec := range subSectionJson {
|
||||
studyFileJson := sec.Get("studyFiles")
|
||||
studyFile := model.StudyFile{
|
||||
Id: studyFileJson.Get("id").String(),
|
||||
FileName: studyFileJson.Get("fileName").String(),
|
||||
FileSize: studyFileJson.Get("fileSize").String(),
|
||||
FileTime: studyFileJson.Get("fileTime").String(),
|
||||
FileType: studyFileJson.Get("fileType").String(),
|
||||
FilePath: studyFileJson.Get("filePath").String(),
|
||||
VideoTime: studyFileJson.Get("videoTime").String(),
|
||||
State: studyFileJson.Get("state").String(),
|
||||
StudyTime: studyFileJson.Get("studyTime").String(),
|
||||
}
|
||||
subSection := model.SubSection{
|
||||
Id: sec.Get("id").String(),
|
||||
SubsectionName: sec.Get("subsectionName").String(),
|
||||
VideoTime: sec.Get("videoTime").String(),
|
||||
StudyFiles: studyFile,
|
||||
}
|
||||
|
||||
subSectionList = append(subSectionList, subSection)
|
||||
}
|
||||
chapter.SubSection = subSectionList
|
||||
chapterList = append(chapterList, chapter)
|
||||
}
|
||||
|
||||
return chapterList
|
||||
}
|
||||
|
||||
func ParseJsonString(str string) string {
|
||||
// eval("module.exports = /*#__PURE__*/JSON.parse('{...}');\n\n//# sourceURL=...
|
||||
re := regexp.MustCompile(`#__PURE__\*/JSON\.parse\('(.+?)'\);`)
|
||||
matches := re.FindStringSubmatch(str)
|
||||
if len(matches) < 2 {
|
||||
return ""
|
||||
}
|
||||
// 处理转义的引号
|
||||
return strings.ReplaceAll(matches[1], `\"`, `"`)
|
||||
}
|
||||
|
||||
func (c *Course) GetJsUrl(ctx context.Context, url string) string {
|
||||
get, err := c.Client.R().Get(url)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("GetJsUrl", url).Wrap)
|
||||
return ParseJsUrl(get.String())
|
||||
}
|
||||
|
||||
func (c *Course) Study(ctx context.Context, c2 model.Course, ch model.Chapter, sec model.SubSection) {
|
||||
// 如果已完成
|
||||
if sec.StudyFiles.State == "3" {
|
||||
return
|
||||
}
|
||||
|
||||
form := map[string]any{
|
||||
"chapterId": ch.Id,
|
||||
"courseId": c2.Id,
|
||||
"subsectionId": sec.Id,
|
||||
}
|
||||
if sec.StudyFiles.State == "1" {
|
||||
form["state"] = "1"
|
||||
c.Refresh(ctx, c2, ch, sec)
|
||||
form["studyTime"] = 5
|
||||
// 如果state为
|
||||
post, err := c.Client.R().SetHeader("X-Token", c.Token).SetBody(form).Post(RecordProcessUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("Study", form).Wrap)
|
||||
str := post.String()
|
||||
if gjson.Get(str, "returnCode").String() != "200" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "学习失败,"+gjson.Get(str, "returnMessage").String()))
|
||||
}
|
||||
logger.Logger.Info("学习成功", zap.String("课程名", c2.CourseName), zap.String("章节名", ch.ChapterName), zap.String("小节名", sec.SubsectionName))
|
||||
}
|
||||
c.Refresh(ctx, c2, ch, sec)
|
||||
atoi, _ := strconv.Atoi(sec.StudyFiles.VideoTime)
|
||||
form["state"] = "3"
|
||||
form["studyTime"] = atoi
|
||||
// 如果state为
|
||||
post, err := c.Client.R().SetHeader("X-Token", c.Token).SetBody(form).Post(RecordProcessUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("Study", form).Wrap)
|
||||
str := post.String()
|
||||
if gjson.Get(str, "returnCode").String() != "200" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "学习失败,"+gjson.Get(str, "returnMessage").String()))
|
||||
}
|
||||
logger.Logger.Info("学习成功", zap.String("课程名", c2.CourseName), zap.String("章节名", ch.ChapterName), zap.String("小节名", sec.SubsectionName))
|
||||
}
|
||||
|
||||
func (c *Course) Refresh(ctx context.Context, c2 model.Course, ch model.Chapter, sec model.SubSection) {
|
||||
form := map[string]any{
|
||||
"chapterId": ch.Id,
|
||||
"courseId": c2.Id,
|
||||
"subsectionId": sec.Id,
|
||||
"studyTime": 0,
|
||||
}
|
||||
post, err := c.Client.R().SetHeader("X-Token", c.Token).SetBody(form).Post(RefresProcessUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("Refresh", form).Wrap)
|
||||
str := post.String()
|
||||
if gjson.Get(str, "returnCode").String() != "200" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "刷新嫉妒失败,"+gjson.Get(str, "returnMessage").String()))
|
||||
}
|
||||
logger.Logger.Info("刷新成功", zap.String("课程名", c2.CourseName), zap.String("章节名", ch.ChapterName), zap.String("小节名", sec.SubsectionName))
|
||||
}
|
||||
|
||||
func ParseJsUrl(str string) string {
|
||||
// <script defer src="https://cdnbasic.scbdc.edu.cn/%E7%9C%81%E7%BA%A7%E8%B5%84%E6%BA%90/hd/chunk-vendors.5e79f5ce155849.js">
|
||||
re := regexp.MustCompile(`https://[^"'\s]+chunk-vendors[^"'\s]+\.js`)
|
||||
match := re.FindString(str)
|
||||
if match == "" {
|
||||
return ""
|
||||
}
|
||||
return strings.Replace(match, "chunk-vendors", "src_views_home_index_vue", 1)
|
||||
}
|
||||
func (c *Course) GetCourseProcees(ctx context.Context, courseId string) (finished string, total string) {
|
||||
post, err := c.Client.R().SetHeader("X-Token", c.Token).SetBody(map[string]any{
|
||||
"courseIds": []string{courseId},
|
||||
}).Post(ApiCourseFinishedHour)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("GetCourseProcees", courseId).Wrap)
|
||||
str := gjson.Get(post.String(), "returnData.finishedHours").String()
|
||||
split := strings.Split(str, ",")
|
||||
return split[0], split[1]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package sc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/application/smartedu/cryptoutil"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/samber/oops"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
LoginPageUrl = "https://basic.sc.smartedu.cn/ThirdPortalService/user/otherlogin!login.ac?appkey=C56DA16ECBC56FBEEC908DA09E45C72C917A80118F057FA1F0B5BAE41CC9CC9DECD5BDB7133FE17C328C5D37B37CA8E7&pkey=5D79CA42E45C5273DF8532D09E1F158B15E25919CDB958940F84D5E63F5F53A1ECD5BDB7133FE17C328C5D37B37CA8E7¶ms=718F83A5347CBFDB7D1A9065FA090FE949D92330BB9A3351FE0715C5B8A3E86F37916C1004E835C7C7F964E3F301477F7D37F04485FA8707845DAAA23356236ED1D326CF5A5E3C263470516EE9B4A2ED"
|
||||
LoginActionUrl = "https://basic.sc.smartedu.cn/ThirdPortalService/login!login.ac"
|
||||
LoginTicketUrl = "https://basic.sc.smartedu.cn/ThirdPortalService/login!getTicket.ac"
|
||||
LoginOOSUrl = "https://basic.sc.smartedu.cn/hd/teacherTraining/api/sso/oauth/token"
|
||||
)
|
||||
|
||||
type Login struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Client *resty.Client
|
||||
}
|
||||
|
||||
func NewLogin(username, password string) *Login {
|
||||
return &Login{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Client: httpclient.NewSmartEdu(),
|
||||
}
|
||||
}
|
||||
func (l *Login) GetCookieValue(key string) string {
|
||||
cookies := l.Client.CookieJar().(*cookiejar.AllSavePersistCookieJar)
|
||||
for _, cookie := range cookies.Cookies(nil) {
|
||||
if cookie.Name == key {
|
||||
return cookie.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (l *Login) GetClient() *resty.Client {
|
||||
return l.Client
|
||||
}
|
||||
func (l *Login) Login(ctx context.Context) string {
|
||||
form := l.getLoginInput(ctx)
|
||||
form["loginName"] = gvar.New(l.Username)
|
||||
form["password"] = gvar.New(l.Password)
|
||||
token := l.GetCookieValue("Client-Tocken")
|
||||
data := cryptoutil.EncData(form, token)
|
||||
post, err := l.Client.R().SetFormData(data).Post(LoginActionUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginActionUrl", LoginActionUrl).Wrap)
|
||||
descData := cryptoutil.DescData(utils.ReadJson[map[string]interface{}](post.Body), token)
|
||||
result := descData["data"].(string)
|
||||
if !strings.Contains(result, `"Flag":0`) {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "登录失败"))
|
||||
}
|
||||
get, err := l.Client.R().Get(LoginTicketUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginTicketUrl", LoginTicketUrl).Wrap)
|
||||
return get.String()
|
||||
}
|
||||
|
||||
func (l *Login) OOSLogin(ctx context.Context, ticket string) string {
|
||||
post, err := l.Client.R().SetQueryParam("ticket", ticket).Post(LoginOOSUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginOOSUrl", LoginOOSUrl).Wrap)
|
||||
defer post.Body.Close()
|
||||
json := utils.ReadJson[map[string]string](post.Body)
|
||||
if json["returnCode"] != "200" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, json["returnMessage"]))
|
||||
}
|
||||
return json["returnData"]
|
||||
}
|
||||
|
||||
func (l *Login) getLoginInput(ctx context.Context) map[string]*gvar.Var {
|
||||
get, err := l.Client.R().WithContext(ctx).Get(LoginPageUrl)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginPageUrl", LoginPageUrl).Wrap)
|
||||
defer get.Body.Close()
|
||||
reader, err := goquery.NewDocumentFromReader(get.Body)
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).With("LoginPageUrl", LoginPageUrl).Wrap)
|
||||
find := reader.Find("#loginContent input")
|
||||
var inputMap = make(map[string]*gvar.Var)
|
||||
find.Each(func(i int, input *goquery.Selection) {
|
||||
key, exists := input.Attr("name")
|
||||
if exists {
|
||||
val, _ := input.Attr("value")
|
||||
inputMap[key] = gvar.New(val)
|
||||
}
|
||||
})
|
||||
return inputMap
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package sc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
login := NewLogin("13551503012", "XIAOzhi520")
|
||||
login.Login(context.Background())
|
||||
login.GetClient()
|
||||
|
||||
}
|
||||
|
||||
func TestStudy(t *testing.T) {
|
||||
login := NewLogin("13551503012", "XIAOzhi520")
|
||||
//ticket := login.Login(context.Background())
|
||||
//token := login.OOSLogin(context.Background(), ticket)
|
||||
client := login.GetClient()
|
||||
course := NewCourse("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkZXZpY2VUeXBlIjoid2ViIiwibG9naW5UeXBlIjoidXNlcnJlYWxtIiwiZXhwIjoxNzY1MjU3NDI1LCJ1c2VySWQiOiIxOTk4MDI1NjQ1NzU5ODM2MTYwIn0.PL5Roy0t9v0mK-ucK73IZg2qTZfw0YhW1sVxG1SYetU", client)
|
||||
courseList := course.GetCourseList(context.Background())
|
||||
for _, c := range courseList {
|
||||
chapterList := course.GetChapterList(context.Background(), c.Id)
|
||||
for _, ch := range chapterList {
|
||||
for _, sec := range ch.SubSection {
|
||||
course.Study(context.Background(), c, ch, sec)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProcess(t *testing.T) {
|
||||
login := NewLogin("13551503012", "XIAOzhi520")
|
||||
//ticket := login.Login(context.Background())
|
||||
//token := login.OOSLogin(context.Background(), ticket)
|
||||
client := login.GetClient()
|
||||
course := NewCourse("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkZXZpY2VUeXBlIjoid2ViIiwibG9naW5UeXBlIjoidXNlcnJlYWxtIiwiZXhwIjoxNzY1MjU3NDI1LCJ1c2VySWQiOiIxOTk4MDI1NjQ1NzU5ODM2MTYwIn0.PL5Roy0t9v0mK-ucK73IZg2qTZfw0YhW1sVxG1SYetU", client)
|
||||
courseList := course.GetCourseList(context.Background())
|
||||
for _, c := range courseList {
|
||||
finished, total := course.GetCourseProcees(context.Background(), c.Id)
|
||||
t.Log(finished, total)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/quesbank"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// AIType 类型别名,引用 quesbank 包
|
||||
type AIType = quesbank.AIType
|
||||
|
||||
// AI类型常量
|
||||
const (
|
||||
AITypeChatGLM = quesbank.AITypeChatGLM
|
||||
AITypeXingHuo = quesbank.AITypeXingHuo
|
||||
AITypeTongYi = quesbank.AITypeTongYi
|
||||
AITypeDouBao = quesbank.AITypeDouBao
|
||||
AITypeOpenAI = quesbank.AITypeOpenAI
|
||||
AITypeOther = quesbank.AITypeOther
|
||||
)
|
||||
|
||||
// AIConfig 类型别名
|
||||
type AIConfig = quesbank.AIConfig
|
||||
|
||||
// convertToQuesbankQuestion 将学习通题目转换为通用题目格式
|
||||
func convertToQuesbankQuestion(q entity.WorkQuestion) quesbank.Question {
|
||||
return quesbank.Question{
|
||||
Qid: q.Qid,
|
||||
Type: quesbank.QuestionType(q.Type),
|
||||
Text: q.Text,
|
||||
Options: q.Options,
|
||||
Answers: q.Answers,
|
||||
BlankCount: q.BlankCount,
|
||||
BlankAnswer: q.BlankAnswer,
|
||||
}
|
||||
}
|
||||
|
||||
// AIAnswerWork AI答题处理作业
|
||||
func (c *XuexitongCourse) AIAnswerWork(ctx context.Context, workDto *entity.WorkDto, aiConfig AIConfig) error {
|
||||
aiService := quesbank.NewAIAnswerService(aiConfig)
|
||||
|
||||
for i := range workDto.Questions {
|
||||
q := &workDto.Questions[i]
|
||||
|
||||
// 转换为通用题目格式
|
||||
question := convertToQuesbankQuestion(*q)
|
||||
|
||||
// 构建提示词
|
||||
prompt := quesbank.BuildQuestionPrompt(question)
|
||||
|
||||
// 获取AI答案
|
||||
aiResponse, err := aiService.GetAnswer(ctx, prompt)
|
||||
if err != nil {
|
||||
logger.Logger.Error("AI答题失败",
|
||||
zap.String("qid", q.Qid),
|
||||
zap.String("text", q.Text),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析答案
|
||||
answers, err := quesbank.ParseAIAnswer(aiResponse)
|
||||
if err != nil {
|
||||
logger.Logger.Error("解析AI答案失败",
|
||||
zap.String("qid", q.Qid),
|
||||
zap.String("aiResponse", aiResponse),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 根据题型处理答案
|
||||
switch q.Type {
|
||||
case entity.QuestionTypeSingleChoice, entity.QuestionTypeMultipleChoice:
|
||||
var matchedAnswers []string
|
||||
for _, ans := range answers {
|
||||
matched := quesbank.MatchAnswerToOption(ans, q.Options)
|
||||
if matched != "" {
|
||||
matchedAnswers = append(matchedAnswers, matched)
|
||||
}
|
||||
}
|
||||
q.Answers = matchedAnswers
|
||||
|
||||
case entity.QuestionTypeTrueFalse:
|
||||
if len(answers) > 0 {
|
||||
q.Answers = answers
|
||||
}
|
||||
|
||||
case entity.QuestionTypeFillBlank:
|
||||
q.BlankAnswer = make(map[string]string)
|
||||
for idx, ans := range answers {
|
||||
key := fmt.Sprintf("第%d空", idx+1)
|
||||
q.BlankAnswer[key] = ans
|
||||
}
|
||||
q.BlankCount = len(answers)
|
||||
|
||||
case entity.QuestionTypeShortAnswer:
|
||||
if len(answers) > 0 {
|
||||
q.Answers = []string{strings.Join(answers, "")}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Logger.Debug("AI答题成功",
|
||||
zap.String("qid", q.Qid),
|
||||
zap.Int("type", int(q.Type)),
|
||||
zap.Strings("answers", q.Answers))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoCompleteWorkWithAI 使用AI自动完成作业
|
||||
func (c *XuexitongCourse) AutoCompleteWorkWithAI(ctx context.Context, p *entity.PointWorkDto, aiConfig AIConfig, autoSubmit bool) (string, error) {
|
||||
// 获取作业题目
|
||||
workDto, err := c.FetchWorkQuestions(ctx, p)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取作业题目失败: %w", err)
|
||||
}
|
||||
if workDto.IsPassed {
|
||||
return "", errors.New("作业已完成")
|
||||
}
|
||||
if len(workDto.Questions) == 0 {
|
||||
return "", errors.New("作业无题目")
|
||||
}
|
||||
|
||||
logger.Logger.Info("开始AI答题",
|
||||
zap.String("title", workDto.Title),
|
||||
zap.Int("questionCount", len(workDto.Questions)))
|
||||
|
||||
// AI答题
|
||||
if err := c.AIAnswerWork(ctx, workDto, aiConfig); err != nil {
|
||||
return "", fmt.Errorf("AI答题失败: %w", err)
|
||||
}
|
||||
|
||||
// 提交答案
|
||||
result, err := c.SubmitWorkAnswers(ctx, workDto, !autoSubmit)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("提交答案失败: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -62,44 +62,33 @@ type XueXiTCourse struct {
|
||||
CourseID string `json:"courseId"` // 课程ID
|
||||
ChatID string `json:"chatId"`
|
||||
CourseTeacher string `json:"courseTeacher"` // 课程老师
|
||||
CourseName string `json:"courseName"` //课程名
|
||||
CourseName string `json:"courseName"` // 课程名
|
||||
CourseImage string `json:"courseImage"`
|
||||
// 两个标识 暂时不知道有什么用
|
||||
CourseDataID int `json:"courseDataId"`
|
||||
ContentID int `json:"ContentID"`
|
||||
IsStart bool `json:"isstart"` //是否开课了,开课了为true,没开课为false。一般来说开课才能刷,不然刷不了的
|
||||
State int `json:"state"` //课程状态,0为正常,1为课程已经结束
|
||||
IsStart bool `json:"isstart"` // 是否开课了,开课了为true,没开课为false
|
||||
State int `json:"state"` // 课程状态,0为正常,1为课程已经结束
|
||||
}
|
||||
|
||||
// XueXiTCourseChapter 课程章节信息
|
||||
type XueXiTCourseChapter struct {
|
||||
ID string `json:"id"` // 章节ID
|
||||
ID int `json:"id"` // 章节ID
|
||||
Name string `json:"name"` // 章节名称
|
||||
ParentNodeID string `json:"parentnodeid"` // 父节点ID
|
||||
ParentNodeID int `json:"parentnodeid"` // 父节点ID
|
||||
Layer int `json:"layer"` // 层级
|
||||
Label string `json:"label"` // 标签
|
||||
IndexOrder int `json:"indexOrder"` // 排序索引
|
||||
JobCount int `json:"jobcount"` // 任务点数量
|
||||
Status int `json:"status"` // 状态
|
||||
Status string `json:"status"` // 状态
|
||||
IsReview int `json:"isReview"` // 是否复习
|
||||
BeginTime int64 `json:"begintime"` // 开始时间
|
||||
EndTime int64 `json:"endtime"` // 结束时间
|
||||
Attachment []XueXiTAttachment `json:"attachment"` // 附件列表
|
||||
PointTotal int `json:"pointTotal"` // 总任务点
|
||||
PointFinish int `json:"pointFinish"` // 已完成任务点
|
||||
Knowledge []XueXiTCourseChapter `json:"knowledge"` // 子章节列表
|
||||
}
|
||||
|
||||
// XueXiTAttachment 附件信息
|
||||
type XueXiTAttachment struct {
|
||||
ID string `json:"id"` // 附件ID
|
||||
Type string `json:"type"` // 附件类型(video/document等)
|
||||
ObjectID string `json:"objectid"` // 对象ID
|
||||
Extension string `json:"extension"` // 扩展信息
|
||||
JobID string `json:"jobid"` // 任务ID
|
||||
Property string `json:"property"` // 属性
|
||||
Name string `json:"name"` // 附件名称
|
||||
IsPassed bool `json:"isPassed"` // 是否已完成
|
||||
}
|
||||
|
||||
// ChapterData 章节数据响应
|
||||
type ChapterData struct {
|
||||
Data []struct {
|
||||
@@ -133,20 +122,7 @@ type ChapterData struct {
|
||||
Belongschoolid string `json:"belongschoolid"`
|
||||
Objectid string `json:"objectid"`
|
||||
Knowledge struct {
|
||||
Data []struct {
|
||||
Jobcount int `json:"jobcount"`
|
||||
Isreview int `json:"isreview"`
|
||||
Attachment struct {
|
||||
Data []interface{} `json:"data"`
|
||||
} `json:"attachment"`
|
||||
Indexorder int `json:"indexorder"`
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Layer int `json:"layer"`
|
||||
Parentnodeid int `json:"parentnodeid"`
|
||||
Status string `json:"status"`
|
||||
} `json:"data"`
|
||||
Data []KnowledgeData `json:"data"`
|
||||
} `json:"knowledge"`
|
||||
} `json:"data"`
|
||||
} `json:"course"`
|
||||
@@ -157,152 +133,15 @@ type ChapterData struct {
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// Card 任务点卡片信息
|
||||
type Card struct {
|
||||
ID string `json:"id"` // 卡片ID
|
||||
Title string `json:"title"` // 标题
|
||||
Description string `json:"description"` // 描述
|
||||
KnowledgeID string `json:"knowledgeid"` // 知识点ID
|
||||
Type string `json:"type"` // 类型(video/document/work等)
|
||||
IsPassed bool `json:"isPassed"` // 是否完成
|
||||
ObjectID string `json:"objectid"` // 对象ID
|
||||
JobID string `json:"jobid"` // 任务ID
|
||||
ResourceID string `json:"resourceid"` // 资源ID
|
||||
Attachment *IframeAttributes `json:"attachment,omitempty"` // iframe属性
|
||||
}
|
||||
|
||||
// IframeAttributes iframe属性
|
||||
type IframeAttributes struct {
|
||||
Src string `json:"src"` // iframe地址
|
||||
Module string `json:"module"` // 模块名
|
||||
ModToken string `json:"modtoken"` // 模块token
|
||||
Width string `json:"width"` // 宽度
|
||||
Height string `json:"height"` // 高度
|
||||
Scrolling string `json:"scrolling"` // 滚动
|
||||
ClassID string `json:"classid"` // 课程ID
|
||||
ObjectID string `json:"objectid"` // 对象ID
|
||||
Enc string `json:"enc"` // 加密参数
|
||||
UtEnc string `json:"utenc"` // ut加密参数
|
||||
OtherInfo string `json:"otherinfo"` // 其他信息
|
||||
JobID string `json:"jobid"` // 任务ID
|
||||
KnowledgeID string `json:"knowledgeid"` // 知识点ID
|
||||
CourseFaceCheck int `json:"coursefacecheck"` // 人脸识别
|
||||
}
|
||||
|
||||
// DataItem 任务点数据项
|
||||
type DataItem struct {
|
||||
ID string `json:"id"`
|
||||
IndexOrder int `json:"indexorder"`
|
||||
KnowledgeID string `json:"knowledgeid"`
|
||||
Attachment struct {
|
||||
ObjectID string `json:"objectid"`
|
||||
Property struct {
|
||||
Name string `json:"name"`
|
||||
IsPassed bool `json:"ispassed"`
|
||||
JobID string `json:"jobid"`
|
||||
} `json:"property"`
|
||||
Type string `json:"type"`
|
||||
} `json:"attachment"`
|
||||
PointList []interface{} `json:"pointList"`
|
||||
JobCount int `json:"jobcount"`
|
||||
Duration int64 `json:"duration"`
|
||||
ClazzID string `json:"clazzid"`
|
||||
CourseID string `json:"courseid"`
|
||||
Module string `json:"module"`
|
||||
}
|
||||
|
||||
// VideoDto 视频信息
|
||||
type VideoDto struct {
|
||||
VideoPath string `json:"videoPath"` // 视频路径
|
||||
ObjectID string `json:"objectid"` // 对象ID
|
||||
OtherInfo string `json:"otherInfo"` // 其他信息
|
||||
JobID string `json:"jobid"` // 任务ID
|
||||
Duration int `json:"duration"` // 时长(秒)
|
||||
DToken string `json:"dtoken"` // dtoken
|
||||
IsAudio bool `json:"isAudio"` // 是否是音频
|
||||
IsPassed bool `json:"isPassed"` // 是否已完成
|
||||
}
|
||||
|
||||
// DocumentDto 文档信息
|
||||
type DocumentDto struct {
|
||||
ObjectID string `json:"objectid"` // 对象ID
|
||||
JobID string `json:"jobid"` // 任务ID
|
||||
OtherInfo string `json:"otherInfo"` // 其他信息
|
||||
JToken string `json:"jtoken"` // jtoken
|
||||
PageCount int `json:"pageCount"` // 页数
|
||||
IsPassed bool `json:"isPassed"` // 是否已完成
|
||||
}
|
||||
|
||||
// FaceQRCodeResponse 人脸识别二维码响应
|
||||
type FaceQRCodeResponse struct {
|
||||
Status bool `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
QR string `json:"qr"` // 二维码base64数据
|
||||
Enc string `json:"enc"` // 加密参数
|
||||
}
|
||||
|
||||
// FaceTokenResponse 人脸识别上传令牌响应
|
||||
type FaceTokenResponse struct {
|
||||
Result int `json:"result"`
|
||||
UploadToken string `json:"uploadtoken"`
|
||||
}
|
||||
|
||||
// FaceUploadResponse 人脸识别上传响应
|
||||
type FaceUploadResponse struct {
|
||||
Result bool `json:"result"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
FileID string `json:"fileId"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// FaceVerifyResponse 人脸识别验证响应
|
||||
type FaceVerifyResponse struct {
|
||||
Status bool `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
// WorkDto 作业信息
|
||||
type WorkDto struct {
|
||||
WorkID string `json:"workid"` // 作业ID
|
||||
CourseID string `json:"courseid"` // 课程ID
|
||||
ClassID string `json:"classid"` // 班级ID
|
||||
KnowledgeID string `json:"knowledgeid"` // 知识点ID
|
||||
JobID string `json:"jobid"` // 任务ID
|
||||
Enc string `json:"enc"` // 加密参数
|
||||
UTEnc string `json:"utenc"` // UT加密参数
|
||||
Title string `json:"title"` // 作业标题
|
||||
IsPassed bool `json:"isPassed"` // 是否已完成
|
||||
Questions []WorkQuestion `json:"questions"` // 题目列表
|
||||
}
|
||||
|
||||
// WorkQuestion 作业题目
|
||||
type WorkQuestion struct {
|
||||
QuestionID string `json:"questionId"` // 题目ID
|
||||
Type int `json:"type"` // 题目类型: 0单选 1多选 2判断 3填空 4简答
|
||||
Title string `json:"title"` // 题目标题
|
||||
Description string `json:"description"` // 题目描述
|
||||
Options []string `json:"options"` // 选项列表(单选/多选/判断)
|
||||
Answer string `json:"answer"` // 答案
|
||||
Score float64 `json:"score"` // 分值
|
||||
}
|
||||
|
||||
// WorkSubmitRequest 作业提交请求
|
||||
type WorkSubmitRequest struct {
|
||||
WorkID string `json:"workid"`
|
||||
TotalScore string `json:"totalScore"`
|
||||
AnswerList []WorkAnswerItem `json:"answerlist"`
|
||||
UserId string `json:"userId"`
|
||||
ClassId string `json:"classId"`
|
||||
CourseId string `json:"courseId"`
|
||||
KnowledgeId string `json:"knowledgeid"`
|
||||
JobId string `json:"jobid"`
|
||||
Enc string `json:"enc"`
|
||||
UTEnc string `json:"utenc"`
|
||||
}
|
||||
|
||||
// WorkAnswerItem 作业答案项
|
||||
type WorkAnswerItem struct {
|
||||
QuestionID string `json:"questionId"`
|
||||
Answer string `json:"answer"`
|
||||
// KnowledgeData 章节知识点数据
|
||||
type KnowledgeData struct {
|
||||
Jobcount int `json:"jobcount"`
|
||||
Isreview int `json:"isreview"`
|
||||
Indexorder int `json:"indexorder"`
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Layer int `json:"layer"`
|
||||
Parentnodeid int `json:"parentnodeid"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
package entity
|
||||
|
||||
// PointType 任务点类型
|
||||
type PointType string
|
||||
|
||||
const (
|
||||
PointTypeVideo PointType = "insertvideo"
|
||||
PointTypeDocument PointType = "insertdoc"
|
||||
PointTypeDocument2 PointType = "document"
|
||||
PointTypeBook PointType = "insertbook"
|
||||
PointTypeWork PointType = "insertwork"
|
||||
PointTypeHyperlink PointType = "insertlink"
|
||||
PointTypeLive PointType = "insertlive"
|
||||
)
|
||||
|
||||
// PointVideoDto 视频任务点
|
||||
type PointVideoDto struct {
|
||||
CardIndex int
|
||||
CourseID string
|
||||
ClassID string
|
||||
KnowledgeID int
|
||||
Cpi string
|
||||
ObjectID string
|
||||
// 从SSR视图中获取
|
||||
IsPassed bool
|
||||
FID int
|
||||
DToken string
|
||||
PlayTime int
|
||||
Duration int
|
||||
JobID string
|
||||
OtherInfo string
|
||||
Title string
|
||||
RT float64
|
||||
VideoFaceCaptureEnc string
|
||||
RandomCaptureTime string
|
||||
AttDurationEnc string
|
||||
Enc string
|
||||
Mid string
|
||||
}
|
||||
|
||||
// PointDocumentDto 文档查看任务点
|
||||
type PointDocumentDto struct {
|
||||
CardIndex int
|
||||
CourseID string
|
||||
ClassID string
|
||||
KnowledgeID int
|
||||
Cpi string
|
||||
ObjectID string
|
||||
Title string
|
||||
JobID string
|
||||
Jtoken string
|
||||
IsJob bool // 是否是任务点
|
||||
Type string // document / insertbook
|
||||
}
|
||||
|
||||
// PointWorkDto 测验/作业任务点
|
||||
type PointWorkDto struct {
|
||||
CardIndex int
|
||||
CourseID string
|
||||
ClassID string
|
||||
KnowledgeID int
|
||||
Cpi string
|
||||
WorkID string
|
||||
SchoolID string
|
||||
JobID string
|
||||
PUID string
|
||||
KToken string
|
||||
Enc string
|
||||
Title string
|
||||
IsPassed bool
|
||||
}
|
||||
|
||||
// PointHyperlinkDto 外链任务点
|
||||
type PointHyperlinkDto struct {
|
||||
CardIndex int
|
||||
CourseID string
|
||||
ClassID string
|
||||
KnowledgeID int
|
||||
Cpi string
|
||||
ObjectID string
|
||||
Title string
|
||||
JobID string
|
||||
Jtoken string
|
||||
LinkType int // 外链类型
|
||||
IsJob bool // 是否是任务点
|
||||
}
|
||||
|
||||
// PointLiveDto 直播任务点
|
||||
type PointLiveDto struct {
|
||||
CardIndex int
|
||||
CourseID string
|
||||
ClassID string
|
||||
KnowledgeID int
|
||||
Cpi string
|
||||
UserId string // 用户ID
|
||||
Live bool
|
||||
LiveId string // 直播ID
|
||||
Vdoid string
|
||||
Mid string
|
||||
Title string // 直播标题
|
||||
JobID string
|
||||
StreamName string // 直播流名称
|
||||
LiveStatusStr string // 直播状态文字
|
||||
LiveStatusCode int // 直播状态码 0=未开始
|
||||
VideoDuration int // 视频时长
|
||||
Aid string
|
||||
Module string // 类型
|
||||
IsJob bool // 是否为任务点
|
||||
AuthEnc string
|
||||
LiveDragEnc string
|
||||
LiveSetEnc string
|
||||
OtherInfo string
|
||||
Enc string
|
||||
LiveSwDsEnc string
|
||||
VideoCompletePercent float64 // 观看进度
|
||||
}
|
||||
|
||||
// Card 任务点卡片信息
|
||||
type Card struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CardOrder int `json:"cardorder"`
|
||||
KnowledgeID int `json:"knowledgeid"`
|
||||
}
|
||||
|
||||
// CardAPIResponse 卡片API响应
|
||||
type CardAPIResponse struct {
|
||||
Data []CardDataItem `json:"data"`
|
||||
}
|
||||
|
||||
// CardDataItem 卡片数据项
|
||||
type CardDataItem struct {
|
||||
ClickCount int `json:"clickcount"`
|
||||
Createtime int64 `json:"createtime"`
|
||||
OpenLock int `json:"openlock"`
|
||||
IndexOrder int `json:"indexorder"`
|
||||
Name string `json:"name"`
|
||||
LastModifyTime int64 `json:"lastmodifytime"`
|
||||
ID int `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Layer int `json:"layer"`
|
||||
Card struct {
|
||||
Data []Card `json:"data"`
|
||||
} `json:"card"`
|
||||
ParentNodeID int `json:"parentnodeid"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// IframeAttributes iframe 的属性
|
||||
type IframeAttributes struct {
|
||||
Data map[string]interface{} `json:"data"`
|
||||
Other map[string]string
|
||||
HasData bool
|
||||
}
|
||||
|
||||
// ChapterPointStatus 章节任务点状态
|
||||
type ChapterPointStatus map[string]struct {
|
||||
ClickCount int `json:"clickcount"`
|
||||
FinishCount int `json:"finishcount"`
|
||||
TotalCount int `json:"totalcount"`
|
||||
OpenLock int `json:"openlock"`
|
||||
UnFinishCount int `json:"unfinishcount"`
|
||||
}
|
||||
|
||||
// AttachmentSetting 附件设置(从页面JS中解析)
|
||||
type AttachmentSetting struct {
|
||||
Attachments []map[string]interface{} `json:"attachments"`
|
||||
Defaults map[string]interface{} `json:"defaults"`
|
||||
}
|
||||
|
||||
// ChapterPoints 章节所有任务点集合(旧版,保留兼容)
|
||||
type ChapterPoints struct {
|
||||
Videos []PointVideoDto
|
||||
Documents []PointDocumentDto
|
||||
Works []PointWorkDto
|
||||
Hyperlinks []PointHyperlinkDto
|
||||
Lives []PointLiveDto
|
||||
}
|
||||
|
||||
// ChapterPointType 任务点类型枚举
|
||||
type ChapterPointType int
|
||||
|
||||
const (
|
||||
ChapterPointTypeVideo ChapterPointType = 1 // 视频
|
||||
ChapterPointTypeDocument ChapterPointType = 2 // 文档
|
||||
ChapterPointTypeWork ChapterPointType = 3 // 作业
|
||||
ChapterPointTypeHyperlink ChapterPointType = 4 // 外链
|
||||
ChapterPointTypeLive ChapterPointType = 5 // 直播
|
||||
)
|
||||
|
||||
// ChapterPoint 统一的章节任务点结构(与Card一一对应)
|
||||
type ChapterPoint struct {
|
||||
CardIndex int // 对应的卡片索引
|
||||
Type ChapterPointType // 任务点类型
|
||||
KnowledgeID int // 知识点ID
|
||||
CourseID string // 课程ID
|
||||
ClassID string // 班级ID
|
||||
Cpi string // cpi
|
||||
JobID string // 任务ID
|
||||
ObjectID string // 对象ID
|
||||
Title string // 标题
|
||||
IsJob bool // 是否是任务点
|
||||
IsPassed bool // 是否已完成
|
||||
|
||||
// 视频相关字段
|
||||
FID int
|
||||
DToken string
|
||||
PlayTime int
|
||||
Duration int
|
||||
OtherInfo string
|
||||
RT float64
|
||||
VideoFaceCaptureEnc string
|
||||
RandomCaptureTime string
|
||||
AttDurationEnc string
|
||||
Enc string
|
||||
Mid string
|
||||
|
||||
// 文档相关字段
|
||||
Jtoken string
|
||||
DocumentType string // document / insertbook
|
||||
|
||||
// 作业相关字段
|
||||
WorkID string
|
||||
SchoolID string
|
||||
PUID string
|
||||
KToken string
|
||||
|
||||
// 外链相关字段
|
||||
LinkType int
|
||||
|
||||
// 直播相关字段
|
||||
UserId string
|
||||
Live bool
|
||||
LiveId string
|
||||
Vdoid string
|
||||
StreamName string
|
||||
LiveStatusStr string
|
||||
LiveStatusCode int
|
||||
VideoDuration int
|
||||
Aid string
|
||||
Module string
|
||||
AuthEnc string
|
||||
LiveDragEnc string
|
||||
LiveSetEnc string
|
||||
LiveSwDsEnc string
|
||||
VideoCompletePercent float64
|
||||
}
|
||||
|
||||
// QuestionType 题目类型
|
||||
type QuestionType int
|
||||
|
||||
const (
|
||||
QuestionTypeSingleChoice QuestionType = 0 // 单选题
|
||||
QuestionTypeMultipleChoice QuestionType = 1 // 多选题
|
||||
QuestionTypeFillBlank QuestionType = 2 // 填空题
|
||||
QuestionTypeTrueFalse QuestionType = 3 // 判断题
|
||||
QuestionTypeShortAnswer QuestionType = 4 // 简答题
|
||||
)
|
||||
|
||||
// WorkQuestion 作业题目
|
||||
type WorkQuestion struct {
|
||||
Qid string `json:"qid"` // 题目ID
|
||||
Type QuestionType `json:"type"` // 题目类型
|
||||
Text string `json:"text"` // 题目内容
|
||||
Options map[string]string `json:"options"` // 选项 (A: xxx, B: xxx)
|
||||
Answers []string `json:"answers"` // 答案
|
||||
BlankCount int `json:"blankCount"` // 填空题空格数量
|
||||
BlankAnswer map[string]string `json:"blankAnswer"` // 填空题答案 (第1空: xxx)
|
||||
}
|
||||
|
||||
// WorkDto 作业详情
|
||||
type WorkDto struct {
|
||||
Title string `json:"title"` // 作业标题
|
||||
IsPassed bool `json:"isPassed"`
|
||||
Cpi string `json:"cpi"` // cpi
|
||||
JobId string `json:"jobId"` // 任务ID
|
||||
WorkId string `json:"workId"` // 作业ID
|
||||
ClassId string `json:"classId"` // 班级ID
|
||||
CourseId string `json:"courseId"` // 课程ID
|
||||
KnowledgeId string `json:"knowledgeId"` // 知识点ID
|
||||
UserId string `json:"userId"` // 用户ID
|
||||
FullScore string `json:"fullScore"` // 满分
|
||||
OldWorkId string `json:"oldWorkId"` // 原始作业ID
|
||||
OldSchoolId string `json:"oldSchoolId"` // 原始学校ID
|
||||
Api string `json:"api"` // API值
|
||||
WorkRelationId string `json:"workRelationId"` // 作业关联ID
|
||||
EncWork string `json:"encWork"` // 加密参数
|
||||
WorkAnswerId string `json:"workAnswerId"` // 作业答案ID
|
||||
AnswerId string `json:"answerId"` // 答案ID
|
||||
TotalQuestionNum string `json:"totalQuestionNum"` // 题目总数
|
||||
Questions []WorkQuestion `json:"questions"` // 题目列表
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// AutoStudyByCourseID 通过课程ID自动刷课(定时任务主入口)
|
||||
func (c *XuexitongCourse) AutoStudyByCourseID(ctx context.Context, courseID string) error {
|
||||
return c.AutoStudyByCourseIDWithOptions(ctx, courseID, nil)
|
||||
}
|
||||
|
||||
// AutoStudyByCourseIDWithOptions 通过课程ID自动刷课(带选项)
|
||||
func (c *XuexitongCourse) AutoStudyByCourseIDWithOptions(ctx context.Context, courseID string, opts *AutoStudyOptions) error {
|
||||
logger.Logger.Info(fmt.Sprintf("开始刷课,课程ID: %s", courseID))
|
||||
|
||||
courses := c.PullCourseList(ctx)
|
||||
if len(courses) == 0 {
|
||||
return fmt.Errorf("没有找到任何课程")
|
||||
}
|
||||
|
||||
var targetCourse *entity.XueXiTCourse
|
||||
for _, course := range courses {
|
||||
if course.CourseID == courseID {
|
||||
targetCourse = &course
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetCourse == nil {
|
||||
return fmt.Errorf("未找到课程ID为 %s 的课程", courseID)
|
||||
}
|
||||
|
||||
if !targetCourse.IsStart {
|
||||
return fmt.Errorf("课程 %s 未开课", targetCourse.CourseName)
|
||||
}
|
||||
|
||||
if targetCourse.State != 0 {
|
||||
return fmt.Errorf("课程 %s 已结束", targetCourse.CourseName)
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
return c.AutoStudyCourseWithOptions(ctx, *targetCourse, opts)
|
||||
}
|
||||
return c.AutoStudyCourse(ctx, *targetCourse)
|
||||
}
|
||||
|
||||
// AutoStudyAllCourses 自动学习所有课程
|
||||
func (c *XuexitongCourse) AutoStudyAllCourses(ctx context.Context) error {
|
||||
logger.Logger.Info("开始自动刷所有课程任务")
|
||||
|
||||
courses := c.PullCourseList(ctx)
|
||||
if len(courses) == 0 {
|
||||
logger.Logger.Warn("没有找到任何课程")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Logger.Info(fmt.Sprintf("找到 %d 个课程", len(courses)))
|
||||
|
||||
for i, course := range courses {
|
||||
logger.Logger.Info(fmt.Sprintf("开始刷课 [%d/%d]: %s", i+1, len(courses), course.CourseName))
|
||||
|
||||
if !course.IsStart {
|
||||
logger.Logger.Info(fmt.Sprintf("课程 %s 未开课,跳过", course.CourseName))
|
||||
continue
|
||||
}
|
||||
|
||||
if course.State != 0 {
|
||||
logger.Logger.Info(fmt.Sprintf("课程 %s 已结束,跳过", course.CourseName))
|
||||
continue
|
||||
}
|
||||
|
||||
err := c.AutoStudyCourse(ctx, course)
|
||||
if err != nil {
|
||||
logger.Logger.Error(fmt.Sprintf("刷课失败: %s", course.CourseName), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Logger.Info(fmt.Sprintf("完成刷课 [%d/%d]: %s", i+1, len(courses), course.CourseName))
|
||||
}
|
||||
|
||||
logger.Logger.Info("所有课程刷课任务完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoStudyCourse 自动学习指定课程的所有章节
|
||||
func (c *XuexitongCourse) AutoStudyCourse(ctx context.Context, course entity.XueXiTCourse) error {
|
||||
return c.AutoStudyCourseWithOptions(ctx, course, nil)
|
||||
}
|
||||
|
||||
// AutoStudyChapter 自动学习指定章节的所有任务点
|
||||
func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.XueXiTCourse, chapter entity.XueXiTCourseChapter) error {
|
||||
if err := c.EnterChapterForwardCall(ctx, course, chapter.ID); err != nil {
|
||||
logger.Logger.Warn("EnterChapterForwardCall失败", zap.Error(err))
|
||||
}
|
||||
|
||||
cards, points, err := c.FetchChapterCards(ctx, course, chapter.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取章节任务点失败: %w", err)
|
||||
}
|
||||
|
||||
if len(cards) == 0 || len(points) == 0 {
|
||||
logger.Logger.Info(fmt.Sprintf("章节 %s 没有任务点", chapter.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := range points {
|
||||
if points[i].Type == entity.ChapterPointTypeVideo {
|
||||
logger.Logger.Debug("视频任务点参数",
|
||||
zap.String("objectID", points[i].ObjectID),
|
||||
zap.String("jobID", points[i].JobID),
|
||||
zap.String("otherInfo", points[i].OtherInfo),
|
||||
zap.String("attDurationEnc", points[i].AttDurationEnc),
|
||||
zap.String("videoFaceCaptureEnc", points[i].VideoFaceCaptureEnc),
|
||||
zap.Int("FID", points[i].FID))
|
||||
}
|
||||
}
|
||||
|
||||
for i := range points {
|
||||
if err := c.checkTimeRestriction(); err != nil {
|
||||
// 更新任务状态为waiting
|
||||
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
|
||||
return err
|
||||
}
|
||||
|
||||
point := &points[i]
|
||||
if point.Type == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch point.Type {
|
||||
case entity.ChapterPointTypeVideo:
|
||||
if err := c.AutoPlayVideoByPoint(ctx, course, point); err != nil {
|
||||
logger.Logger.Error("视频刷课失败", zap.Error(err), zap.String("title", point.Title))
|
||||
}
|
||||
case entity.ChapterPointTypeDocument:
|
||||
if err := c.AutoReadDocumentByPoint(ctx, course, point); err != nil {
|
||||
logger.Logger.Error("文档刷课失败", zap.Error(err), zap.String("title", point.Title))
|
||||
}
|
||||
case entity.ChapterPointTypeWork:
|
||||
// 作业需要AI配置,这里暂时跳过,使用 AutoStudyChapterWithAI 方法处理
|
||||
logger.Logger.Info("作业任务点需要AI配置,跳过", zap.String("title", point.Title))
|
||||
case entity.ChapterPointTypeHyperlink:
|
||||
if err := c.AutoCompleteHyperlinkByPoint(ctx, course, point); err != nil {
|
||||
logger.Logger.Error("外链刷课失败", zap.Error(err), zap.String("title", point.Title))
|
||||
}
|
||||
case entity.ChapterPointTypeLive:
|
||||
if err := c.AutoWatchLiveByPoint(ctx, course, point); err != nil {
|
||||
logger.Logger.Error("直播刷课失败", zap.Error(err), zap.String("title", point.Title))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoStudyOptions 自动学习选项
|
||||
type AutoStudyOptions struct {
|
||||
AIConfig *AIConfig // AI配置(用于作业答题)
|
||||
AutoSubmit bool // 是否自动提交作业
|
||||
Thread int // 线程数
|
||||
}
|
||||
|
||||
// AutoStudyCourseWithOptions 带选项的自动学习课程
|
||||
func (c *XuexitongCourse) AutoStudyCourseWithOptions(ctx context.Context, course entity.XueXiTCourse, opts *AutoStudyOptions) error {
|
||||
logger.Logger.Info(fmt.Sprintf("开始刷课程: %s", course.CourseName))
|
||||
|
||||
chapters := c.PullCourseChapterList(ctx, course)
|
||||
if len(chapters) == 0 {
|
||||
logger.Logger.Warn(fmt.Sprintf("课程 %s 没有章节", course.CourseName))
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := c.FetchChapterPointStatus(ctx, course, chapters)
|
||||
if err != nil {
|
||||
logger.Logger.Warn("获取章节状态失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 获取线程数配置,默认为1(单线程)
|
||||
threadCount := 1
|
||||
if opts != nil && opts.Thread > 0 {
|
||||
threadCount = opts.Thread
|
||||
}
|
||||
|
||||
// 如果是单线程,使用原有逻辑
|
||||
if threadCount == 1 {
|
||||
for _, chapter := range chapters {
|
||||
if err := c.checkTimeRestriction(); err != nil {
|
||||
logger.Logger.Info("时间限制,暂停刷课", zap.String("course", course.CourseName))
|
||||
return err
|
||||
}
|
||||
|
||||
if chapter.JobCount > 0 && chapter.PointTotal > chapter.PointFinish {
|
||||
err := c.AutoStudyChapterWithOptions(ctx, course, chapter, opts)
|
||||
if err != nil {
|
||||
if err == ErrTimeRestriction {
|
||||
return err
|
||||
}
|
||||
logger.Logger.Error(fmt.Sprintf("刷章节失败: %s", chapter.Name), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 多线程并发处理
|
||||
logger.Logger.Info(fmt.Sprintf("使用 %d 线程并发刷课", threadCount))
|
||||
|
||||
// 筛选需要处理的章节
|
||||
var targetChapters []entity.XueXiTCourseChapter
|
||||
for _, chapter := range chapters {
|
||||
if chapter.JobCount > 0 && chapter.PointTotal > chapter.PointFinish {
|
||||
targetChapters = append(targetChapters, chapter)
|
||||
}
|
||||
}
|
||||
|
||||
if len(targetChapters) == 0 {
|
||||
logger.Logger.Info("没有需要处理的章节")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建信号量控制并发数
|
||||
semaphore := make(chan struct{}, threadCount)
|
||||
errChan := make(chan error, len(targetChapters))
|
||||
doneChan := make(chan bool, len(targetChapters))
|
||||
|
||||
// 并发处理章节
|
||||
for _, chapter := range targetChapters {
|
||||
semaphore <- struct{}{} // 获取信号量
|
||||
|
||||
go func(ch entity.XueXiTCourseChapter) {
|
||||
defer func() {
|
||||
<-semaphore // 释放信号量
|
||||
doneChan <- true
|
||||
}()
|
||||
|
||||
if err := c.checkTimeRestriction(); err != nil {
|
||||
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", ch.Name))
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
err := c.AutoStudyChapterWithOptions(ctx, course, ch, opts)
|
||||
if err != nil {
|
||||
if err == ErrTimeRestriction {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
logger.Logger.Error(fmt.Sprintf("刷章节失败: %s", ch.Name), zap.Error(err))
|
||||
}
|
||||
}(chapter)
|
||||
}
|
||||
|
||||
// 等待所有任务完成
|
||||
for i := 0; i < len(targetChapters); i++ {
|
||||
<-doneChan
|
||||
}
|
||||
close(errChan)
|
||||
|
||||
// 检查是否有时间限制错误
|
||||
for err := range errChan {
|
||||
if err == ErrTimeRestriction {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Logger.Info(fmt.Sprintf("课程完成: %s", course.CourseName))
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoStudyChapterWithOptions 带选项的自动学习章节
|
||||
func (c *XuexitongCourse) AutoStudyChapterWithOptions(ctx context.Context, course entity.XueXiTCourse, chapter entity.XueXiTCourseChapter, opts *AutoStudyOptions) error {
|
||||
if err := c.EnterChapterForwardCall(ctx, course, chapter.ID); err != nil {
|
||||
logger.Logger.Warn("EnterChapterForwardCall失败", zap.Error(err))
|
||||
}
|
||||
|
||||
cards, points, err := c.FetchChapterCards(ctx, course, chapter.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取章节任务点失败: %w", err)
|
||||
}
|
||||
|
||||
if len(cards) == 0 || len(points) == 0 {
|
||||
logger.Logger.Info(fmt.Sprintf("章节 %s 没有任务点", chapter.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
//logger.Logger.Info("开始处理章节任务点",
|
||||
// zap.String("chapter", chapter.Name),
|
||||
// zap.Int("pointCount", len(points)))
|
||||
|
||||
for i := range points {
|
||||
if err := c.checkTimeRestriction(); err != nil {
|
||||
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
|
||||
return err
|
||||
}
|
||||
|
||||
point := &points[i]
|
||||
if point.IsPassed {
|
||||
logger.Logger.Debug("任务点已通过,跳过", zap.String("title", point.Title))
|
||||
continue
|
||||
}
|
||||
if point.Type == 0 {
|
||||
logger.Logger.Debug("跳过类型为0的任务点", zap.Int("index", i))
|
||||
continue
|
||||
}
|
||||
|
||||
//logger.Logger.Info("处理任务点",
|
||||
// zap.Int("index", i),
|
||||
// zap.Any("type", point.Type),
|
||||
// zap.String("title", point.Title),
|
||||
// zap.String("jobID", point.JobID),
|
||||
// zap.Bool("isPassed", point.IsPassed))
|
||||
|
||||
switch point.Type {
|
||||
case entity.ChapterPointTypeVideo:
|
||||
if err := c.AutoPlayVideoByPoint(ctx, course, point); err != nil {
|
||||
logger.Logger.Error("视频刷课失败", zap.Error(err), zap.String("title", point.Title))
|
||||
}
|
||||
case entity.ChapterPointTypeDocument:
|
||||
if err := c.AutoReadDocumentByPoint(ctx, course, point); err != nil {
|
||||
logger.Logger.Error("文档刷课失败", zap.Error(err), zap.String("title", point.Title))
|
||||
}
|
||||
case entity.ChapterPointTypeWork:
|
||||
logger.Logger.Info("发现作业任务点",
|
||||
zap.String("title", point.Title),
|
||||
zap.String("workID", point.WorkID),
|
||||
zap.String("jobID", point.JobID),
|
||||
zap.Int("cardIndex", point.CardIndex),
|
||||
zap.Int("knowledgeID", point.KnowledgeID),
|
||||
zap.String("cpi", point.Cpi),
|
||||
zap.Bool("hasAIConfig", opts != nil && opts.AIConfig != nil))
|
||||
|
||||
if opts != nil && opts.AIConfig != nil {
|
||||
if err := c.AutoCompleteWorkByPoint(ctx, course, point, opts.AIConfig, opts.AutoSubmit); err != nil {
|
||||
logger.Logger.Error("作业刷课失败", zap.Error(err), zap.String("title", point.Title))
|
||||
}
|
||||
} else {
|
||||
logger.Logger.Info("作业任务点需要AI配置,跳过", zap.String("title", point.Title))
|
||||
}
|
||||
case entity.ChapterPointTypeHyperlink:
|
||||
if err := c.AutoCompleteHyperlinkByPoint(ctx, course, point); err != nil {
|
||||
logger.Logger.Error("外链刷课失败", zap.Error(err), zap.String("title", point.Title))
|
||||
}
|
||||
case entity.ChapterPointTypeLive:
|
||||
if err := c.AutoWatchLiveByPoint(ctx, course, point); err != nil {
|
||||
logger.Logger.Error("直播刷课失败", zap.Error(err), zap.String("title", point.Title))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -9,10 +9,8 @@ import (
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
var randChar []string = []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "A", "B", "C", "D", "E", "F"}
|
||||
|
||||
// ApiVerifyCodeBytes 获取学习通验证码图片
|
||||
func ApiVerifyCodeBytes(client *resty.Client) ([]byte, error) {
|
||||
|
||||
urlStr := "https://mooc1-api.chaoxing.com/processVerifyPng.ac?t=" + fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||
|
||||
req := client.R()
|
||||
@@ -32,22 +30,24 @@ func ApiVerifyCodeBytes(client *resty.Client) ([]byte, error) {
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func ApiPassVerifyCode(client *resty.Client, code string, retry int) bool {
|
||||
if retry < 0 {
|
||||
// ApiPassVerifyCode 提交学习通验证码
|
||||
func ApiPassVerifyCode(client *resty.Client, code string, retryCount int) bool {
|
||||
if retryCount < 0 {
|
||||
return false
|
||||
}
|
||||
urlStr := "https://mooc1-api.chaoxing.com/html/processVerify.ac"
|
||||
//urlStr := "https://mooc1-api.chaoxing.com/html/processVerify.ac"
|
||||
urlStr := "https://mooc1-api.chaoxing.com/json/processVerify.ac"
|
||||
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
req := client.R()
|
||||
req.SetMultipartField("ucode", "", "", strings.NewReader(code))
|
||||
req.SetMultipartField("app", "", "", strings.NewReader("0"))
|
||||
req.SetMultipartField("app", "", "", strings.NewReader("1"))
|
||||
req.Header.Add("User-Agent", GetUA("mobile"))
|
||||
req.Header.Add("Accept", "*/*")
|
||||
req.Header.Add("Host", "mooc1-api.chaoxing.com")
|
||||
req.Header.Add("Connection", "keep-alive")
|
||||
res, err := req.Post(urlStr)
|
||||
if err != nil {
|
||||
return ApiPassVerifyCode(client, code, retry-1)
|
||||
return ApiPassVerifyCode(client, code, retryCount-1)
|
||||
}
|
||||
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
return res.StatusCode() == 302
|
||||
|
||||
@@ -0,0 +1,762 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/captcha"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
errors2 "likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/samber/oops"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// PullCourseChapterList 拉取学习通课程章节列表并返回
|
||||
func (c *XuexitongCourse) PullCourseChapterList(ctx context.Context, course entity.XueXiTCourse) []entity.XueXiTCourseChapter {
|
||||
var chapterString string
|
||||
key, _ := strconv.Atoi(course.Key)
|
||||
|
||||
e := retry.Do(func() error {
|
||||
params := url.Values{}
|
||||
params.Add("id", strconv.Itoa(key))
|
||||
params.Add("personid", strconv.Itoa(course.Cpi))
|
||||
params.Add("fields", "id,bbsid,classscore,isstart,allowdownload,chatid,name,state,isfiled,visiblescore,hideclazz,begindate,forbidintoclazz,coursesetting.fields(id,courseid,hiddencoursecover,coursefacecheck),course.fields(id,belongschoolid,name,infocontent,objectid,app,bulletformat,mappingcourseid,imageurl,teacherfactor,jobcount,knowledge.fields(id,name,indexOrder,parentnodeid,status,isReview,layer,label,jobcount,begintime,endtime,attachment.fields(id,type,objectid,extension).type(video)))")
|
||||
params.Add("view", "json")
|
||||
|
||||
get, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("mobile"),
|
||||
"Accept-Language": "zh_CN",
|
||||
"Host": "mooc1-api.chaoxing.com",
|
||||
"Connection": "Keep-Alive",
|
||||
}).Get(ApiPullChapter + "?" + params.Encode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chapterString = get.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(5*time.Second))
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).With("chapterString", chapterString).Wrap)
|
||||
|
||||
var chapterMap map[string]interface{}
|
||||
err := json.Unmarshal([]byte(chapterString), &chapterMap)
|
||||
if err != nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(503, "解析章节数据失败").WithInnerError(err), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
dataArr, ok := chapterMap["data"].([]interface{})
|
||||
if !ok || len(dataArr) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
firstData, ok := dataArr[0].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
courseData, ok := firstData["course"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
courseDataArr, ok := courseData["data"].([]interface{})
|
||||
if !ok || len(courseDataArr) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
firstCourse, ok := courseDataArr[0].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
knowledgeData, ok := firstCourse["knowledge"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
knowledgeArr, ok := knowledgeData["data"].([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var chapters []entity.XueXiTCourseChapter
|
||||
for _, kd := range knowledgeArr {
|
||||
kdMap, ok := kd.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
chapter := entity.XueXiTCourseChapter{
|
||||
ID: int(kdMap["id"].(float64)),
|
||||
Name: kdMap["name"].(string),
|
||||
ParentNodeID: int(kdMap["parentnodeid"].(float64)),
|
||||
Layer: int(kdMap["layer"].(float64)),
|
||||
Label: kdMap["label"].(string),
|
||||
IndexOrder: int(kdMap["indexorder"].(float64)),
|
||||
JobCount: int(kdMap["jobcount"].(float64)),
|
||||
Status: kdMap["status"].(string),
|
||||
IsReview: int(kdMap["isreview"].(float64)),
|
||||
}
|
||||
chapters = append(chapters, chapter)
|
||||
}
|
||||
|
||||
sort.Slice(chapters, func(i, j int) bool {
|
||||
iLabelParts := strings.Split(chapters[i].Label, ".")
|
||||
jLabelParts := strings.Split(chapters[j].Label, ".")
|
||||
for k := range iLabelParts {
|
||||
if k >= len(jLabelParts) {
|
||||
return false
|
||||
}
|
||||
iv, _ := strconv.Atoi(iLabelParts[k])
|
||||
jv, _ := strconv.Atoi(jLabelParts[k])
|
||||
if iv != jv {
|
||||
return iv < jv
|
||||
}
|
||||
}
|
||||
return len(iLabelParts) < len(jLabelParts)
|
||||
})
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
// FetchChapterPointStatus 获取章节任务点状态
|
||||
func (c *XuexitongCourse) FetchChapterPointStatus(ctx context.Context, course entity.XueXiTCourse, chapters []entity.XueXiTCourseChapter) (entity.ChapterPointStatus, error) {
|
||||
var nodes []string
|
||||
for _, ch := range chapters {
|
||||
nodes = append(nodes, strconv.Itoa(ch.ID))
|
||||
}
|
||||
|
||||
ts := time.Now().UnixMilli()
|
||||
classID, _ := strconv.Atoi(course.Key)
|
||||
courseID, _ := strconv.Atoi(course.CourseID)
|
||||
|
||||
values := url.Values{
|
||||
"view": {"json"},
|
||||
"nodes": {strings.Join(nodes, ",")},
|
||||
"clazzid": {strconv.Itoa(classID)},
|
||||
"time": {strconv.FormatInt(ts, 10)},
|
||||
"userid": {c.userID},
|
||||
"cpi": {strconv.Itoa(course.Cpi)},
|
||||
"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{
|
||||
"User-Agent": GetUA("mobile"),
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}).SetBody(values.Encode()).Post(ApiChapterPoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// EnterChapterForwardCall 进入章节前的前置调用
|
||||
func (c *XuexitongCourse) EnterChapterForwardCall(ctx context.Context, course entity.XueXiTCourse, chapterID int) error {
|
||||
urlStr := fmt.Sprintf("%s?courseId=%s&clazzid=%s&chapterId=%d&cpi=%d&verificationcode=&mooc2=1&toComputer=falseµTopicId=0",
|
||||
ApiEnterChapter, course.CourseID, course.Key, chapterID, course.Cpi)
|
||||
_, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("mobile"),
|
||||
}).Get(urlStr)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FetchChapterCards 获取章节任务点卡片(JSON方式)- 支持所有任务点类型
|
||||
func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.XueXiTCourse, chapterID int) ([]entity.Card, []entity.ChapterPoint, error) {
|
||||
var body string
|
||||
courseID, _ := strconv.Atoi(course.CourseID)
|
||||
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("mobile"),
|
||||
"Accept-Language": "zh_CN",
|
||||
}).SetQueryParams(map[string]string{
|
||||
"id": strconv.Itoa(chapterID),
|
||||
"courseid": strconv.Itoa(courseID),
|
||||
"fields": "id,parentnodeid,indexorder,label,layer,name,begintime,createtime,lastmodifytime,status,jobUnfinishedCount,clickcount,openlock,card.fields(id,knowledgeid,title,knowledgeTitile,description,cardorder).contentcard(all)",
|
||||
"view": "json",
|
||||
"token": "4faa8662c59590c6f43ae9fe5b002b42",
|
||||
"_time": strconv.FormatInt(time.Now().UnixMilli(), 10),
|
||||
}).Get(ApiChapterCards)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 触发202 需要重新登录获取cookie
|
||||
if resp.StatusCode() == 202 {
|
||||
|
||||
}
|
||||
body = resp.String()
|
||||
if strings.Contains(body, "请输入验证码") {
|
||||
logger.Logger.Warn("触发验证码,正在进行AI智能识别绕过.....")
|
||||
e := retry.Do(func() error {
|
||||
bytes, e := ApiVerifyCodeBytes(c.client)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
codeResult := captcha.ApiSemiOCRVerification(bytes, 1, 23)
|
||||
time.Sleep(5 * time.Second)
|
||||
if ApiPassVerifyCode(c.client, codeResult, 5) {
|
||||
return nil
|
||||
}
|
||||
return errors2.NewBusinessError(503, "验证码识别失败")
|
||||
}, retry.Attempts(5), retry.Delay(5*time.Second))
|
||||
return e
|
||||
}
|
||||
|
||||
statusCode := resp.StatusCode()
|
||||
if statusCode == 500 || statusCode == 202 || statusCode == 400 || statusCode == 403 {
|
||||
return errors2.NewBusinessError(statusCode, fmt.Sprintf("获取任务点失败,状态码: %d", statusCode))
|
||||
}
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(5*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var apiResp entity.CardAPIResponse
|
||||
if err := json.Unmarshal([]byte(body), &apiResp); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if len(apiResp.Data) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
dataItem := apiResp.Data[0]
|
||||
cards := dataItem.Card.Data
|
||||
|
||||
var points []entity.ChapterPoint
|
||||
cpiStr := strconv.Itoa(course.Cpi)
|
||||
|
||||
for cardIndex, card := range cards {
|
||||
logger.Logger.Debug("处理卡片",
|
||||
zap.Int("cardIndex", cardIndex),
|
||||
zap.Int("cardID", card.ID),
|
||||
zap.Int("cardKnowledgeID", card.KnowledgeID),
|
||||
zap.String("cardTitle", card.Title),
|
||||
zap.Int("descriptionLength", len(card.Description)))
|
||||
|
||||
if card.Description == "" {
|
||||
continue
|
||||
}
|
||||
iframes, err := c.parseIframeData(card.Description)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("解析卡片失败", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
for _, iframe := range iframes {
|
||||
if !iframe.HasData {
|
||||
continue
|
||||
}
|
||||
pointType, ok := iframe.Other["module"]
|
||||
logger.Logger.Debug("解析到任务点", zap.String("module", pointType), zap.Bool("hasModule", ok), zap.Any("data", iframe.Data))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
point := entity.ChapterPoint{
|
||||
CardIndex: cardIndex,
|
||||
KnowledgeID: card.KnowledgeID,
|
||||
CourseID: course.CourseID,
|
||||
ClassID: course.Key,
|
||||
Cpi: cpiStr,
|
||||
Title: card.Title,
|
||||
}
|
||||
|
||||
switch pointType {
|
||||
case "insertvideo", "insertaudio":
|
||||
objectID, ok := iframe.Data["objectid"].(string)
|
||||
if !ok || objectID == "" {
|
||||
continue
|
||||
}
|
||||
jobID, _ := iframe.Data["_jobid"].(string)
|
||||
point.Type = entity.ChapterPointTypeVideo
|
||||
point.ObjectID = objectID
|
||||
point.JobID = jobID
|
||||
|
||||
cords2, err := c.FetchChapterCards2(ctx, course, card.KnowledgeID)
|
||||
if err == nil && cords2 != nil {
|
||||
c.fillVideoPointFromCards2(cords2, &point, objectID)
|
||||
}
|
||||
|
||||
case "insertdoc", "document":
|
||||
objectID, _ := iframe.Data["objectid"].(string)
|
||||
jobID, _ := iframe.Data["_jobid"].(string)
|
||||
if jobID == "" {
|
||||
continue
|
||||
}
|
||||
point.Type = entity.ChapterPointTypeDocument
|
||||
point.ObjectID = objectID
|
||||
point.JobID = jobID
|
||||
point.DocumentType = "document"
|
||||
|
||||
case "insertbook", "insertreadV2":
|
||||
jobID, _ := iframe.Data["_jobid"].(string)
|
||||
if jobID == "" {
|
||||
continue
|
||||
}
|
||||
point.Type = entity.ChapterPointTypeDocument
|
||||
point.JobID = jobID
|
||||
point.DocumentType = "insertbook"
|
||||
|
||||
case "work", "insertwork":
|
||||
workID, _ := iframe.Data["workid"].(string)
|
||||
schoolID, _ := iframe.Data["schoolid"].(string)
|
||||
jobID, _ := iframe.Data["_jobid"].(string)
|
||||
if workID == "" || jobID == "" {
|
||||
continue
|
||||
}
|
||||
if schoolID == "" {
|
||||
schoolID = "0"
|
||||
}
|
||||
point.Type = entity.ChapterPointTypeWork
|
||||
point.WorkID = workID
|
||||
point.SchoolID = schoolID
|
||||
point.JobID = jobID
|
||||
|
||||
case "hyperlink", "insertlink":
|
||||
jobID, _ := iframe.Data["_jobid"].(string)
|
||||
if jobID == "" {
|
||||
continue
|
||||
}
|
||||
linkType := 0
|
||||
if lt, ok := iframe.Data["linkType"].(float64); ok {
|
||||
linkType = int(lt)
|
||||
}
|
||||
point.Type = entity.ChapterPointTypeHyperlink
|
||||
point.JobID = jobID
|
||||
point.LinkType = linkType
|
||||
|
||||
case "insertlive":
|
||||
jobID, _ := iframe.Data["_jobid"].(string)
|
||||
if jobID == "" {
|
||||
continue
|
||||
}
|
||||
title, _ := iframe.Data["title"].(string)
|
||||
liveStatus, _ := iframe.Data["liveStatus"].(string)
|
||||
streamName, _ := iframe.Data["streamName"].(string)
|
||||
vdoid, _ := iframe.Data["vdoid"].(string)
|
||||
liveId := ""
|
||||
if lid, ok := iframe.Data["liveId"].(float64); ok {
|
||||
liveId = strconv.FormatInt(int64(lid), 10)
|
||||
}
|
||||
live, _ := iframe.Data["live"].(bool)
|
||||
|
||||
point.Type = entity.ChapterPointTypeLive
|
||||
point.JobID = jobID
|
||||
point.Title = title
|
||||
point.LiveStatusStr = liveStatus
|
||||
point.StreamName = streamName
|
||||
point.Vdoid = vdoid
|
||||
point.LiveId = liveId
|
||||
point.Live = live
|
||||
point.Module = pointType
|
||||
point.UserId = c.userID
|
||||
|
||||
default:
|
||||
logger.Logger.Debug("未知的任务点类型", zap.String("type", pointType))
|
||||
continue
|
||||
}
|
||||
|
||||
points = append(points, point)
|
||||
}
|
||||
}
|
||||
|
||||
return cards, points, nil
|
||||
}
|
||||
|
||||
// fillVideoPointFromCards2 从 FetchChapterCards2 结果填充视频任务点参数
|
||||
func (c *XuexitongCourse) fillVideoPointFromCards2(cords2 map[string]interface{}, point *entity.ChapterPoint, objectID string) {
|
||||
if attachments, ok := cords2["attachments"].([]interface{}); ok {
|
||||
for _, att := range attachments {
|
||||
if attMap, ok := att.(map[string]interface{}); ok {
|
||||
attObjectID, _ := attMap["objectId"].(string)
|
||||
if attObjectID == "" {
|
||||
if prop, ok := attMap["property"].(map[string]interface{}); ok {
|
||||
attObjectID, _ = prop["objectid"].(string)
|
||||
}
|
||||
}
|
||||
if attObjectID == objectID {
|
||||
if otherInfo, ok := attMap["otherInfo"].(string); ok && len(otherInfo) > 80 {
|
||||
parts := strings.SplitN(otherInfo, "&", 2)
|
||||
if len(parts) > 0 {
|
||||
point.OtherInfo = parts[0]
|
||||
}
|
||||
}
|
||||
if jid, ok := attMap["jobid"].(string); ok && jid != "" {
|
||||
point.JobID = jid
|
||||
}
|
||||
if playTime, ok := attMap["playTime"].(float64); ok {
|
||||
point.PlayTime = int(playTime) / 1000
|
||||
}
|
||||
if ade, ok := attMap["attDurationEnc"].(string); ok {
|
||||
point.AttDurationEnc = ade
|
||||
}
|
||||
if vfce, ok := attMap["videoFaceCaptureEnc"].(string); ok {
|
||||
point.VideoFaceCaptureEnc = vfce
|
||||
}
|
||||
if isPassed, ok := attMap["isPassed"].(bool); ok {
|
||||
point.IsPassed = isPassed
|
||||
}
|
||||
point.RT = 0.9
|
||||
if prop, ok := attMap["property"].(map[string]interface{}); ok {
|
||||
if rtObj, ok := prop["rt"]; ok {
|
||||
switch v := rtObj.(type) {
|
||||
case float64:
|
||||
point.RT = v
|
||||
case string:
|
||||
if rt, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
point.RT = rt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.Logger.Debug("从 FetchChapterCards2 获取到视频参数",
|
||||
zap.String("objectID", objectID),
|
||||
zap.String("otherInfo", point.OtherInfo),
|
||||
zap.String("attDurationEnc", point.AttDurationEnc),
|
||||
zap.String("videoFaceCaptureEnc", point.VideoFaceCaptureEnc),
|
||||
zap.Float64("RT", point.RT))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if defaults, ok := cords2["defaults"].(map[string]interface{}); ok {
|
||||
if fid, ok := defaults["fid"].(string); ok {
|
||||
point.FID, _ = strconv.Atoi(fid)
|
||||
}
|
||||
if cpiVal, ok := defaults["cpi"]; ok {
|
||||
switch v := cpiVal.(type) {
|
||||
case float64:
|
||||
point.Cpi = strconv.FormatInt(int64(v), 10)
|
||||
case string:
|
||||
point.Cpi = v
|
||||
}
|
||||
}
|
||||
if userid, ok := defaults["userid"].(string); ok {
|
||||
c.userID = userid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseIframeData 从HTML中解析iframe数据
|
||||
func (c *XuexitongCourse) parseIframeData(htmlString string) ([]entity.IframeAttributes, error) {
|
||||
node, err := html.Parse(strings.NewReader(htmlString))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var iframes []entity.IframeAttributes
|
||||
var traverse func(n *html.Node)
|
||||
traverse = func(n *html.Node) {
|
||||
if n.Type == html.ElementNode && n.Data == "iframe" {
|
||||
attrs := entity.IframeAttributes{
|
||||
Other: make(map[string]string),
|
||||
}
|
||||
hasData := false
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "data" && strings.TrimSpace(attr.Val) != "" {
|
||||
hasData = true
|
||||
cleanedData := strings.ReplaceAll(attr.Val, """, "\"")
|
||||
cleanedData = regexp.MustCompile(`\s+`).ReplaceAllString(cleanedData, "")
|
||||
if err := json.Unmarshal([]byte(cleanedData), &attrs.Data); err != nil {
|
||||
logger.Logger.Debug("Failed to decode JSON", zap.Error(err))
|
||||
}
|
||||
} else {
|
||||
attrs.Other[attr.Key] = attr.Val
|
||||
}
|
||||
}
|
||||
attrs.HasData = hasData
|
||||
iframes = append(iframes, attrs)
|
||||
}
|
||||
for ch := n.FirstChild; ch != nil; ch = ch.NextSibling {
|
||||
traverse(ch)
|
||||
}
|
||||
}
|
||||
traverse(node)
|
||||
return iframes, nil
|
||||
}
|
||||
|
||||
// FetchChapterCards2 获取章节卡片(新接口,用于获取 otherInfo 等)
|
||||
func (c *XuexitongCourse) FetchChapterCards2(ctx context.Context, course entity.XueXiTCourse, knowledgeID int) (map[string]interface{}, error) {
|
||||
urlStr := fmt.Sprintf("%s?clazzid=%s&courseid=%s&knowledgeid=%d&num=0&ut=s&cpi=%d&v=2025-0424-1038-3&mooc2=1&isMicroCourse=false&editorPreview=0",
|
||||
ApiChapterCards2, course.Key, course.CourseID, knowledgeID, course.Cpi)
|
||||
|
||||
logger.Logger.Debug("FetchChapterCards2 请求",
|
||||
zap.String("url", urlStr),
|
||||
zap.Int("knowledgeID", knowledgeID))
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("mobile"),
|
||||
}).Get(urlStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.Error("FetchChapterCards2 请求失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Logger.Debug("FetchChapterCards2 响应",
|
||||
zap.Int("bodyLength", len(body)),
|
||||
zap.Bool("containsMArg", strings.Contains(body, "mArg")))
|
||||
|
||||
re := regexp.MustCompile(`mArg = ([^;]{6,})`)
|
||||
matches := re.FindStringSubmatch(body)
|
||||
if len(matches) <= 1 {
|
||||
logger.Logger.Warn("FetchChapterCards2 未找到 mArg")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
logger.Logger.Debug("FetchChapterCards2 提取到 mArg", zap.Int("jsonLength", len(matches[1])))
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(matches[1]), &result); err != nil {
|
||||
logger.Logger.Error("FetchChapterCards2 解析 JSON 失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if attachments, ok := result["attachments"].([]interface{}); ok {
|
||||
logger.Logger.Debug("FetchChapterCards2 解析成功",
|
||||
zap.Int("attachmentsCount", len(attachments)))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PageMobileChapterCard 获取章节卡片页面(SSR方式,用于获取附件信息)
|
||||
func (c *XuexitongCourse) PageMobileChapterCard(ctx context.Context, course entity.XueXiTCourse, knowledgeID, cardIndex int) (*entity.AttachmentSetting, string, error) {
|
||||
classID, _ := strconv.Atoi(course.Key)
|
||||
courseID, _ := strconv.Atoi(course.CourseID)
|
||||
|
||||
params := url.Values{}
|
||||
params.Add("clazzid", strconv.Itoa(classID))
|
||||
params.Add("courseid", strconv.Itoa(courseID))
|
||||
params.Add("knowledgeid", strconv.Itoa(knowledgeID))
|
||||
params.Add("num", strconv.Itoa(cardIndex))
|
||||
params.Add("isPhone", "1")
|
||||
params.Add("control", "true")
|
||||
params.Add("cpi", strconv.Itoa(course.Cpi))
|
||||
|
||||
requestURL := PageChapterCard + "?" + params.Encode()
|
||||
logger.Logger.Debug("PageMobileChapterCard 请求",
|
||||
zap.String("url", requestURL),
|
||||
zap.Int("knowledgeID", knowledgeID),
|
||||
zap.Int("cardIndex", cardIndex))
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("mobile"),
|
||||
}).Get(requestURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.Error("PageMobileChapterCard 请求失败", zap.Error(err))
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
logger.Logger.Debug("PageMobileChapterCard 响应",
|
||||
zap.Int("bodyLength", len(body)),
|
||||
zap.Bool("containsAttachmentSetting", strings.Contains(body, "window.AttachmentSetting")),
|
||||
zap.Bool("containsBlankTips", strings.Contains(body, "blankTips")),
|
||||
zap.String("bodyPreview", body[:min(len(body), 500)]))
|
||||
|
||||
doc, err := html.Parse(strings.NewReader(body))
|
||||
if err != nil {
|
||||
logger.Logger.Error("解析 HTML 失败", zap.Error(err))
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
var scriptContent string
|
||||
var scriptCount int
|
||||
var traverse func(*html.Node)
|
||||
traverse = func(n *html.Node) {
|
||||
if scriptContent != "" {
|
||||
return
|
||||
}
|
||||
if n.Type == html.ElementNode && n.Data == "script" {
|
||||
hasTypeAttr := false
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "type" && strings.TrimSpace(attr.Val) == "text/javascript" {
|
||||
hasTypeAttr = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasTypeAttr {
|
||||
scriptCount++
|
||||
if n.FirstChild != nil && n.FirstChild.Type == html.TextNode {
|
||||
scriptContent = strings.TrimSpace(n.FirstChild.Data)
|
||||
logger.Logger.Debug("找到 script 标签",
|
||||
zap.Int("scriptIndex", scriptCount),
|
||||
zap.Int("contentLength", len(scriptContent)),
|
||||
zap.Bool("hasAttachmentSetting", strings.Contains(scriptContent, "window.AttachmentSetting")))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ch := n.FirstChild; ch != nil; ch = ch.NextSibling {
|
||||
traverse(ch)
|
||||
}
|
||||
}
|
||||
traverse(doc)
|
||||
|
||||
logger.Logger.Debug("HTML 解析完成", zap.Int("scriptCount", scriptCount), zap.Bool("hasScriptContent", scriptContent != ""))
|
||||
|
||||
if scriptContent == "" || !strings.Contains(scriptContent, "window.AttachmentSetting") {
|
||||
var blankTips string
|
||||
traverse = func(n *html.Node) {
|
||||
if n.Type == html.ElementNode && n.Data == "p" {
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "class" && strings.Contains(attr.Val, "blankTips") {
|
||||
if n.FirstChild != nil {
|
||||
blankTips = strings.TrimSpace(n.FirstChild.Data)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
traverse(c)
|
||||
}
|
||||
}
|
||||
traverse(doc)
|
||||
|
||||
if blankTips != "" {
|
||||
logger.Logger.Warn("章节提示信息", zap.String("blankTips", blankTips))
|
||||
}
|
||||
|
||||
if strings.Contains(body, "window.AttachmentSetting") {
|
||||
scriptContent = body
|
||||
logger.Logger.Debug("从原始 body 中找到 window.AttachmentSetting")
|
||||
} else {
|
||||
logger.Logger.Warn("未找到 window.AttachmentSetting",
|
||||
zap.Int("knowledgeID", knowledgeID),
|
||||
zap.Int("cardIndex", cardIndex),
|
||||
zap.String("blankTips", blankTips))
|
||||
return nil, "", nil
|
||||
}
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`window\.AttachmentSetting\s*=\s*(\{.*?\});`)
|
||||
matches := re.FindStringSubmatch(scriptContent)
|
||||
var jsonStr string
|
||||
if len(matches) > 1 {
|
||||
jsonStr = matches[1]
|
||||
logger.Logger.Debug("正则提取 JSON 成功", zap.Int("jsonLength", len(jsonStr)))
|
||||
} else {
|
||||
jsonStr = extractJSONObject(scriptContent, "window.AttachmentSetting")
|
||||
if jsonStr == "" {
|
||||
logger.Logger.Warn("无法提取 JSON",
|
||||
zap.Int("scriptContentLength", len(scriptContent)))
|
||||
return nil, "", nil
|
||||
}
|
||||
logger.Logger.Debug("括号匹配提取 JSON 成功", zap.Int("jsonLength", len(jsonStr)))
|
||||
}
|
||||
|
||||
var attachment entity.AttachmentSetting
|
||||
if err := json.Unmarshal([]byte(jsonStr), &attachment); err != nil {
|
||||
logger.Logger.Error("解析 AttachmentSetting 失败",
|
||||
zap.Error(err),
|
||||
zap.String("jsonStr", jsonStr[:min(len(jsonStr), 500)]))
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
logger.Logger.Debug("解析 AttachmentSetting 成功",
|
||||
zap.Int("attachmentsCount", len(attachment.Attachments)),
|
||||
zap.Any("defaults", attachment.Defaults))
|
||||
|
||||
for idx, att := range attachment.Attachments {
|
||||
property, _ := att["property"].(map[string]interface{})
|
||||
objectid := ""
|
||||
workid := ""
|
||||
if property != nil {
|
||||
if oid, ok := property["objectid"].(string); ok {
|
||||
objectid = oid
|
||||
}
|
||||
if wid, ok := property["workid"].(string); ok {
|
||||
workid = wid
|
||||
}
|
||||
}
|
||||
otherInfo, _ := att["otherInfo"].(string)
|
||||
attDurationEnc, _ := att["attDurationEnc"].(string)
|
||||
videoFaceCaptureEnc, _ := att["videoFaceCaptureEnc"].(string)
|
||||
enc, _ := att["enc"].(string)
|
||||
logger.Logger.Debug("附件信息",
|
||||
zap.Int("index", idx),
|
||||
zap.String("objectid", objectid),
|
||||
zap.String("workid", workid),
|
||||
zap.String("enc", enc),
|
||||
zap.Bool("hasOtherInfo", otherInfo != ""),
|
||||
zap.Bool("hasAttDurationEnc", attDurationEnc != ""),
|
||||
zap.Bool("hasVideoFaceCaptureEnc", videoFaceCaptureEnc != ""))
|
||||
}
|
||||
|
||||
reEnc := regexp.MustCompile(`<input type="hidden" id="from" value="[^_]+_[^_]+_[^_]+_([^"]+)"/>`)
|
||||
matchesEnc := reEnc.FindStringSubmatch(body)
|
||||
enc := ""
|
||||
if len(matchesEnc) > 1 {
|
||||
enc = matchesEnc[1]
|
||||
}
|
||||
|
||||
return &attachment, enc, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,231 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
errors2 "likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// attachmentDetectDocument 从附件设置中检测并填充文档信息
|
||||
func (c *XuexitongCourse) attachmentDetectDocument(attachment *entity.AttachmentSetting, p *entity.ChapterPoint) {
|
||||
for _, att := range attachment.Attachments {
|
||||
typeStr := ""
|
||||
// 进行不同方案的类型值获取
|
||||
if t, ok := att["type"].(string); ok {
|
||||
typeStr = t
|
||||
} else if property, ok := att["property"].(map[string]interface{}); ok {
|
||||
if module, ok := property["module"].(string); ok {
|
||||
typeStr = module
|
||||
}
|
||||
}
|
||||
|
||||
property, ok := att["property"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果未给出文档类型,先进行文档解析尝试
|
||||
if typeStr == "" {
|
||||
if name, ok := property["name"].(string); ok {
|
||||
p.Title = name
|
||||
}
|
||||
if jid, ok := property["jobid"].(string); ok {
|
||||
p.JobID = jid
|
||||
}
|
||||
if jtoken, ok := att["jtoken"].(string); ok {
|
||||
p.Jtoken = jtoken
|
||||
}
|
||||
if isJob, ok := att["job"].(bool); ok {
|
||||
p.IsJob = isJob
|
||||
} else {
|
||||
p.IsJob = false
|
||||
}
|
||||
if bookname, ok := property["bookname"].(string); ok {
|
||||
p.Title = bookname
|
||||
}
|
||||
// 过滤匹配
|
||||
objectid, _ := property["objectid"].(string)
|
||||
jobid, _ := property["jobid"].(string)
|
||||
if (p.ObjectID != "" && objectid == p.ObjectID) || (jobid != "" && p.JobID == jobid) {
|
||||
break
|
||||
}
|
||||
} else if typeStr == "insertbook" || typeStr == "read" {
|
||||
if jtoken, ok := att["jtoken"].(string); ok {
|
||||
p.Jtoken = jtoken
|
||||
}
|
||||
if isJob, ok := att["job"].(bool); ok {
|
||||
p.IsJob = isJob
|
||||
} else {
|
||||
p.IsJob = false
|
||||
}
|
||||
if bookname, ok := property["bookname"].(string); ok {
|
||||
p.Title = bookname
|
||||
}
|
||||
if typeStr == "insertbook" {
|
||||
p.DocumentType = "insertbook"
|
||||
} else {
|
||||
p.DocumentType = "read"
|
||||
}
|
||||
|
||||
// 过滤匹配
|
||||
objectid, _ := property["objectid"].(string)
|
||||
jobid, _ := property["jobid"].(string)
|
||||
if (p.ObjectID != "" && objectid == p.ObjectID) || (jobid != "" && p.JobID == jobid) {
|
||||
break
|
||||
}
|
||||
} else if typeStr == "document" || typeStr == "insertdoc" {
|
||||
if isJob, ok := att["job"].(bool); ok {
|
||||
p.IsJob = isJob
|
||||
}
|
||||
objectid, _ := property["objectid"].(string)
|
||||
if objectid == p.ObjectID {
|
||||
if name, ok := property["name"].(string); ok {
|
||||
p.Title = name
|
||||
}
|
||||
if jid, ok := property["jobid"].(string); ok {
|
||||
p.JobID = jid
|
||||
}
|
||||
if jtoken, ok := att["jtoken"].(string); ok {
|
||||
p.Jtoken = jtoken
|
||||
}
|
||||
p.DocumentType = "document"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DocumentDtoReadingReport 提交文档阅读完成
|
||||
func (c *XuexitongCourse) DocumentDtoReadingReport(ctx context.Context, p *entity.PointDocumentDto) (string, error) {
|
||||
params := url.Values{}
|
||||
params.Add("jobid", p.JobID)
|
||||
params.Add("knowledgeid", strconv.Itoa(p.KnowledgeID))
|
||||
params.Add("courseid", p.CourseID)
|
||||
params.Add("clazzid", p.ClassID)
|
||||
params.Add("jtoken", p.Jtoken)
|
||||
params.Add("_dc", strconv.FormatInt(time.Now().UnixMilli(), 10))
|
||||
|
||||
apiUrl := ApiDocumentReport
|
||||
if p.Cpi == "insertbook" {
|
||||
apiUrl = ApiDocumentBook
|
||||
}
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("mobile"),
|
||||
}).Get(apiUrl + "?" + params.Encode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), "提交文档阅读失败")
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// AutoReadDocumentByPoint 自动阅读文档(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) AutoReadDocumentByPoint(ctx context.Context, course entity.XueXiTCourse, p *entity.ChapterPoint) error {
|
||||
// 获取附件信息填充 Jtoken、IsJob 等参数
|
||||
attachment, _, err := c.PageMobileChapterCard(ctx, course, p.KnowledgeID, p.CardIndex)
|
||||
if err != nil {
|
||||
logger.Logger.Error("获取章节卡片失败", zap.String("title", p.Title), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if attachment != nil {
|
||||
c.attachmentDetectDocument(attachment, p)
|
||||
}
|
||||
|
||||
logger.Logger.Debug("文档参数",
|
||||
zap.String("jobID", p.JobID),
|
||||
zap.String("objectID", p.ObjectID),
|
||||
zap.String("jtoken", p.Jtoken),
|
||||
zap.Bool("isJob", p.IsJob),
|
||||
zap.String("documentType", p.DocumentType))
|
||||
|
||||
if !p.IsJob {
|
||||
logger.Logger.Info("文档不是任务点,跳过", zap.String("jobID", p.JobID), zap.String("title", p.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Logger.Info("开始刷文档",
|
||||
zap.String("jobID", p.JobID),
|
||||
zap.String("title", p.Title))
|
||||
|
||||
report, err := c.DocumentDtoReadingReportByPoint(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
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 {
|
||||
logger.Logger.Warn("文档无法正常学习", zap.String("msg", msg))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DocumentDtoReadingReportByPoint 提交文档阅读完成(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) DocumentDtoReadingReportByPoint(ctx context.Context, p *entity.ChapterPoint) (string, error) {
|
||||
formParams := map[string]string{
|
||||
"jobid": p.JobID,
|
||||
"knowledgeid": strconv.Itoa(p.KnowledgeID),
|
||||
"courseid": p.CourseID,
|
||||
"clazzid": p.ClassID,
|
||||
"jtoken": p.Jtoken,
|
||||
"_dc": strconv.FormatInt(time.Now().UnixMilli(), 10),
|
||||
}
|
||||
|
||||
apiUrl := ApiDocumentReport
|
||||
if p.DocumentType == "insertbook" {
|
||||
apiUrl = ApiDocumentBook
|
||||
} else if p.DocumentType == "read" {
|
||||
apiUrl = ApiDocumentReadV2
|
||||
}
|
||||
// read 类型使用 document 接口
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("mobile"),
|
||||
}).SetQueryParams(formParams).Get(apiUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), "提交文档阅读失败")
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
errors2 "likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// attachmentDetectHyperlink 从附件设置中检测并填充外链信息
|
||||
func (c *XuexitongCourse) attachmentDetectHyperlink(attachment *entity.AttachmentSetting, p *entity.ChapterPoint) {
|
||||
for _, att := range attachment.Attachments {
|
||||
jobid, _ := att["jobid"].(string)
|
||||
if jobid != p.JobID {
|
||||
continue
|
||||
}
|
||||
|
||||
property, ok := att["property"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if title, ok := property["title"].(string); ok {
|
||||
p.Title = title
|
||||
}
|
||||
if jtoken, ok := att["jtoken"].(string); ok {
|
||||
p.Jtoken = jtoken
|
||||
}
|
||||
if isJob, ok := att["job"].(bool); ok {
|
||||
p.IsJob = isJob
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// AttachmentsDetectionHyperlink 从附件设置中检测并填充外链信息
|
||||
func (c *XuexitongCourse) AttachmentsDetectionHyperlink(attachment *entity.AttachmentSetting, p *entity.PointHyperlinkDto) error {
|
||||
if attachment == nil {
|
||||
return errors2.NewBusinessError(503, "附件设置为空")
|
||||
}
|
||||
|
||||
for _, att := range attachment.Attachments {
|
||||
jobid, _ := att["jobid"].(string)
|
||||
if jobid != p.JobID {
|
||||
continue
|
||||
}
|
||||
|
||||
property, ok := att["property"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if title, ok := property["title"].(string); ok {
|
||||
p.Title = title
|
||||
}
|
||||
if jtoken, ok := att["jtoken"].(string); ok {
|
||||
p.Jtoken = jtoken
|
||||
}
|
||||
if isJob, ok := att["job"].(bool); ok {
|
||||
p.IsJob = isJob
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoCompleteHyperlink 自动完成外链任务点
|
||||
func (c *XuexitongCourse) AutoCompleteHyperlink(ctx context.Context, course entity.XueXiTCourse, p *entity.PointHyperlinkDto) error {
|
||||
if !p.IsJob {
|
||||
logger.Logger.Info("外链不是任务点,跳过", zap.String("jobID", p.JobID), zap.String("title", p.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Logger.Info("开始刷外链",
|
||||
zap.String("jobID", p.JobID),
|
||||
zap.String("title", p.Title))
|
||||
|
||||
report, err := c.HyperlinkDtoCompleteReport(ctx, p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("提交外链完成失败: %w", err)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
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 {
|
||||
logger.Logger.Warn("外链无法正常学习", zap.String("msg", msg))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoCompleteHyperlinkByPoint 自动完成外链任务点(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) AutoCompleteHyperlinkByPoint(ctx context.Context, course entity.XueXiTCourse, p *entity.ChapterPoint) error {
|
||||
if !p.IsJob {
|
||||
logger.Logger.Info("外链不是任务点,跳过", zap.String("jobID", p.JobID), zap.String("title", p.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Logger.Info("开始刷外链",
|
||||
zap.String("jobID", p.JobID),
|
||||
zap.String("title", p.Title))
|
||||
|
||||
report, err := c.HyperlinkDtoCompleteReportByPoint(ctx, p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("提交外链完成失败: %w", err)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
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 {
|
||||
logger.Logger.Warn("外链无法正常学习", zap.String("msg", msg))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HyperlinkDtoCompleteReport 提交外链完成
|
||||
func (c *XuexitongCourse) HyperlinkDtoCompleteReport(ctx context.Context, p *entity.PointHyperlinkDto) (string, error) {
|
||||
urlStr := fmt.Sprintf("https://mooc1.chaoxing.com/ananas/job/hyperlink?jobid=%s&knowledgeid=%d&courseid=%s&clazzid=%s&jtoken=%s&checkMicroTopic=trueµTopicId=undefined&_dc=%d",
|
||||
p.JobID, p.KnowledgeID, p.CourseID, p.ClassID, p.Jtoken, time.Now().UnixMilli())
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("web"),
|
||||
}).Get(urlStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), "提交外链完成失败")
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// HyperlinkDtoCompleteReportByPoint 提交外链完成(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) HyperlinkDtoCompleteReportByPoint(ctx context.Context, p *entity.ChapterPoint) (string, error) {
|
||||
urlStr := fmt.Sprintf("https://mooc1.chaoxing.com/ananas/job/hyperlink?jobid=%s&knowledgeid=%d&courseid=%s&clazzid=%s&jtoken=%s&checkMicroTopic=trueµTopicId=undefined&_dc=%d",
|
||||
p.JobID, p.KnowledgeID, p.CourseID, p.ClassID, p.Jtoken, time.Now().UnixMilli())
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("web"),
|
||||
}).Get(urlStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), "提交外链完成失败")
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
errors2 "likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// attachmentDetectLive 从附件设置中检测并填充直播信息
|
||||
func (c *XuexitongCourse) attachmentDetectLive(attachment *entity.AttachmentSetting, p *entity.ChapterPoint) {
|
||||
for _, att := range attachment.Attachments {
|
||||
jobid, _ := att["jobid"].(string)
|
||||
if jobid != p.JobID {
|
||||
continue
|
||||
}
|
||||
|
||||
if authEnc, ok := att["authEnc"].(string); ok {
|
||||
p.AuthEnc = authEnc
|
||||
}
|
||||
if liveDragEnc, ok := att["liveDragEnc"].(string); ok {
|
||||
p.LiveDragEnc = liveDragEnc
|
||||
}
|
||||
if liveSetEnc, ok := att["liveSetEnc"].(string); ok {
|
||||
p.LiveSetEnc = liveSetEnc
|
||||
}
|
||||
if otherInfo, ok := att["otherInfo"].(string); ok {
|
||||
p.OtherInfo = otherInfo
|
||||
}
|
||||
if enc, ok := att["enc"].(string); ok {
|
||||
p.Enc = enc
|
||||
}
|
||||
if liveSwDsEnc, ok := att["liveSwDsEnc"].(string); ok {
|
||||
p.LiveSwDsEnc = liveSwDsEnc
|
||||
}
|
||||
if isJob, ok := att["job"].(bool); ok {
|
||||
p.IsJob = isJob
|
||||
}
|
||||
if aid, ok := att["aid"].(float64); ok {
|
||||
p.Aid = strconv.FormatInt(int64(aid), 10)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// AttachmentsDetectionLive 从附件设置中检测并填充直播信息
|
||||
func (c *XuexitongCourse) AttachmentsDetectionLive(attachment *entity.AttachmentSetting, p *entity.PointLiveDto) error {
|
||||
if attachment == nil {
|
||||
return errors2.NewBusinessError(503, "附件设置为空")
|
||||
}
|
||||
|
||||
for _, att := range attachment.Attachments {
|
||||
jobid, _ := att["jobid"].(string)
|
||||
if jobid != p.JobID {
|
||||
continue
|
||||
}
|
||||
|
||||
if authEnc, ok := att["authEnc"].(string); ok {
|
||||
p.AuthEnc = authEnc
|
||||
}
|
||||
if liveDragEnc, ok := att["liveDragEnc"].(string); ok {
|
||||
p.LiveDragEnc = liveDragEnc
|
||||
}
|
||||
if liveSetEnc, ok := att["liveSetEnc"].(string); ok {
|
||||
p.LiveSetEnc = liveSetEnc
|
||||
}
|
||||
if otherInfo, ok := att["otherInfo"].(string); ok {
|
||||
p.OtherInfo = otherInfo
|
||||
}
|
||||
if enc, ok := att["enc"].(string); ok {
|
||||
p.Enc = enc
|
||||
}
|
||||
if liveSwDsEnc, ok := att["liveSwDsEnc"].(string); ok {
|
||||
p.LiveSwDsEnc = liveSwDsEnc
|
||||
}
|
||||
if isJob, ok := att["job"].(bool); ok {
|
||||
p.IsJob = isJob
|
||||
}
|
||||
if aid, ok := att["aid"].(float64); ok {
|
||||
p.Aid = strconv.FormatInt(int64(aid), 10)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoWatchLive 自动观看直播任务点
|
||||
func (c *XuexitongCourse) AutoWatchLive(ctx context.Context, course entity.XueXiTCourse, p *entity.PointLiveDto) error {
|
||||
if !p.IsJob {
|
||||
logger.Logger.Info("直播不是任务点,跳过", zap.String("jobID", p.JobID), zap.String("title", p.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.PullLiveInfo(ctx, p); err != nil {
|
||||
logger.Logger.Warn("获取直播信息失败", zap.Error(err))
|
||||
}
|
||||
|
||||
if p.LiveStatusCode == 0 {
|
||||
logger.Logger.Info("直播还未开始,跳过", zap.String("title", p.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.VideoCompletePercent >= 90 {
|
||||
logger.Logger.Info("直播已完成", zap.String("title", p.Title), zap.Float64("percent", p.VideoCompletePercent))
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Logger.Info("开始刷直播",
|
||||
zap.String("jobID", p.JobID),
|
||||
zap.String("title", p.Title),
|
||||
zap.Float64("currentPercent", p.VideoCompletePercent))
|
||||
|
||||
if _, err := c.LiveRelationReport(ctx, p); err != nil {
|
||||
logger.Logger.Warn("建立直播连接失败", zap.Error(err))
|
||||
}
|
||||
|
||||
for {
|
||||
report, err := c.LiveSaveTimeReport(ctx, p)
|
||||
if err != nil {
|
||||
logger.Logger.Error("提交直播观看进度失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.PullLiveInfo(ctx, p); err != nil {
|
||||
logger.Logger.Warn("更新直播信息失败", zap.Error(err))
|
||||
}
|
||||
|
||||
logger.Logger.Debug("直播观看中",
|
||||
zap.String("title", p.Title),
|
||||
zap.Float64("percent", p.VideoCompletePercent),
|
||||
zap.String("report", report))
|
||||
|
||||
if p.VideoCompletePercent >= 90 {
|
||||
logger.Logger.Info("直播刷课完成", zap.String("title", p.Title))
|
||||
c.onTaskComplete()
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(30 * time.Second)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoWatchLiveByPoint 自动观看直播任务点(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) AutoWatchLiveByPoint(ctx context.Context, course entity.XueXiTCourse, p *entity.ChapterPoint) error {
|
||||
if !p.IsJob {
|
||||
logger.Logger.Info("直播不是任务点,跳过", zap.String("jobID", p.JobID), zap.String("title", p.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.PullLiveInfoByPoint(ctx, p); err != nil {
|
||||
logger.Logger.Warn("获取直播信息失败", zap.Error(err))
|
||||
}
|
||||
|
||||
if p.LiveStatusCode == 0 {
|
||||
logger.Logger.Info("直播还未开始,跳过", zap.String("title", p.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.VideoCompletePercent >= 90 {
|
||||
logger.Logger.Info("直播已完成", zap.String("title", p.Title), zap.Float64("percent", p.VideoCompletePercent))
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Logger.Info("开始刷直播",
|
||||
zap.String("jobID", p.JobID),
|
||||
zap.String("title", p.Title),
|
||||
zap.Float64("currentPercent", p.VideoCompletePercent))
|
||||
|
||||
if _, err := c.LiveRelationReportByPoint(ctx, p); err != nil {
|
||||
logger.Logger.Warn("建立直播连接失败", zap.Error(err))
|
||||
}
|
||||
|
||||
for {
|
||||
report, err := c.LiveSaveTimeReportByPoint(ctx, p)
|
||||
if err != nil {
|
||||
logger.Logger.Error("提交直播观看进度失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.PullLiveInfoByPoint(ctx, p); err != nil {
|
||||
logger.Logger.Warn("更新直播信息失败", zap.Error(err))
|
||||
}
|
||||
|
||||
logger.Logger.Debug("直播观看中",
|
||||
zap.String("title", p.Title),
|
||||
zap.Float64("percent", p.VideoCompletePercent),
|
||||
zap.String("report", report))
|
||||
|
||||
if p.VideoCompletePercent >= 90 {
|
||||
logger.Logger.Info("直播刷课完成", zap.String("title", p.Title))
|
||||
c.onTaskComplete()
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(30 * time.Second)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PullLiveInfo 获取直播信息
|
||||
func (c *XuexitongCourse) PullLiveInfo(ctx context.Context, p *entity.PointLiveDto) error {
|
||||
urlStr := fmt.Sprintf("https://mooc1.chaoxing.com/ananas/live/liveinfo?liveid=%s&userid=%s&clazzid=%s&knowledgeid=%d&courseid=%s&jobid=%s&ut=s",
|
||||
p.LiveId, p.UserId, p.ClassID, p.KnowledgeID, p.CourseID, p.JobID)
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("web"),
|
||||
}).Get(urlStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), "获取直播信息失败")
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(body), &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if status, ok := result["status"].(bool); ok && status {
|
||||
if temp, ok := result["temp"].(map[string]interface{}); ok {
|
||||
if data, ok := temp["data"].(map[string]interface{}); ok {
|
||||
if percentValue, ok := data["percentValue"].(float64); ok {
|
||||
p.VideoCompletePercent = percentValue
|
||||
}
|
||||
if duration, ok := data["duration"].(float64); ok {
|
||||
p.VideoDuration = int(duration)
|
||||
}
|
||||
if liveStatus, ok := data["liveStatus"].(float64); ok {
|
||||
p.LiveStatusCode = int(liveStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PullLiveInfoByPoint 获取直播信息(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) PullLiveInfoByPoint(ctx context.Context, p *entity.ChapterPoint) error {
|
||||
urlStr := fmt.Sprintf("https://mooc1.chaoxing.com/ananas/live/liveinfo?liveid=%s&userid=%s&clazzid=%s&knowledgeid=%d&courseid=%s&jobid=%s&ut=s",
|
||||
p.LiveId, p.UserId, p.ClassID, p.KnowledgeID, p.CourseID, p.JobID)
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("web"),
|
||||
}).Get(urlStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), "获取直播信息失败")
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(body), &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if status, ok := result["status"].(bool); ok && status {
|
||||
if temp, ok := result["temp"].(map[string]interface{}); ok {
|
||||
if data, ok := temp["data"].(map[string]interface{}); ok {
|
||||
if percentValue, ok := data["percentValue"].(float64); ok {
|
||||
p.VideoCompletePercent = percentValue
|
||||
}
|
||||
if duration, ok := data["duration"].(float64); ok {
|
||||
p.VideoDuration = int(duration)
|
||||
}
|
||||
if liveStatus, ok := data["liveStatus"].(float64); ok {
|
||||
p.LiveStatusCode = int(liveStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LiveRelationReport 建立直播连接
|
||||
func (c *XuexitongCourse) LiveRelationReport(ctx context.Context, p *entity.PointLiveDto) (string, error) {
|
||||
urlStr := fmt.Sprintf("https://mooc1.chaoxing.com/mooc-ans/live/relation?courseid=%s&knowledgeid=%d&ut=s&jobid=%s&aid=%s",
|
||||
p.CourseID, p.KnowledgeID, p.JobID, p.Aid)
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("web"),
|
||||
}).Get(urlStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), "建立直播连接失败")
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// LiveRelationReportByPoint 建立直播连接(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) LiveRelationReportByPoint(ctx context.Context, p *entity.ChapterPoint) (string, error) {
|
||||
urlStr := fmt.Sprintf("https://mooc1.chaoxing.com/mooc-ans/live/relation?courseid=%s&knowledgeid=%d&ut=s&jobid=%s&aid=%s",
|
||||
p.CourseID, p.KnowledgeID, p.JobID, p.Aid)
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("web"),
|
||||
}).Get(urlStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), "建立直播连接失败")
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// LiveSaveTimeReport 提交直播观看进度
|
||||
func (c *XuexitongCourse) LiveSaveTimeReport(ctx context.Context, p *entity.PointLiveDto) (string, error) {
|
||||
urlStr := fmt.Sprintf("https://zhibo.chaoxing.com/saveTimePc?streamName=%s&vdoid=%s&userId=%s&isStart=1&t=%d&courseId=%s",
|
||||
p.StreamName, p.Vdoid, p.UserId, time.Now().UnixMilli(), p.CourseID)
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("web"),
|
||||
}).Get(urlStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), "提交直播观看进度失败")
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// LiveSaveTimeReportByPoint 提交直播观看进度(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) LiveSaveTimeReportByPoint(ctx context.Context, p *entity.ChapterPoint) (string, error) {
|
||||
urlStr := fmt.Sprintf("https://zhibo.chaoxing.com/saveTimePc?streamName=%s&vdoid=%s&userId=%s&isStart=1&t=%d&courseId=%s",
|
||||
p.StreamName, p.Vdoid, p.UserId, time.Now().UnixMilli(), p.CourseID)
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("web"),
|
||||
}).Get(urlStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), "提交直播观看进度失败")
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
@@ -2,11 +2,68 @@ package xuexitong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
"likedream-goframe/internal/application/quesbank"
|
||||
"likedream-goframe/internal/common/db"
|
||||
"likedream-goframe/internal/ent/sysconfig"
|
||||
"likedream-goframe/internal/framework/errors"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// getAIConfig 获取AI配置
|
||||
func getAIConfig(ctx context.Context) *quesbank.AIConfig {
|
||||
db.InitDB()
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ("ai.config")).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("未配置AI,作业将跳过", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
var aiConfig quesbank.AIConfig
|
||||
if err := sonic.Unmarshal([]byte(config.Value), &aiConfig); err != nil {
|
||||
logger.Logger.Warn("AI配置解析失败", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &aiConfig
|
||||
}
|
||||
func TestSchedule(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
xuexitongLogin := NewXuexitongLogin(ctx)
|
||||
err := xuexitongLogin.Login(context.Background(), "13151832241", "yyz@083422", 5)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
client := xuexitongLogin.GetClient()
|
||||
courseService := NewXuexitongCourse(client)
|
||||
|
||||
// 人人必修的人工智能课
|
||||
config := getAIConfig(ctx)
|
||||
if config != nil {
|
||||
logger.Logger.Info("AI配置加载成功",
|
||||
zap.String("aiType", string(config.Type)),
|
||||
zap.String("apiURL", config.URL),
|
||||
zap.String("model", config.Model))
|
||||
} else {
|
||||
logger.Logger.Warn("AI配置为空,作业将被跳过")
|
||||
}
|
||||
|
||||
err = courseService.AutoStudyByCourseIDWithOptions(ctx, "258109701", &AutoStudyOptions{
|
||||
AIConfig: config,
|
||||
AutoSubmit: true, // 默认不自动提交,仅保存
|
||||
Thread: 1,
|
||||
})
|
||||
if err != nil {
|
||||
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "刷课失败", err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuexitongLogin(t *testing.T) {
|
||||
xuexitongLogin := NewXuexitongLogin(context.Background())
|
||||
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
|
||||
@@ -39,7 +96,7 @@ func TestPullChapter(t *testing.T) {
|
||||
}
|
||||
|
||||
// 获取第一个课程的章节
|
||||
chapters := courseAction.PullCourseChapterList(context.Background(), courseList[0].CourseID)
|
||||
chapters := courseAction.PullCourseChapterList(context.Background(), courseList[0])
|
||||
t.Logf("课程 %s 的章节数量: %d", courseList[0].CourseName, len(chapters))
|
||||
|
||||
for _, chapter := range chapters {
|
||||
@@ -64,149 +121,23 @@ func TestFetchChapterCards(t *testing.T) {
|
||||
course := courseList[0]
|
||||
|
||||
// 获取章节列表
|
||||
chapters := courseAction.PullCourseChapterList(context.Background(), course.CourseID)
|
||||
chapters := courseAction.PullCourseChapterList(context.Background(), course)
|
||||
if len(chapters) == 0 {
|
||||
t.Fatal("没有找到章节")
|
||||
}
|
||||
|
||||
// 找一个有任务点的章节
|
||||
var targetChapter *entity.XueXiTCourseChapter
|
||||
for i := range chapters {
|
||||
if chapters[i].JobCount > 0 {
|
||||
targetChapter = &chapters[i]
|
||||
var targetChapterID int
|
||||
for _, ch := range chapters {
|
||||
if ch.JobCount > 0 {
|
||||
targetChapterID = ch.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetChapter == nil {
|
||||
if targetChapterID == 0 {
|
||||
t.Fatal("没有找到有任务点的章节")
|
||||
}
|
||||
|
||||
// 获取任务点卡片
|
||||
cards, err := courseAction.FetchChapterCards(context.Background(), course, targetChapter.ID, targetChapter.ID)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
t.Logf("章节 %s 的任务点数量: %d", targetChapter.Name, len(cards))
|
||||
|
||||
for _, card := range cards {
|
||||
t.Logf("任务点: 类型=%s, JobID=%s, ObjectID=%s", card.Type, card.JobID, card.ObjectID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoPlayVideo(t *testing.T) {
|
||||
xuexitongLogin := NewXuexitongLogin(context.Background())
|
||||
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
client := xuexitongLogin.GetClient()
|
||||
courseAction := NewXuexitongCourse(client)
|
||||
|
||||
// 获取课程列表
|
||||
courseList := courseAction.PullCourseList(context.Background())
|
||||
if len(courseList) == 0 {
|
||||
t.Fatal("没有找到课程")
|
||||
}
|
||||
|
||||
course := courseList[0]
|
||||
|
||||
// 获取章节列表
|
||||
chapters := courseAction.PullCourseChapterList(context.Background(), course.CourseID)
|
||||
if len(chapters) == 0 {
|
||||
t.Fatal("没有找到章节")
|
||||
}
|
||||
|
||||
// 找一个有任务点的章节
|
||||
var targetChapter *entity.XueXiTCourseChapter
|
||||
for i := range chapters {
|
||||
if chapters[i].JobCount > 0 {
|
||||
targetChapter = &chapters[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetChapter == nil {
|
||||
t.Fatal("没有找到有任务点的章节")
|
||||
}
|
||||
|
||||
// 获取任务点卡片
|
||||
cards, err := courseAction.FetchChapterCards(context.Background(), course, targetChapter.ID, targetChapter.ID)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 找视频任务点
|
||||
for _, card := range cards {
|
||||
if card.Type == "video" {
|
||||
t.Logf("开始处理视频任务点: %s", card.JobID)
|
||||
|
||||
// 获取视频信息
|
||||
videoDto, err := courseAction.FetchVideoDto(context.Background(), card.ObjectID, "", card.JobID)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 自动播放视频
|
||||
err = courseAction.AutoPlayVideo(context.Background(), course, card, videoDto)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
t.Logf("视频任务点完成: %s", card.JobID)
|
||||
break // 只测试一个视频
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoReadDocument(t *testing.T) {
|
||||
xuexitongLogin := NewXuexitongLogin(context.Background())
|
||||
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
client := xuexitongLogin.GetClient()
|
||||
courseAction := NewXuexitongCourse(client)
|
||||
|
||||
// 获取课程列表
|
||||
courseList := courseAction.PullCourseList(context.Background())
|
||||
if len(courseList) == 0 {
|
||||
t.Fatal("没有找到课程")
|
||||
}
|
||||
|
||||
course := courseList[0]
|
||||
|
||||
// 获取章节列表
|
||||
chapters := courseAction.PullCourseChapterList(context.Background(), course.CourseID)
|
||||
if len(chapters) == 0 {
|
||||
t.Fatal("没有找到章节")
|
||||
}
|
||||
|
||||
// 找一个有任务点的章节
|
||||
var targetChapter *entity.XueXiTCourseChapter
|
||||
for i := range chapters {
|
||||
if chapters[i].JobCount > 0 {
|
||||
targetChapter = &chapters[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetChapter == nil {
|
||||
t.Fatal("没有找到有任务点的章节")
|
||||
}
|
||||
|
||||
// 获取任务点卡片
|
||||
cards, err := courseAction.FetchChapterCards(context.Background(), course, targetChapter.ID, targetChapter.ID)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 找文档任务点
|
||||
for _, card := range cards {
|
||||
if card.Type == "document" {
|
||||
t.Logf("开始处理文档任务点: %s", card.JobID)
|
||||
|
||||
// 获取文档信息
|
||||
documentDto, err := courseAction.FetchDocumentDto(context.Background(), card.ObjectID, "", card.JobID)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 自动阅读文档
|
||||
err = courseAction.AutoReadDocument(context.Background(), course, card, documentDto)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
t.Logf("文档任务点完成: %s", card.JobID)
|
||||
break // 只测试一个文档
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoStudyByCourseID 测试通过课程ID刷课(定时任务使用)
|
||||
@@ -251,3 +182,9 @@ func TestAutoStudyAllCourses(t *testing.T) {
|
||||
|
||||
t.Log("所有课程刷课完成")
|
||||
}
|
||||
|
||||
func TestSplitWords(t *testing.T) {
|
||||
str := "ABCD"
|
||||
split := strings.Split(str, "")
|
||||
t.Log(split)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package xuexitong
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
crand "crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ func GetUA(uaType string) string {
|
||||
|
||||
return strings.Join([]string{
|
||||
fmt.Sprintf("Mozilla/5.0 (Linux; %s; %s Build/OPM1.171019.019; wv)", ANDROID_VERSION, DEVICE_VENDOR),
|
||||
fmt.Sprintf("AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36"),
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36",
|
||||
fmt.Sprintf("(schild:%s)", MobileUASign(DEVICE_VENDOR, "zh_CN", APP_VERSION, BUILD, IMEI)),
|
||||
fmt.Sprintf("(device:%s)", DEVICE_VENDOR),
|
||||
"Language/zh_CN",
|
||||
@@ -48,15 +48,14 @@ func GetUA(uaType string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// 用于生成IMEI
|
||||
func TokenHex(len int) string {
|
||||
b := make([]byte, 16)
|
||||
_, err := rand.Read(b)
|
||||
// TokenHex 用于生成IMEI
|
||||
func TokenHex(length int) string {
|
||||
b := make([]byte, length)
|
||||
_, err := crand.Read(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
token := hex.EncodeToString(b)
|
||||
return token
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func MobileUASign(model, locale, version, build, imei string) string {
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
errors2 "likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// VideoDtoFetch 获取视频详细信息
|
||||
func (c *XuexitongCourse) VideoDtoFetch(ctx context.Context, p *entity.PointVideoDto) error {
|
||||
params := url.Values{}
|
||||
params.Set("k", strconv.Itoa(p.FID))
|
||||
params.Set("flag", "normal")
|
||||
params.Set("_dc", strconv.FormatInt(time.Now().UnixMilli(), 10))
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("mobile"),
|
||||
}).Get(fmt.Sprintf("%s/%s?%s", ApiVideoStatus, p.ObjectID, params.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), "获取视频信息失败")
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(body), &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dtoken, ok := result["dtoken"].(string); ok {
|
||||
p.DToken = dtoken
|
||||
}
|
||||
if duration, ok := result["duration"].(float64); ok {
|
||||
p.Duration = int(duration)
|
||||
}
|
||||
if filename, ok := result["filename"].(string); ok {
|
||||
if decoded, err := url.QueryUnescape(filename); err == nil {
|
||||
p.Title = decoded
|
||||
} else {
|
||||
p.Title = filename
|
||||
}
|
||||
}
|
||||
if status, ok := result["status"].(string); ok && status == "success" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors2.NewBusinessError(503, "视频状态异常")
|
||||
}
|
||||
|
||||
// AttachmentsDetectionVideo 从附件设置中检测并填充视频信息(旧版,保留兼容)
|
||||
func (c *XuexitongCourse) AttachmentsDetectionVideo(attachment *entity.AttachmentSetting, p *entity.PointVideoDto) error {
|
||||
if attachment == nil {
|
||||
return errors2.NewBusinessError(503, "附件设置为空")
|
||||
}
|
||||
|
||||
for _, att := range attachment.Attachments {
|
||||
property, ok := att["property"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
objectid, _ := property["objectid"].(string)
|
||||
if objectid != p.ObjectID {
|
||||
continue
|
||||
}
|
||||
|
||||
if jobid, ok := property["jobid"].(string); ok {
|
||||
p.JobID = jobid
|
||||
} else if jobidFloat, ok := property["jobid"].(float64); ok {
|
||||
p.JobID = strconv.FormatFloat(jobidFloat, 'f', -1, 64)
|
||||
}
|
||||
|
||||
if otherInfo, ok := att["otherInfo"].(string); ok {
|
||||
parts := strings.SplitN(otherInfo, "&", 2)
|
||||
if len(parts) > 0 {
|
||||
p.OtherInfo = parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
if isPassed, ok := att["isPassed"].(bool); ok {
|
||||
p.IsPassed = isPassed
|
||||
}
|
||||
|
||||
if rtObj, ok := property["rt"]; ok {
|
||||
switch v := rtObj.(type) {
|
||||
case float64:
|
||||
p.RT = v
|
||||
case string:
|
||||
if rt, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
p.RT = rt
|
||||
} else {
|
||||
p.RT = 0.9
|
||||
}
|
||||
}
|
||||
} else {
|
||||
p.RT = 0.9
|
||||
}
|
||||
|
||||
if playTime, ok := att["playTime"].(float64); ok {
|
||||
p.PlayTime = int(playTime) / 1000
|
||||
}
|
||||
|
||||
if mid, ok := att["mid"].(string); ok {
|
||||
p.Mid = mid
|
||||
}
|
||||
|
||||
if rct, ok := att["randomCaptureTime"].(string); ok {
|
||||
p.RandomCaptureTime = rct
|
||||
}
|
||||
|
||||
if ade, ok := att["attDurationEnc"].(string); ok {
|
||||
p.AttDurationEnc = ade
|
||||
}
|
||||
|
||||
if vfce, ok := att["videoFaceCaptureEnc"].(string); ok {
|
||||
p.VideoFaceCaptureEnc = vfce
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if defaults := attachment.Defaults; defaults != nil {
|
||||
if fid, ok := defaults["fid"].(string); ok {
|
||||
p.FID, _ = strconv.Atoi(fid)
|
||||
}
|
||||
if userid, ok := defaults["userid"].(string); ok {
|
||||
c.userID = userid
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// attachmentDetectVideo 从附件设置中检测并填充视频信息
|
||||
func (c *XuexitongCourse) attachmentDetectVideo(attachment *entity.AttachmentSetting, p *entity.ChapterPoint) {
|
||||
logger.Logger.Debug("attachmentDetectVideo 开始匹配",
|
||||
zap.String("targetObjectID", p.ObjectID),
|
||||
zap.Int("attachmentsCount", len(attachment.Attachments)))
|
||||
|
||||
found := false
|
||||
for idx, att := range attachment.Attachments {
|
||||
property, ok := att["property"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
objectid := property["objectid"]
|
||||
if objectid == nil {
|
||||
continue
|
||||
}
|
||||
objectidStr, _ := objectid.(string)
|
||||
|
||||
logger.Logger.Debug("检查附件",
|
||||
zap.Int("index", idx),
|
||||
zap.String("objectid", objectidStr),
|
||||
zap.String("targetObjectID", p.ObjectID))
|
||||
|
||||
if objectidStr != p.ObjectID {
|
||||
continue
|
||||
}
|
||||
|
||||
found = true
|
||||
logger.Logger.Debug("找到匹配的附件", zap.Int("index", idx))
|
||||
|
||||
if jobid, ok := property["jobid"].(string); ok {
|
||||
p.JobID = jobid
|
||||
} else if jobidFloat, ok := property["jobid"].(float64); ok {
|
||||
p.JobID = strconv.FormatFloat(jobidFloat, 'f', -1, 64)
|
||||
}
|
||||
|
||||
if otherInfo, ok := att["otherInfo"].(string); ok {
|
||||
parts := strings.SplitN(otherInfo, "&", 2)
|
||||
if len(parts) > 0 {
|
||||
p.OtherInfo = parts[0]
|
||||
}
|
||||
logger.Logger.Debug("获取到 otherInfo", zap.String("otherInfo", p.OtherInfo))
|
||||
}
|
||||
|
||||
if isPassed, ok := att["isPassed"].(bool); ok {
|
||||
p.IsPassed = isPassed
|
||||
}
|
||||
|
||||
if rtObj, ok := property["rt"]; ok {
|
||||
switch v := rtObj.(type) {
|
||||
case float64:
|
||||
p.RT = v
|
||||
case string:
|
||||
if rt, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
p.RT = rt
|
||||
} else {
|
||||
p.RT = 0.9
|
||||
}
|
||||
}
|
||||
} else {
|
||||
p.RT = 0.9
|
||||
}
|
||||
|
||||
if playTime, ok := att["playTime"].(float64); ok {
|
||||
p.PlayTime = int(playTime) / 1000
|
||||
}
|
||||
if mid, ok := att["mid"].(string); ok {
|
||||
p.Mid = mid
|
||||
}
|
||||
if rct, ok := att["randomCaptureTime"].(string); ok {
|
||||
p.RandomCaptureTime = rct
|
||||
}
|
||||
if ade, ok := att["attDurationEnc"].(string); ok {
|
||||
p.AttDurationEnc = ade
|
||||
logger.Logger.Debug("获取到 attDurationEnc", zap.String("attDurationEnc", ade))
|
||||
}
|
||||
if vfce, ok := att["videoFaceCaptureEnc"].(string); ok {
|
||||
p.VideoFaceCaptureEnc = vfce
|
||||
logger.Logger.Debug("获取到 videoFaceCaptureEnc", zap.String("videoFaceCaptureEnc", vfce))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if !found {
|
||||
logger.Logger.Warn("未找到匹配的视频附件", zap.String("objectID", p.ObjectID))
|
||||
}
|
||||
|
||||
if defaults := attachment.Defaults; defaults != nil {
|
||||
if fid, ok := defaults["fid"].(string); ok {
|
||||
p.FID, _ = strconv.Atoi(fid)
|
||||
}
|
||||
if userid, ok := defaults["userid"].(string); ok {
|
||||
c.userID = userid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AutoPlayVideoByPoint 自动播放视频(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) AutoPlayVideoByPoint(ctx context.Context, course entity.XueXiTCourse, p *entity.ChapterPoint) error {
|
||||
if err := c.VideoDtoFetchByPoint(ctx, p); err != nil {
|
||||
return fmt.Errorf("获取视频信息失败: %w", err)
|
||||
}
|
||||
|
||||
if p.Duration <= 0 {
|
||||
logger.Logger.Warn("视频时长为0,跳过", zap.String("jobID", p.JobID))
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.IsPassed {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Logger.Info("开始刷视频",
|
||||
zap.String("jobID", p.JobID),
|
||||
zap.Int("duration", p.Duration),
|
||||
zap.Int("playTime", p.PlayTime),
|
||||
zap.String("title", p.Title),
|
||||
zap.String("userID", c.userID),
|
||||
zap.String("cpi", p.Cpi),
|
||||
zap.Int("FID", p.FID),
|
||||
zap.String("otherInfo", p.OtherInfo))
|
||||
|
||||
playingTime := p.PlayTime
|
||||
if !p.IsPassed && p.PlayTime == p.Duration {
|
||||
playingTime = 0
|
||||
}
|
||||
|
||||
selectSec := 58
|
||||
isFirstSubmit := true
|
||||
|
||||
for {
|
||||
var playReport string
|
||||
var err error
|
||||
|
||||
isdrag := 0
|
||||
if isFirstSubmit && playingTime == p.PlayTime {
|
||||
isdrag = 3
|
||||
}
|
||||
playReport, err = c.VideoSubmitStudyTimeByPoint(ctx, p, playingTime, isdrag)
|
||||
isFirstSubmit = false
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.Error("提交视频进度失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
logger.Logger.Info("视频播放", zap.Int("playingTime", playingTime), zap.Int("duration", p.Duration))
|
||||
|
||||
if p.Duration-playingTime < selectSec && p.Duration != playingTime {
|
||||
sleepTime := p.Duration - playingTime
|
||||
playingTime = p.Duration
|
||||
time.Sleep(time.Duration(sleepTime) * time.Second)
|
||||
} else if playingTime >= p.Duration {
|
||||
time.Sleep(5 * time.Second)
|
||||
} else {
|
||||
playingTime += selectSec
|
||||
time.Sleep(time.Duration(selectSec) * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VideoDtoFetchByPoint 获取视频详细信息(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) VideoDtoFetchByPoint(ctx context.Context, p *entity.ChapterPoint) error {
|
||||
params := url.Values{}
|
||||
params.Set("k", strconv.Itoa(p.FID))
|
||||
params.Set("flag", "normal")
|
||||
params.Set("_dc", strconv.FormatInt(time.Now().UnixMilli(), 10))
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("mobile"),
|
||||
}).Get(fmt.Sprintf("%s/%s?%s", ApiVideoStatus, p.ObjectID, params.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), "获取视频信息失败")
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(body), &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dtoken, ok := result["dtoken"].(string); ok {
|
||||
p.DToken = dtoken
|
||||
}
|
||||
if duration, ok := result["duration"].(float64); ok {
|
||||
p.Duration = int(duration)
|
||||
}
|
||||
if filename, ok := result["filename"].(string); ok {
|
||||
if decoded, err := url.QueryUnescape(filename); err == nil {
|
||||
p.Title = decoded
|
||||
} else {
|
||||
p.Title = filename
|
||||
}
|
||||
}
|
||||
if status, ok := result["status"].(string); ok && status == "success" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors2.NewBusinessError(503, "视频状态异常")
|
||||
}
|
||||
|
||||
// VideoSubmitStudyTimeByPoint 提交视频观看进度(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) VideoSubmitStudyTimeByPoint(ctx context.Context, p *entity.ChapterPoint, playingTime, isdrag int) (string, error) {
|
||||
clipTime := fmt.Sprintf("0_%d", p.Duration)
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("[%s][%s][%s][%s][%d][%s][%d][%s]",
|
||||
p.ClassID, c.userID, p.JobID, p.ObjectID, playingTime*1000, VideoEncKey, p.Duration*1000, clipTime)))
|
||||
enc := hex.EncodeToString(hash[:])
|
||||
|
||||
urlStr := "https://mooc1.chaoxing.com/mooc-ans/multimedia/log/a/" + p.Cpi + "/" + p.DToken +
|
||||
"?clazzId=" + p.ClassID +
|
||||
"&playingTime=" + strconv.Itoa(playingTime) +
|
||||
"&duration=" + strconv.Itoa(p.Duration) +
|
||||
"&clipTime=" + clipTime +
|
||||
"&objectId=" + p.ObjectID +
|
||||
"&otherInfo=" + p.OtherInfo +
|
||||
"&courseId=" + p.CourseID +
|
||||
"&jobid=" + p.JobID +
|
||||
"&userid=" + c.userID +
|
||||
"&isdrag=" + strconv.Itoa(isdrag) +
|
||||
"&view=pc" +
|
||||
"&enc=" + enc +
|
||||
"&rt=" + fmt.Sprintf("%.2f", p.RT) +
|
||||
"&videoFaceCaptureEnc=" + p.VideoFaceCaptureEnc +
|
||||
"&dtype=Video" +
|
||||
"&_t=" + strconv.FormatInt(time.Now().UnixMilli(), 10) +
|
||||
"&attDuration=" + strconv.Itoa(p.Duration) +
|
||||
"&attDurationEnc=" + p.AttDurationEnc
|
||||
|
||||
logger.Logger.Debug("VideoSubmitStudyTimeByPoint 请求参数",
|
||||
zap.String("cpi", p.Cpi),
|
||||
zap.String("dtoken", p.DToken),
|
||||
zap.String("classID", p.ClassID),
|
||||
zap.String("courseID", p.CourseID),
|
||||
zap.String("objectID", p.ObjectID),
|
||||
zap.String("jobID", p.JobID),
|
||||
zap.String("userID", c.userID),
|
||||
zap.Int("playingTime", playingTime),
|
||||
zap.Int("duration", p.Duration),
|
||||
zap.Int("isdrag", isdrag),
|
||||
zap.String("otherInfo", p.OtherInfo),
|
||||
zap.String("attDurationEnc", p.AttDurationEnc),
|
||||
zap.String("videoFaceCaptureEnc", p.VideoFaceCaptureEnc),
|
||||
zap.Float64("RT", p.RT),
|
||||
zap.Int("FID", p.FID),
|
||||
zap.String("enc", enc))
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "*/*",
|
||||
"Host": "mooc1.chaoxing.com",
|
||||
"Connection": "keep-alive",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Pragma": "no-cache",
|
||||
"Sec-Ch-Ua-Platform": "Windows",
|
||||
}).SetCookies([]*http.Cookie{
|
||||
{Name: "fanyamoocs", Value: "11401F839C536D9E"},
|
||||
{Name: "thirdRegist", Value: "0"},
|
||||
{Name: "videojs_id", Value: "1778753"},
|
||||
}).Get(urlStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return errors2.NewBusinessError(resp.StatusCode(), fmt.Sprintf("提交视频进度失败,状态码: %d", resp.StatusCode()))
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/avast/retry-go/v4"
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
// attachmentDetectWork 从附件设置中检测并填充作业信息
|
||||
func (c *XuexitongCourse) attachmentDetectWork(attachment *entity.AttachmentSetting, p *entity.ChapterPoint) {
|
||||
for _, att := range attachment.Attachments {
|
||||
property, ok := att["property"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
workId, _ := property["workid"].(string)
|
||||
if workId != p.WorkID {
|
||||
continue
|
||||
}
|
||||
|
||||
if enc, ok := att["enc"].(string); ok {
|
||||
p.Enc = enc
|
||||
}
|
||||
if title, ok := property["title"].(string); ok {
|
||||
p.Title = title
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if defaults := attachment.Defaults; defaults != nil {
|
||||
if ktoken, ok := defaults["ktoken"].(string); ok {
|
||||
p.KToken = ktoken
|
||||
}
|
||||
if userid, ok := defaults["userid"].(string); ok {
|
||||
p.PUID = userid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AttachmentsDetectionWork 从附件设置中检测并填充作业信息
|
||||
func (c *XuexitongCourse) AttachmentsDetectionWork(attachment *entity.AttachmentSetting, p *entity.PointWorkDto) error {
|
||||
if attachment == nil {
|
||||
logger.Logger.Warn("attachment 为空")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Logger.Debug("AttachmentsDetectionWork 开始",
|
||||
zap.Int("attachmentsCount", len(attachment.Attachments)),
|
||||
zap.String("targetWorkID", p.WorkID))
|
||||
|
||||
foundMatch := false
|
||||
for i, att := range attachment.Attachments {
|
||||
property, ok := att["property"].(map[string]interface{})
|
||||
if !ok {
|
||||
logger.Logger.Debug("附件 property 不存在", zap.Int("index", i))
|
||||
continue
|
||||
}
|
||||
workId, _ := property["workid"].(string)
|
||||
logger.Logger.Debug("检查附件",
|
||||
zap.Int("index", i),
|
||||
zap.String("workId", workId),
|
||||
zap.String("targetWorkID", p.WorkID),
|
||||
zap.Bool("match", workId == p.WorkID))
|
||||
|
||||
if workId != p.WorkID {
|
||||
continue
|
||||
}
|
||||
|
||||
foundMatch = true
|
||||
if enc, ok := att["enc"].(string); ok {
|
||||
p.Enc = enc
|
||||
logger.Logger.Debug("从附件获取 enc", zap.String("enc", enc))
|
||||
}
|
||||
if title, ok := property["title"].(string); ok {
|
||||
p.Title = title
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if !foundMatch {
|
||||
logger.Logger.Warn("未找到匹配的作业附件", zap.String("workID", p.WorkID))
|
||||
}
|
||||
|
||||
if defaults := attachment.Defaults; defaults != nil {
|
||||
logger.Logger.Debug("defaults 内容", zap.Any("defaults", defaults))
|
||||
if ktoken, ok := defaults["ktoken"].(string); ok {
|
||||
p.KToken = ktoken
|
||||
logger.Logger.Debug("从 defaults 获取 ktoken", zap.String("ktoken", ktoken))
|
||||
} else {
|
||||
logger.Logger.Warn("defaults 中没有 ktoken")
|
||||
}
|
||||
if userid, ok := defaults["userid"].(string); ok {
|
||||
p.PUID = userid
|
||||
logger.Logger.Debug("从 defaults 获取 userid", zap.String("userid", userid))
|
||||
} else {
|
||||
logger.Logger.Warn("defaults 中没有 userid")
|
||||
}
|
||||
// 检查 defaults 中是否有 cpi,如果有则使用
|
||||
if cpi, ok := defaults["cpi"]; ok {
|
||||
switch v := cpi.(type) {
|
||||
case string:
|
||||
p.Cpi = v
|
||||
logger.Logger.Debug("从 defaults 获取 cpi (string)", zap.String("cpi", v))
|
||||
case float64:
|
||||
p.Cpi = strconv.FormatInt(int64(v), 10)
|
||||
logger.Logger.Debug("从 defaults 获取 cpi (float64)", zap.String("cpi", p.Cpi))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.Logger.Warn("attachment.Defaults 为空")
|
||||
}
|
||||
|
||||
logger.Logger.Debug("AttachmentsDetectionWork 完成",
|
||||
zap.String("PUID", p.PUID),
|
||||
zap.String("KToken", p.KToken),
|
||||
zap.String("Enc", p.Enc))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoCompleteWorkByPoint 自动完成作业任务点(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) AutoCompleteWorkByPoint(ctx context.Context, course entity.XueXiTCourse, p *entity.ChapterPoint, aiConfig *AIConfig, autoSubmit bool) error {
|
||||
if p.IsPassed {
|
||||
logger.Logger.Info("作业已完成", zap.String("jobID", p.JobID), zap.String("title", p.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果没有AI配置,跳过
|
||||
if aiConfig == nil {
|
||||
logger.Logger.Warn("未配置AI,跳过作业", zap.String("title", p.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转换为 PointWorkDto
|
||||
workDto := &entity.PointWorkDto{
|
||||
CardIndex: p.CardIndex,
|
||||
CourseID: p.CourseID,
|
||||
ClassID: p.ClassID,
|
||||
KnowledgeID: p.KnowledgeID,
|
||||
Cpi: p.Cpi,
|
||||
WorkID: p.WorkID,
|
||||
SchoolID: p.SchoolID,
|
||||
JobID: p.JobID,
|
||||
PUID: p.PUID,
|
||||
KToken: p.KToken,
|
||||
Enc: p.Enc,
|
||||
Title: p.Title,
|
||||
IsPassed: p.IsPassed,
|
||||
}
|
||||
|
||||
// 关键修复:获取 AttachmentSetting 来填充 PUID、KToken、Enc 等参数
|
||||
attachment, enc, err := c.PageMobileChapterCard(ctx, course, p.KnowledgeID, p.CardIndex)
|
||||
if err != nil {
|
||||
logger.Logger.Error("获取章节卡片失败", zap.String("title", p.Title), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if attachment != nil {
|
||||
// 填充作业参数
|
||||
if err := c.AttachmentsDetectionWork(attachment, workDto); err != nil {
|
||||
logger.Logger.Error("填充作业参数失败", zap.String("title", p.Title), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
// 只有当附件中没有获取到 enc 时,才使用从页面获取的 enc
|
||||
if workDto.Enc == "" && enc != "" {
|
||||
workDto.Enc = enc
|
||||
logger.Logger.Debug("使用页面 enc", zap.String("enc", enc))
|
||||
}
|
||||
}
|
||||
|
||||
logger.Logger.Debug("作业参数",
|
||||
zap.String("workID", workDto.WorkID),
|
||||
zap.String("PUID", workDto.PUID),
|
||||
zap.String("KToken", workDto.KToken),
|
||||
zap.String("Enc", workDto.Enc),
|
||||
zap.String("Cpi", workDto.Cpi))
|
||||
|
||||
// 使用AI自动完成作业
|
||||
result, err := c.AutoCompleteWorkWithAI(ctx, workDto, *aiConfig, autoSubmit)
|
||||
if err != nil {
|
||||
logger.Logger.Error("AI答题失败", zap.String("title", p.Title), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Logger.Info("AI答题完成", zap.String("title", p.Title), zap.String("result", result))
|
||||
return nil
|
||||
}
|
||||
|
||||
// FetchWorkQuestions 获取作业题目
|
||||
func (c *XuexitongCourse) FetchWorkQuestions(ctx context.Context, p *entity.PointWorkDto) (*entity.WorkDto, error) {
|
||||
c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
defer func() {
|
||||
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
}()
|
||||
params := url.Values{}
|
||||
params.Add("courseid", p.CourseID)
|
||||
workID := p.WorkID
|
||||
if p.SchoolID != "" && p.SchoolID != "0" {
|
||||
workID = p.SchoolID + "-" + p.WorkID
|
||||
}
|
||||
params.Add("workid", workID)
|
||||
params.Add("jobid", p.JobID)
|
||||
params.Add("needRedirect", "true")
|
||||
params.Add("knowledgeid", strconv.Itoa(p.KnowledgeID))
|
||||
// 关键修复:使用 p.PUID 而不是 c.userID
|
||||
params.Add("userid", p.PUID)
|
||||
params.Add("ut", "s")
|
||||
params.Add("clazzId", p.ClassID)
|
||||
params.Add("cpi", p.Cpi)
|
||||
params.Add("ktoken", p.KToken)
|
||||
params.Add("enc", p.Enc)
|
||||
|
||||
logger.Logger.Debug("FetchWorkQuestions 请求参数",
|
||||
zap.String("courseid", p.CourseID),
|
||||
zap.String("workid", workID),
|
||||
zap.String("jobid", p.JobID),
|
||||
zap.Int("knowledgeid", p.KnowledgeID),
|
||||
zap.String("userid", p.PUID),
|
||||
zap.String("clazzId", p.ClassID),
|
||||
zap.String("cpi", p.Cpi),
|
||||
zap.String("ktoken", p.KToken),
|
||||
zap.String("enc", p.Enc))
|
||||
|
||||
var body string
|
||||
requestURL := ApiWorkFetch + "?" + params.Encode()
|
||||
logger.Logger.Debug("FetchWorkQuestions 请求URL", zap.String("url", requestURL))
|
||||
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("mobile"),
|
||||
"Sec-Ch-Ua-Platform": "Windows",
|
||||
"Accept": "*/*",
|
||||
"Host": "mooc1-api.chaoxing.com",
|
||||
"Connection": "keep-alive",
|
||||
"Accept-Language": "zh_CN",
|
||||
}).Get(requestURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = resp.String()
|
||||
logger.Logger.Debug("FetchWorkQuestions 响应", zap.String("body", body[:min(len(body), 500)]))
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workDto, err := c.parseWorkHTML(body, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return workDto, nil
|
||||
}
|
||||
|
||||
// parseWorkHTML 解析作业HTML页面,提取题目信息(使用 goquery,与 pkg/yatori-go-core 保持一致)
|
||||
func (c *XuexitongCourse) parseWorkHTML(htmlContent string, p *entity.PointWorkDto) (*entity.WorkDto, error) {
|
||||
workDto := &entity.WorkDto{
|
||||
WorkId: p.WorkID,
|
||||
ClassId: p.ClassID,
|
||||
CourseId: p.CourseID,
|
||||
KnowledgeId: strconv.Itoa(p.KnowledgeID),
|
||||
Cpi: p.Cpi,
|
||||
JobId: p.JobID,
|
||||
UserId: p.PUID,
|
||||
}
|
||||
|
||||
// 使用 goquery 解析 HTML
|
||||
doc, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(htmlContent)))
|
||||
if err != nil {
|
||||
logger.Logger.Error("解析作业HTML失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析作业信息(与 pkg/yatori-go-core/utils/XueXiTParseWorkUtils.go 中的 ParseWorkInform 一致)
|
||||
findList := []string{"userId", "courseId", "classId", "api", "workAnswerId",
|
||||
"answerId", "totalQuestionNum", "fullScore", "knowledgeid", "oldSchoolId",
|
||||
"oldWorkId", "jobid", "workRelationId", "enc", "enc_work", "cpi", "workId"}
|
||||
for _, find := range findList {
|
||||
doc.Find("input#" + find).Each(func(i int, s *goquery.Selection) {
|
||||
val, exists := s.Attr("value")
|
||||
if exists {
|
||||
switch find {
|
||||
case "workAnswerId":
|
||||
workDto.WorkAnswerId = val
|
||||
case "answerId":
|
||||
workDto.AnswerId = val
|
||||
case "totalQuestionNum":
|
||||
workDto.TotalQuestionNum = val
|
||||
case "fullScore":
|
||||
workDto.FullScore = val
|
||||
case "oldWorkId":
|
||||
workDto.OldWorkId = val
|
||||
case "oldSchoolId":
|
||||
workDto.OldSchoolId = val
|
||||
case "api":
|
||||
workDto.Api = val
|
||||
case "workRelationId":
|
||||
workDto.WorkRelationId = val
|
||||
case "enc_work":
|
||||
workDto.EncWork = val
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 截取试卷标题
|
||||
doc.Find(".chapter-title").Each(func(i int, s *goquery.Selection) {
|
||||
val, exists := s.Attr("workname")
|
||||
if exists {
|
||||
workDto.Title = val
|
||||
}
|
||||
})
|
||||
|
||||
// 解析题目(与 pkg/yatori-go-core/aggregation/xuexitong/XueXiTongCardAction.go 中的 ParseWorkQuestionAction 一致)
|
||||
questionNodes := doc.Find("div.Py-mian1")
|
||||
questionNodes.Each(func(i int, questionNode *goquery.Selection) {
|
||||
// 获取题目ID
|
||||
qid, _ := questionNode.Attr("data")
|
||||
|
||||
// 获取题目HTML
|
||||
questionHTML, _ := questionNode.Html()
|
||||
qdoc, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(questionHTML)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 提取题目类型
|
||||
quesTypeText := qdoc.Find(".Py-m1-title .quesType").Text()
|
||||
quesType := extractQuestionType(quesTypeText)
|
||||
|
||||
// 提取题目文本
|
||||
var quesTextBuilder strings.Builder
|
||||
qdoc.Find(".Py-m1-title").Contents().Each(func(i int, n *goquery.Selection) {
|
||||
quesTextBuilder.WriteString(n.Text())
|
||||
})
|
||||
quesText := cleanText(quesTextBuilder.String())
|
||||
|
||||
question := entity.WorkQuestion{
|
||||
Qid: qid,
|
||||
Text: quesText,
|
||||
Options: make(map[string]string),
|
||||
}
|
||||
|
||||
// 根据题型解析选项
|
||||
switch quesType {
|
||||
case "单选题":
|
||||
question.Type = entity.QuestionTypeSingleChoice
|
||||
qdoc.Find(".answerList.singleChoice li").Each(func(i int, s *goquery.Selection) {
|
||||
optionLetter := s.Find("em.choose-opt").Text()
|
||||
ccContent := s.Find("cc").Text()
|
||||
if ccContent == "" {
|
||||
img, exists := s.Find("cc img").Attr("src")
|
||||
if exists {
|
||||
ccContent = "Image: " + img
|
||||
}
|
||||
}
|
||||
question.Options[optionLetter] = strings.TrimSpace(ccContent)
|
||||
})
|
||||
|
||||
case "多选题":
|
||||
question.Type = entity.QuestionTypeMultipleChoice
|
||||
qdoc.Find(".answerList.multiChoice li").Each(func(i int, s *goquery.Selection) {
|
||||
optionLetter := s.Find("em.choose-opt").Text()
|
||||
ccContent := s.Find("cc").Text()
|
||||
if ccContent == "" {
|
||||
img, exists := s.Find("cc img").Attr("src")
|
||||
if exists {
|
||||
ccContent = "Image: " + img
|
||||
}
|
||||
}
|
||||
question.Options[optionLetter] = strings.TrimSpace(ccContent)
|
||||
})
|
||||
|
||||
case "判断题":
|
||||
question.Type = entity.QuestionTypeTrueFalse
|
||||
qdoc.Find(".answerList.panduan li").Each(func(i int, s *goquery.Selection) {
|
||||
optionLetter := s.Find("em").Text()
|
||||
pContent := s.Find("p").Text()
|
||||
question.Options[optionLetter] = strings.TrimSpace(pContent)
|
||||
})
|
||||
|
||||
case "填空题":
|
||||
question.Type = entity.QuestionTypeFillBlank
|
||||
// 统计填空数量
|
||||
blankCount := 0
|
||||
qdoc.Find("ul.blankList2").Each(func(i int, selection *goquery.Selection) {
|
||||
blankCount++
|
||||
})
|
||||
question.BlankCount = blankCount
|
||||
if question.BlankCount == 0 {
|
||||
question.BlankCount = 1
|
||||
}
|
||||
|
||||
case "简答题":
|
||||
question.Type = entity.QuestionTypeShortAnswer
|
||||
}
|
||||
|
||||
workDto.Questions = append(workDto.Questions, question)
|
||||
})
|
||||
|
||||
logger.Logger.Debug("解析作业完成",
|
||||
zap.String("title", workDto.Title),
|
||||
zap.Int("questionCount", len(workDto.Questions)))
|
||||
|
||||
return workDto, nil
|
||||
}
|
||||
|
||||
// extractQuestionType 从文本中提取题目类型
|
||||
func extractQuestionType(text string) string {
|
||||
start := strings.Index(text, "[")
|
||||
end := strings.Index(text, "]")
|
||||
if start != -1 && end != -1 && end > start {
|
||||
return strings.TrimSpace(text[start+1 : end])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// cleanText 清理文本中的多余空白
|
||||
func cleanText(text string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
// 移除多余的空白字符
|
||||
text = strings.Join(strings.Fields(text), " ")
|
||||
return text
|
||||
}
|
||||
|
||||
// SubmitWorkAnswers 提交作业答案
|
||||
func (c *XuexitongCourse) SubmitWorkAnswers(ctx context.Context, workDto *entity.WorkDto, isSave bool) (string, error) {
|
||||
|
||||
pyFlag := ""
|
||||
if isSave {
|
||||
pyFlag = "1"
|
||||
}
|
||||
formMap := map[string]string{
|
||||
"pyFlag": pyFlag,
|
||||
"courseId": workDto.CourseId,
|
||||
"classId": workDto.ClassId,
|
||||
"api": workDto.Api,
|
||||
"mooc": "0",
|
||||
"workAnswerId": workDto.WorkAnswerId,
|
||||
"answerId": "",
|
||||
"totalQuestionNum": workDto.TotalQuestionNum,
|
||||
"fullScore": workDto.FullScore,
|
||||
"knowledgeid": workDto.KnowledgeId,
|
||||
"oldSchoolId": workDto.OldSchoolId,
|
||||
"oldWorkId": workDto.OldWorkId,
|
||||
"jobid": workDto.JobId,
|
||||
"workRelationId": workDto.WorkRelationId,
|
||||
"enc_work": workDto.EncWork,
|
||||
"isphone": "true",
|
||||
"userId": workDto.UserId,
|
||||
"workTimesEnc": "",
|
||||
"isAccessibleCustomFid": "0",
|
||||
}
|
||||
answerwqbid := ""
|
||||
for _, q := range workDto.Questions {
|
||||
if q.Qid != "" {
|
||||
answerwqbid += q.Qid + ","
|
||||
}
|
||||
|
||||
switch q.Type {
|
||||
case entity.QuestionTypeSingleChoice:
|
||||
answer := strings.Join(q.Answers, "")
|
||||
formMap["answer"+q.Qid] = answer
|
||||
formMap["answertype"+q.Qid] = "0"
|
||||
|
||||
case entity.QuestionTypeMultipleChoice:
|
||||
answer := strings.Join(q.Answers, "")
|
||||
formMap["answer"+q.Qid] = answer
|
||||
formMap["answertype"+q.Qid] = "1"
|
||||
|
||||
case entity.QuestionTypeFillBlank:
|
||||
for key, val := range q.BlankAnswer {
|
||||
re := regexp.MustCompile(`\d+`)
|
||||
numbers := re.FindAllString(key, -1)
|
||||
if len(numbers) > 0 {
|
||||
formMap["answer"+q.Qid+numbers[0]] = val
|
||||
}
|
||||
}
|
||||
formMap["tiankongsize"+q.Qid] = strconv.Itoa(q.BlankCount)
|
||||
formMap["answertype"+q.Qid] = "2"
|
||||
|
||||
case entity.QuestionTypeTrueFalse:
|
||||
answer := ""
|
||||
for _, a := range q.Answers {
|
||||
if a == "正确" || a == "对" {
|
||||
answer = "true"
|
||||
} else if a == "错误" || a == "错" {
|
||||
answer = "false"
|
||||
} else {
|
||||
answer = a
|
||||
}
|
||||
}
|
||||
formMap["answer"+q.Qid] = answer
|
||||
formMap["answertype"+q.Qid] = "3"
|
||||
|
||||
case entity.QuestionTypeShortAnswer:
|
||||
answer := strings.Join(q.Answers, "")
|
||||
formMap["answer"+q.Qid] = answer
|
||||
formMap["answertype"+q.Qid] = "4"
|
||||
}
|
||||
}
|
||||
formMap["answerwqbid"] = answerwqbid
|
||||
|
||||
urlStr := fmt.Sprintf("%s?_classId=%s&courseid=%s&token=%s&totalQuestionNum=%s&ua=pc&formType=post&saveStatus=1&version=1&tempsave=1",
|
||||
ApiWorkSubmit, workDto.ClassId, workDto.CourseId, workDto.EncWork, workDto.TotalQuestionNum)
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
"User-Agent": GetUA("mobile"),
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}).SetFormData(formMap).Post(urlStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = resp.String()
|
||||
return nil
|
||||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package cst
|
||||
|
||||
const (
|
||||
BasePointCpdaily = 1
|
||||
)
|
||||
@@ -37,7 +37,18 @@ const (
|
||||
ConfigEmailFromName = "email.fromName"
|
||||
|
||||
// 基础积分配置
|
||||
ConfigBasePointCpdaily = "basePoint.cpdaily"
|
||||
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 (
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/service"
|
||||
"likedream-goframe/internal/utils"
|
||||
)
|
||||
|
||||
// AIConfigCtrlV1 AI配置控制器
|
||||
type AIConfigCtrlV1 struct{}
|
||||
|
||||
// NewAIConfigCtrlV1 创建AI配置控制器
|
||||
func NewAIConfigCtrlV1() IAIConfigCtrlV1 {
|
||||
return &AIConfigCtrlV1{}
|
||||
}
|
||||
|
||||
// Get 获取AI配置
|
||||
func (c *AIConfigCtrlV1) Get(ctx context.Context, req *v1.AIConfigGetReq) (res *utils.Response[*v1.AIConfigGetResp], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.AIConfig.Get(ctx, req), nil
|
||||
}
|
||||
|
||||
// Update 更新AI配置
|
||||
func (c *AIConfigCtrlV1) Update(ctx context.Context, req *v1.AIConfigUpdateReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.AIConfig.Update(ctx, req), nil
|
||||
}
|
||||
|
||||
// Test 测试AI配置
|
||||
func (c *AIConfigCtrlV1) Test(ctx context.Context, req *v1.AIConfigTestReq) (res *utils.Response[*v1.AIConfigTestResp], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.AIConfig.Test(ctx, req), nil
|
||||
}
|
||||
|
||||
// GetTypeList 获取支持的AI类型列表
|
||||
func (c *AIConfigCtrlV1) GetTypeList(ctx context.Context, req *v1.AITypeListReq) (res *utils.Response[*v1.AITypeListResp], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.AIConfig.GetTypeList(ctx, req), nil
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/utils"
|
||||
)
|
||||
|
||||
// IAIConfigCtrlV1 AI配置控制器接口
|
||||
type IAIConfigCtrlV1 interface {
|
||||
// Get 获取AI配置
|
||||
Get(ctx context.Context, req *v1.AIConfigGetReq) (*utils.Response[*v1.AIConfigGetResp], error)
|
||||
// Update 更新AI配置
|
||||
Update(ctx context.Context, req *v1.AIConfigUpdateReq) (*utils.Response[any], error)
|
||||
// Test 测试AI配置
|
||||
Test(ctx context.Context, req *v1.AIConfigTestReq) (*utils.Response[*v1.AIConfigTestResp], error)
|
||||
// GetTypeList 获取支持的AI类型列表
|
||||
GetTypeList(ctx context.Context, req *v1.AITypeListReq) (*utils.Response[*v1.AITypeListResp], error)
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,12 @@ import (
|
||||
"context"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/framework/captcha"
|
||||
"likedream-goframe/internal/framework/redis"
|
||||
"likedream-goframe/internal/service"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type CpUserCtrlV1 struct {
|
||||
@@ -68,10 +72,32 @@ func (c CpUserCtrlV1) AuthorizeRandom(ctx context.Context, req *v1.CpUserAuthori
|
||||
}
|
||||
func (c CpUserCtrlV1) LoginGetCode(ctx context.Context, req *v1.LoginGetCode) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
client := redis.GetClient()
|
||||
dotsStr := client.GetDel(ctx, req.CaptchaKey).Val()
|
||||
if dotsStr == "" {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
var dots captcha.ConfirmData
|
||||
err = sonic.Unmarshal([]byte(dotsStr), &dots)
|
||||
utils.OkOrPanic(err)
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
return service.Global.CpUser.LoginGetCode(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c CpUserCtrlV1) AddByCode(ctx context.Context, req *v1.AddByCode) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
client := redis.GetClient()
|
||||
dotsStr := client.GetDel(ctx, req.CaptchaKey).Val()
|
||||
if dotsStr == "" {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
var dots captcha.ConfirmData
|
||||
err = sonic.Unmarshal([]byte(dotsStr), &dots)
|
||||
utils.OkOrPanic(err)
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
return service.Global.CpUser.AddByCode(ctx, req), nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ func (c *UserLoginV1) Login(ctx context.Context, req *v1.UserLoginReq) (res *uti
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
|
||||
return service.Global.UserLogin.Login(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)
|
||||
|
||||
+12
-1
@@ -39,6 +39,8 @@ type CpUser struct {
|
||||
Cookie string `json:"cookie,omitempty"`
|
||||
// MobileType holds the value of the "mobile_type" field.
|
||||
MobileType cpuser.MobileType `json:"mobile_type,omitempty"`
|
||||
// LoginType holds the value of the "login_type" field.
|
||||
LoginType cpuser.LoginType `json:"login_type,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
// The values are being populated by the CpUserQuery when eager-loading is set.
|
||||
Edges CpUserEdges `json:"edges"`
|
||||
@@ -123,7 +125,7 @@ func (*CpUser) scanValues(columns []string) ([]any, error) {
|
||||
values[i] = new(ent2.CustomTime)
|
||||
case cpuser.FieldID, cpuser.FieldUserID:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case cpuser.FieldUsername, cpuser.FieldPassword, cpuser.FieldSchoolID, cpuser.FieldEmail, cpuser.FieldMobileCode, cpuser.FieldCookie, cpuser.FieldMobileType:
|
||||
case cpuser.FieldUsername, cpuser.FieldPassword, cpuser.FieldSchoolID, cpuser.FieldEmail, cpuser.FieldMobileCode, cpuser.FieldCookie, cpuser.FieldMobileType, cpuser.FieldLoginType:
|
||||
values[i] = new(sql.NullString)
|
||||
default:
|
||||
values[i] = new(sql.UnknownType)
|
||||
@@ -206,6 +208,12 @@ func (_m *CpUser) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
_m.MobileType = cpuser.MobileType(value.String)
|
||||
}
|
||||
case cpuser.FieldLoginType:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field login_type", values[i])
|
||||
} else if value.Valid {
|
||||
_m.LoginType = cpuser.LoginType(value.String)
|
||||
}
|
||||
default:
|
||||
_m.selectValues.Set(columns[i], values[i])
|
||||
}
|
||||
@@ -296,6 +304,9 @@ func (_m *CpUser) String() string {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("mobile_type=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.MobileType))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("login_type=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.LoginType))
|
||||
builder.WriteByte(')')
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ const (
|
||||
FieldCookie = "cookie"
|
||||
// FieldMobileType holds the string denoting the mobile_type field in the database.
|
||||
FieldMobileType = "mobile_type"
|
||||
// FieldLoginType holds the string denoting the login_type field in the database.
|
||||
FieldLoginType = "login_type"
|
||||
// EdgeCptasks holds the string denoting the cptasks edge name in mutations.
|
||||
EdgeCptasks = "cptasks"
|
||||
// EdgeCpLeave holds the string denoting the cp_leave edge name in mutations.
|
||||
@@ -98,6 +100,7 @@ var Columns = []string{
|
||||
FieldMobileCode,
|
||||
FieldCookie,
|
||||
FieldMobileType,
|
||||
FieldLoginType,
|
||||
}
|
||||
|
||||
// ValidColumn reports if the column name is valid (part of the table columns).
|
||||
@@ -123,8 +126,6 @@ var (
|
||||
DefaultUpdateTime func() *ent2.CustomTime
|
||||
// UsernameValidator is a validator for the "username" field. It is called by the builders before save.
|
||||
UsernameValidator func(string) error
|
||||
// PasswordValidator is a validator for the "password" field. It is called by the builders before save.
|
||||
PasswordValidator func(string) error
|
||||
// SchoolIDValidator is a validator for the "school_id" field. It is called by the builders before save.
|
||||
SchoolIDValidator func(string) error
|
||||
)
|
||||
@@ -152,6 +153,32 @@ func MobileTypeValidator(mt MobileType) error {
|
||||
}
|
||||
}
|
||||
|
||||
// LoginType defines the type for the "login_type" enum field.
|
||||
type LoginType string
|
||||
|
||||
// LoginTypeAccount is the default value of the LoginType enum.
|
||||
const DefaultLoginType = LoginTypeAccount
|
||||
|
||||
// LoginType values.
|
||||
const (
|
||||
LoginTypeAccount LoginType = "account"
|
||||
LoginTypePhone LoginType = "phone"
|
||||
)
|
||||
|
||||
func (lt LoginType) String() string {
|
||||
return string(lt)
|
||||
}
|
||||
|
||||
// LoginTypeValidator is a validator for the "login_type" field enum values. It is called by the builders before save.
|
||||
func LoginTypeValidator(lt LoginType) error {
|
||||
switch lt {
|
||||
case LoginTypeAccount, LoginTypePhone:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("cpuser: invalid enum value for login_type field: %q", lt)
|
||||
}
|
||||
}
|
||||
|
||||
// OrderOption defines the ordering options for the CpUser queries.
|
||||
type OrderOption func(*sql.Selector)
|
||||
|
||||
@@ -210,6 +237,11 @@ func ByMobileType(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldMobileType, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByLoginType orders the results by the login_type field.
|
||||
func ByLoginType(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldLoginType, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByCptasksCount orders the results by cptasks count.
|
||||
func ByCptasksCount(opts ...sql.OrderTermOption) OrderOption {
|
||||
return func(s *sql.Selector) {
|
||||
|
||||
@@ -381,6 +381,16 @@ func PasswordHasSuffix(v string) predicate.CpUser {
|
||||
return predicate.CpUser(sql.FieldHasSuffix(FieldPassword, v))
|
||||
}
|
||||
|
||||
// PasswordIsNil applies the IsNil predicate on the "password" field.
|
||||
func PasswordIsNil() predicate.CpUser {
|
||||
return predicate.CpUser(sql.FieldIsNull(FieldPassword))
|
||||
}
|
||||
|
||||
// PasswordNotNil applies the NotNil predicate on the "password" field.
|
||||
func PasswordNotNil() predicate.CpUser {
|
||||
return predicate.CpUser(sql.FieldNotNull(FieldPassword))
|
||||
}
|
||||
|
||||
// PasswordEqualFold applies the EqualFold predicate on the "password" field.
|
||||
func PasswordEqualFold(v string) predicate.CpUser {
|
||||
return predicate.CpUser(sql.FieldEqualFold(FieldPassword, v))
|
||||
@@ -701,6 +711,26 @@ func MobileTypeNotIn(vs ...MobileType) predicate.CpUser {
|
||||
return predicate.CpUser(sql.FieldNotIn(FieldMobileType, vs...))
|
||||
}
|
||||
|
||||
// LoginTypeEQ applies the EQ predicate on the "login_type" field.
|
||||
func LoginTypeEQ(v LoginType) predicate.CpUser {
|
||||
return predicate.CpUser(sql.FieldEQ(FieldLoginType, v))
|
||||
}
|
||||
|
||||
// LoginTypeNEQ applies the NEQ predicate on the "login_type" field.
|
||||
func LoginTypeNEQ(v LoginType) predicate.CpUser {
|
||||
return predicate.CpUser(sql.FieldNEQ(FieldLoginType, v))
|
||||
}
|
||||
|
||||
// LoginTypeIn applies the In predicate on the "login_type" field.
|
||||
func LoginTypeIn(vs ...LoginType) predicate.CpUser {
|
||||
return predicate.CpUser(sql.FieldIn(FieldLoginType, vs...))
|
||||
}
|
||||
|
||||
// LoginTypeNotIn applies the NotIn predicate on the "login_type" field.
|
||||
func LoginTypeNotIn(vs ...LoginType) predicate.CpUser {
|
||||
return predicate.CpUser(sql.FieldNotIn(FieldLoginType, vs...))
|
||||
}
|
||||
|
||||
// HasCptasks applies the HasEdge predicate on the "cptasks" edge.
|
||||
func HasCptasks() predicate.CpUser {
|
||||
return predicate.CpUser(func(s *sql.Selector) {
|
||||
|
||||
@@ -57,6 +57,14 @@ func (_c *CpUserCreate) SetPassword(v string) *CpUserCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillablePassword sets the "password" field if the given value is not nil.
|
||||
func (_c *CpUserCreate) SetNillablePassword(v *string) *CpUserCreate {
|
||||
if v != nil {
|
||||
_c.SetPassword(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetSchoolID sets the "school_id" field.
|
||||
func (_c *CpUserCreate) SetSchoolID(v string) *CpUserCreate {
|
||||
_c.mutation.SetSchoolID(v)
|
||||
@@ -111,6 +119,20 @@ func (_c *CpUserCreate) SetMobileType(v cpuser.MobileType) *CpUserCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetLoginType sets the "login_type" field.
|
||||
func (_c *CpUserCreate) SetLoginType(v cpuser.LoginType) *CpUserCreate {
|
||||
_c.mutation.SetLoginType(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableLoginType sets the "login_type" field if the given value is not nil.
|
||||
func (_c *CpUserCreate) SetNillableLoginType(v *cpuser.LoginType) *CpUserCreate {
|
||||
if v != nil {
|
||||
_c.SetLoginType(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// AddCptaskIDs adds the "cptasks" edge to the CpTask entity by IDs.
|
||||
func (_c *CpUserCreate) AddCptaskIDs(ids ...int64) *CpUserCreate {
|
||||
_c.mutation.AddCptaskIDs(ids...)
|
||||
@@ -223,6 +245,10 @@ func (_c *CpUserCreate) defaults() error {
|
||||
v := cpuser.DefaultUpdateTime()
|
||||
_c.mutation.SetUpdateTime(v)
|
||||
}
|
||||
if _, ok := _c.mutation.LoginType(); !ok {
|
||||
v := cpuser.DefaultLoginType
|
||||
_c.mutation.SetLoginType(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -245,14 +271,6 @@ func (_c *CpUserCreate) check() error {
|
||||
return &ValidationError{Name: "username", err: fmt.Errorf(`ent: validator failed for field "CpUser.username": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.Password(); !ok {
|
||||
return &ValidationError{Name: "password", err: errors.New(`ent: missing required field "CpUser.password"`)}
|
||||
}
|
||||
if v, ok := _c.mutation.Password(); ok {
|
||||
if err := cpuser.PasswordValidator(v); err != nil {
|
||||
return &ValidationError{Name: "password", err: fmt.Errorf(`ent: validator failed for field "CpUser.password": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.SchoolID(); !ok {
|
||||
return &ValidationError{Name: "school_id", err: errors.New(`ent: missing required field "CpUser.school_id"`)}
|
||||
}
|
||||
@@ -269,6 +287,14 @@ func (_c *CpUserCreate) check() error {
|
||||
return &ValidationError{Name: "mobile_type", err: fmt.Errorf(`ent: validator failed for field "CpUser.mobile_type": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.LoginType(); !ok {
|
||||
return &ValidationError{Name: "login_type", err: errors.New(`ent: missing required field "CpUser.login_type"`)}
|
||||
}
|
||||
if v, ok := _c.mutation.LoginType(); ok {
|
||||
if err := cpuser.LoginTypeValidator(v); err != nil {
|
||||
return &ValidationError{Name: "login_type", err: fmt.Errorf(`ent: validator failed for field "CpUser.login_type": %w`, err)}
|
||||
}
|
||||
}
|
||||
if len(_c.mutation.SysuserIDs()) == 0 {
|
||||
return &ValidationError{Name: "sysuser", err: errors.New(`ent: missing required edge "CpUser.sysuser"`)}
|
||||
}
|
||||
@@ -335,6 +361,10 @@ func (_c *CpUserCreate) createSpec() (*CpUser, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(cpuser.FieldMobileType, field.TypeEnum, value)
|
||||
_node.MobileType = value
|
||||
}
|
||||
if value, ok := _c.mutation.LoginType(); ok {
|
||||
_spec.SetField(cpuser.FieldLoginType, field.TypeEnum, value)
|
||||
_node.LoginType = value
|
||||
}
|
||||
if nodes := _c.mutation.CptasksIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
@@ -522,6 +552,12 @@ func (u *CpUserUpsert) UpdatePassword() *CpUserUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearPassword clears the value of the "password" field.
|
||||
func (u *CpUserUpsert) ClearPassword() *CpUserUpsert {
|
||||
u.SetNull(cpuser.FieldPassword)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetSchoolID sets the "school_id" field.
|
||||
func (u *CpUserUpsert) SetSchoolID(v string) *CpUserUpsert {
|
||||
u.Set(cpuser.FieldSchoolID, v)
|
||||
@@ -600,6 +636,18 @@ func (u *CpUserUpsert) UpdateMobileType() *CpUserUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetLoginType sets the "login_type" field.
|
||||
func (u *CpUserUpsert) SetLoginType(v cpuser.LoginType) *CpUserUpsert {
|
||||
u.Set(cpuser.FieldLoginType, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateLoginType sets the "login_type" field to the value that was provided on create.
|
||||
func (u *CpUserUpsert) UpdateLoginType() *CpUserUpsert {
|
||||
u.SetExcluded(cpuser.FieldLoginType)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateNewValues updates the mutable fields using the new values that were set on create.
|
||||
// Using this option is equivalent to using:
|
||||
//
|
||||
@@ -701,6 +749,13 @@ func (u *CpUserUpsertOne) UpdatePassword() *CpUserUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// ClearPassword clears the value of the "password" field.
|
||||
func (u *CpUserUpsertOne) ClearPassword() *CpUserUpsertOne {
|
||||
return u.Update(func(s *CpUserUpsert) {
|
||||
s.ClearPassword()
|
||||
})
|
||||
}
|
||||
|
||||
// SetSchoolID sets the "school_id" field.
|
||||
func (u *CpUserUpsertOne) SetSchoolID(v string) *CpUserUpsertOne {
|
||||
return u.Update(func(s *CpUserUpsert) {
|
||||
@@ -792,6 +847,20 @@ func (u *CpUserUpsertOne) UpdateMobileType() *CpUserUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetLoginType sets the "login_type" field.
|
||||
func (u *CpUserUpsertOne) SetLoginType(v cpuser.LoginType) *CpUserUpsertOne {
|
||||
return u.Update(func(s *CpUserUpsert) {
|
||||
s.SetLoginType(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateLoginType sets the "login_type" field to the value that was provided on create.
|
||||
func (u *CpUserUpsertOne) UpdateLoginType() *CpUserUpsertOne {
|
||||
return u.Update(func(s *CpUserUpsert) {
|
||||
s.UpdateLoginType()
|
||||
})
|
||||
}
|
||||
|
||||
// Exec executes the query.
|
||||
func (u *CpUserUpsertOne) Exec(ctx context.Context) error {
|
||||
if len(u.create.conflict) == 0 {
|
||||
@@ -1059,6 +1128,13 @@ func (u *CpUserUpsertBulk) UpdatePassword() *CpUserUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// ClearPassword clears the value of the "password" field.
|
||||
func (u *CpUserUpsertBulk) ClearPassword() *CpUserUpsertBulk {
|
||||
return u.Update(func(s *CpUserUpsert) {
|
||||
s.ClearPassword()
|
||||
})
|
||||
}
|
||||
|
||||
// SetSchoolID sets the "school_id" field.
|
||||
func (u *CpUserUpsertBulk) SetSchoolID(v string) *CpUserUpsertBulk {
|
||||
return u.Update(func(s *CpUserUpsert) {
|
||||
@@ -1150,6 +1226,20 @@ func (u *CpUserUpsertBulk) UpdateMobileType() *CpUserUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetLoginType sets the "login_type" field.
|
||||
func (u *CpUserUpsertBulk) SetLoginType(v cpuser.LoginType) *CpUserUpsertBulk {
|
||||
return u.Update(func(s *CpUserUpsert) {
|
||||
s.SetLoginType(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateLoginType sets the "login_type" field to the value that was provided on create.
|
||||
func (u *CpUserUpsertBulk) UpdateLoginType() *CpUserUpsertBulk {
|
||||
return u.Update(func(s *CpUserUpsert) {
|
||||
s.UpdateLoginType()
|
||||
})
|
||||
}
|
||||
|
||||
// Exec executes the query.
|
||||
func (u *CpUserUpsertBulk) Exec(ctx context.Context) error {
|
||||
if u.create.err != nil {
|
||||
|
||||
@@ -83,6 +83,12 @@ func (_u *CpUserUpdate) SetNillablePassword(v *string) *CpUserUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearPassword clears the value of the "password" field.
|
||||
func (_u *CpUserUpdate) ClearPassword() *CpUserUpdate {
|
||||
_u.mutation.ClearPassword()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetSchoolID sets the "school_id" field.
|
||||
func (_u *CpUserUpdate) SetSchoolID(v string) *CpUserUpdate {
|
||||
_u.mutation.SetSchoolID(v)
|
||||
@@ -171,6 +177,20 @@ func (_u *CpUserUpdate) SetNillableMobileType(v *cpuser.MobileType) *CpUserUpdat
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetLoginType sets the "login_type" field.
|
||||
func (_u *CpUserUpdate) SetLoginType(v cpuser.LoginType) *CpUserUpdate {
|
||||
_u.mutation.SetLoginType(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableLoginType sets the "login_type" field if the given value is not nil.
|
||||
func (_u *CpUserUpdate) SetNillableLoginType(v *cpuser.LoginType) *CpUserUpdate {
|
||||
if v != nil {
|
||||
_u.SetLoginType(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddCptaskIDs adds the "cptasks" edge to the CpTask entity by IDs.
|
||||
func (_u *CpUserUpdate) AddCptaskIDs(ids ...int64) *CpUserUpdate {
|
||||
_u.mutation.AddCptaskIDs(ids...)
|
||||
@@ -346,11 +366,6 @@ func (_u *CpUserUpdate) check() error {
|
||||
return &ValidationError{Name: "username", err: fmt.Errorf(`ent: validator failed for field "CpUser.username": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.Password(); ok {
|
||||
if err := cpuser.PasswordValidator(v); err != nil {
|
||||
return &ValidationError{Name: "password", err: fmt.Errorf(`ent: validator failed for field "CpUser.password": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.SchoolID(); ok {
|
||||
if err := cpuser.SchoolIDValidator(v); err != nil {
|
||||
return &ValidationError{Name: "school_id", err: fmt.Errorf(`ent: validator failed for field "CpUser.school_id": %w`, err)}
|
||||
@@ -361,6 +376,11 @@ func (_u *CpUserUpdate) check() error {
|
||||
return &ValidationError{Name: "mobile_type", err: fmt.Errorf(`ent: validator failed for field "CpUser.mobile_type": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.LoginType(); ok {
|
||||
if err := cpuser.LoginTypeValidator(v); err != nil {
|
||||
return &ValidationError{Name: "login_type", err: fmt.Errorf(`ent: validator failed for field "CpUser.login_type": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _u.mutation.SysuserCleared() && len(_u.mutation.SysuserIDs()) > 0 {
|
||||
return errors.New(`ent: clearing a required unique edge "CpUser.sysuser"`)
|
||||
}
|
||||
@@ -397,6 +417,9 @@ func (_u *CpUserUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if value, ok := _u.mutation.Password(); ok {
|
||||
_spec.SetField(cpuser.FieldPassword, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.PasswordCleared() {
|
||||
_spec.ClearField(cpuser.FieldPassword, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.Email(); ok {
|
||||
_spec.SetField(cpuser.FieldEmail, field.TypeString, value)
|
||||
}
|
||||
@@ -418,6 +441,9 @@ func (_u *CpUserUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if value, ok := _u.mutation.MobileType(); ok {
|
||||
_spec.SetField(cpuser.FieldMobileType, field.TypeEnum, value)
|
||||
}
|
||||
if value, ok := _u.mutation.LoginType(); ok {
|
||||
_spec.SetField(cpuser.FieldLoginType, field.TypeEnum, value)
|
||||
}
|
||||
if _u.mutation.CptasksCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
@@ -696,6 +722,12 @@ func (_u *CpUserUpdateOne) SetNillablePassword(v *string) *CpUserUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearPassword clears the value of the "password" field.
|
||||
func (_u *CpUserUpdateOne) ClearPassword() *CpUserUpdateOne {
|
||||
_u.mutation.ClearPassword()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetSchoolID sets the "school_id" field.
|
||||
func (_u *CpUserUpdateOne) SetSchoolID(v string) *CpUserUpdateOne {
|
||||
_u.mutation.SetSchoolID(v)
|
||||
@@ -784,6 +816,20 @@ func (_u *CpUserUpdateOne) SetNillableMobileType(v *cpuser.MobileType) *CpUserUp
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetLoginType sets the "login_type" field.
|
||||
func (_u *CpUserUpdateOne) SetLoginType(v cpuser.LoginType) *CpUserUpdateOne {
|
||||
_u.mutation.SetLoginType(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableLoginType sets the "login_type" field if the given value is not nil.
|
||||
func (_u *CpUserUpdateOne) SetNillableLoginType(v *cpuser.LoginType) *CpUserUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetLoginType(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddCptaskIDs adds the "cptasks" edge to the CpTask entity by IDs.
|
||||
func (_u *CpUserUpdateOne) AddCptaskIDs(ids ...int64) *CpUserUpdateOne {
|
||||
_u.mutation.AddCptaskIDs(ids...)
|
||||
@@ -972,11 +1018,6 @@ func (_u *CpUserUpdateOne) check() error {
|
||||
return &ValidationError{Name: "username", err: fmt.Errorf(`ent: validator failed for field "CpUser.username": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.Password(); ok {
|
||||
if err := cpuser.PasswordValidator(v); err != nil {
|
||||
return &ValidationError{Name: "password", err: fmt.Errorf(`ent: validator failed for field "CpUser.password": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.SchoolID(); ok {
|
||||
if err := cpuser.SchoolIDValidator(v); err != nil {
|
||||
return &ValidationError{Name: "school_id", err: fmt.Errorf(`ent: validator failed for field "CpUser.school_id": %w`, err)}
|
||||
@@ -987,6 +1028,11 @@ func (_u *CpUserUpdateOne) check() error {
|
||||
return &ValidationError{Name: "mobile_type", err: fmt.Errorf(`ent: validator failed for field "CpUser.mobile_type": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.LoginType(); ok {
|
||||
if err := cpuser.LoginTypeValidator(v); err != nil {
|
||||
return &ValidationError{Name: "login_type", err: fmt.Errorf(`ent: validator failed for field "CpUser.login_type": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _u.mutation.SysuserCleared() && len(_u.mutation.SysuserIDs()) > 0 {
|
||||
return errors.New(`ent: clearing a required unique edge "CpUser.sysuser"`)
|
||||
}
|
||||
@@ -1040,6 +1086,9 @@ func (_u *CpUserUpdateOne) sqlSave(ctx context.Context) (_node *CpUser, err erro
|
||||
if value, ok := _u.mutation.Password(); ok {
|
||||
_spec.SetField(cpuser.FieldPassword, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.PasswordCleared() {
|
||||
_spec.ClearField(cpuser.FieldPassword, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.Email(); ok {
|
||||
_spec.SetField(cpuser.FieldEmail, field.TypeString, value)
|
||||
}
|
||||
@@ -1061,6 +1110,9 @@ func (_u *CpUserUpdateOne) sqlSave(ctx context.Context) (_node *CpUser, err erro
|
||||
if value, ok := _u.mutation.MobileType(); ok {
|
||||
_spec.SetField(cpuser.FieldMobileType, field.TypeEnum, value)
|
||||
}
|
||||
if value, ok := _u.mutation.LoginType(); ok {
|
||||
_spec.SetField(cpuser.FieldLoginType, field.TypeEnum, value)
|
||||
}
|
||||
if _u.mutation.CptasksCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
|
||||
@@ -190,6 +190,7 @@ var schemaGraph = func() *sqlgraph.Schema {
|
||||
cpuser.FieldMobileCode: {Type: field.TypeString, Column: cpuser.FieldMobileCode},
|
||||
cpuser.FieldCookie: {Type: field.TypeString, Column: cpuser.FieldCookie},
|
||||
cpuser.FieldMobileType: {Type: field.TypeEnum, Column: cpuser.FieldMobileType},
|
||||
cpuser.FieldLoginType: {Type: field.TypeEnum, Column: cpuser.FieldLoginType},
|
||||
},
|
||||
}
|
||||
graph.Nodes[7] = &sqlgraph.Node{
|
||||
@@ -1634,6 +1635,11 @@ func (f *CpUserFilter) WhereMobileType(p entql.StringP) {
|
||||
f.Where(p.Field(cpuser.FieldMobileType))
|
||||
}
|
||||
|
||||
// WhereLoginType applies the entql string predicate on the login_type field.
|
||||
func (f *CpUserFilter) WhereLoginType(p entql.StringP) {
|
||||
f.Where(p.Field(cpuser.FieldLoginType))
|
||||
}
|
||||
|
||||
// WhereHasCptasks applies a predicate to check if query has an edge cptasks.
|
||||
func (f *CpUserFilter) WhereHasCptasks() {
|
||||
f.Where(entql.HasEdge("cptasks"))
|
||||
|
||||
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"}},
|
||||
@@ -201,11 +201,12 @@ var (
|
||||
{Name: "create_time", Type: field.TypeString, SchemaType: map[string]string{"mysql": "datetime", "postgres": "timestamp", "sqlite3": "datetime"}},
|
||||
{Name: "update_time", Type: field.TypeString, SchemaType: map[string]string{"mysql": "datetime", "postgres": "timestamp", "sqlite3": "datetime"}},
|
||||
{Name: "username", Type: field.TypeString},
|
||||
{Name: "password", Type: field.TypeString},
|
||||
{Name: "password", Type: field.TypeString, Nullable: true},
|
||||
{Name: "email", Type: field.TypeString, Nullable: true},
|
||||
{Name: "mobile_code", Type: field.TypeString, Nullable: true},
|
||||
{Name: "cookie", Type: field.TypeString, Nullable: true, Size: 2147483647},
|
||||
{Name: "mobile_type", Type: field.TypeEnum, Enums: []string{"random", "authorize"}},
|
||||
{Name: "login_type", Type: field.TypeEnum, Enums: []string{"account", "phone"}, Default: "account"},
|
||||
{Name: "school_id", Type: field.TypeString},
|
||||
{Name: "user_id", Type: field.TypeInt64},
|
||||
}
|
||||
@@ -217,13 +218,13 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "cp_users_cp_schools_cpusers",
|
||||
Columns: []*schema.Column{CpUsersColumns[9]},
|
||||
Columns: []*schema.Column{CpUsersColumns[10]},
|
||||
RefColumns: []*schema.Column{CpSchoolsColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "cp_users_sys_user_cpuser",
|
||||
Columns: []*schema.Column{CpUsersColumns[10]},
|
||||
Columns: []*schema.Column{CpUsersColumns[11]},
|
||||
RefColumns: []*schema.Column{SysUserColumns[0]},
|
||||
OnDelete: schema.Cascade,
|
||||
},
|
||||
|
||||
@@ -6252,6 +6252,7 @@ type CpUserMutation struct {
|
||||
mobile_code *string
|
||||
cookie *string
|
||||
mobile_type *cpuser.MobileType
|
||||
login_type *cpuser.LoginType
|
||||
clearedFields map[string]struct{}
|
||||
cptasks map[int64]struct{}
|
||||
removedcptasks map[int64]struct{}
|
||||
@@ -6544,9 +6545,22 @@ func (m *CpUserMutation) OldPassword(ctx context.Context) (v string, err error)
|
||||
return oldValue.Password, nil
|
||||
}
|
||||
|
||||
// ClearPassword clears the value of the "password" field.
|
||||
func (m *CpUserMutation) ClearPassword() {
|
||||
m.password = nil
|
||||
m.clearedFields[cpuser.FieldPassword] = struct{}{}
|
||||
}
|
||||
|
||||
// PasswordCleared returns if the "password" field was cleared in this mutation.
|
||||
func (m *CpUserMutation) PasswordCleared() bool {
|
||||
_, ok := m.clearedFields[cpuser.FieldPassword]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetPassword resets all changes to the "password" field.
|
||||
func (m *CpUserMutation) ResetPassword() {
|
||||
m.password = nil
|
||||
delete(m.clearedFields, cpuser.FieldPassword)
|
||||
}
|
||||
|
||||
// SetSchoolID sets the "school_id" field.
|
||||
@@ -6768,6 +6782,42 @@ func (m *CpUserMutation) ResetMobileType() {
|
||||
m.mobile_type = nil
|
||||
}
|
||||
|
||||
// SetLoginType sets the "login_type" field.
|
||||
func (m *CpUserMutation) SetLoginType(ct cpuser.LoginType) {
|
||||
m.login_type = &ct
|
||||
}
|
||||
|
||||
// LoginType returns the value of the "login_type" field in the mutation.
|
||||
func (m *CpUserMutation) LoginType() (r cpuser.LoginType, exists bool) {
|
||||
v := m.login_type
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldLoginType returns the old "login_type" field's value of the CpUser entity.
|
||||
// If the CpUser object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *CpUserMutation) OldLoginType(ctx context.Context) (v cpuser.LoginType, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldLoginType is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldLoginType requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldLoginType: %w", err)
|
||||
}
|
||||
return oldValue.LoginType, nil
|
||||
}
|
||||
|
||||
// ResetLoginType resets all changes to the "login_type" field.
|
||||
func (m *CpUserMutation) ResetLoginType() {
|
||||
m.login_type = nil
|
||||
}
|
||||
|
||||
// AddCptaskIDs adds the "cptasks" edge to the CpTask entity by ids.
|
||||
func (m *CpUserMutation) AddCptaskIDs(ids ...int64) {
|
||||
if m.cptasks == nil {
|
||||
@@ -7031,7 +7081,7 @@ func (m *CpUserMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *CpUserMutation) Fields() []string {
|
||||
fields := make([]string, 0, 10)
|
||||
fields := make([]string, 0, 11)
|
||||
if m.create_time != nil {
|
||||
fields = append(fields, cpuser.FieldCreateTime)
|
||||
}
|
||||
@@ -7062,6 +7112,9 @@ func (m *CpUserMutation) Fields() []string {
|
||||
if m.mobile_type != nil {
|
||||
fields = append(fields, cpuser.FieldMobileType)
|
||||
}
|
||||
if m.login_type != nil {
|
||||
fields = append(fields, cpuser.FieldLoginType)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -7090,6 +7143,8 @@ func (m *CpUserMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.Cookie()
|
||||
case cpuser.FieldMobileType:
|
||||
return m.MobileType()
|
||||
case cpuser.FieldLoginType:
|
||||
return m.LoginType()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -7119,6 +7174,8 @@ func (m *CpUserMutation) OldField(ctx context.Context, name string) (ent.Value,
|
||||
return m.OldCookie(ctx)
|
||||
case cpuser.FieldMobileType:
|
||||
return m.OldMobileType(ctx)
|
||||
case cpuser.FieldLoginType:
|
||||
return m.OldLoginType(ctx)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown CpUser field %s", name)
|
||||
}
|
||||
@@ -7198,6 +7255,13 @@ func (m *CpUserMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetMobileType(v)
|
||||
return nil
|
||||
case cpuser.FieldLoginType:
|
||||
v, ok := value.(cpuser.LoginType)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetLoginType(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown CpUser field %s", name)
|
||||
}
|
||||
@@ -7231,6 +7295,9 @@ func (m *CpUserMutation) AddField(name string, value ent.Value) error {
|
||||
// mutation.
|
||||
func (m *CpUserMutation) ClearedFields() []string {
|
||||
var fields []string
|
||||
if m.FieldCleared(cpuser.FieldPassword) {
|
||||
fields = append(fields, cpuser.FieldPassword)
|
||||
}
|
||||
if m.FieldCleared(cpuser.FieldEmail) {
|
||||
fields = append(fields, cpuser.FieldEmail)
|
||||
}
|
||||
@@ -7254,6 +7321,9 @@ func (m *CpUserMutation) FieldCleared(name string) bool {
|
||||
// error if the field is not defined in the schema.
|
||||
func (m *CpUserMutation) ClearField(name string) error {
|
||||
switch name {
|
||||
case cpuser.FieldPassword:
|
||||
m.ClearPassword()
|
||||
return nil
|
||||
case cpuser.FieldEmail:
|
||||
m.ClearEmail()
|
||||
return nil
|
||||
@@ -7301,6 +7371,9 @@ func (m *CpUserMutation) ResetField(name string) error {
|
||||
case cpuser.FieldMobileType:
|
||||
m.ResetMobileType()
|
||||
return nil
|
||||
case cpuser.FieldLoginType:
|
||||
m.ResetLoginType()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown CpUser field %s", name)
|
||||
}
|
||||
|
||||
@@ -185,10 +185,6 @@ func init() {
|
||||
cpuserDescUsername := cpuserFields[1].Descriptor()
|
||||
// cpuser.UsernameValidator is a validator for the "username" field. It is called by the builders before save.
|
||||
cpuser.UsernameValidator = cpuserDescUsername.Validators[0].(func(string) error)
|
||||
// cpuserDescPassword is the schema descriptor for password field.
|
||||
cpuserDescPassword := cpuserFields[2].Descriptor()
|
||||
// cpuser.PasswordValidator is a validator for the "password" field. It is called by the builders before save.
|
||||
cpuser.PasswordValidator = cpuserDescPassword.Validators[0].(func(string) error)
|
||||
// cpuserDescSchoolID is the schema descriptor for school_id field.
|
||||
cpuserDescSchoolID := cpuserFields[3].Descriptor()
|
||||
// cpuser.SchoolIDValidator is a validator for the "school_id" field. It is called by the builders before save.
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ func (CpUser) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.Int64("user_id"),
|
||||
field.String("username").NotEmpty(),
|
||||
field.String("password").NotEmpty(),
|
||||
field.String("password").Optional(),
|
||||
field.String("school_id").NotEmpty(),
|
||||
field.String("email").Optional(),
|
||||
field.String("mobile_code").Optional(),
|
||||
field.Text("cookie").Optional(),
|
||||
field.Enum("mobile_type").Values("random", "authorize"),
|
||||
field.Enum("login_type").Values("account", "phone").Default("account"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/process"
|
||||
)
|
||||
|
||||
func IsDebug() bool {
|
||||
return isGolandDebug() || isDebugByPath()
|
||||
|
||||
}
|
||||
|
||||
func isDebugByPath() bool {
|
||||
exePath := strings.ToLower(os.Args[0])
|
||||
// 获取绝对路径
|
||||
absPath, err := filepath.Abs(exePath)
|
||||
if err != nil {
|
||||
absPath = exePath
|
||||
}
|
||||
|
||||
// 检查路径中是否包含常见的临时目录或"go-build"字样
|
||||
tmpDir := strings.ToLower(getTempDir())
|
||||
isTemp := strings.Contains(absPath, tmpDir) || strings.Contains(absPath, "go-build") || strings.Contains(absPath, "go_build_")
|
||||
return isTemp
|
||||
}
|
||||
func getTempDir() string {
|
||||
tempDir := os.TempDir()
|
||||
|
||||
// 尝试转换为长路径
|
||||
if longPath, err := filepath.EvalSymlinks(tempDir); err == nil {
|
||||
if absPath, err := filepath.Abs(longPath); err == nil {
|
||||
return absPath
|
||||
}
|
||||
return longPath
|
||||
}
|
||||
|
||||
return filepath.Dir(tempDir)
|
||||
}
|
||||
|
||||
func isGolandDebug() bool {
|
||||
// 获取当前进程ID
|
||||
pid := os.Getpid()
|
||||
|
||||
// 获取当前进程信息
|
||||
currentProcess, err := process.NewProcess(int32(pid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取父进程
|
||||
parent, err := currentProcess.Parent()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取父进程名称
|
||||
name, err := parent.Name()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查父进程是否是dlv(Go调试器)或GoLand相关进程
|
||||
name = strings.ToLower(name)
|
||||
debugProcessNames := []string{
|
||||
"dlv.exe", "dlv",
|
||||
"goland.exe", "goland64.exe",
|
||||
"goland", "idea.exe",
|
||||
}
|
||||
|
||||
for _, debugName := range debugProcessNames {
|
||||
if strings.Contains(name, debugName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,18 +1,35 @@
|
||||
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()
|
||||
if IsDebug() {
|
||||
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel) // Enable debug logs
|
||||
}
|
||||
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,11 +86,20 @@ 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
|
||||
restyClient := login.GetClient(ctx, cookie)
|
||||
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, restyClient) {
|
||||
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, restyClient, targetTask.Edges.CpUser.LoginType) {
|
||||
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
|
||||
loginExecutor.Login(ctx, &schoolDetail, targetTask.Edges.CpUser.Username, targetTask.Edges.CpUser.Password)
|
||||
cookie = loginExecutor.GetCookieString()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,20 +4,44 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"likedream-goframe/internal/application/quesbank"
|
||||
"likedream-goframe/internal/application/xuexitong"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
"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/framework/errors"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/samber/oops"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// getAIConfig 获取AI配置
|
||||
func getAIConfig(ctx context.Context) *quesbank.AIConfig {
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ("ai.config")).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("未配置AI,作业将跳过", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
var aiConfig quesbank.AIConfig
|
||||
if err := json.Unmarshal([]byte(config.Value), &aiConfig); err != nil {
|
||||
logger.Logger.Warn("AI配置解析失败", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &aiConfig
|
||||
}
|
||||
|
||||
type XuexitongTaskHandler struct{}
|
||||
|
||||
func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task) (err error) {
|
||||
@@ -38,7 +62,7 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
|
||||
|
||||
// 检查是否暂停
|
||||
if targetCourse.Status == courseentity.StatusPause {
|
||||
g.Log().Debugf(ctx, "课程ID %d 已暂停,跳过执行", targetCourse.ID)
|
||||
logger.Logger.Debug("课程已暂停,跳过执行", zap.Int64("courseID", targetCourse.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -54,8 +78,6 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
|
||||
currentMinutes := currentHour*60 + currentMinute
|
||||
|
||||
if currentMinutes < startTime || currentMinutes >= endTime {
|
||||
g.Log().Debugf(ctx, "课程ID %d 非暴力模式,当前时间 %02d:%02d 不在执行时间范围内(8:30-17:30),跳过执行",
|
||||
targetCourse.ID, currentHour, currentMinute)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -77,7 +99,9 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
|
||||
errorMessage = "未知的错误导致任务执行失败: " + err.Error()
|
||||
}
|
||||
|
||||
g.Log().Errorf(ctx, "学习通刷课失败 - 课程: %s, 错误: %s", targetCourse.CourseName, errorMessage)
|
||||
logger.Logger.Error("学习通刷课失败",
|
||||
zap.String("courseName", targetCourse.CourseName),
|
||||
zap.String("error", errorMessage))
|
||||
panic(err)
|
||||
})
|
||||
|
||||
@@ -96,33 +120,124 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
|
||||
// 创建课程服务
|
||||
courseService := xuexitong.NewXuexitongCourse(client)
|
||||
|
||||
// 执行刷课
|
||||
err = courseService.AutoStudyByCourseID(ctx, targetCourse.CourseID)
|
||||
// 非暴力模式下,设置时间检查回调,每个任务点完成后都会检查时间
|
||||
if targetCourse.Violence == courseentity.ViolenceNo {
|
||||
courseService.SetTimeCheckFunc(func() bool {
|
||||
currentTime := time.Now()
|
||||
currentHour := currentTime.Hour()
|
||||
currentMinute := currentTime.Minute()
|
||||
|
||||
startTime := 8*60 + 30 // 8:30
|
||||
endTime := 17*60 + 30 // 17:30
|
||||
currentMinutes := currentHour*60 + currentMinute
|
||||
needStudy := currentMinutes >= startTime && currentMinutes < endTime
|
||||
if !needStudy {
|
||||
// 更新状态为等待中
|
||||
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).SetStatus(courseentity.StatusWaiting).ExecX(ctx)
|
||||
}
|
||||
return needStudy
|
||||
})
|
||||
}
|
||||
|
||||
// 先获取课程列表找到目标课程
|
||||
courseList := courseService.PullCourseList(ctx)
|
||||
var course *entity.XueXiTCourse
|
||||
for _, c := range courseList {
|
||||
if c.CourseID == targetCourse.CourseID {
|
||||
course = &c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if course == nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(404, "未找到课程"))
|
||||
}
|
||||
|
||||
// 开始刷课前,先获取并更新任务点统计
|
||||
chapters := courseService.PullCourseChapterList(ctx, *course)
|
||||
// FetchChapterPointStatus 会更新 chapters 中每个章节的 PointTotal 和 PointFinish
|
||||
_, statusErr := courseService.FetchChapterPointStatus(ctx, *course, chapters)
|
||||
if statusErr != nil {
|
||||
logger.Logger.Warn("获取章节任务点状态失败", zap.Error(statusErr))
|
||||
} else {
|
||||
// 通过累加每个章节的 PointTotal 和 PointFinish 计算总数
|
||||
var totalPointCount, finishedPointCount int
|
||||
for _, chapter := range chapters {
|
||||
totalPointCount += chapter.PointTotal
|
||||
finishedPointCount += chapter.PointFinish
|
||||
}
|
||||
logger.Logger.Info("课程任务点统计",
|
||||
zap.String("courseName", targetCourse.CourseName),
|
||||
zap.Int("totalPointCount", totalPointCount),
|
||||
zap.Int("finishedPointCount", 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)
|
||||
})
|
||||
|
||||
// 获取AI配置(用于作业答题)
|
||||
aiConfig := getAIConfig(ctx)
|
||||
|
||||
// 执行刷课(带AI配置)
|
||||
err = courseService.AutoStudyByCourseIDWithOptions(ctx, targetCourse.CourseID, &xuexitong.AutoStudyOptions{
|
||||
AIConfig: aiConfig,
|
||||
AutoSubmit: true, // 默认不自动提交,仅保存
|
||||
Thread: 5,
|
||||
})
|
||||
if err != nil {
|
||||
// 如果是时间限制导致的停止,不算失败
|
||||
if err == xuexitong.ErrTimeRestriction {
|
||||
logger.Logger.Info("课程因时间限制暂停刷课,等待下次执行", zap.Int64("courseID", 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, "刷课完成,正在统计任务情况...")
|
||||
// 刷课完成后,重新统计任务情况
|
||||
logger.Logger.Info("刷课完成,正在重新统计任务情况...")
|
||||
|
||||
// 获取章节列表
|
||||
chapters := courseService.PullCourseChapterList(ctx, targetCourse.CourseID)
|
||||
// 重新获取章节列表和任务点状态
|
||||
chaptersAfter := courseService.PullCourseChapterList(ctx, *course)
|
||||
// FetchChapterPointStatus 会更新 chapters 中每个章节的 PointTotal 和 PointFinish
|
||||
_, statusErrAfter := courseService.FetchChapterPointStatus(ctx, *course, chaptersAfter)
|
||||
|
||||
// 统计总任务数(累加所有章节的JobCount)
|
||||
totalTasks := 0
|
||||
for _, chapter := range chapters {
|
||||
totalTasks += chapter.JobCount
|
||||
var totalPointCountAfter, finishedPointCountAfter int
|
||||
if statusErrAfter != nil {
|
||||
logger.Logger.Warn("获取章节任务点状态失败", zap.Error(statusErrAfter))
|
||||
} else {
|
||||
// 通过累加每个章节的 PointTotal 和 PointFinish 计算总数
|
||||
for _, chapter := range chaptersAfter {
|
||||
totalPointCountAfter += chapter.PointTotal
|
||||
finishedPointCountAfter += chapter.PointFinish
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "课程 %s 统计完成: 总任务点 %d", targetCourse.CourseName, totalTasks)
|
||||
logger.Logger.Info("课程统计完成",
|
||||
zap.String("courseName", targetCourse.CourseName),
|
||||
zap.Int("totalPointCount", totalPointCountAfter),
|
||||
zap.Int("finishedPointCount", 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)
|
||||
logger.Logger.Info("学习通刷课完成", zap.String("courseName", targetCourse.CourseName))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ type BalanceServiceV1 struct {
|
||||
}
|
||||
|
||||
func (v *BalanceServiceV1) ChangeUserBalance(ctx context.Context, userId int64, balance decimal.Decimal, reason string) {
|
||||
if decimal.Zero.Equal(balance) {
|
||||
return
|
||||
}
|
||||
err := db.WithTx(ctx, func(c context.Context, tx *ent.Tx) {
|
||||
// 使用 SELECT ... FOR UPDATE 锁定用户记录
|
||||
user, err := tx.SysUser.Query().
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/quesbank"
|
||||
"likedream-goframe/internal/common/db"
|
||||
errors2 "likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/ent/sysconfig"
|
||||
"likedream-goframe/internal/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AI配置键常量
|
||||
const (
|
||||
AIConfigKey = "ai.config"
|
||||
)
|
||||
|
||||
// AIConfigServiceV1 AI配置服务
|
||||
type AIConfigServiceV1 struct{}
|
||||
|
||||
// NewAIConfigServiceV1 创建AI配置服务
|
||||
func NewAIConfigServiceV1() *AIConfigServiceV1 {
|
||||
return &AIConfigServiceV1{}
|
||||
}
|
||||
|
||||
// Get 获取AI配置
|
||||
func (s *AIConfigServiceV1) Get(ctx context.Context, req *v1.AIConfigGetReq) *utils.Response[*v1.AIConfigGetResp] {
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ(AIConfigKey)).
|
||||
Only(ctx)
|
||||
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
return utils.SuccessWithData(&v1.AIConfigGetResp{
|
||||
Type: "chatglm",
|
||||
Model: "glm-4",
|
||||
APIKey: "",
|
||||
})
|
||||
}
|
||||
utils.OkOrPanic(err)
|
||||
}
|
||||
|
||||
var aiConfig quesbank.AIConfig
|
||||
if err := json.Unmarshal([]byte(config.Value), &aiConfig); err != nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(500, "AI配置解析失败"))
|
||||
}
|
||||
|
||||
maskedKey := maskAPIKey(aiConfig.APIKey)
|
||||
|
||||
return utils.SuccessWithData(&v1.AIConfigGetResp{
|
||||
Type: string(aiConfig.Type),
|
||||
URL: aiConfig.URL,
|
||||
Model: aiConfig.Model,
|
||||
APIKey: maskedKey,
|
||||
})
|
||||
}
|
||||
|
||||
// Update 更新AI配置
|
||||
func (s *AIConfigServiceV1) Update(ctx context.Context, req *v1.AIConfigUpdateReq) *utils.Response[any] {
|
||||
if req.Type == "other" && req.URL == "" {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "自定义AI类型必须填写API地址"))
|
||||
}
|
||||
|
||||
aiConfig := quesbank.AIConfig{
|
||||
Type: quesbank.AIType(req.Type),
|
||||
URL: req.URL,
|
||||
Model: req.Model,
|
||||
APIKey: req.APIKey,
|
||||
}
|
||||
|
||||
configValue, err := json.Marshal(aiConfig)
|
||||
if err != nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(500, "配置序列化失败"))
|
||||
}
|
||||
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ(AIConfigKey)).
|
||||
Only(ctx)
|
||||
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
db.Global.SysConfig.Create().
|
||||
SetKey(AIConfigKey).
|
||||
SetValue(string(configValue)).
|
||||
SetDescription("AI答题配置").
|
||||
SetType("json").
|
||||
SaveX(ctx)
|
||||
return utils.SuccessWithMsg("AI配置创建成功")
|
||||
}
|
||||
utils.OkOrPanic(err)
|
||||
}
|
||||
|
||||
db.Global.SysConfig.UpdateOne(config).
|
||||
SetValue(string(configValue)).
|
||||
ExecX(ctx)
|
||||
|
||||
return utils.SuccessWithMsg("AI配置更新成功")
|
||||
}
|
||||
|
||||
// Test 测试AI配置
|
||||
func (s *AIConfigServiceV1) Test(ctx context.Context, req *v1.AIConfigTestReq) *utils.Response[*v1.AIConfigTestResp] {
|
||||
if req.Type == "other" && req.URL == "" {
|
||||
return utils.SuccessWithData(&v1.AIConfigTestResp{
|
||||
Success: false,
|
||||
Message: "自定义AI类型必须填写API地址",
|
||||
})
|
||||
}
|
||||
|
||||
aiConfig := quesbank.AIConfig{
|
||||
Type: quesbank.AIType(req.Type),
|
||||
URL: req.URL,
|
||||
Model: req.Model,
|
||||
APIKey: req.APIKey,
|
||||
}
|
||||
|
||||
aiService := quesbank.NewAIAnswerService(aiConfig)
|
||||
testMessages := quesbank.AIChatMessages{
|
||||
Messages: []quesbank.AIChatMessage{
|
||||
{Role: "user", Content: "你好,请回复\"测试成功\""},
|
||||
},
|
||||
}
|
||||
|
||||
response, err := aiService.GetAnswer(ctx, testMessages)
|
||||
if err != nil {
|
||||
return utils.SuccessWithData(&v1.AIConfigTestResp{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("AI连接测试失败: %v", err),
|
||||
})
|
||||
}
|
||||
|
||||
return utils.SuccessWithData(&v1.AIConfigTestResp{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("AI连接测试成功,响应: %s", truncateString(response, 100)),
|
||||
})
|
||||
}
|
||||
|
||||
// GetTypeList 获取支持的AI类型列表
|
||||
func (s *AIConfigServiceV1) GetTypeList(ctx context.Context, req *v1.AITypeListReq) *utils.Response[*v1.AITypeListResp] {
|
||||
types := []v1.AITypeItem{
|
||||
{Type: "chatglm", Name: "智谱AI (ChatGLM)", Description: "智谱AI大模型,支持glm-4等模型", NeedURL: false},
|
||||
{Type: "xinghuo", Name: "讯飞星火", Description: "讯飞星火认知大模型", NeedURL: false},
|
||||
{Type: "tongyi", Name: "通义千问", Description: "阿里云通义千问大模型", NeedURL: false},
|
||||
{Type: "doubao", Name: "豆包", Description: "字节跳动豆包大模型", NeedURL: false},
|
||||
{Type: "openai", Name: "OpenAI", Description: "OpenAI GPT系列模型", NeedURL: false},
|
||||
{Type: "other", Name: "其他 (兼容OpenAI格式)", Description: "支持任何兼容OpenAI API格式的服务", NeedURL: true},
|
||||
}
|
||||
return utils.SuccessWithData(&v1.AITypeListResp{Types: types})
|
||||
}
|
||||
|
||||
// GetAIConfig 获取AI配置(内部使用)
|
||||
func (s *AIConfigServiceV1) GetAIConfig(ctx context.Context) (*quesbank.AIConfig, error) {
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ(AIConfigKey)).
|
||||
Only(ctx)
|
||||
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("AI配置未设置")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var aiConfig quesbank.AIConfig
|
||||
if err := json.Unmarshal([]byte(config.Value), &aiConfig); err != nil {
|
||||
return nil, fmt.Errorf("AI配置解析失败: %w", err)
|
||||
}
|
||||
|
||||
return &aiConfig, nil
|
||||
}
|
||||
|
||||
func maskAPIKey(key string) string {
|
||||
if len(key) <= 8 {
|
||||
return strings.Repeat("*", len(key))
|
||||
}
|
||||
return key[:4] + strings.Repeat("*", len(key)-8) + key[len(key)-4:]
|
||||
}
|
||||
|
||||
func truncateString(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
@@ -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,15 +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"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
@@ -26,7 +29,7 @@ type CourseServiceV1 struct {
|
||||
func (s CourseServiceV1) List(ctx context.Context, req *v1.CourseListReq) *utils.Response[[]*ent.CourseEntity] {
|
||||
user := utils.GetUserInfoFromCtx(ctx)
|
||||
query := db2.Global.CourseEntity.Query().
|
||||
Where(courseentity.HasSysUserWith(sysuser.ID(user.ID))).
|
||||
Where(courseentity.HasSysUserWith(sysuser.ID(user.ID)), courseentity.TypeEQ(courseentity.TypeXxt)).
|
||||
Order(ent.Desc(courseentity.FieldCreateTime))
|
||||
|
||||
if req.Type != "" {
|
||||
@@ -78,78 +81,27 @@ func (s CourseServiceV1) AddCourse(ctx context.Context, req *v1.AddCourseReq) *u
|
||||
|
||||
client := loginClient.GetClient()
|
||||
courseService := xuexitong.NewXuexitongCourse(client)
|
||||
|
||||
// 获取课程章节列表
|
||||
chapters := courseService.PullCourseChapterList(ctx, req.CourseID)
|
||||
|
||||
// 定义节点信息结构
|
||||
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"`
|
||||
}
|
||||
|
||||
var allNodes []NodeInfo
|
||||
var completedNodes []string
|
||||
|
||||
// 获取课程信息用于API调用
|
||||
courses := courseService.PullCourseList(ctx)
|
||||
var targetCourse *entity.XueXiTCourse
|
||||
for _, c := range courses {
|
||||
if c.CourseID == req.CourseID {
|
||||
targetCourse = &c
|
||||
// 获取课程章节列表
|
||||
courseList := courseService.PullCourseList(ctx)
|
||||
for _, course := range courseList {
|
||||
if course.CourseID == req.CourseID {
|
||||
targetCourse = &course
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetCourse == nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "未找到该课程"))
|
||||
}
|
||||
|
||||
// 递归获取所有章节的任务点
|
||||
var collectNodes func(chapters []entity.XueXiTCourseChapter)
|
||||
collectNodes = func(chapters []entity.XueXiTCourseChapter) {
|
||||
for _, chapter := range chapters {
|
||||
// 如果有任务点,获取卡片信息
|
||||
if chapter.JobCount > 0 {
|
||||
cards, err := courseService.FetchChapterCards(ctx, *targetCourse, chapter.ID, chapter.ID)
|
||||
if err != nil {
|
||||
logger.Logger.Warn("获取章节任务点失败", zap.String("chapter", chapter.Name), zap.Error(err))
|
||||
} else {
|
||||
for _, card := range cards {
|
||||
nodeInfo := NodeInfo{
|
||||
ChapterID: chapter.ID,
|
||||
ChapterName: chapter.Name,
|
||||
CardID: card.ID,
|
||||
CardTitle: card.Title,
|
||||
CardType: card.Type,
|
||||
}
|
||||
allNodes = append(allNodes, nodeInfo)
|
||||
|
||||
// 检查是否已完成
|
||||
if card.IsPassed {
|
||||
nodeKey := chapter.ID + ":" + card.ID
|
||||
completedNodes = append(completedNodes, nodeKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理子章节
|
||||
if len(chapter.Knowledge) > 0 {
|
||||
collectNodes(chapter.Knowledge)
|
||||
}
|
||||
}
|
||||
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(), "学习通刷课")
|
||||
|
||||
collectNodes(chapters)
|
||||
|
||||
// 将节点信息序列化为JSON
|
||||
nodesJSON, _ := sonic.MarshalString(allNodes)
|
||||
completedNodesJSON, _ := sonic.MarshalString(completedNodes)
|
||||
|
||||
// 添加课程时不获取任务点统计,由定时任务处理
|
||||
db2.Global.CourseEntity.Create().
|
||||
SetUserID(user.ID).
|
||||
SetUsername(req.Username).
|
||||
@@ -161,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"
|
||||
)
|
||||
|
||||
@@ -45,7 +45,7 @@ func (v CpLeaveServiceV1) GetLeaveListFromSystem(ctx context.Context, req *v1.Ge
|
||||
client := login.GetClient(ctx, cookie)
|
||||
|
||||
// 检查cookie是否有效
|
||||
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, client) {
|
||||
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()
|
||||
@@ -110,24 +110,16 @@ 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.ConfigBasePointCpdaily).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)
|
||||
}
|
||||
|
||||
// 先扣款
|
||||
balance.BalanceService.ChangeUserBalance(ctx, user.ID, basePoint.Neg(), "销假")
|
||||
balance.BalanceService.ChangeUserBalance(ctx, user.ID, basePoint.Neg(), "今日校园销假")
|
||||
|
||||
// 创建销假记录
|
||||
leaveRecord := db2.Global.CpLeave.Create().
|
||||
@@ -161,7 +153,7 @@ func (v CpLeaveServiceV1) Report(ctx context.Context, req *v1.CpLeaveReportReq)
|
||||
client := login.GetClient(ctx, cookie)
|
||||
|
||||
// 检查cookie是否有效
|
||||
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, client) {
|
||||
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()
|
||||
@@ -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"
|
||||
@@ -43,7 +44,7 @@ func (v CpTaskServiceV1) GetRecentTasks(ctx context.Context, req *v1.GetRecentTa
|
||||
}
|
||||
schoolDetail := school.GetSchoolDetail(ctx, cpUser.SchoolID)
|
||||
client := login.GetClient(ctx, cpUser.Cookie)
|
||||
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, client) {
|
||||
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()
|
||||
@@ -118,7 +119,7 @@ func (v CpTaskServiceV1) TaskFormData(ctx context.Context, req *v1.GetCpTaskForm
|
||||
schoolDetail := school.GetSchoolDetail(ctx, cpTask.Edges.CpUser.SchoolID)
|
||||
cookie := cpUser.Cookie
|
||||
client := login.GetClient(ctx, cookie)
|
||||
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, client) {
|
||||
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()
|
||||
@@ -180,7 +181,7 @@ func (v CpTaskServiceV1) PostExecute(ctx context.Context, req *v1.PostCpTaskExec
|
||||
}
|
||||
|
||||
//扣积分
|
||||
basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst2.ConfigBasePointCpdaily).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),
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ package cpdaily
|
||||
import (
|
||||
"context"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/application/cpdaily/login"
|
||||
"likedream-goframe/internal/application/cpdaily/school"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
@@ -10,14 +11,17 @@ 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"
|
||||
"likedream-goframe/internal/utils"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/samber/oops"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type CpUserServiceV1 struct {
|
||||
@@ -66,6 +70,7 @@ func (v CpUserServiceV1) ConfirmAdd(ctx context.Context, req *v1.CpUserConfirmAd
|
||||
SetMobileType(cpuser.MobileTypeRandom).
|
||||
SetSchoolID(sc.Id).
|
||||
SetSysuserID(user.ID).
|
||||
SetLoginType(cpuser.LoginTypeAccount).
|
||||
SaveX(ctx)
|
||||
return utils.SuccessWithMsg("添加成功")
|
||||
}
|
||||
@@ -88,6 +93,7 @@ func (v CpUserServiceV1) List(ctx context.Context, req *v1.CpUserListReq) *utils
|
||||
// 忽略密码
|
||||
for _, v := range data {
|
||||
v.Password = ""
|
||||
v.Cookie = ""
|
||||
}
|
||||
return utils.SuccessWithTotal[[]*ent.CpUser](data, total)
|
||||
}
|
||||
@@ -174,11 +180,55 @@ func (v CpUserServiceV1) AuthorizeRandom(ctx context.Context, req *v1.CpUserAuth
|
||||
}
|
||||
|
||||
func (v CpUserServiceV1) LoginGetCode(ctx context.Context, req *v1.LoginGetCode) *utils.Response[any] {
|
||||
return utils.SuccessWithMsg("")
|
||||
user := utils.GetUserInfoFromCtx(ctx)
|
||||
// 扣费
|
||||
//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)
|
||||
}
|
||||
if user.Balance.Sub(basePoint).IsNegative() {
|
||||
return utils.Fail(400, "余额不足")
|
||||
}
|
||||
loginExecutor := login.NewSmsWithoutDeviceID()
|
||||
loginExecutor.SendSms(ctx, req.Mobile)
|
||||
balance.BalanceService.ChangeUserBalance(ctx, user.ID, basePoint.Neg(), "手机号登录 - 验证码")
|
||||
return utils.SuccessWithMsg("发送成功")
|
||||
}
|
||||
|
||||
func (v CpUserServiceV1) AddByCode(ctx context.Context, req *v1.AddByCode) *utils.Response[any] {
|
||||
return utils.SuccessWithMsg("")
|
||||
user := utils.GetUserInfoFromCtx(ctx)
|
||||
jar := cookiejar.NewPersistCookieJar()
|
||||
loginExecutor := login.NewSmsWithoutDeviceID()
|
||||
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(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)
|
||||
}
|
||||
balance.BalanceService.ChangeUserBalance(ctx, user.ID, basePoint.Neg(), "手机号登录 - 登录")
|
||||
if req.Id == 0 {
|
||||
db.Global.CpUser.Create().
|
||||
SetUsername(req.Mobile).
|
||||
SetEmail(req.Email).
|
||||
SetSchoolID(req.SchoolId).
|
||||
SetCookie(loginExecutor.GetCookieString()).
|
||||
SetMobileCode(utils.UUID()).
|
||||
SetMobileType(cpuser.MobileTypeRandom).
|
||||
SetSysuserID(user.ID).
|
||||
SetLoginType(cpuser.LoginTypePhone).
|
||||
SaveX(ctx)
|
||||
return utils.SuccessWithMsg("添加成功")
|
||||
} else {
|
||||
db.Global.CpUser.UpdateOneID(req.Id).
|
||||
SetCookie(loginExecutor.GetCookieString()).
|
||||
ExecX(ctx)
|
||||
return utils.SuccessWithMsg("更新成功")
|
||||
}
|
||||
}
|
||||
|
||||
func (v CpUserServiceV1) LoginUrl(ctx context.Context, req *v1.CpUserLoginUrlReq) *utils.Response[any] {
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user