Compare commits

..

13 Commits

Author SHA1 Message Date
sugarBecomer c206a0863e ✨ 学习通优化 & 注册开关 2025-12-08 14:11:49 +08:00
sugarBecomer b068e178da 🐛 邮件只重试一次 2025-12-08 10:47:57 +08:00
sugarBecomer 02737ef743 🐛 销假 2025-12-08 10:44:29 +08:00
sugarBecomer 74a1134cf1 🐛 邮件调整 2025-12-08 10:03:21 +08:00
sugarBecomer b6f777f997 🐛 调整任务执行 2025-12-08 09:55:50 +08:00
sugarBecomer 2dc233631e 🐛 调整任务执行 2025-12-08 09:55:27 +08:00
sugarBecomer 0c60eb5ae4 🐛 销假 2025-12-07 22:37:28 +08:00
sugarBecomer 450c026a6f 🐛 销假 2025-12-06 21:58:51 +08:00
sugarBecomer d9d6acee5f 🐛 销假 2025-12-06 15:12:43 +08:00
sugarBecomer 164d8956de 🐛 销假 & 学习通 2025-12-06 15:10:47 +08:00
sugarBecomer 3f1a08374e 🐛 日志格式 & 邮箱错误处理 2025-12-06 12:53:52 +08:00
sugarBecomer 334d597f9a 🐛 邮箱发送失败 2025-12-06 12:34:12 +08:00
sugarBecomer 98eb15f4e4 ✨ 手机号登录 2025-12-06 02:16:20 +08:00
43 changed files with 972 additions and 1344 deletions
+5 -5
View File
@@ -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"]
@@ -53,6 +53,6 @@ sliderIds = ["1018764144934965", "sdhyxy", "hqdx"]
[basePoint]
cpdailyTask = "0.5"
cpdailyLeave = "4"
cpdailyPhone = "0.25"
[task]
concurrency = 20
concurrency = 80
-403
View File
@@ -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 | 服务器错误 |
+1 -1
View File
@@ -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#配置键不能为空"`
}
+7 -4
View File
@@ -19,8 +19,11 @@ 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"` // 地址(可选,不传则使用系统默认)
}
+3
View File
@@ -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 {
@@ -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))
}
+30 -7
View File
@@ -10,6 +10,7 @@ import (
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/utils"
"github.com/bytedance/sonic"
"github.com/gogf/gf/v2/container/gvar"
"github.com/google/uuid"
"github.com/samber/oops"
@@ -74,6 +75,14 @@ func (c *CpdailyReportApp) GetLeaveList() []leavetype.LeaveListOne {
AceEndTime: row.Get("actEndTime").String(),
Status: row.Get("status").String(),
}
// 获取详情以填充位置信息
detail := c.LeaveDetail(leaveOne.Id)
lon, lat := c.GetLocation(detail)
leaveOne.Longitude = lon
leaveOne.Latitude = lat
leaveOne.Address = detail.Get("detail.applyLocation.address").String()
listList = append(listList, leaveOne)
}
return listList
@@ -104,16 +113,30 @@ func (c *CpdailyReportApp) LeaveDetail(leaveId string) gjson.Result {
return datas
}
type Attach struct {
Type string `json:"type"`
Url string `json:"url"`
}
func (c *CpdailyReportApp) FillReportForm(detail gjson.Result) map[string]*gvar.Var {
resultForm := make(map[string]*gvar.Var)
resultForm["id"] = gvar.New(detail.Get("detail.id").String())
resultForm["needApproval"] = gvar.New(1)
resultForm["leaveType"] = gvar.New(detail.Get("leaveType.code").Int())
resultForm["leaveType"] = gvar.New(detail.Get("leaveType.code").String())
resultForm["reportLocation"] = gvar.New(detail.Get("detail.applyLocation.address").String())
resultForm["approverWid"] = gvar.New("")
resultForm["reportApplyExplain"] = gvar.New(nil)
resultForm["reportAttach"] = gvar.New(detail.Get("detail.applyAttach").Array())
//resultForm["reportAttach"] = gvar.New(.Array())
resultForm["reportAttach"] = gvar.New([]string{})
resultForm["uaIsCpadaily"] = gvar.New(true)
attachStr := detail.Get("detail.applyAttach").String()
if attachStr != "" {
var attach []Attach
_ = sonic.Unmarshal([]byte(attachStr), &attach)
resultForm["reportAttach"] = gvar.New(attach)
} else {
resultForm["reportAttach"] = gvar.New([]string{})
}
return resultForm
}
@@ -141,11 +164,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),
+7 -2
View File
@@ -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 {
+14 -9
View File
@@ -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()
}
@@ -7,4 +7,8 @@ type LeaveListOne struct {
AceEndTime string `json:"aceEndTime"`
Reason string `json:"reason"`
Status string `json:"status"`
// 销假时填充的位置信息
Longitude string `json:"longitude"` // 经度
Latitude string `json:"latitude"` // 纬度
Address string `json:"address"` // 地址
}
@@ -44,9 +44,20 @@ const (
VideoEncKey = "d_yHJ!$pdA~5"
)
// ErrTimeRestriction 时间限制错误,表示当前时间不在允许执行的范围内
var ErrTimeRestriction = errors2.NewBusinessError(499, "当前时间不在允许执行的范围内")
// TimeCheckFunc 时间检查回调函数类型,返回 true 表示可以继续执行
type TimeCheckFunc func() bool
// TaskCompleteCallback 任务点完成回调函数类型
type TaskCompleteCallback func()
type XuexitongCourse struct {
client *resty.Client
userID string
client *resty.Client
userID string
timeCheckFunc TimeCheckFunc // 时间检查回调,每个任务点完成后调用
taskCompleteCallback TaskCompleteCallback // 任务点完成回调
}
func NewXuexitongCourse(client *resty.Client) *XuexitongCourse {
@@ -55,6 +66,31 @@ func NewXuexitongCourse(client *resty.Client) *XuexitongCourse {
}
}
// SetTimeCheckFunc 设置时间检查回调函数
func (c *XuexitongCourse) SetTimeCheckFunc(fn TimeCheckFunc) {
c.timeCheckFunc = fn
}
// SetTaskCompleteCallback 设置任务点完成回调函数
func (c *XuexitongCourse) SetTaskCompleteCallback(fn TaskCompleteCallback) {
c.taskCompleteCallback = fn
}
// onTaskComplete 任务点完成时调用
func (c *XuexitongCourse) onTaskComplete() {
if c.taskCompleteCallback != nil {
c.taskCompleteCallback()
}
}
// checkTimeRestriction 检查时间限制,如果设置了回调且返回 false,则返回时间限制错误
func (c *XuexitongCourse) checkTimeRestriction() error {
if c.timeCheckFunc != nil && !c.timeCheckFunc() {
return ErrTimeRestriction
}
return nil
}
func (c *XuexitongCourse) SetUserID(userID string) {
c.userID = userID
}
@@ -118,7 +154,13 @@ func (c *XuexitongCourse) PullCourseList(ctx context.Context) []entity.XueXiTCou
courseID = strings.Split(strings.Split(sqUrl, "courseId=")[1], "&")[0]
}
if strings.Contains(sqUrl, "userId=") {
c.userID = strings.Split(sqUrl, "userId=")[1]
userIDPart := strings.Split(sqUrl, "userId=")[1]
// 处理 userId 后面可能还有其他参数的情况
if idx := strings.Index(userIDPart, "&"); idx != -1 {
c.userID = userIDPart[:idx]
} else {
c.userID = userIDPart
}
}
course := entity.XueXiTCourse{
@@ -295,6 +337,13 @@ func (c *XuexitongCourse) FetchChapterPointStatus(ctx context.Context, course en
"courseid": {strconv.Itoa(courseID)},
}
logger.Logger.Debug("FetchChapterPointStatus 请求参数",
zap.String("userID", c.userID),
zap.Int("classID", classID),
zap.Int("courseID", courseID),
zap.Int("cpi", course.Cpi),
zap.Int("nodesCount", len(nodes)))
var body string
err := retry.Do(func() error {
resp, err := c.client.R().SetHeaders(map[string]string{
@@ -312,12 +361,24 @@ func (c *XuexitongCourse) FetchChapterPointStatus(ctx context.Context, course en
return nil, err
}
logger.Logger.Debug("FetchChapterPointStatus 响应", zap.String("body", body))
var status entity.ChapterPointStatus
err = json.Unmarshal([]byte(body), &status)
if err != nil {
logger.Logger.Error("FetchChapterPointStatus 解析失败", zap.Error(err), zap.String("body", body))
return nil, err
}
// 更新章节的 PointTotal 和 PointFinish 字段
for i := range chapters {
chapterIDStr := strconv.Itoa(chapters[i].ID)
if pointData, exists := status[chapterIDStr]; exists {
chapters[i].PointTotal = pointData.UnFinishCount + pointData.FinishCount
chapters[i].PointFinish = pointData.FinishCount
}
}
return status, nil
}
@@ -412,6 +473,8 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
continue
}
pointType, ok := iframe.Other["module"]
// 调试日志:打印每个 iframe 的 module 类型
logger.Logger.Debug("解析到任务点", zap.String("module", pointType), zap.Bool("hasModule", ok), zap.Any("data", iframe.Data))
if !ok {
continue
}
@@ -465,7 +528,7 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
Type: "insertbook",
})
case "insertwork":
case "work", "insertwork":
workID, _ := iframe.Data["workid"].(string)
schoolID, _ := iframe.Data["schoolid"].(string)
jobID, _ := iframe.Data["_jobid"].(string)
@@ -486,7 +549,7 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
JobID: jobID,
})
case "insertlink":
case "hyperlink", "insertlink":
jobID, _ := iframe.Data["_jobid"].(string)
if jobID == "" {
continue
@@ -989,6 +1052,7 @@ func (c *XuexitongCourse) AutoPlayVideo(ctx context.Context, course entity.XueXi
if err := json.Unmarshal([]byte(playReport), &result); err == nil {
if isPassed, ok := result["isPassed"].(bool); ok && isPassed {
logger.Logger.Info("视频播放完成", zap.String("jobID", p.JobID))
c.onTaskComplete() // 任务点完成回调
return nil
}
}
@@ -1005,6 +1069,7 @@ func (c *XuexitongCourse) AutoPlayVideo(ctx context.Context, course entity.XueXi
if err := json.Unmarshal([]byte(playReport), &result); err == nil {
if isPassed, ok := result["isPassed"].(bool); ok && isPassed {
logger.Logger.Info("视频播放完成", zap.String("jobID", p.JobID))
c.onTaskComplete() // 任务点完成回调
return nil
}
}
@@ -1035,6 +1100,7 @@ func (c *XuexitongCourse) AutoReadDocument(ctx context.Context, course entity.Xu
if err := json.Unmarshal([]byte(report), &result); err == nil {
if status, ok := result["status"].(bool); ok && status {
logger.Logger.Info("文档刷课完成", zap.String("jobID", p.JobID), zap.String("title", p.Title))
c.onTaskComplete() // 任务点完成回调
return nil
}
if msg, ok := result["msg"].(string); ok {
@@ -1129,32 +1195,30 @@ func (c *XuexitongCourse) AutoStudyCourse(ctx context.Context, course entity.Xue
logger.Logger.Info(fmt.Sprintf("课程 %s 共有 %d 个章节", course.CourseName, len(chapters)))
// 获取章节任务点状态
status, err := c.FetchChapterPointStatus(ctx, course, chapters)
// 获取章节任务点状态(会自动更新 chapters 中每个章节的 PointTotal 和 PointFinish)
_, err := c.FetchChapterPointStatus(ctx, course, chapters)
if err != nil {
logger.Logger.Warn("获取章节状态失败", zap.Error(err))
} else {
// 更新章节状态
for i := range chapters {
if pointData, exists := status[strconv.Itoa(chapters[i].ID)]; exists {
if pointData.UnFinishCount != 0 && pointData.TotalCount == 0 {
chapters[i].PointTotal = pointData.UnFinishCount
} else {
chapters[i].PointTotal = pointData.TotalCount
}
chapters[i].PointFinish = pointData.FinishCount
}
}
}
// 遍历所有章节
for i, chapter := range chapters {
// 检查时间限制
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("course", course.CourseName))
return err
}
logger.Logger.Info(fmt.Sprintf("章节 [%d/%d]: %s (任务点: %d, 已完成: %d)",
i+1, len(chapters), chapter.Name, chapter.PointTotal, chapter.PointFinish))
if chapter.JobCount > 0 || chapter.PointTotal > chapter.PointFinish {
err := c.AutoStudyChapter(ctx, course, chapter)
if err != nil {
// 如果是时间限制错误,向上传递
if err == ErrTimeRestriction {
return err
}
logger.Logger.Error(fmt.Sprintf("刷章节失败: %s", chapter.Name), zap.Error(err))
continue
}
@@ -1267,6 +1331,11 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
// 刷视频
for i, video := range points.Videos {
// 检查时间限制
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
return err
}
logger.Logger.Info(fmt.Sprintf(" 视频任务点 [%d/%d]: %s", i+1, len(points.Videos), video.Title))
if err := c.AutoPlayVideo(ctx, course, &points.Videos[i]); err != nil {
logger.Logger.Error("视频刷课失败", zap.Error(err), zap.String("title", video.Title))
@@ -1276,6 +1345,11 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
// 刷文档
for i, doc := range points.Documents {
// 检查时间限制
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
return err
}
logger.Logger.Info(fmt.Sprintf(" 文档任务点 [%d/%d]: %s", i+1, len(points.Documents), doc.Title))
if err := c.AutoReadDocument(ctx, course, &points.Documents[i]); err != nil {
logger.Logger.Error("文档刷课失败", zap.Error(err), zap.String("title", doc.Title))
@@ -1285,6 +1359,11 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
// 刷作业
for i, work := range points.Works {
// 检查时间限制
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
return err
}
logger.Logger.Info(fmt.Sprintf(" 作业任务点 [%d/%d]: %s", i+1, len(points.Works), work.Title))
if err := c.AutoCompleteWork(ctx, course, &points.Works[i]); err != nil {
logger.Logger.Error("作业刷课失败", zap.Error(err), zap.String("title", work.Title))
@@ -1294,6 +1373,11 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
// 刷外链
for i, link := range points.Hyperlinks {
// 检查时间限制
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
return err
}
logger.Logger.Info(fmt.Sprintf(" 外链任务点 [%d/%d]: %s", i+1, len(points.Hyperlinks), link.Title))
if err := c.AutoCompleteHyperlink(ctx, course, &points.Hyperlinks[i]); err != nil {
logger.Logger.Error("外链刷课失败", zap.Error(err), zap.String("title", link.Title))
@@ -1303,6 +1387,11 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
// 刷直播
for i, live := range points.Lives {
// 检查时间限制
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
return err
}
logger.Logger.Info(fmt.Sprintf(" 直播任务点 [%d/%d]: %s", i+1, len(points.Lives), live.Title))
if err := c.AutoWatchLive(ctx, course, &points.Lives[i]); err != nil {
logger.Logger.Error("直播刷课失败", zap.Error(err), zap.String("title", live.Title))
@@ -1446,6 +1535,7 @@ func (c *XuexitongCourse) AutoCompleteHyperlink(ctx context.Context, course enti
if err := json.Unmarshal([]byte(report), &result); err == nil {
if status, ok := result["status"].(bool); ok && status {
logger.Logger.Info("外链刷课完成", zap.String("jobID", p.JobID), zap.String("title", p.Title))
c.onTaskComplete() // 任务点完成回调
return nil
}
if msg, ok := result["msg"].(string); ok {
@@ -1537,6 +1627,7 @@ func (c *XuexitongCourse) AutoWatchLive(ctx context.Context, course entity.XueXi
if p.VideoCompletePercent >= 90 {
logger.Logger.Info("直播刷课完成", zap.String("title", p.Title))
c.onTaskComplete() // 任务点完成回调
break
}
+4
View File
@@ -39,6 +39,10 @@ const (
// 基础积分配置
ConfigBasePointCpdailyTask = "basePoint.cpdailyTask"
ConfigBasePointCpdailyLeave = "basePoint.cpdailyLeave"
ConfigBasePointCpdailyPhone = "basePoint.cpdailyPhone"
// 系统配置
ConfigSystemAllowRegister = "system.allowRegister" // 是否允许注册
)
const (
@@ -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
}
@@ -5,7 +5,6 @@ import (
"encoding/json"
"likedream-goframe/internal/api/v1"
"likedream-goframe/internal/framework/captcha"
"likedream-goframe/internal/framework/errors"
"likedream-goframe/internal/framework/redis"
"likedream-goframe/internal/service"
"likedream-goframe/internal/utils"
@@ -64,7 +63,6 @@ func (c *UserLoginV1) Register(ctx context.Context, req *v1.UserRegisterReq) (re
if !captcha.VerifyCaptcha(dots, req.Dots) {
return utils.Fail(10003, "验证码错误"), nil
}
utils.OkOrPanic(errors.NewBusinessError(400, "注册功能暂时关闭,请等待重新开放"))
return service.Global.UserLogin.Register(ctx, req), nil
}
+4 -6
View File
@@ -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)
+12 -1
View File
@@ -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()
}
+34 -2
View File
@@ -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) {
+30
View File
@@ -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) {
+98 -8
View File
@@ -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 {
+62 -10
View File
@@ -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,
+6
View File
@@ -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
+4 -3
View File
@@ -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,
},
+74 -1
View File
@@ -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)
}
-4
View File
@@ -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.
+2 -2
View File
@@ -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(),
}
}
+2 -1
View File
@@ -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),
)
}
-496
View File
@@ -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))
}
+16 -2
View File
@@ -1,18 +1,32 @@
package logger
import (
"go.uber.org/zap"
"log"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var Logger *zap.Logger
func init() {
logger, err := zap.NewProduction()
// Create a production config and customize the time encoder
config := zap.NewProductionConfig()
config.EncoderConfig.EncodeTime = BeijingTimeEncoder
// Build the logger with the custom config
logger, err := config.Build()
if err != nil {
log.Fatalf("cannot init zap logger: %+v", err)
}
Logger = logger
}
// BeijingTimeEncoder formats time in Beijing timezone
func BeijingTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
// Set timezone to Beijing (UTC+8)
beijingLoc, _ := time.LoadLocation("Asia/Shanghai")
enc.AppendString(t.In(beijingLoc).Format("2006-01-02 15:04:05"))
}
+18 -13
View File
@@ -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()
+14 -3
View File
@@ -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)
+68 -1
View File
@@ -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)
+97 -3
View File
@@ -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)
}
}
}
+39 -12
View File
@@ -12,13 +12,19 @@ import (
"github.com/hibiken/asynq"
)
var globalServer *asynq.Server
var (
// 主任务服务器:处理 cpdaily、xuexitong 等业务任务
globalServer *asynq.Server
// 辅助任务服务器:处理 email、reset 等轻量级任务
auxiliaryServer *asynq.Server
)
func initServer(ctx context.Context) {
cfg := g.Cfg()
taskConcurrency := cfg.MustGetWithEnv(ctx, cst2.ConfigTaskConcurrency).Int()
// 主服务器:专门处理业务任务,使用大部分 worker
globalServer = asynq.NewServerFromRedisClient(redis.GetClient(), asynq.Config{
Concurrency: taskConcurrency,
BaseContext: gctx.GetInitCtx,
@@ -28,24 +34,45 @@ func initServer(ctx context.Context) {
},
DelayedTaskCheckInterval: 2 * time.Second,
ErrorHandler: taskErrorHandler{},
// 只处理业务队列,使用严格优先级模式
StrictPriority: true,
Queues: map[string]int{
cst2.TaskQueueEmail: 2,
cst2.TaskQueueCpdaily: 10,
cst2.TaskQueueXuexitong: 5,
cst2.TaskQueueCpdaily: 10, // 最高优先级
cst2.TaskQueueXuexitongVio: 8,
cst2.TaskQueueDefault: 1,
cst2.TaskQueueXuexitong: 5,
},
})
mux := asynq.NewServeMux()
// 辅助服务器:专门处理邮件等轻量级任务,独立的 worker 池
auxiliaryServer = asynq.NewServerFromRedisClient(redis.GetClient(), asynq.Config{
Concurrency: 5, // 邮件任务不需要太多并发
BaseContext: gctx.GetInitCtx,
TaskCheckInterval: 10 * time.Second,
RetryDelayFunc: func(n int, _ error, _ *asynq.Task) time.Duration {
return time.Duration(n) * 2 * time.Second
},
DelayedTaskCheckInterval: 2 * time.Second,
Queues: map[string]int{
cst2.TaskQueueEmail: 2,
cst2.TaskQueueDefault: 1,
},
})
mux.Handle(cst2.TaskCpdaily, &CpdailyTaskHandler{})
mux.Handle(cst2.TaskEmail, &EmailTaskHandler{})
mux.Handle(cst2.TaskResetDaily, &ResetTaskHandler{})
mux.Handle(cst2.TaskXuexitong, &XuexitongTaskHandler{})
mux.Handle(cst2.TaskXuexitongVio, &XuexitongTaskHandler{})
// 主服务器的任务处理器
mainMux := asynq.NewServeMux()
mainMux.Handle(cst2.TaskCpdaily, &CpdailyTaskHandler{})
mainMux.Handle(cst2.TaskXuexitong, &XuexitongTaskHandler{})
mainMux.Handle(cst2.TaskXuexitongVio, &XuexitongTaskHandler{})
e := globalServer.Start(mux)
// 辅助服务器的任务处理器
auxMux := asynq.NewServeMux()
auxMux.Handle(cst2.TaskEmail, &EmailTaskHandler{})
auxMux.Handle(cst2.TaskResetDaily, &ResetTaskHandler{})
e := globalServer.Start(mainMux)
utils.OkOrPanic(e)
e = auxiliaryServer.Start(auxMux)
utils.OkOrPanic(e)
}
@@ -97,14 +97,20 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
// 创建课程服务
courseService := xuexitong.NewXuexitongCourse(client)
// 执行刷课
err = courseService.AutoStudyByCourseID(ctx, targetCourse.CourseID)
if err != nil {
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "刷课失败", err))
}
// 非暴力模式下,设置时间检查回调,每个任务点完成后都会检查时间
if targetCourse.Violence == courseentity.ViolenceNo {
courseService.SetTimeCheckFunc(func() bool {
currentTime := time.Now()
currentHour := currentTime.Hour()
currentMinute := currentTime.Minute()
// 刷课完成后,统计任务情况
g.Log().Infof(ctx, "刷课完成,正在统计任务情况...")
startTime := 8*60 + 30 // 8:30
endTime := 17*60 + 30 // 17:30
currentMinutes := currentHour*60 + currentMinute
return currentMinutes >= startTime && currentMinutes < endTime
})
}
// 先获取课程列表找到目标课程
courseList := courseService.PullCourseList(ctx)
@@ -116,23 +122,81 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
}
}
totalTasks := 0
if course != nil {
// 获取章节列表
chapters := courseService.PullCourseChapterList(ctx, *course)
if course == nil {
utils.OkOrPanic(errors.NewBusinessError(404, "未找到课程"))
}
// 统计总任务数(累加所有章节的JobCount)
// 开始刷课前,先获取并更新任务点统计
g.Log().Infof(ctx, "课程 %s 正在获取任务点统计...", targetCourse.CourseName)
chapters := courseService.PullCourseChapterList(ctx, *course)
// FetchChapterPointStatus 会更新 chapters 中每个章节的 PointTotal 和 PointFinish
_, statusErr := courseService.FetchChapterPointStatus(ctx, *course, chapters)
if statusErr != nil {
g.Log().Warningf(ctx, "获取章节任务点状态失败: %v", statusErr)
} else {
// 通过累加每个章节的 PointTotal 和 PointFinish 计算总数
var totalPointCount, finishedPointCount int
for _, chapter := range chapters {
totalTasks += chapter.JobCount
totalPointCount += chapter.PointTotal
finishedPointCount += chapter.PointFinish
}
g.Log().Infof(ctx, "课程 %s 任务点统计: 总数 %d, 已完成 %d", targetCourse.CourseName, totalPointCount, finishedPointCount)
// 更新数据库
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).
SetTaskCount(totalPointCount).
SetSuccessCount(finishedPointCount).
ExecX(ctx)
}
// 设置任务点完成回调,每完成一个任务点更新 success_count
courseService.SetTaskCompleteCallback(func() {
// 增加 success_count
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).
AddSuccessCount(1).
ExecX(ctx)
})
// 执行刷课
err = courseService.AutoStudyByCourseID(ctx, targetCourse.CourseID)
if err != nil {
// 如果是时间限制导致的停止,不算失败
if err == xuexitong.ErrTimeRestriction {
g.Log().Infof(ctx, "课程ID %d 因时间限制暂停刷课,等待下次执行", targetCourse.ID)
// 更新状态为等待中,等待下次定时任务
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).SetStatus(courseentity.StatusWaiting).ExecX(ctx)
return nil
}
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "刷课失败", err))
}
// 刷课完成后,重新统计任务情况
g.Log().Infof(ctx, "刷课完成,正在重新统计任务情况...")
// 重新获取章节列表和任务点状态
chaptersAfter := courseService.PullCourseChapterList(ctx, *course)
// FetchChapterPointStatus 会更新 chapters 中每个章节的 PointTotal 和 PointFinish
_, statusErrAfter := courseService.FetchChapterPointStatus(ctx, *course, chaptersAfter)
var totalPointCountAfter, finishedPointCountAfter int
if statusErrAfter != nil {
g.Log().Warningf(ctx, "获取章节任务点状态失败: %v", statusErrAfter)
} else {
// 通过累加每个章节的 PointTotal 和 PointFinish 计算总数
for _, chapter := range chaptersAfter {
totalPointCountAfter += chapter.PointTotal
finishedPointCountAfter += chapter.PointFinish
}
}
g.Log().Infof(ctx, "课程 %s 统计完成: 总任务点 %d", targetCourse.CourseName, totalTasks)
g.Log().Infof(ctx, "课程 %s 统计完成: 总任务点 %d, 已完成 %d", targetCourse.CourseName, totalPointCountAfter, finishedPointCountAfter)
// 更新数据库(暂时不统计完成数,只统计总数)
// 更新数据库
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).
SetStatus(courseentity.StatusDone).
SetTaskCount(totalTasks).
SetTaskCount(totalPointCountAfter).
SetSuccessCount(finishedPointCountAfter).
ExecX(ctx)
g.Log().Infof(ctx, "学习通刷课完成 - 课程: %s", targetCourse.CourseName)
+1 -1
View File
@@ -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 {
+3 -108
View File
@@ -13,9 +13,7 @@ import (
"likedream-goframe/internal/ent/sysuser"
"likedream-goframe/internal/framework/logger"
"likedream-goframe/internal/utils"
"strconv"
"github.com/bytedance/sonic"
"go.uber.org/zap"
"resty.dev/v3"
)
@@ -91,109 +89,8 @@ func (s CourseServiceV1) AddCourse(ctx context.Context, req *v1.AddCourseReq) *u
if targetCourse == nil {
utils.OkOrPanic(errors2.NewBusinessError(400, "未找到该课程"))
}
chapters := courseService.PullCourseChapterList(ctx, *targetCourse)
// 定义节点信息结构
type NodeInfo struct {
ChapterID string `json:"chapter_id"`
ChapterName string `json:"chapter_name"`
CardID string `json:"card_id"`
CardTitle string `json:"card_title"`
CardType string `json:"card_type"`
}
var allNodes []NodeInfo
var completedNodes []string
// 递归获取所有章节的任务点
var collectNodes func(chapters []entity.XueXiTCourseChapter)
collectNodes = func(chapters []entity.XueXiTCourseChapter) {
for _, chapter := range chapters {
// 如果有任务点,获取卡片信息
if chapter.JobCount > 0 {
cards, points, err := courseService.FetchChapterCards(ctx, *targetCourse, chapter.ID)
if err != nil {
logger.Logger.Warn("获取章节任务点失败", zap.String("chapter", chapter.Name), zap.Error(err))
} else if points != nil {
chapterIDStr := strconv.Itoa(chapter.ID)
// 统计视频任务点
for _, video := range points.Videos {
nodeInfo := NodeInfo{
ChapterID: chapterIDStr,
ChapterName: chapter.Name,
CardID: video.JobID,
CardTitle: video.Title,
CardType: "video",
}
allNodes = append(allNodes, nodeInfo)
if video.IsPassed {
completedNodes = append(completedNodes, chapterIDStr+":"+video.JobID)
}
}
// 统计文档任务点
for _, doc := range points.Documents {
nodeInfo := NodeInfo{
ChapterID: chapterIDStr,
ChapterName: chapter.Name,
CardID: doc.JobID,
CardTitle: doc.Title,
CardType: "document",
}
allNodes = append(allNodes, nodeInfo)
}
// 统计作业任务点
for _, work := range points.Works {
nodeInfo := NodeInfo{
ChapterID: chapterIDStr,
ChapterName: chapter.Name,
CardID: work.JobID,
CardTitle: work.Title,
CardType: "work",
}
allNodes = append(allNodes, nodeInfo)
if work.IsPassed {
completedNodes = append(completedNodes, chapterIDStr+":"+work.JobID)
}
}
// 统计外链任务点
for _, link := range points.Hyperlinks {
nodeInfo := NodeInfo{
ChapterID: chapterIDStr,
ChapterName: chapter.Name,
CardID: link.JobID,
CardTitle: link.Title,
CardType: "hyperlink",
}
allNodes = append(allNodes, nodeInfo)
}
// 统计直播任务点
for _, live := range points.Lives {
nodeInfo := NodeInfo{
ChapterID: chapterIDStr,
ChapterName: chapter.Name,
CardID: live.JobID,
CardTitle: live.Title,
CardType: "live",
}
allNodes = append(allNodes, nodeInfo)
}
_ = cards // cards 暂时不使用
}
}
// 递归处理子章节
if len(chapter.Knowledge) > 0 {
collectNodes(chapter.Knowledge)
}
}
}
collectNodes(chapters)
// 将节点信息序列化为JSON
nodesJSON, _ := sonic.MarshalString(allNodes)
completedNodesJSON, _ := sonic.MarshalString(completedNodes)
// 添加课程时不获取任务点统计,由定时任务处理
db2.Global.CourseEntity.Create().
SetUserID(user.ID).
SetUsername(req.Username).
@@ -205,10 +102,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("添加成功")
+16 -13
View File
@@ -17,6 +17,7 @@ import (
"likedream-goframe/internal/service/balance"
"likedream-goframe/internal/utils"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/frame/g"
"github.com/shopspring/decimal"
)
@@ -45,7 +46,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,15 +111,6 @@ func (v CpLeaveServiceV1) Report(ctx context.Context, req *v1.CpLeaveReportReq)
utils.OkOrPanic(errors2.NewBusinessError(400, "非法的今日校园账号"))
}
// 检查是否已经存在该销假记录
existLeave, _ := db2.Global.CpLeave.Query().
Where(cpleave.LeaveIDEQ(req.LeaveId), cpleave.CpUserIDEQ(req.CpUserId)).
First(ctx)
if existLeave != nil {
utils.OkOrPanic(errors2.NewBusinessError(400, fmt.Sprintf("该请假记录已处理,状态:%s", existLeave.Status)))
}
// 计算扣款金额
basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst2.ConfigBasePointCpdailyLeave).String())
utils.OkOrPanic(err)
@@ -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()
@@ -178,8 +170,19 @@ func (v CpLeaveServiceV1) Report(ctx context.Context, req *v1.CpLeaveReportReq)
// 填充表单
form := reportApp.FillReportForm(detail)
// 获取位置信息
lon, lat := reportApp.GetLocation(detail)
// 获取位置信息,优先使用前端传入的
var lon, lat string
if req.Longitude != "" && req.Latitude != "" {
lon = req.Longitude
lat = req.Latitude
} else {
lon, lat = reportApp.GetLocation(detail)
}
// 如果前端传入了地址,使用前端的地址
if req.Address != "" {
form["reportLocation"] = gvar.New(req.Address)
}
// 提交销假
reportApp.SubmitReport(ctx, form, lon, lat, cpUser.Username, cpUser.MobileCode)
+3 -3
View File
@@ -43,7 +43,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 +118,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()
@@ -265,7 +265,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),
)
+50 -2
View File
@@ -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"
@@ -12,12 +13,15 @@ import (
"likedream-goframe/internal/ent/cpuser"
"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/gogf/gf/v2/frame/g"
"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,53 @@ 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())
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())
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] {
+2 -2
View File
@@ -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
@@ -4,10 +4,11 @@ import (
"context"
"crypto/rand"
"encoding/base64"
"likedream-goframe/internal/api/v1"
v1 "likedream-goframe/internal/api/v1"
cst2 "likedream-goframe/internal/common/cst"
"likedream-goframe/internal/common/db"
"likedream-goframe/internal/ent"
"likedream-goframe/internal/ent/sysconfig"
"likedream-goframe/internal/ent/sysrole"
"likedream-goframe/internal/ent/sysuser"
"likedream-goframe/internal/framework/redis"
@@ -47,6 +48,12 @@ func (s *UserLoginServiceV1) Login(ctx context.Context, req *v1.UserLoginReq) *u
}
func (s *UserLoginServiceV1) Register(ctx context.Context, req *v1.UserRegisterReq) *utils.Response[any] {
// 检查是否允许注册
allowRegister := s.IsRegistrationAllowed(ctx)
if !allowRegister {
return utils.FailParam("系统当前不允许注册")
}
//邀请码查询
var parent *ent.SysUser
if req.InviteCode != "" {
@@ -255,3 +262,18 @@ func (s *UserLoginServiceV1) GetResetPasswordInfo(ctx context.Context, req *v1.R
Email: user.Email,
})
}
// IsRegistrationAllowed 检查是否允许注册
func (s *UserLoginServiceV1) IsRegistrationAllowed(ctx context.Context) bool {
config, err := db.Global.SysConfig.Query().
Where(sysconfig.KeyEQ(cst2.ConfigSystemAllowRegister)).
Only(ctx)
// 如果配置不存在或查询出错,默认允许注册
if err != nil {
return true
}
// 检查配置值,"true" 或 "1" 表示允许注册
return config.Value == "true" || config.Value == "1"
}