Compare commits

...

11 Commits

Author SHA1 Message Date
sugarBecomer 60789ded9d 调整golang版本 2026-03-12 00:01:13 +08:00
sugarBecomer 7d8998d83f 🐛 修复淮南联合大学登录功能 2026-03-11 21:11:50 +08:00
sugarBecomer 8974bd26a5 🐛 修复淮南联合大学登录功能 2025-12-27 14:36:33 +08:00
sugarBecomer 6f25652183 🐛 修复淮南联合大学登录功能 2025-12-27 14:36:16 +08:00
sugarBecomer a8f4a2afa6 🐛 修复学习通刷课问题 2025-12-22 17:25:51 +08:00
sugarBecomer d14ddc212d 🐛 暂时还是单线程 没有解决好验证码问题 2025-12-19 23:20:13 +08:00
sugarBecomer 55fc94c37f 🐛 文档修复 2025-12-19 23:08:16 +08:00
sugarBecomer a590f58c6c 🐛 学习通刷课 2025-12-19 22:19:03 +08:00
sugarBecomer 0959ca117c 🐛 学习通刷课 2025-12-19 15:47:51 +08:00
sugarBecomer 3641fb1782 🐛 学习通答题 2025-12-12 11:49:57 +08:00
sugarBecomer d0e7652bf6 🐛 学习通答题 2025-12-12 11:37:33 +08:00
64 changed files with 2092 additions and 425 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:alpine AS backend-builder
FROM golang:1.24-alpine AS backend-builder
WORKDIR /build
COPY . .
+2 -2
View File
@@ -50,8 +50,8 @@ func Run() {
g.Log().Info(ctx, "server is going to shut down...")
api.StopServer()
g.Log().Info(ctx, "api server stopped")
//schedule.Stop()
//g.Log().Info(ctx, "schedule stopped")
schedule.Stop()
g.Log().Info(ctx, "schedule stopped")
redis.Shutdown()
g.Log().Info(ctx, "redis client closed")
os.Exit(0)
+5 -1
View File
@@ -1,13 +1,17 @@
[ocr]
url = "https://devocr.likedream.xyz"
[quesbank]
url = "http://tk.swk.tw/api/search.php"
key = "sk-acacac439b346036df45e90c85c177c375568ef72c469e49"
[database]
type = "mysql"
dsn = "test_likedream_go:EpXckXmxawDEQGxJ@tcp(database.out:3306)/test_likedream_go?parseTime=true&loc=Asia%2FShanghai"
dsn2 = "host=database.out user=dev password=JPmKjmpcaG3EYAJZ dbname=dev port=5432 sslmode=disable TimeZone=Asia/Shanghai"
# host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai
[server]
port = 8090
port = 8091
url = "https://devocr.likedream.xyz"
openapiPath= "/api.json"
swaggerPath= "/swagger"
+7 -1
View File
@@ -31,7 +31,7 @@ require (
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.43.0
golang.org/x/net v0.45.0
golang.org/x/net v0.46.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
resty.dev/v3 v3.0.0-beta.3
@@ -87,15 +87,21 @@ require (
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/samber/lo v1.51.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tiendc/go-deepcopy v1.7.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/excelize/v2 v2.10.0 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zclconf/go-cty v1.14.4 // indirect
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
+15
View File
@@ -163,6 +163,11 @@ github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -203,6 +208,8 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
@@ -215,6 +222,12 @@ github.com/wenlng/go-captcha/v2 v2.0.4 h1:5cSUF36ZyA03qeDMjKmeXGpbYJMXEexZIYK3Vg
github.com/wenlng/go-captcha/v2 v2.0.4/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8=
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4=
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
@@ -270,6 +283,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+6 -7
View File
@@ -1,16 +1,15 @@
package v1
import (
"likedream-goframe/internal/ent/cptask"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
type UploadImageReq struct {
g.Meta `path:"/image/upload" method:"post" tags:"Image" summary:"上传图片"`
File *ghttp.UploadFile `json:"file" v:"required#请上传图片"`
}
type PreviewImageReq struct {
g.Meta `path:"/image/preview" method:"get" tags:"Image" summary:"预览图片"`
Key string `json:"key" v:"required#请输入图片key"`
g.Meta `path:"/image/upload" method:"post" tags:"Image" summary:"上传图片"`
File *ghttp.UploadFile `json:"file" v:"required#请上传图片"`
CpUserId int64 `json:"cpUserId" v:"required#请指定今日校园用户"`
TaskType cptask.TaskType `json:"taskType" v:"required#请指定任务类型"`
}
+1 -2
View File
@@ -1,6 +1,5 @@
package v1
type UploadImageRes struct {
FileKey string `json:"key"`
FileSize *int64 `json:"size"`
PreviewUrl string `json:"previewUrl"`
}
+1
View File
@@ -51,6 +51,7 @@ type PostCpTaskExecuteReq struct {
NotificationType cptask.NotificationType `json:"notification_type"`
ExecuteHour int `json:"execute_hour"`
ExecuteMinute int `json:"execute_minute"`
ExecuteDays []int `json:"execute_days"`
}
type DeleteTaskReq struct {
+4 -3
View File
@@ -85,7 +85,8 @@ type CpUserAuthorizeConfirmReq struct {
DataKey string `json:"dataKey"`
}
type CpUserAuthorizeRandomReq struct {
g.Meta `path:"/user/authorizeRandom" method:"post" tags:"CpUser" summary:"随机设备码"`
Id int64 `json:"id"`
type CpUserCustomDeviceReq struct {
g.Meta `path:"/user/customDevice" method:"post" tags:"CpUser" summary:"自定义设备码"`
Id int64 `json:"id" v:"required#请指定用户"`
MobileCode string `json:"mobileCode" v:"required#请输入设备码"`
}
@@ -51,6 +51,24 @@ func ApiAutoOCRVerification(bytes []byte) string {
result := post.String()
return gjson.Get(result, "data").String()
}
func ApiAutoCalcVerification(b64 string) string {
ctx := gctx.GetInitCtx()
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/calc"
client := httpclient.Cpdaily.Clone(ctx)
client.SetDoNotParseResponse(false)
post, err := client.R().SetBody(map[string]any{
"base64": b64,
}).Post(baseUrl)
utils.OkOrPanic(err, func(err error) error {
return oops.
With("AutoOCRVerification", baseUrl).
WithContext(ctx).
Wrap(err)
})
result := post.String()
return gjson.Get(result, "data").String()
}
func Bytes2Base64(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
@@ -22,6 +22,8 @@ func GetCpdailyLoginBySchoolInfo(ctx context.Context, info *school.SchoolDetailI
// 河北石油职业技术大学
case "01e270d1-00e2-4ce9-b0af-5d5fbd462b8a":
return NewIap(ctx)
case "676c9395-a29c-4068-97a8-b48e05dae499":
return NewHnuu(ctx)
default:
if info.IsIap() {
return NewIap(ctx)
+65 -3
View File
@@ -4,13 +4,16 @@ import (
"context"
"fmt"
"likedream-goframe/internal/application/cookiejar"
"likedream-goframe/internal/application/cpdaily/school"
"likedream-goframe/internal/application/httpclient"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/ent/cpuser"
"likedream-goframe/internal/framework/logger"
"likedream-goframe/internal/utils"
"strings"
"github.com/samber/oops"
"go.uber.org/zap"
"resty.dev/v3"
)
@@ -97,12 +100,71 @@ func CookieEnabled(ctx context.Context, host string, client *resty.Client, login
utils.OkOrPanic(err)
needLogin := sessionResponse.StatusCode() == 302
if needLogin && loginType == cpuser.LoginTypePhone {
utils.OkOrPanic(errors.NewBusinessError(500, "登录信息失效,请重新通过短信验证码登录"))
}
return !needLogin
}
// RefreshCookieParams 刷新 cookie 所需的用户参数
type RefreshCookieParams struct {
ID int64
Cookie string
Tgc string
LoginType cpuser.LoginType
Username string
Password string
SchoolID string
}
// RefreshCookieResult 刷新 cookie 的结果
type RefreshCookieResult struct {
Client *resty.Client
Cookie string
Tgc string
}
// EnsureCookieValid 检测 cookie 是否过期,过期则自动刷新。
// phone 类型用 TGC 刷新,account 类型用账号密码重新登录。
func EnsureCookieValid(ctx context.Context, params *RefreshCookieParams, schoolDetail *school.SchoolDetailInfo) *RefreshCookieResult {
client := GetClient(ctx, params.Cookie)
if CookieEnabled(ctx, schoolDetail.WecloudUrl, client, params.LoginType) {
logger.Logger.Info("[TGC] cookie有效,无需刷新",
zap.Int64("userId", params.ID),
zap.String("loginType", string(params.LoginType)),
)
return &RefreshCookieResult{Client: client, Cookie: params.Cookie, Tgc: params.Tgc}
}
logger.Logger.Info("[TGC] cookie已失效,开始刷新",
zap.Int64("userId", params.ID),
zap.String("loginType", string(params.LoginType)),
zap.Bool("tgcEmpty", params.Tgc == ""),
)
var newCookie, newTgc string
if params.LoginType == cpuser.LoginTypePhone {
if params.Tgc == "" {
utils.OkOrPanic(errors.NewBusinessError(500, "TGC为空,请重新通过短信验证码登录"))
}
smsHandler := NewSmsWithoutDeviceID()
newCookie, newTgc = smsHandler.RefreshByTgc(ctx, params.Tgc, params.SchoolID)
logger.Logger.Info("[TGC] RefreshByTgc完成",
zap.Int64("userId", params.ID),
zap.Bool("cookieChanged", newCookie != params.Cookie),
)
} else {
loginExecutor := GetCpdailyLoginBySchoolInfo(ctx, schoolDetail)
loginExecutor.Login(ctx, schoolDetail, params.Username, params.Password)
newCookie = loginExecutor.GetCookieString()
newTgc = params.Tgc
}
return &RefreshCookieResult{
Client: GetClient(ctx, newCookie),
Cookie: newCookie,
Tgc: newTgc,
}
}
func GetClient(ctx context.Context, cookie string) *resty.Client {
cookieJar := cookiejar.NewPersistCookieJar()
cookieJar.Deserialize(cookie)
+180
View File
@@ -0,0 +1,180 @@
package login
import (
"bytes"
"context"
"fmt"
"io"
"likedream-goframe/internal/application/cookiejar"
"likedream-goframe/internal/application/cpdaily/cryptoutil"
"likedream-goframe/internal/application/cpdaily/school"
"likedream-goframe/internal/application/httpclient"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/utils"
"net/http"
"net/url"
"github.com/PuerkitoBio/goquery"
"github.com/samber/oops"
"resty.dev/v3"
)
var RSA_KEY_MAP = map[string]string{
"public_exponent": "010001",
"private_exponent": "413798867d69babed22e0dd3d4031c635f3e9dbca0fa50a32974a0e230787b7f7ba78caefbee828a051c690357a8cc31dba8efc738b4db22e887571ef1ec5a5a55b6d866f6a67527f6a7d78a127c9f687008bb540228b50aa2d1ca5a4ff71107234f936b611ac46432a26da9c302eaa7180820df70593353b3f8c0247fe97a45",
"modulus": "00b5eeb166e069920e80bebd1fea4829d3d1f3216f2aabe79b6c47a3c18dcee5fd22c2e7ac519cab59198ece036dcf289ea8201e2a0b9ded307f8fb704136eaeb670286f5ad44e691005ba9ea5af04ada5367cd724b5a26fdb5120cc95b6431604bd219c6b7d83a6f8f24b43918ea988a76f93c333aa5a20991493d4eb1117e7b1",
}
// 淮南联合大学登录实现 RSA加密 & 验证码必出(加法验证码)
type CpdailyHnuuLogin struct {
client *resty.Client
}
func (c CpdailyHnuuLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
jar := cookiejar.NewPersistCookieJar()
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
// 获取合法的登录URL
loginUrl := getCasValidLoginUrl(schoolDetail)
// 请求登录页面并获取真正的登录URL
loginPageResponse, err := c.client.R().
WithContext(ctx).
Get(loginUrl)
utils.OkOrPanic(err)
defer loginPageResponse.Body.Close()
loginPageBytes, err := io.ReadAll(loginPageResponse.Body)
utils.OkOrPanic(err)
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
// 解析登录页面
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
utils.OkOrPanic(err)
// 判断登录类型
var (
allInputs = map[string]string{}
formNode = loginPage.Find(".ant-form").First()
)
inputs := formNode.Find("input")
for _, input := range inputs.EachIter() {
name, exists := input.Attr("name")
if !exists {
continue
}
value, _ := input.Attr("value")
allInputs[name] = value
}
allInputs["username"] = username
pair := cryptoutil.NewNutRSAKeyPair(RSA_KEY_MAP["public_exponent"], "", RSA_KEY_MAP["modulus"])
// 处理密码
allInputs["password"] = pair.EncryptedString(password)
// 解决验证码
// 解决常规图片验证码
captchaUrl := getHnuuCaptchaUrl(loginResponseUrl)
code, uid := ResolveCalcCaptcha(ctx, captchaUrl, c.client)
allInputs["code"] = code
allInputs["id"] = uid
jar = cookiejar.NewPersistCookieJar()
c.client.SetCookieJar(jar)
// 登录提交
loginPostResponse, err := c.client.R().
SetFormData(allInputs).
WithContext(ctx).
Post(getHnuuLoginPostUrl(loginResponseUrl))
utils.OkOrPanic(err, func(err error) error {
return oops.
WithContext(ctx).
With("loginResponseUrl", loginResponseUrl).
With("allInputs", allInputs).
Wrap(err)
})
defer loginPostResponse.Body.Close()
type LoginResponse struct {
Meta struct {
Success bool `json:"success"`
StatusCode int `json:"statusCode"`
Message string `json:"message"`
} `json:"meta"`
Data struct {
Code string `json:"code"`
} `json:"data"`
Tgt string `json:"tgt"`
Ticket string `json:"ticket"`
}
json := utils.ReadJson[LoginResponse](loginPostResponse.Body)
if json.Tgt != "" {
handleHnuuLoginSuccess(ctx, c.client, loginResponseUrl.Query().Get("service"), json.Ticket, json.Tgt, schoolDetail.WecloudUrl)
} else {
errMsg := ""
if json.Data.Code == "CODEFALSE" {
errMsg = "验证码错误"
} else if json.Data.Code == "FALSE" {
errMsg = "请检查账号密码是否正确"
} else if json.Data.Code != "PASSERROR" {
errMsg = "密码错误"
} else if json.Data.Code != "NOUSER" {
errMsg = "账号不存在"
} else if json.Data.Code != "USERLOCK" {
errMsg = "账号锁定"
} else {
errMsg = "未知错误" + json.Data.Code
}
utils.OkOrPanic(errors.NewBusinessError(500, errMsg))
}
}
func handleHnuuLoginSuccess(ctx context.Context, client *resty.Client, url, ticket string, tgt string, host string) {
redirectUrl := fmt.Sprintf("%s&ticket=%s", url, ticket)
client.R().SetHeader("Referer", "https://cas.hnuu.edu.cn/").Get(redirectUrl)
response, err2 := client.R().SetHeader("Referer", "https://cas.hnuu.edu.cn/").Get("https://hnuu.campusphere.net/iap/login")
utils.OkOrPanic(err2)
newUrl := response.RawResponse.Request.URL
ticketUrl := fmt.Sprintf("https://cas.hnuu.edu.cn/lyuapServer/v1/tickets/%s", tgt)
get, err := client.R().SetFormData(map[string]string{
"service": url,
"loginToken": "loginToken",
}).Post(ticketUrl)
utils.OkOrPanic(err)
defer get.Body.Close()
ticket = utils.ReadString(get.Body)
redirectUrl = fmt.Sprintf("%s&ticket=%s", newUrl.Query().Get("service"), ticket)
client.R().SetHeader("Referer", "https://cas.hnuu.edu.cn/").Get(redirectUrl)
ActivateSession(ctx, host, client)
}
func getHnuuLoginPostUrl(url *url.URL) string {
// https://cas.hnuu.edu.cn/lyuapServer/v1/tickets
return fmt.Sprintf("%s://%s/lyuapServer/v1/tickets", url.Scheme, url.Host)
}
func getHnuuCaptchaUrl(url *url.URL) string {
// https://cas.hnuu.edu.cn/lyuapServer/kaptcha?_t=1766803822&uid=
return fmt.Sprintf("%s://%s/lyuapServer/kaptcha?_t=%d&uid=", url.Scheme, url.Host, utils.GetCurrentTimeMillis())
}
func (c CpdailyHnuuLogin) GetCookieJar() http.CookieJar {
return c.client.CookieJar()
}
func (c CpdailyHnuuLogin) GetCookieString() string {
cookieJar := c.GetCookieJar()
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
return serializableJar.Serialize()
}
func (c CpdailyHnuuLogin) GetClient() *resty.Client {
return c.client
}
func NewHnuu(ctx context.Context) CpdailyLogin {
return &CpdailyHnuuLogin{
client: httpclient.NewCpdailyClient(),
}
}
+26
View File
@@ -15,6 +15,32 @@ func ResolveOcrCaptcha(ctx context.Context, url string, client *resty.Client) st
return captcha.ApiAutoOCRVerification(byteData)
}
func ResolveCalcCaptcha(ctx context.Context, url string, client *resty.Client) (string, string) {
b64, uid := DownBase64(ctx, url, client)
return captcha.ApiAutoCalcVerification(b64), uid
}
func DownBase64(ctx context.Context, url string, client *resty.Client) (string, string) {
res, err := client.R().
WithContext(ctx).
Get(url)
utils.OkOrPanic(err, func(err error) error {
return oops.
With("captchaUrl", url).
WithContext(ctx).
Wrap(err)
})
defer res.Body.Close()
type CaptchResponse struct {
KaptchaType string `json:"kaptchaType"`
UID string `json:"uid"`
Content string `json:"content"`
Timeout int `json:"timeout"`
}
json := utils.ReadJson[CaptchResponse](res.Body)
return json.Content, json.UID
}
func DownCode2Byte(ctx context.Context, url string, client *resty.Client) []byte {
res, err := client.R().
WithContext(ctx).
+30
View File
@@ -11,12 +11,14 @@ import (
"likedream-goframe/internal/application/httpclient"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/framework/logger"
"likedream-goframe/internal/utils"
"net/http"
"net/url"
"github.com/bytedance/sonic"
"github.com/samber/oops"
"go.uber.org/zap"
"resty.dev/v3"
)
@@ -199,6 +201,7 @@ func (s *SmsLoginHandler) SetLoginCookie(ctx context.Context, jar http.CookieJar
jar.SetCookies(targetSchoolUrl, []*http.Cookie{cookie})
jar.SetCookies(targetApiUrl, []*http.Cookie{cookie})
}
s.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(50))
req := s.client.R().
SetHeader("AmpCookies", ampToken).
@@ -278,3 +281,30 @@ func (s *SmsLoginHandler) GetCookieString() string {
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
return serializableJar.Serialize()
}
// RefreshByTgc 使用保存的完整 cookie(包含 CASTGC 等长期凭证)重新去学校换取 CAS cookie。
// tgc 参数是 SetLoginCookie 之后序列化的完整 cookie jar,包含 CASTGC(有效期10年)、
// sessionToken 等认证信息,可以直接用来重新激活 session。
func (s *SmsLoginHandler) RefreshByTgc(ctx context.Context, tgc string, schoolId string) (cookie string, newTgc string) {
jar := cookiejar.NewPersistCookieJar()
jar.Deserialize(tgc)
s.client = httpclient.NewCpdailyClient()
s.client.SetCookieJar(jar)
detail := school.GetSchoolDetail(ctx, schoolId)
logger.Logger.Info("[TGC] RefreshByTgc 开始用CASTGC重新激活session",
zap.String("schoolId", schoolId),
zap.String("wecloudUrl", detail.WecloudUrl),
zap.Int("tgcLen", len(tgc)),
)
// 用保存的 cookie(含 CASTGC)重新激活 session
ActivateSession(ctx, detail.WecloudUrl, s.client)
logger.Logger.Info("[TGC] RefreshByTgc ActivateSession完成")
newTgc = tgc // TGC 保持不变(CASTGC 有效期10年)
cookie = s.GetCookieString()
return
}
@@ -5,10 +5,8 @@ import (
"fmt"
"likedream-goframe/internal/application/cpdaily"
"likedream-goframe/internal/application/cpdaily/cryptoutil"
"likedream-goframe/internal/application/cpdaily/imageuploader"
"likedream-goframe/internal/application/cpdaily/task/formtype"
"likedream-goframe/internal/application/cpdaily/taskhelper"
"likedream-goframe/internal/application/cpdaily/types"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/utils"
"maps"
@@ -454,31 +452,26 @@ func (c *CpdailyCollectionTask) FillCustomForm(ctx context.Context, form map[str
newRowObj["value"] = gvar.New(strings.Join(values, ","))
}
} else if c.isPicType(fieldType) {
// 图片类型
// 图片类型 - content 中存储的是今日校园的 previewUrl,逗号分隔
if param.Content == "" {
utils.OkOrPanic(errors.NewBusinessError(400, "查寝需要图片,但是当前图片为空"), oops.WithContext(ctx).Wrap)
}
imageUuidSeq := strings.Split(param.Content, ",")
var imageUuids []string
for _, imageUuid := range imageUuidSeq {
imageUuid = strings.TrimSpace(imageUuid)
if imageUuid == "" {
continue
previewUrls := strings.Split(param.Content, ",")
var validUrls []string
for _, url := range previewUrls {
url = strings.TrimSpace(url)
if url != "" {
validUrls = append(validUrls, url)
}
imageUuids = append(imageUuids, imageUuid)
}
if len(imageUuids) == 0 {
if len(validUrls) == 0 {
utils.OkOrPanic(errors.NewBusinessError(400, "查寝照片未上传"), oops.WithContext(ctx).Wrap)
}
// 随机选择一张照片
imageUuid := imageUuids[rand.IntN(len(imageUuids))]
imageId, e := uuid.Parse(imageUuid)
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeCollection, c.client)
previewUrl := validUrls[rand.IntN(len(validUrls))]
newRowObj["value"] = gvar.New(previewUrl)
} else {
utils.OkOrPanic(errors.NewBusinessError(500, "出现未知类型表单,请反馈表单类型:"+fieldType), oops.WithContext(ctx).Wrap)
+153 -28
View File
@@ -5,14 +5,15 @@ import (
"fmt"
"likedream-goframe/internal/application/cpdaily"
"likedream-goframe/internal/application/cpdaily/cryptoutil"
"likedream-goframe/internal/application/cpdaily/imageuploader"
"likedream-goframe/internal/application/cpdaily/submitcaptcha"
"likedream-goframe/internal/application/cpdaily/task/formtype"
"likedream-goframe/internal/application/cpdaily/taskhelper"
"likedream-goframe/internal/application/cpdaily/types"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/utils"
"slices"
"strconv"
"strings"
"time"
@@ -20,6 +21,7 @@ import (
"github.com/gogf/gf/v2/container/gvar"
"github.com/google/uuid"
"github.com/samber/oops"
"github.com/tidwall/gjson"
"github.com/tidwall/match"
"resty.dev/v3"
)
@@ -221,6 +223,82 @@ func (c *CpdailyAutoSignTask) QueryFormByTitle(ctx context.Context, titleGlob st
result["realTitle"] = gvar.New(realTitle)
result["taskInfo"] = gvar.New(matchTask)
result["rows"] = gvar.New(formDatas)
// 尝试从已签到的历史任务中获取历史数据用于预填充
signedMatch := findMatchTitle(signedTasks)
if signedMatch != nil && (signedMatch["signInstanceWid"] == nil || matchTask["signInstanceWid"] == nil || signedMatch["signInstanceWid"].String() != matchTask["signInstanceWid"].String()) {
histResp, histErr := c.client.R().
SetBody(map[string]*gvar.Var{
"taskName": gvar.New(signedMatch["taskName"].String()),
"signWid": signedMatch["signWid"],
"signInstanceWid": signedMatch["signInstanceWid"],
}).
WithContext(ctx).
Post(c.getReqUrl("detailSignInstance"))
if histErr == nil {
histJSON := utils.ReadJson[map[string]*gvar.Var](histResp.Body)
histDatas := histJSON["datas"].MapStrVar()
if len(histDatas) > 0 {
result["historyRows"] = gvar.New(histDatas)
}
}
}
// 今天没有已签到的同名任务,从历史月份中只查 signedTasks
if result["historyRows"] == nil {
func() {
defer func() { recover() }()
now := time.Now().In(cst.TimeZoneShangHai)
year := strconv.Itoa(now.Year())
month := strconv.Itoa(int(now.Month()))
if len(month) == 1 {
month = "0" + month
}
resp, e := c.client.R().
SetBody(map[string]any{
"statisticYearMonth": fmt.Sprintf("%s-%s", year, month),
}).
WithContext(ctx).
Post(c.getReqUrl("getStuSignInfosByWeekMonth"))
if e != nil {
return
}
respJSON := utils.ReadJson[map[string]*gvar.Var](resp.Body)
monthDatas := respJSON["datas"].MapStrVar()
monthRows := monthDatas["rows"].Vars()
for _, row := range monthRows {
rowMap := row.MapStrVar()
signed := rowMap["signedTasks"].Vars()
for _, st := range signed {
item := st.MapStrVar()
tn, exist := item["taskName"]
if !exist || tn.String() == "" {
continue
}
if match.Match(tn.String(), titleGlob) {
dr, de := c.client.R().
SetBody(map[string]*gvar.Var{
"signInstanceWid": item["signInstanceWid"],
"signWid": item["signWid"],
"taskName": gvar.New(tn.String()),
}).
WithContext(ctx).
Post(c.getReqUrl("detailSignInstance"))
if de != nil {
continue
}
dj := utils.ReadJson[map[string]*gvar.Var](dr.Body)
dd := dj["datas"].MapStrVar()
if len(dd) > 0 {
result["historyRows"] = gvar.New(dd)
return
}
}
}
}
}()
}
return result
}
@@ -356,30 +434,75 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
utils.OkOrPanic(errors.NewBusinessError(503, "任务表单为空"), oops.WithContext(ctx).Wrap)
}
// 优先从 historyRows(已签到的历史任务详情)中提取历史值,否则从 rows 中提取
histRows := rows
if hr := rawJson["historyRows"]; hr != nil {
if hrMap := hr.MapStrVar(); len(hrMap) > 0 {
histRows = hrMap
}
}
var list []*formtype.CpTaskFormItem
// 经纬度
//dto := &formtype.CpTaskFormItem{
// Title: "经度",
// ColName: "lon",
// Type: formtype.ItemTypeInput,
// Required: true,
//}
//list = append(list, dto)
//
//dto = &formtype.CpTaskFormItem{
// Title: "纬度",
// ColName: "lat",
// Type: formtype.ItemTypeInput,
// Required: true,
//}
//list = append(list, dto)
// 从任务详情中提取历史值用于预填充
histLatAndLon := ""
histAddress := ""
histSignPhotoUrl := ""
histIsMalposition := ""
// 优先从 signPlaceSelected 提取经纬度和地址
if sps := histRows["signPlaceSelected"]; sps != nil && sps.String() != "" && sps.String() != "[]" {
placeArr := gjson.Parse(sps.String()).Array()
if len(placeArr) > 0 {
place := placeArr[0]
lng := place.Get("longitude").String()
lat := place.Get("latitude").String()
addr := place.Get("address").String()
if lng != "" && lat != "" {
histLatAndLon = lng + "," + lat
}
if addr != "" {
histAddress = addr
}
}
}
if histLatAndLon == "" {
if lon := histRows["longitude"]; lon != nil && lon.String() != "" {
if lat := histRows["latitude"]; lat != nil && lat.String() != "" {
histLatAndLon = lon.String() + "," + lat.String()
}
}
}
if histAddress == "" {
if pos := histRows["position"]; pos != nil && pos.String() != "" {
histAddress = pos.String()
}
if histAddress == "" {
if addr := histRows["signAddress"]; addr != nil && addr.String() != "" {
histAddress = addr.String()
}
}
}
if photo := histRows["signPhotoUrl"]; photo != nil && photo.String() != "" {
histSignPhotoUrl = photo.String()
}
if mal := histRows["isMalposition"]; mal != nil {
if mal.Int() == 1 {
histIsMalposition = "是"
} else {
histIsMalposition = "否"
}
}
dto := &formtype.CpTaskFormItem{
Title: "经纬度",
ColName: "latAndLon",
Type: formtype.ItemTypeLatAndLon,
Required: true,
Tip: "您可以通过地址搜索签到经纬度,如果觉得不够准确可以自行修改",
Title: "经纬度",
ColName: "latAndLon",
Type: formtype.ItemTypeLatAndLon,
Required: true,
Content: histLatAndLon,
Default: histLatAndLon,
OtherValue: histAddress,
Tip: "您可以通过地址搜索签到经纬度,如果觉得不够准确可以自行修改",
}
list = append(list, dto)
@@ -388,6 +511,8 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
ColName: "address",
Type: formtype.ItemTypeInput,
Required: true,
Content: histAddress,
Default: histAddress,
Tip: "这个就是指签到完毕后查看时最底部那个最详细的地址信息哦",
}
list = append(list, dto)
@@ -398,6 +523,8 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
ColName: "signImg",
Type: formtype.ItemTypeImage,
Required: true,
Content: histSignPhotoUrl,
Default: histSignPhotoUrl,
Tip: "您可以上传多张图片,会顺序轮换更新的哦~",
}
list = append(list, dto)
@@ -409,6 +536,8 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
ColName: "isMalposition",
Type: formtype.ItemTypeSingle,
Required: true,
Content: histIsMalposition,
Default: histIsMalposition,
Options: []formtype.CpTaskFormItemOption{
{
Content: "是",
@@ -527,12 +656,8 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
utils.OkOrPanic(errors.NewBusinessError(400, "签到照片未上传"), oops.WithContext(ctx).Wrap)
}
imageId, e := uuid.Parse(imgId)
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSign, c.client)
resultForm["signPhotoUrl"] = gvar.New(previewUrl)
// imgId 现在直接就是今日校园的 previewUrl,无需再上传
resultForm["signPhotoUrl"] = gvar.New(imgId)
}
// 处理验证码
+161 -28
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"likedream-goframe/internal/application/cpdaily"
"likedream-goframe/internal/application/cpdaily/cryptoutil"
"likedream-goframe/internal/application/cpdaily/imageuploader"
"likedream-goframe/internal/application/cpdaily/submitcaptcha"
"likedream-goframe/internal/application/cpdaily/task/formtype"
"likedream-goframe/internal/application/cpdaily/taskhelper"
@@ -222,6 +221,7 @@ func (c *CpdailySleepCheckTask) QueryFormByTitle(ctx context.Context, titleGlob
signedTasks := datas["signedTasks"].Array()
var matchTask map[string]*gvar.Var
isFromSigned := false
matchTask = findMatchTitle(unSignedTasks)
if matchTask == nil {
@@ -229,6 +229,9 @@ func (c *CpdailySleepCheckTask) QueryFormByTitle(ctx context.Context, titleGlob
if matchTask == nil {
matchTask = findMatchTitle(signedTasks)
if matchTask != nil {
isFromSigned = true
}
if matchTask == nil {
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("没有查询到查寝任务: %s", titleGlob)), func(err error) error {
@@ -277,6 +280,87 @@ func (c *CpdailySleepCheckTask) QueryFormByTitle(ctx context.Context, titleGlob
result["realTitle"] = gvar.New(realTitle)
result["taskInfo"] = gvar.New(matchTask)
result["rows"] = gvar.New(datas)
// 如果当前任务不是已签到的,尝试从已签到的历史任务中获取历史数据用于预填充
if !isFromSigned {
// 先看今天有没有已签到的同名任务
signedMatch := findMatchTitle(signedTasks)
if signedMatch != nil {
histResp, histErr := c.client.R().
SetBody(map[string]*gvar.Var{
"taskName": gvar.New(signedMatch["taskName"].String()),
"signWid": signedMatch["signWid"],
"signInstanceWid": signedMatch["signInstanceWid"],
}).
WithContext(ctx).
Post(c.getReqUrl("detailSignInstance"))
if histErr == nil {
histJSON := utils.ReadJson[map[string]*gvar.Var](histResp.Body)
histDatas := histJSON["datas"].MapStrVar()
if len(histDatas) > 0 {
result["historyRows"] = gvar.New(histDatas)
}
}
}
// 今天没有已签到的同名任务,从历史月份中只查 signedTasks
if result["historyRows"] == nil {
func() {
defer func() { recover() }()
now := time.Now().In(cst.TimeZoneShangHai)
year := strconv.Itoa(now.Year())
month := strconv.Itoa(int(now.Month()))
if len(month) == 1 {
month = "0" + month
}
resp, e := c.client.R().
SetBody(map[string]any{
"statisticYearMonth": fmt.Sprintf("%s-%s", year, month),
}).
WithContext(ctx).
Post(c.getReqUrl("getStuSignInfosByWeekMonth"))
if e != nil {
return
}
respJSON := utils.ReadJson[map[string]*gvar.Var](resp.Body)
monthDatas := respJSON["datas"].MapStrVar()
monthRows := monthDatas["rows"].Vars()
// 只从 signedTasks 中查找匹配的已签到任务
for _, row := range monthRows {
rowMap := row.MapStrVar()
signed := rowMap["signedTasks"].Vars()
for _, st := range signed {
item := st.MapStrVar()
tn, exist := item["taskName"]
if !exist || tn.String() == "" {
continue
}
if match.Match(tn.String(), titleGlob) {
// 找到已签到的历史任务,获取详情
dr, de := c.client.R().
SetBody(map[string]*gvar.Var{
"signInstanceWid": item["signInstanceWid"],
"signWid": item["signWid"],
"taskName": gvar.New(tn.String()),
}).
WithContext(ctx).
Post(c.getReqUrl("detailSignInstance"))
if de != nil {
continue
}
dj := utils.ReadJson[map[string]*gvar.Var](dr.Body)
dd := dj["datas"].MapStrVar()
if len(dd) > 0 {
result["historyRows"] = gvar.New(dd)
return
}
}
}
}
}()
}
}
return result
}
@@ -432,30 +516,77 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
utils.OkOrPanic(errors.NewBusinessError(503, "任务表单为空"), oops.WithContext(ctx).Wrap)
}
// 优先从 historyRows(已签到的历史任务详情)中提取历史值,否则从 rows 中提取
histRows := rows
if hr := rawJson["historyRows"]; hr != nil {
if hrMap := hr.MapStrVar(); len(hrMap) > 0 {
histRows = hrMap
}
}
var list []*formtype.CpTaskFormItem
// 经纬度
//dto := &formtype.CpTaskFormItem{
// Title: "经度",
// ColName: "lon",
// Type: formtype.ItemTypeInput,
// Required: true,
//}
//list = append(list, dto)
//
//dto = &formtype.CpTaskFormItem{
// Title: "纬度",
// ColName: "lat",
// Type: formtype.ItemTypeInput,
// Required: true,
//}
//list = append(list, dto)
// 从任务详情中提取历史值用于预填充
// signPlaceSelected 包含签到地点信息(经纬度、地址)
histLatAndLon := ""
histAddress := ""
histSignPhotoUrl := ""
histIsMalposition := ""
// 优先从 signPlaceSelected 提取经纬度和地址
if sps := histRows["signPlaceSelected"]; sps != nil && sps.String() != "" && sps.String() != "[]" {
placeArr := gjson.Parse(sps.String()).Array()
if len(placeArr) > 0 {
place := placeArr[0]
lng := place.Get("longitude").String()
lat := place.Get("latitude").String()
addr := place.Get("address").String()
if lng != "" && lat != "" {
histLatAndLon = lng + "," + lat
}
if addr != "" {
histAddress = addr
}
}
}
// 降级:直接从 longitude/latitude/position 字段取
if histLatAndLon == "" {
if lon := histRows["longitude"]; lon != nil && lon.String() != "" {
if lat := histRows["latitude"]; lat != nil && lat.String() != "" {
histLatAndLon = lon.String() + "," + lat.String()
}
}
}
if histAddress == "" {
if pos := histRows["position"]; pos != nil && pos.String() != "" {
histAddress = pos.String()
}
if histAddress == "" {
if addr := histRows["signAddress"]; addr != nil && addr.String() != "" {
histAddress = addr.String()
}
}
}
if photo := histRows["signPhotoUrl"]; photo != nil && photo.String() != "" {
histSignPhotoUrl = photo.String()
}
if mal := histRows["isMalposition"]; mal != nil {
if mal.Int() == 1 {
histIsMalposition = "是"
} else {
histIsMalposition = "否"
}
}
dto := &formtype.CpTaskFormItem{
Title: "经纬度",
ColName: "latAndLon",
Type: formtype.ItemTypeLatAndLon,
Required: true,
Tip: "您可以通过地址搜索签到经纬度,如果觉得不够准确可以自行修改",
Title: "经纬度",
ColName: "latAndLon",
Type: formtype.ItemTypeLatAndLon,
Required: true,
Content: histLatAndLon,
Default: histLatAndLon,
OtherValue: histAddress,
Tip: "您可以通过地址搜索签到经纬度,如果觉得不够准确可以自行修改",
}
list = append(list, dto)
dto = &formtype.CpTaskFormItem{
@@ -463,6 +594,8 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
ColName: "address",
Type: formtype.ItemTypeInput,
Required: true,
Content: histAddress,
Default: histAddress,
Tip: "这个就是指签到完毕后查看时最底部那个最详细的地址信息哦",
}
list = append(list, dto)
@@ -473,6 +606,8 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
ColName: "signImg",
Type: formtype.ItemTypeImage,
Required: true,
Content: histSignPhotoUrl,
Default: histSignPhotoUrl,
Tip: "您可以上传多张图片,会顺序轮换更新的哦~",
}
list = append(list, dto)
@@ -485,6 +620,8 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
ColName: "isMalposition",
Type: formtype.ItemTypeSingle,
Required: true,
Content: histIsMalposition,
Default: histIsMalposition,
Options: []formtype.CpTaskFormItemOption{
{
Content: "是",
@@ -577,12 +714,8 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
utils.OkOrPanic(errors.NewBusinessError(503, "未找到查寝照片参数,请上传照片"), oops.WithContext(ctx).Wrap)
}
imageId, e := uuid.Parse(imgId)
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSleepCheck, c.client)
resultForm["signPhotoUrl"] = gvar.New(previewUrl)
// imgId 现在直接就是今日校园的 previewUrl,无需再上传
resultForm["signPhotoUrl"] = gvar.New(imgId)
}
// 判断是否存在二维码查寝
+11 -1
View File
@@ -1,8 +1,10 @@
package httpclient
import (
"resty.dev/v3"
"likedream-goframe/internal/utils"
"time"
"resty.dev/v3"
)
var Common = resty.New().
@@ -10,3 +12,11 @@ var Common = resty.New().
SetRetryCount(5).
SetTimeout(30*time.Second).
SetHeader("User-Agent", "likedream")
func init() {
if !utils.IsDebug() {
Common.SetDebug(false)
} else {
Common.SetProxy("http://127.0.0.1:9000")
}
}
+11
View File
@@ -0,0 +1,11 @@
package music
import (
"testing"
)
func Test_GetMusic(t *testing.T) {
ctx := t.Context()
result := MusicDownloader.Search(ctx, "天外来物")
t.Log(result)
}
+3 -1
View File
@@ -121,7 +121,9 @@ func (c *XuexitongCourse) AutoCompleteWorkWithAI(ctx context.Context, p *entity.
if err != nil {
return "", fmt.Errorf("获取作业题目失败: %w", err)
}
if workDto.IsPassed {
return "", errors.New("作业已完成")
}
if len(workDto.Questions) == 0 {
return "", errors.New("作业无题目")
}
@@ -11,6 +11,7 @@ const (
PointTypeWork PointType = "insertwork"
PointTypeHyperlink PointType = "insertlink"
PointTypeLive PointType = "insertlive"
PointTypeExam PointType = "insertexam" // 考试任务点
)
// PointVideoDto 视频任务点
@@ -85,6 +86,25 @@ type PointHyperlinkDto struct {
IsJob bool // 是否是任务点
}
// PointExamDto 考试任务点
type PointExamDto struct {
CardIndex int
CourseID string
ClassID string
KnowledgeID int
Cpi string
ExamID string // 考试ID
Title string // 考试标题
JobID string // 任务ID
Enc string // 加密参数
PaperId string // 试卷ID
ExamRelationId string // 考试关联ID
TotalScore float64 // 总分
Duration int // 考试时长(分钟)
IsPassed bool // 是否已完成
IsJob bool // 是否是任务点
}
// PointLiveDto 直播任务点
type PointLiveDto struct {
CardIndex int
@@ -187,6 +207,7 @@ const (
ChapterPointTypeWork ChapterPointType = 3 // 作业
ChapterPointTypeHyperlink ChapterPointType = 4 // 外链
ChapterPointTypeLive ChapterPointType = 5 // 直播
ChapterPointTypeExam ChapterPointType = 6 // 考试
)
// ChapterPoint 统一的章节任务点结构(与Card一一对应)
@@ -245,6 +266,13 @@ type ChapterPoint struct {
LiveSetEnc string
LiveSwDsEnc string
VideoCompletePercent float64
// 考试相关字段
ExamID string // 考试ID
PaperId string // 试卷ID
ExamRelationId string // 考试关联ID
TotalScore float64 // 总分
ExamDuration int // 考试时长(分钟)
}
// QuestionType 题目类型
@@ -271,7 +299,8 @@ type WorkQuestion struct {
// WorkDto 作业详情
type WorkDto struct {
Title string `json:"title"` // 作业标题
Title string `json:"title"` // 作业标题
IsPassed bool `json:"isPassed"`
Cpi string `json:"cpi"` // cpi
JobId string `json:"jobId"` // 任务ID
WorkId string `json:"workId"` // 作业ID
@@ -89,39 +89,7 @@ func (c *XuexitongCourse) AutoStudyAllCourses(ctx context.Context) error {
// AutoStudyCourse 自动学习指定课程的所有章节
func (c *XuexitongCourse) AutoStudyCourse(ctx context.Context, course entity.XueXiTCourse) error {
logger.Logger.Info(fmt.Sprintf("开始刷课程: %s", course.CourseName))
chapters := c.PullCourseChapterList(ctx, course)
if len(chapters) == 0 {
logger.Logger.Warn(fmt.Sprintf("课程 %s 没有章节", course.CourseName))
return nil
}
_, err := c.FetchChapterPointStatus(ctx, course, chapters)
if err != nil {
logger.Logger.Warn("获取章节状态失败", zap.Error(err))
}
for _, chapter := range chapters {
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("course", course.CourseName))
return err
}
if chapter.JobCount > 0 || chapter.PointTotal > chapter.PointFinish {
err := c.AutoStudyChapter(ctx, course, chapter)
if err != nil {
if err == ErrTimeRestriction {
return err
}
logger.Logger.Error(fmt.Sprintf("刷章节失败: %s", chapter.Name), zap.Error(err))
continue
}
}
}
logger.Logger.Info(fmt.Sprintf("课程完成: %s", course.CourseName))
return nil
return c.AutoStudyCourseWithOptions(ctx, course, nil)
}
// AutoStudyChapter 自动学习指定章节的所有任务点
@@ -154,6 +122,7 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
for i := range points {
if err := c.checkTimeRestriction(); err != nil {
// 更新任务状态为waiting
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
return err
}
@@ -193,6 +162,7 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
type AutoStudyOptions struct {
AIConfig *AIConfig // AI配置(用于作业答题)
AutoSubmit bool // 是否自动提交作业
Thread int // 线程数
}
// AutoStudyCourseWithOptions 带选项的自动学习课程
@@ -210,21 +180,90 @@ func (c *XuexitongCourse) AutoStudyCourseWithOptions(ctx context.Context, course
logger.Logger.Warn("获取章节状态失败", zap.Error(err))
}
for _, chapter := range chapters {
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("course", course.CourseName))
return err
// 获取线程数配置,默认为1(单线程)
threadCount := 1
if opts != nil && opts.Thread > 0 {
threadCount = opts.Thread
}
// 如果是单线程,使用原有逻辑
if threadCount == 1 {
for _, chapter := range chapters {
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("course", course.CourseName))
return err
}
if chapter.JobCount > 0 && chapter.PointTotal > chapter.PointFinish {
err := c.AutoStudyChapterWithOptions(ctx, course, chapter, opts)
if err != nil {
if err == ErrTimeRestriction {
return err
}
logger.Logger.Error(fmt.Sprintf("刷章节失败: %s", chapter.Name), zap.Error(err))
continue
}
}
}
} else {
// 多线程并发处理
logger.Logger.Info(fmt.Sprintf("使用 %d 线程并发刷课", threadCount))
// 筛选需要处理的章节
var targetChapters []entity.XueXiTCourseChapter
for _, chapter := range chapters {
if chapter.JobCount > 0 && chapter.PointTotal > chapter.PointFinish {
targetChapters = append(targetChapters, chapter)
}
}
if chapter.JobCount > 0 && chapter.PointTotal > chapter.PointFinish {
//logger.Logger.Info("开始处理章节", zap.String("chapter", chapter.Name))
err := c.AutoStudyChapterWithOptions(ctx, course, chapter, opts)
if err != nil {
if err == ErrTimeRestriction {
return err
if len(targetChapters) == 0 {
logger.Logger.Info("没有需要处理的章节")
return nil
}
// 创建信号量控制并发数
semaphore := make(chan struct{}, threadCount)
errChan := make(chan error, len(targetChapters))
doneChan := make(chan bool, len(targetChapters))
// 并发处理章节
for _, chapter := range targetChapters {
semaphore <- struct{}{} // 获取信号量
go func(ch entity.XueXiTCourseChapter) {
defer func() {
<-semaphore // 释放信号量
doneChan <- true
}()
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", ch.Name))
errChan <- err
return
}
logger.Logger.Error(fmt.Sprintf("刷章节失败: %s", chapter.Name), zap.Error(err))
continue
err := c.AutoStudyChapterWithOptions(ctx, course, ch, opts)
if err != nil {
if err == ErrTimeRestriction {
errChan <- err
return
}
logger.Logger.Error(fmt.Sprintf("刷章节失败: %s", ch.Name), zap.Error(err))
}
}(chapter)
}
// 等待所有任务完成
for i := 0; i < len(targetChapters); i++ {
<-doneChan
}
close(errChan)
// 检查是否有时间限制错误
for err := range errChan {
if err == ErrTimeRestriction {
return err
}
}
}
@@ -10,8 +10,11 @@ import (
)
// ApiVerifyCodeBytes 获取学习通验证码图片
func ApiVerifyCodeBytes(client *resty.Client) ([]byte, error) {
urlStr := "https://mooc1-api.chaoxing.com/processVerifyPng.ac?t=" + fmt.Sprintf("%d", time.Now().UnixMilli())
func ApiVerifyCodeBytes(client *resty.Client, prefixUrl string) ([]byte, error) {
if prefixUrl == "" {
prefixUrl = "https://mooc1-api.chaoxing.com"
}
urlStr := prefixUrl + "/processVerifyPng.ac?t=" + fmt.Sprintf("%d", time.Now().UnixMilli())
req := client.R()
req.Header.Add("User-Agent", GetUA("mobile"))
@@ -31,23 +34,27 @@ func ApiVerifyCodeBytes(client *resty.Client) ([]byte, error) {
}
// ApiPassVerifyCode 提交学习通验证码
func ApiPassVerifyCode(client *resty.Client, code string, retryCount int) bool {
func ApiPassVerifyCode(client *resty.Client, code string, prefixUrl string, retryCount int) bool {
if retryCount < 0 {
return false
}
urlStr := "https://mooc1-api.chaoxing.com/html/processVerify.ac"
if prefixUrl == "" {
prefixUrl = "https://mooc1-api.chaoxing.com"
}
//urlStr := "https://mooc1-api.chaoxing.com/html/processVerify.ac"
urlStr := prefixUrl + "/html/processVerify.ac"
client.SetRedirectPolicy(resty.NoRedirectPolicy())
req := client.R()
req.SetMultipartField("ucode", "", "", strings.NewReader(code))
req.SetMultipartField("app", "", "", strings.NewReader("0"))
req.SetMultipartField("app", "", "", strings.NewReader("1"))
req.Header.Add("User-Agent", GetUA("mobile"))
req.Header.Add("Accept", "*/*")
req.Header.Add("Host", "mooc1-api.chaoxing.com")
req.Header.Add("Connection", "keep-alive")
res, err := req.Post(urlStr)
if err != nil {
return ApiPassVerifyCode(client, code, retryCount-1)
return ApiPassVerifyCode(client, code, prefixUrl, retryCount-1)
}
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
return res.StatusCode() == 302
return res.StatusCode() == 302 || strings.Contains(res.String(), "解锁成功")
}
@@ -224,18 +224,21 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
if err != nil {
return err
}
// 触发202 需要重新登录获取cookie
if resp.StatusCode() == 202 {
}
body = resp.String()
if strings.Contains(body, "请输入验证码") {
logger.Logger.Warn("触发验证码,正在进行AI智能识别绕过.....")
e := retry.Do(func() error {
bytes, e := ApiVerifyCodeBytes(c.client)
bytes, e := ApiVerifyCodeBytes(c.client, "")
if e != nil {
return e
}
codeResult := captcha.ApiSemiOCRVerification(bytes, 1, 23)
time.Sleep(5 * time.Second)
if ApiPassVerifyCode(c.client, codeResult, 5) {
if ApiPassVerifyCode(c.client, codeResult, "", 5) {
return nil
}
return errors2.NewBusinessError(503, "验证码识别失败")
@@ -285,7 +288,9 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
logger.Logger.Debug("解析卡片失败", zap.Error(err))
continue
}
if iframes == nil {
continue
}
for _, iframe := range iframes {
if !iframe.HasData {
continue
@@ -302,6 +307,7 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
CourseID: course.CourseID,
ClassID: course.Key,
Cpi: cpiStr,
Title: card.Title,
}
switch pointType {
@@ -315,7 +321,7 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
point.ObjectID = objectID
point.JobID = jobID
cords2, err := c.FetchChapterCards2(ctx, course, card.KnowledgeID)
cords2, err := c.FetchChapterCards2(ctx, course, cardIndex, card.KnowledgeID)
if err == nil && cords2 != nil {
c.fillVideoPointFromCards2(cords2, &point, objectID)
}
@@ -331,7 +337,7 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
point.JobID = jobID
point.DocumentType = "document"
case "insertbook":
case "insertbook", "insertreadV2":
jobID, _ := iframe.Data["_jobid"].(string)
if jobID == "" {
continue
@@ -520,9 +526,9 @@ func (c *XuexitongCourse) parseIframeData(htmlString string) ([]entity.IframeAtt
}
// FetchChapterCards2 获取章节卡片(新接口,用于获取 otherInfo 等)
func (c *XuexitongCourse) FetchChapterCards2(ctx context.Context, course entity.XueXiTCourse, knowledgeID int) (map[string]interface{}, error) {
urlStr := fmt.Sprintf("%s?clazzid=%s&courseid=%s&knowledgeid=%d&num=0&ut=s&cpi=%d&v=2025-0424-1038-3&mooc2=1&isMicroCourse=false&editorPreview=0",
ApiChapterCards2, course.Key, course.CourseID, knowledgeID, course.Cpi)
func (c *XuexitongCourse) FetchChapterCards2(ctx context.Context, course entity.XueXiTCourse, num int, knowledgeID int) (map[string]interface{}, error) {
urlStr := fmt.Sprintf("%s?clazzid=%s&courseid=%s&knowledgeid=%d&num=%d&ut=s&cpi=%d&v=2025-0424-1038-3&mooc2=1&isMicroCourse=false&editorPreview=0",
ApiChapterCards2, course.Key, course.CourseID, knowledgeID, num, course.Cpi)
logger.Logger.Debug("FetchChapterCards2 请求",
zap.String("url", urlStr),
@@ -31,6 +31,7 @@ const (
ApiVideoPlayLog = "https://mooc1.chaoxing.com/mooc-ans/multimedia/log/a"
ApiDocumentReport = "https://mooc1.chaoxing.com/ananas/job/document"
ApiDocumentBook = "https://mooc1.chaoxing.com/ananas/job"
ApiDocumentReadV2 = "https://mooc1.chaoxing.com/ananas/job/readv2"
ApiWorkFetch = "https://mooc1-api.chaoxing.com/android/mworkspecial"
ApiWorkSubmit = "https://mooc1.chaoxing.com/mooc-ans/work/addStudentWorkNew"
VideoEncKey = "d_yHJ!$pdA~5"
@@ -104,12 +105,12 @@ func (c *XuexitongCourse) PullCourseList(ctx context.Context) []entity.XueXiTCou
if strings.Contains(body, "请输入验证码") {
logger.Logger.Warn("触发验证码,正在进行AI智能识别绕过.....")
e := retry.Do(func() error {
bytes, e := ApiVerifyCodeBytes(c.client)
bytes, e := ApiVerifyCodeBytes(c.client, "")
if e != nil {
return e
}
codeResult := captcha.ApiSemiOCRVerification(bytes, 1, 23)
if ApiPassVerifyCode(c.client, codeResult, 5) {
if ApiPassVerifyCode(c.client, codeResult, "", 5) {
return nil
}
return errors2.NewBusinessError(503, "验证码识别失败")
@@ -16,44 +16,9 @@ import (
// attachmentDetectDocument 从附件设置中检测并填充文档信息
func (c *XuexitongCourse) attachmentDetectDocument(attachment *entity.AttachmentSetting, p *entity.ChapterPoint) {
for _, att := range attachment.Attachments {
property, ok := att["property"].(map[string]interface{})
if !ok {
continue
}
jobid, _ := property["jobid"].(string)
if jobid != p.JobID && p.JobID != "" {
continue
}
if name, ok := property["name"].(string); ok {
p.Title = name
}
if bookname, ok := property["bookname"].(string); ok {
p.Title = bookname
}
if jid, ok := property["jobid"].(string); ok {
p.JobID = jid
}
if jtoken, ok := att["jtoken"].(string); ok {
p.Jtoken = jtoken
}
if isJob, ok := att["job"].(bool); ok {
p.IsJob = isJob
}
break
}
}
// AttachmentsDetectionDocument 从附件设置中检测并填充文档信息
func (c *XuexitongCourse) AttachmentsDetectionDocument(attachment *entity.AttachmentSetting, p *entity.PointDocumentDto) error {
if attachment == nil {
return errors2.NewBusinessError(503, "附件设置为空")
}
for _, att := range attachment.Attachments {
typeStr := ""
// 进行不同方案的类型值获取
if t, ok := att["type"].(string); ok {
typeStr = t
} else if property, ok := att["property"].(map[string]interface{}); ok {
@@ -67,37 +32,75 @@ func (c *XuexitongCourse) AttachmentsDetectionDocument(attachment *entity.Attach
continue
}
jobid, _ := property["jobid"].(string)
if jobid != p.JobID && p.JobID != "" {
continue
}
// 如果未给出文档类型,先进行文档解析尝试
if typeStr == "" {
if name, ok := property["name"].(string); ok {
p.Title = name
}
if jid, ok := property["jobid"].(string); ok {
p.JobID = jid
}
if jtoken, ok := att["jtoken"].(string); ok {
p.Jtoken = jtoken
}
if isJob, ok := att["job"].(bool); ok {
p.IsJob = isJob
} else {
p.IsJob = false
}
if bookname, ok := property["bookname"].(string); ok {
p.Title = bookname
}
// 过滤匹配
objectid, _ := property["objectid"].(string)
jobid, _ := property["jobid"].(string)
if (p.ObjectID != "" && objectid == p.ObjectID) || (jobid != "" && p.JobID == jobid) {
break
}
} else if typeStr == "insertbook" || typeStr == "read" {
if jtoken, ok := att["jtoken"].(string); ok {
p.Jtoken = jtoken
}
if isJob, ok := att["job"].(bool); ok {
p.IsJob = isJob
} else {
p.IsJob = false
}
if bookname, ok := property["bookname"].(string); ok {
p.Title = bookname
}
if typeStr == "insertbook" {
p.DocumentType = "insertbook"
} else {
p.DocumentType = "read"
}
if name, ok := property["name"].(string); ok {
p.Title = name
// 过滤匹配
objectid, _ := property["objectid"].(string)
jobid, _ := property["jobid"].(string)
if (p.ObjectID != "" && objectid == p.ObjectID) || (jobid != "" && p.JobID == jobid) {
break
}
} else if typeStr == "document" || typeStr == "insertdoc" {
if isJob, ok := att["job"].(bool); ok {
p.IsJob = isJob
}
objectid, _ := property["objectid"].(string)
if objectid == p.ObjectID {
if name, ok := property["name"].(string); ok {
p.Title = name
}
if jid, ok := property["jobid"].(string); ok {
p.JobID = jid
}
if jtoken, ok := att["jtoken"].(string); ok {
p.Jtoken = jtoken
}
p.DocumentType = "document"
break
}
}
if bookname, ok := property["bookname"].(string); ok {
p.Title = bookname
}
if jid, ok := property["jobid"].(string); ok {
p.JobID = jid
}
if jtoken, ok := att["jtoken"].(string); ok {
p.Jtoken = jtoken
}
if isJob, ok := att["job"].(bool); ok {
p.IsJob = isJob
}
if typeStr == "document" || typeStr == "insertdoc" {
p.Type = "document"
} else if typeStr == "insertbook" {
p.Type = "insertbook"
}
break
}
return nil
}
// DocumentDtoReadingReport 提交文档阅读完成
@@ -111,7 +114,7 @@ func (c *XuexitongCourse) DocumentDtoReadingReport(ctx context.Context, p *entit
params.Add("_dc", strconv.FormatInt(time.Now().UnixMilli(), 10))
apiUrl := ApiDocumentReport
if p.Type == "insertbook" {
if p.Cpi == "insertbook" {
apiUrl = ApiDocumentBook
}
@@ -137,39 +140,26 @@ func (c *XuexitongCourse) DocumentDtoReadingReport(ctx context.Context, p *entit
return body, nil
}
// AutoReadDocument 自动阅读文档(刷课)
func (c *XuexitongCourse) AutoReadDocument(ctx context.Context, course entity.XueXiTCourse, p *entity.PointDocumentDto) error {
if !p.IsJob {
logger.Logger.Info("文档不是任务点,跳过", zap.String("jobID", p.JobID), zap.String("title", p.Title))
return nil
}
logger.Logger.Info("开始刷文档",
zap.String("jobID", p.JobID),
zap.String("title", p.Title))
report, err := c.DocumentDtoReadingReport(ctx, p)
// AutoReadDocumentByPoint 自动阅读文档(使用统一的 ChapterPoint)
func (c *XuexitongCourse) AutoReadDocumentByPoint(ctx context.Context, course entity.XueXiTCourse, p *entity.ChapterPoint) error {
// 获取附件信息填充 Jtoken、IsJob 等参数
attachment, _, err := c.PageMobileChapterCard(ctx, course, p.KnowledgeID, p.CardIndex)
if err != nil {
logger.Logger.Error("获取章节卡片失败", zap.String("title", p.Title), zap.Error(err))
return err
}
var result map[string]interface{}
if err := json.Unmarshal([]byte(report), &result); err == nil {
if status, ok := result["status"].(bool); ok && status {
logger.Logger.Info("文档刷课完成", zap.String("jobID", p.JobID), zap.String("title", p.Title))
c.onTaskComplete()
return nil
}
if msg, ok := result["msg"].(string); ok {
logger.Logger.Warn("文档无法正常学习", zap.String("msg", msg))
}
if attachment != nil {
c.attachmentDetectDocument(attachment, p)
}
return nil
}
logger.Logger.Debug("文档参数",
zap.String("jobID", p.JobID),
zap.String("objectID", p.ObjectID),
zap.String("jtoken", p.Jtoken),
zap.Bool("isJob", p.IsJob),
zap.String("documentType", p.DocumentType))
// AutoReadDocumentByPoint 自动阅读文档(使用统一的 ChapterPoint)
func (c *XuexitongCourse) AutoReadDocumentByPoint(ctx context.Context, course entity.XueXiTCourse, p *entity.ChapterPoint) error {
if !p.IsJob {
logger.Logger.Info("文档不是任务点,跳过", zap.String("jobID", p.JobID), zap.String("title", p.Title))
return nil
@@ -201,24 +191,28 @@ func (c *XuexitongCourse) AutoReadDocumentByPoint(ctx context.Context, course en
// DocumentDtoReadingReportByPoint 提交文档阅读完成(使用统一的 ChapterPoint)
func (c *XuexitongCourse) DocumentDtoReadingReportByPoint(ctx context.Context, p *entity.ChapterPoint) (string, error) {
params := url.Values{}
params.Add("jobid", p.JobID)
params.Add("knowledgeid", strconv.Itoa(p.KnowledgeID))
params.Add("courseid", p.CourseID)
params.Add("clazzid", p.ClassID)
params.Add("jtoken", p.Jtoken)
params.Add("_dc", strconv.FormatInt(time.Now().UnixMilli(), 10))
formParams := map[string]string{
"jobid": p.JobID,
"knowledgeid": strconv.Itoa(p.KnowledgeID),
"courseid": p.CourseID,
"clazzid": p.ClassID,
"jtoken": p.Jtoken,
"_dc": strconv.FormatInt(time.Now().UnixMilli(), 10),
}
apiUrl := ApiDocumentReport
if p.DocumentType == "insertbook" {
apiUrl = ApiDocumentBook
} else if p.DocumentType == "read" {
apiUrl = ApiDocumentReadV2
}
// read 类型使用 document 接口
var body string
err := retry.Do(func() error {
resp, err := c.client.R().SetHeaders(map[string]string{
"User-Agent": GetUA("mobile"),
}).Get(apiUrl + "?" + params.Encode())
}).SetQueryParams(formParams).Get(apiUrl)
if err != nil {
return err
}
@@ -3,11 +3,14 @@ package xuexitong
import (
"context"
"likedream-goframe/internal/application/quesbank"
"likedream-goframe/internal/application/xuexitong/entity"
"likedream-goframe/internal/common/db"
"likedream-goframe/internal/ent/sysconfig"
"likedream-goframe/internal/framework/errors"
"likedream-goframe/internal/framework/logger"
"likedream-goframe/internal/utils"
"strconv"
"strings"
"testing"
"github.com/bytedance/sonic"
@@ -36,7 +39,7 @@ func getAIConfig(ctx context.Context) *quesbank.AIConfig {
func TestSchedule(t *testing.T) {
ctx := context.Background()
xuexitongLogin := NewXuexitongLogin(ctx)
err := xuexitongLogin.Login(context.Background(), "13151832241", "yyz@083422", 5)
err := xuexitongLogin.Login(context.Background(), "15754238084", "1575423yuan.dby", 5)
utils.OkOrPanic(err)
client := xuexitongLogin.GetClient()
@@ -53,9 +56,10 @@ func TestSchedule(t *testing.T) {
logger.Logger.Warn("AI配置为空,作业将被跳过")
}
err = courseService.AutoStudyByCourseIDWithOptions(ctx, "258109701", &AutoStudyOptions{
err = courseService.AutoStudyByCourseIDWithOptions(ctx, "255690793", &AutoStudyOptions{
AIConfig: config,
AutoSubmit: true, // 默认不自动提交,仅保存
Thread: 1,
})
if err != nil {
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "刷课失败", err))
@@ -180,3 +184,235 @@ func TestAutoStudyAllCourses(t *testing.T) {
t.Log("所有课程刷课完成")
}
func TestSplitWords(t *testing.T) {
str := "ABCD"
split := strings.Split(str, "")
t.Log(split)
}
// TestExam 测试考试功能 - 完成整个考试流程但不提交(只暂存)
func TestExam(t *testing.T) {
ctx := context.Background()
// 登录
xuexitongLogin := NewXuexitongLogin(ctx)
err := xuexitongLogin.Login(ctx, "17320926947", "zhijie2001", 5)
utils.OkOrPanic(err)
client := xuexitongLogin.GetClient()
courseService := NewXuexitongCourse(client)
// 获取AI配置(可选,用于AI答题兜底)
aiConfig := getAIConfig(ctx)
if aiConfig != nil {
t.Logf("AI配置加载成功: %s %s", aiConfig.Type, aiConfig.Model)
} else {
t.Log("未配置AI,将仅使用题库答题")
}
// 创建统一答题服务(本地题库 -> 第三方题库 -> AI答题)
answerService := quesbank.NewUnifiedAnswerService(aiConfig)
// 获取课程列表,找到指定课程
courseList := courseService.PullCourseList(ctx)
var targetCourse *entity.XueXiTCourse
for i, course := range courseList {
if course.CourseID == "259828097" {
targetCourse = &courseList[i]
break
}
}
if targetCourse == nil {
t.Fatal("未找到指定课程")
}
t.Logf("找到课程: %s (ID: %s)", targetCourse.CourseName, targetCourse.CourseID)
// 使用考试列表API获取考试
examList, err := courseService.FetchExamTaskList(ctx, *targetCourse)
if err != nil {
t.Fatalf("获取考试列表失败: %v", err)
}
t.Logf("考试数量: %d", len(examList))
for _, exam := range examList {
t.Logf("考试: %s (状态: %s, TaskRefId: %s)",
exam.Name, exam.Status, exam.TaskRefId)
}
// 找一个待做的考试
var targetExam *ExamTaskItem
for i := range examList {
if examList[i].Status == "待做" {
targetExam = &examList[i]
break
}
}
if targetExam == nil {
t.Log("没有待做的考试")
return
}
t.Logf("开始考试: %s", targetExam.Name)
// 进入考试
err = courseService.EnterExam(ctx, targetExam)
if err != nil {
t.Fatalf("进入考试失败: %v", err)
}
t.Logf("进入考试成功, 题目数: %d, ExamRelationId: %s, AnswerId: %s",
targetExam.QuestionTotal, targetExam.ExamRelationId, targetExam.AnswerId)
if targetExam.QuestionTotal == 0 {
t.Log("考试没有题目")
return
}
// 遍历所有题目
for i := 0; i < targetExam.QuestionTotal; i++ {
t.Logf("========== 第 %d/%d 题 ==========", i, targetExam.QuestionTotal)
// 拉取题目
question, err := courseService.PullExamQuestion(ctx, targetExam, i)
if err != nil {
t.Logf("拉取第%d题失败: %v", i, err)
continue
}
t.Logf("题目类型: %s", question.QuestionTypeStr)
t.Logf("题目内容: %s", question.Content)
if len(question.Options) > 0 {
t.Logf("选项: %v", question.Options)
}
// 使用统一答题服务获取答案(本地题库 -> 第三方题库 -> AI答题)
questionInfo := quesbank.QuestionInfo{
Content: question.Content,
TypeCode: question.QuestionTypeCode,
TypeName: quesbank.GetQuestionTypeName(question.QuestionTypeCode),
Options: question.Options,
CourseName: targetCourse.CourseName,
QuestionID: question.QuestionId,
}
result, err := answerService.GetAnswer(ctx, questionInfo)
if err != nil {
t.Logf("获取答案失败: %v, 使用默认答案", err)
question.Answers = []string{"A"}
} else {
question.Answers = result.Answers
t.Logf("答案来源: %s, 答案: %v", result.Source, result.Answers)
}
// 提交答案(暂存,不交卷)
// tempSave=true 表示暂存,不交卷
submitResult, err := courseService.SubmitExamAnswer(ctx, question, true)
if err != nil {
t.Logf("提交第%d题答案失败: %v", i, err)
} else {
t.Logf("提交第%d题答案成功: %s", i, submitResult)
}
}
t.Log("========== 考试完成(已暂存,未交卷)==========")
}
// buildExamQuestionPrompt 构建考试题目的AI提示词
func buildExamQuestionPrompt(q *ExamQuestion) string {
var prompt strings.Builder
switch q.QuestionTypeCode {
case "0": // 单选题
prompt.WriteString("题目类型:单选题\n")
prompt.WriteString("题目内容:" + q.Content + "\n")
for _, opt := range q.Options {
prompt.WriteString(opt + "\n")
}
prompt.WriteString("\n请只回答选项字母,不要解释。格式:[\"A\"]")
case "1": // 多选题
prompt.WriteString("题目类型:多选题\n")
prompt.WriteString("题目内容:" + q.Content + "\n")
for _, opt := range q.Options {
prompt.WriteString(opt + "\n")
}
prompt.WriteString("\n请只回答选项字母,不要解释。格式:[\"A\",\"B\"]")
case "3": // 判断题
prompt.WriteString("题目类型:判断题\n")
prompt.WriteString("题目内容:" + q.Content + "\n")
for _, opt := range q.Options {
prompt.WriteString(opt + "\n")
}
prompt.WriteString("\n请只回答\"A\"(正确)或\"B\"(错误),不要解释。格式:[\"A\"]")
case "2": // 填空题
prompt.WriteString("题目类型:填空题\n")
prompt.WriteString("题目内容:" + q.Content + "\n")
blankCount := len(q.Options)
if blankCount == 0 {
blankCount = 1
}
prompt.WriteString("共有" + strconv.Itoa(blankCount) + "个空\n")
prompt.WriteString("\n请按顺序回答每个空的答案,不要解释。格式:[\"答案1\",\"答案2\"]")
case "4", "6": // 简答题、论述题
prompt.WriteString("题目类型:简答题\n")
prompt.WriteString("题目内容:" + q.Content + "\n")
prompt.WriteString("\n请直接回答问题,格式:[\"你的答案\"]")
default:
prompt.WriteString("题目:" + q.Content + "\n")
prompt.WriteString("请回答这道题,格式:[\"答案\"]")
}
return prompt.String()
}
// TestExamDetail 测试获取考试详情
func TestExamDetail(t *testing.T) {
ctx := context.Background()
// 登录
xuexitongLogin := NewXuexitongLogin(ctx)
err := xuexitongLogin.Login(ctx, "13151832241", "yyz@083422", 5)
utils.OkOrPanic(err)
client := xuexitongLogin.GetClient()
courseService := NewXuexitongCourse(client)
// 获取课程列表,找到指定课程
courseList := courseService.PullCourseList(ctx)
var targetCourse *entity.XueXiTCourse
for i, course := range courseList {
if course.CourseID == "258109701" {
targetCourse = &courseList[i]
break
}
}
if targetCourse == nil {
t.Fatal("未找到指定课程 courseId=258109701")
}
// 获取考试列表
examList, err := courseService.FetchExamTaskList(ctx, *targetCourse)
if err != nil {
t.Fatalf("获取考试列表失败: %v", err)
}
if len(examList) == 0 {
t.Log("该课程没有考试")
return
}
// 打印所有考试信息
for i, exam := range examList {
t.Logf("考试%d: 标题=%s, 状态=%s, TaskRefId=%s, CourseId=%s, ClazzId=%s",
i+1, exam.Name, exam.Status, exam.TaskRefId, exam.CourseId, exam.ClazzId)
}
}
@@ -252,6 +252,7 @@ func (c *XuexitongCourse) attachmentDetectVideo(attachment *entity.AttachmentSet
// AutoPlayVideoByPoint 自动播放视频(使用统一的 ChapterPoint)
func (c *XuexitongCourse) AutoPlayVideoByPoint(ctx context.Context, course entity.XueXiTCourse, p *entity.ChapterPoint) error {
if err := c.VideoDtoFetchByPoint(ctx, p); err != nil {
return fmt.Errorf("获取视频信息失败: %w", err)
}
@@ -439,32 +439,32 @@ func cleanText(text string) string {
// SubmitWorkAnswers 提交作业答案
func (c *XuexitongCourse) SubmitWorkAnswers(ctx context.Context, workDto *entity.WorkDto, isSave bool) (string, error) {
formData := url.Values{}
pyFlag := ""
if isSave {
pyFlag = "1"
}
formData.Set("pyFlag", pyFlag)
formData.Set("courseId", workDto.CourseId)
formData.Set("classId", workDto.ClassId)
formData.Set("api", workDto.Api)
formData.Set("workAnswerId", workDto.WorkAnswerId)
formData.Set("answerId", workDto.AnswerId)
formData.Set("totalQuestionNum", workDto.TotalQuestionNum)
formData.Set("fullScore", workDto.FullScore)
formData.Set("knowledgeid", workDto.KnowledgeId)
formData.Set("oldSchoolId", workDto.OldSchoolId)
formData.Set("oldWorkId", workDto.OldWorkId)
formData.Set("jobid", workDto.JobId)
formData.Set("workRelationId", workDto.WorkRelationId)
formData.Set("enc", "")
formData.Set("enc_work", workDto.EncWork)
formData.Set("userId", workDto.UserId)
formData.Set("cpi", workDto.Cpi)
formData.Set("workTimesEnc", "")
formData.Set("randomOptions", "false")
formData.Set("isAccessibleCustomFid", "0")
formMap := map[string]string{
"pyFlag": pyFlag,
"courseId": workDto.CourseId,
"classId": workDto.ClassId,
"api": workDto.Api,
"mooc": "0",
"workAnswerId": workDto.WorkAnswerId,
"answerId": "",
"totalQuestionNum": workDto.TotalQuestionNum,
"fullScore": workDto.FullScore,
"knowledgeid": workDto.KnowledgeId,
"oldSchoolId": workDto.OldSchoolId,
"oldWorkId": workDto.OldWorkId,
"jobid": workDto.JobId,
"workRelationId": workDto.WorkRelationId,
"enc_work": workDto.EncWork,
"isphone": "true",
"userId": workDto.UserId,
"workTimesEnc": "",
"isAccessibleCustomFid": "0",
}
answerwqbid := ""
for _, q := range workDto.Questions {
if q.Qid != "" {
@@ -474,46 +474,46 @@ func (c *XuexitongCourse) SubmitWorkAnswers(ctx context.Context, workDto *entity
switch q.Type {
case entity.QuestionTypeSingleChoice:
answer := strings.Join(q.Answers, "")
formData.Set("answer"+q.Qid, answer)
formData.Set("answertype"+q.Qid, "0")
formMap["answer"+q.Qid] = answer
formMap["answertype"+q.Qid] = "0"
case entity.QuestionTypeMultipleChoice:
answer := strings.Join(q.Answers, "")
formData.Set("answer"+q.Qid, answer)
formData.Set("answertype"+q.Qid, "1")
formMap["answer"+q.Qid] = answer
formMap["answertype"+q.Qid] = "1"
case entity.QuestionTypeFillBlank:
for key, val := range q.BlankAnswer {
re := regexp.MustCompile(`\d+`)
numbers := re.FindAllString(key, -1)
if len(numbers) > 0 {
formData.Set("answer"+q.Qid+numbers[0], val)
formMap["answer"+q.Qid+numbers[0]] = val
}
}
formData.Set("tiankongsize"+q.Qid, strconv.Itoa(q.BlankCount))
formData.Set("answertype"+q.Qid, "2")
formMap["tiankongsize"+q.Qid] = strconv.Itoa(q.BlankCount)
formMap["answertype"+q.Qid] = "2"
case entity.QuestionTypeTrueFalse:
answer := ""
for _, a := range q.Answers {
if a == "正确" {
if a == "正确" || a == "对" {
answer = "true"
} else if a == "错误" {
} else if a == "错误" || a == "错" {
answer = "false"
} else {
answer = a
}
}
formData.Set("answer"+q.Qid, answer)
formData.Set("answertype"+q.Qid, "3")
formMap["answer"+q.Qid] = answer
formMap["answertype"+q.Qid] = "3"
case entity.QuestionTypeShortAnswer:
answer := strings.Join(q.Answers, "")
formData.Set("answer"+q.Qid, answer)
formData.Set("answertype"+q.Qid, "4")
formMap["answer"+q.Qid] = answer
formMap["answertype"+q.Qid] = "4"
}
}
formData.Set("answerwqbid", answerwqbid)
formMap["answerwqbid"] = answerwqbid
urlStr := fmt.Sprintf("%s?_classId=%s&courseid=%s&token=%s&totalQuestionNum=%s&ua=pc&formType=post&saveStatus=1&version=1&tempsave=1",
ApiWorkSubmit, workDto.ClassId, workDto.CourseId, workDto.EncWork, workDto.TotalQuestionNum)
@@ -523,7 +523,7 @@ func (c *XuexitongCourse) SubmitWorkAnswers(ctx context.Context, workDto *entity
resp, err := c.client.R().SetHeaders(map[string]string{
"User-Agent": GetUA("mobile"),
"Content-Type": "application/x-www-form-urlencoded",
}).SetBody(formData.Encode()).Post(urlStr)
}).SetFormData(formMap).Post(urlStr)
if err != nil {
return err
}
+4
View File
@@ -49,6 +49,10 @@ const (
ConfigRedisCpdailyPhone = "price.cpdaily_phone"
ConfigRedisXxt = "price.xxt"
ConfigRedisSmarteduSc = "price.smartedu_sc"
// 第三方题库配置
ConfigQuesBankUrl = "quesbank.url"
ConfigQuesBankKey = "quesbank.key"
)
const (
+2 -2
View File
@@ -66,9 +66,9 @@ func (c CpUserCtrlV1) AuthorizeConfirm(ctx context.Context, req *v1.CpUserAuthor
return service.Global.CpUser.AuthorizeConfirm(ctx, req), nil
}
func (c CpUserCtrlV1) AuthorizeRandom(ctx context.Context, req *v1.CpUserAuthorizeRandomReq) (res *utils.Response[any], err error) {
func (c CpUserCtrlV1) CustomDevice(ctx context.Context, req *v1.CpUserCustomDeviceReq) (res *utils.Response[any], err error) {
defer utils.Pass(&err)
return service.Global.CpUser.AuthorizeRandom(ctx, req), nil
return service.Global.CpUser.CustomDevice(ctx, req), nil
}
func (c CpUserCtrlV1) LoginGetCode(ctx context.Context, req *v1.LoginGetCode) (res *utils.Response[any], err error) {
defer utils.Pass(&err)
+2 -2
View File
@@ -25,8 +25,8 @@ type ICpUserCtrlV1 interface {
AuthorizeDevice(ctx context.Context, req *v1.CpUserAuthorizeDeviceReq) (*utils.Response[any], error)
// 验证授权是否成功
AuthorizeConfirm(ctx context.Context, req *v1.CpUserAuthorizeConfirmReq) (*utils.Response[any], error)
// 随机授权
AuthorizeRandom(ctx context.Context, req *v1.CpUserAuthorizeRandomReq) (res *utils.Response[any], err error)
// 自定义设备码
CustomDevice(ctx context.Context, req *v1.CpUserCustomDeviceReq) (res *utils.Response[any], err error)
// 手机登录获取验证码
LoginGetCode(ctx context.Context, req *v1.LoginGetCode) (*utils.Response[any], error)
@@ -8,5 +8,4 @@ import (
type ICpImageCtrlV1 interface {
Upload(ctx context.Context, req *v1.UploadImageReq) (res *utils.Response[any], err error)
Preview(ctx context.Context, req *v1.PreviewImageReq) (res *utils.Response[any], err error)
}
@@ -15,11 +15,6 @@ func (c CpImageCtrlV1) Upload(ctx context.Context, req *v1.UploadImageReq) (res
return service.Global.CpImage.Upload(ctx, req), nil
}
func (c CpImageCtrlV1) Preview(ctx context.Context, req *v1.PreviewImageReq) (res *utils.Response[any], err error) {
defer utils.Pass(&err)
return service.Global.CpImage.Preview(ctx, req), nil
}
func NewCpImageCtrlV1() ICpImageCtrlV1 {
return &CpImageCtrlV1{}
}
+16
View File
@@ -3,6 +3,7 @@
package ent
import (
"encoding/json"
"fmt"
"likedream-goframe/internal/ent/cptask"
"likedream-goframe/internal/ent/cpuser"
@@ -51,6 +52,8 @@ type CpTask struct {
ExecuteMinute *int `json:"execute_minute,omitempty"`
// NextExecuteTime holds the value of the "next_execute_time" field.
NextExecuteTime *int64 `json:"next_execute_time,omitempty"`
// 执行星期 1-7,1=周一,7=周日
ExecuteDays []int `json:"execute_days,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the CpTaskQuery when eager-loading is set.
Edges CpTaskEdges `json:"edges"`
@@ -107,6 +110,8 @@ func (*CpTask) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case cptask.FieldExecuteDays:
values[i] = new([]byte)
case cptask.FieldCreateTime, cptask.FieldUpdateTime:
values[i] = new(ent2.CustomTime)
case cptask.FieldID, cptask.FieldCpUserID, cptask.FieldUserID, cptask.FieldFrequency, cptask.FieldSuccessCount, cptask.FieldExecuteHour, cptask.FieldExecuteMinute, cptask.FieldNextExecuteTime:
@@ -233,6 +238,14 @@ func (_m *CpTask) assignValues(columns []string, values []any) error {
_m.NextExecuteTime = new(int64)
*_m.NextExecuteTime = value.Int64
}
case cptask.FieldExecuteDays:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field execute_days", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &_m.ExecuteDays); err != nil {
return fmt.Errorf("unmarshal field execute_days: %w", err)
}
}
default:
_m.selectValues.Set(columns[i], values[i])
}
@@ -337,6 +350,9 @@ func (_m *CpTask) String() string {
builder.WriteString("next_execute_time=")
builder.WriteString(fmt.Sprintf("%v", *v))
}
builder.WriteString(", ")
builder.WriteString("execute_days=")
builder.WriteString(fmt.Sprintf("%v", _m.ExecuteDays))
builder.WriteByte(')')
return builder.String()
}
+3
View File
@@ -48,6 +48,8 @@ const (
FieldExecuteMinute = "execute_minute"
// FieldNextExecuteTime holds the string denoting the next_execute_time field in the database.
FieldNextExecuteTime = "next_execute_time"
// FieldExecuteDays holds the string denoting the execute_days field in the database.
FieldExecuteDays = "execute_days"
// EdgeCpUser holds the string denoting the cp_user edge name in mutations.
EdgeCpUser = "cp_user"
// EdgeSysUser holds the string denoting the sys_user edge name in mutations.
@@ -98,6 +100,7 @@ var Columns = []string{
FieldExecuteHour,
FieldExecuteMinute,
FieldNextExecuteTime,
FieldExecuteDays,
}
// ValidColumn reports if the column name is valid (part of the table columns).
+10
View File
@@ -821,6 +821,16 @@ func NextExecuteTimeNotNil() predicate.CpTask {
return predicate.CpTask(sql.FieldNotNull(FieldNextExecuteTime))
}
// ExecuteDaysIsNil applies the IsNil predicate on the "execute_days" field.
func ExecuteDaysIsNil() predicate.CpTask {
return predicate.CpTask(sql.FieldIsNull(FieldExecuteDays))
}
// ExecuteDaysNotNil applies the NotNil predicate on the "execute_days" field.
func ExecuteDaysNotNil() predicate.CpTask {
return predicate.CpTask(sql.FieldNotNull(FieldExecuteDays))
}
// HasCpUser applies the HasEdge predicate on the "cp_user" edge.
func HasCpUser() predicate.CpTask {
return predicate.CpTask(func(s *sql.Selector) {
+70
View File
@@ -177,6 +177,12 @@ func (_c *CpTaskCreate) SetNillableNextExecuteTime(v *int64) *CpTaskCreate {
return _c
}
// SetExecuteDays sets the "execute_days" field.
func (_c *CpTaskCreate) SetExecuteDays(v []int) *CpTaskCreate {
_c.mutation.SetExecuteDays(v)
return _c
}
// SetCpUser sets the "cp_user" edge to the CpUser entity.
func (_c *CpTaskCreate) SetCpUser(v *CpUser) *CpTaskCreate {
return _c.SetCpUserID(v.ID)
@@ -420,6 +426,10 @@ func (_c *CpTaskCreate) createSpec() (*CpTask, *sqlgraph.CreateSpec) {
_spec.SetField(cptask.FieldNextExecuteTime, field.TypeInt64, value)
_node.NextExecuteTime = &value
}
if value, ok := _c.mutation.ExecuteDays(); ok {
_spec.SetField(cptask.FieldExecuteDays, field.TypeJSON, value)
_node.ExecuteDays = value
}
if nodes := _c.mutation.CpUserIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
@@ -765,6 +775,24 @@ func (u *CpTaskUpsert) ClearNextExecuteTime() *CpTaskUpsert {
return u
}
// SetExecuteDays sets the "execute_days" field.
func (u *CpTaskUpsert) SetExecuteDays(v []int) *CpTaskUpsert {
u.Set(cptask.FieldExecuteDays, v)
return u
}
// UpdateExecuteDays sets the "execute_days" field to the value that was provided on create.
func (u *CpTaskUpsert) UpdateExecuteDays() *CpTaskUpsert {
u.SetExcluded(cptask.FieldExecuteDays)
return u
}
// ClearExecuteDays clears the value of the "execute_days" field.
func (u *CpTaskUpsert) ClearExecuteDays() *CpTaskUpsert {
u.SetNull(cptask.FieldExecuteDays)
return u
}
// UpdateNewValues updates the mutable fields using the new values that were set on create.
// Using this option is equivalent to using:
//
@@ -1090,6 +1118,27 @@ func (u *CpTaskUpsertOne) ClearNextExecuteTime() *CpTaskUpsertOne {
})
}
// SetExecuteDays sets the "execute_days" field.
func (u *CpTaskUpsertOne) SetExecuteDays(v []int) *CpTaskUpsertOne {
return u.Update(func(s *CpTaskUpsert) {
s.SetExecuteDays(v)
})
}
// UpdateExecuteDays sets the "execute_days" field to the value that was provided on create.
func (u *CpTaskUpsertOne) UpdateExecuteDays() *CpTaskUpsertOne {
return u.Update(func(s *CpTaskUpsert) {
s.UpdateExecuteDays()
})
}
// ClearExecuteDays clears the value of the "execute_days" field.
func (u *CpTaskUpsertOne) ClearExecuteDays() *CpTaskUpsertOne {
return u.Update(func(s *CpTaskUpsert) {
s.ClearExecuteDays()
})
}
// Exec executes the query.
func (u *CpTaskUpsertOne) Exec(ctx context.Context) error {
if len(u.create.conflict) == 0 {
@@ -1581,6 +1630,27 @@ func (u *CpTaskUpsertBulk) ClearNextExecuteTime() *CpTaskUpsertBulk {
})
}
// SetExecuteDays sets the "execute_days" field.
func (u *CpTaskUpsertBulk) SetExecuteDays(v []int) *CpTaskUpsertBulk {
return u.Update(func(s *CpTaskUpsert) {
s.SetExecuteDays(v)
})
}
// UpdateExecuteDays sets the "execute_days" field to the value that was provided on create.
func (u *CpTaskUpsertBulk) UpdateExecuteDays() *CpTaskUpsertBulk {
return u.Update(func(s *CpTaskUpsert) {
s.UpdateExecuteDays()
})
}
// ClearExecuteDays clears the value of the "execute_days" field.
func (u *CpTaskUpsertBulk) ClearExecuteDays() *CpTaskUpsertBulk {
return u.Update(func(s *CpTaskUpsert) {
s.ClearExecuteDays()
})
}
// Exec executes the query.
func (u *CpTaskUpsertBulk) Exec(ctx context.Context) error {
if u.create.err != nil {
+59
View File
@@ -16,6 +16,7 @@ import (
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/dialect/sql/sqljson"
"entgo.io/ent/schema/field"
)
@@ -300,6 +301,24 @@ func (_u *CpTaskUpdate) ClearNextExecuteTime() *CpTaskUpdate {
return _u
}
// SetExecuteDays sets the "execute_days" field.
func (_u *CpTaskUpdate) SetExecuteDays(v []int) *CpTaskUpdate {
_u.mutation.SetExecuteDays(v)
return _u
}
// AppendExecuteDays appends value to the "execute_days" field.
func (_u *CpTaskUpdate) AppendExecuteDays(v []int) *CpTaskUpdate {
_u.mutation.AppendExecuteDays(v)
return _u
}
// ClearExecuteDays clears the value of the "execute_days" field.
func (_u *CpTaskUpdate) ClearExecuteDays() *CpTaskUpdate {
_u.mutation.ClearExecuteDays()
return _u
}
// SetCpUser sets the "cp_user" edge to the CpUser entity.
func (_u *CpTaskUpdate) SetCpUser(v *CpUser) *CpTaskUpdate {
return _u.SetCpUserID(v.ID)
@@ -519,6 +538,17 @@ func (_u *CpTaskUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if _u.mutation.NextExecuteTimeCleared() {
_spec.ClearField(cptask.FieldNextExecuteTime, field.TypeInt64)
}
if value, ok := _u.mutation.ExecuteDays(); ok {
_spec.SetField(cptask.FieldExecuteDays, field.TypeJSON, value)
}
if value, ok := _u.mutation.AppendedExecuteDays(); ok {
_spec.AddModifier(func(u *sql.UpdateBuilder) {
sqljson.Append(u, cptask.FieldExecuteDays, value)
})
}
if _u.mutation.ExecuteDaysCleared() {
_spec.ClearField(cptask.FieldExecuteDays, field.TypeJSON)
}
if _u.mutation.CpUserCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
@@ -920,6 +950,24 @@ func (_u *CpTaskUpdateOne) ClearNextExecuteTime() *CpTaskUpdateOne {
return _u
}
// SetExecuteDays sets the "execute_days" field.
func (_u *CpTaskUpdateOne) SetExecuteDays(v []int) *CpTaskUpdateOne {
_u.mutation.SetExecuteDays(v)
return _u
}
// AppendExecuteDays appends value to the "execute_days" field.
func (_u *CpTaskUpdateOne) AppendExecuteDays(v []int) *CpTaskUpdateOne {
_u.mutation.AppendExecuteDays(v)
return _u
}
// ClearExecuteDays clears the value of the "execute_days" field.
func (_u *CpTaskUpdateOne) ClearExecuteDays() *CpTaskUpdateOne {
_u.mutation.ClearExecuteDays()
return _u
}
// SetCpUser sets the "cp_user" edge to the CpUser entity.
func (_u *CpTaskUpdateOne) SetCpUser(v *CpUser) *CpTaskUpdateOne {
return _u.SetCpUserID(v.ID)
@@ -1169,6 +1217,17 @@ func (_u *CpTaskUpdateOne) sqlSave(ctx context.Context) (_node *CpTask, err erro
if _u.mutation.NextExecuteTimeCleared() {
_spec.ClearField(cptask.FieldNextExecuteTime, field.TypeInt64)
}
if value, ok := _u.mutation.ExecuteDays(); ok {
_spec.SetField(cptask.FieldExecuteDays, field.TypeJSON, value)
}
if value, ok := _u.mutation.AppendedExecuteDays(); ok {
_spec.AddModifier(func(u *sql.UpdateBuilder) {
sqljson.Append(u, cptask.FieldExecuteDays, value)
})
}
if _u.mutation.ExecuteDaysCleared() {
_spec.ClearField(cptask.FieldExecuteDays, field.TypeJSON)
}
if _u.mutation.CpUserCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
+12 -1
View File
@@ -37,6 +37,8 @@ type CpUser struct {
MobileCode string `json:"mobile_code,omitempty"`
// Cookie holds the value of the "cookie" field.
Cookie string `json:"cookie,omitempty"`
// Tgc holds the value of the "tgc" field.
Tgc string `json:"tgc,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.
@@ -125,7 +127,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, cpuser.FieldLoginType:
case cpuser.FieldUsername, cpuser.FieldPassword, cpuser.FieldSchoolID, cpuser.FieldEmail, cpuser.FieldMobileCode, cpuser.FieldCookie, cpuser.FieldTgc, cpuser.FieldMobileType, cpuser.FieldLoginType:
values[i] = new(sql.NullString)
default:
values[i] = new(sql.UnknownType)
@@ -202,6 +204,12 @@ func (_m *CpUser) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.Cookie = value.String
}
case cpuser.FieldTgc:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field tgc", values[i])
} else if value.Valid {
_m.Tgc = value.String
}
case cpuser.FieldMobileType:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field mobile_type", values[i])
@@ -302,6 +310,9 @@ func (_m *CpUser) String() string {
builder.WriteString("cookie=")
builder.WriteString(_m.Cookie)
builder.WriteString(", ")
builder.WriteString("tgc=")
builder.WriteString(_m.Tgc)
builder.WriteString(", ")
builder.WriteString("mobile_type=")
builder.WriteString(fmt.Sprintf("%v", _m.MobileType))
builder.WriteString(", ")
+8
View File
@@ -34,6 +34,8 @@ const (
FieldMobileCode = "mobile_code"
// FieldCookie holds the string denoting the cookie field in the database.
FieldCookie = "cookie"
// FieldTgc holds the string denoting the tgc field in the database.
FieldTgc = "tgc"
// 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.
@@ -99,6 +101,7 @@ var Columns = []string{
FieldEmail,
FieldMobileCode,
FieldCookie,
FieldTgc,
FieldMobileType,
FieldLoginType,
}
@@ -232,6 +235,11 @@ func ByCookie(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCookie, opts...).ToFunc()
}
// ByTgc orders the results by the tgc field.
func ByTgc(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldTgc, opts...).ToFunc()
}
// ByMobileType orders the results by the mobile_type field.
func ByMobileType(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldMobileType, opts...).ToFunc()
+80
View File
@@ -101,6 +101,11 @@ func Cookie(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldEQ(FieldCookie, v))
}
// Tgc applies equality check predicate on the "tgc" field. It's identical to TgcEQ.
func Tgc(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldEQ(FieldTgc, v))
}
// CreateTimeEQ applies the EQ predicate on the "create_time" field.
func CreateTimeEQ(v *ent2.CustomTime) predicate.CpUser {
return predicate.CpUser(sql.FieldEQ(FieldCreateTime, v))
@@ -691,6 +696,81 @@ func CookieContainsFold(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldContainsFold(FieldCookie, v))
}
// TgcEQ applies the EQ predicate on the "tgc" field.
func TgcEQ(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldEQ(FieldTgc, v))
}
// TgcNEQ applies the NEQ predicate on the "tgc" field.
func TgcNEQ(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldNEQ(FieldTgc, v))
}
// TgcIn applies the In predicate on the "tgc" field.
func TgcIn(vs ...string) predicate.CpUser {
return predicate.CpUser(sql.FieldIn(FieldTgc, vs...))
}
// TgcNotIn applies the NotIn predicate on the "tgc" field.
func TgcNotIn(vs ...string) predicate.CpUser {
return predicate.CpUser(sql.FieldNotIn(FieldTgc, vs...))
}
// TgcGT applies the GT predicate on the "tgc" field.
func TgcGT(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldGT(FieldTgc, v))
}
// TgcGTE applies the GTE predicate on the "tgc" field.
func TgcGTE(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldGTE(FieldTgc, v))
}
// TgcLT applies the LT predicate on the "tgc" field.
func TgcLT(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldLT(FieldTgc, v))
}
// TgcLTE applies the LTE predicate on the "tgc" field.
func TgcLTE(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldLTE(FieldTgc, v))
}
// TgcContains applies the Contains predicate on the "tgc" field.
func TgcContains(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldContains(FieldTgc, v))
}
// TgcHasPrefix applies the HasPrefix predicate on the "tgc" field.
func TgcHasPrefix(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldHasPrefix(FieldTgc, v))
}
// TgcHasSuffix applies the HasSuffix predicate on the "tgc" field.
func TgcHasSuffix(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldHasSuffix(FieldTgc, v))
}
// TgcIsNil applies the IsNil predicate on the "tgc" field.
func TgcIsNil() predicate.CpUser {
return predicate.CpUser(sql.FieldIsNull(FieldTgc))
}
// TgcNotNil applies the NotNil predicate on the "tgc" field.
func TgcNotNil() predicate.CpUser {
return predicate.CpUser(sql.FieldNotNull(FieldTgc))
}
// TgcEqualFold applies the EqualFold predicate on the "tgc" field.
func TgcEqualFold(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldEqualFold(FieldTgc, v))
}
// TgcContainsFold applies the ContainsFold predicate on the "tgc" field.
func TgcContainsFold(v string) predicate.CpUser {
return predicate.CpUser(sql.FieldContainsFold(FieldTgc, v))
}
// MobileTypeEQ applies the EQ predicate on the "mobile_type" field.
func MobileTypeEQ(v MobileType) predicate.CpUser {
return predicate.CpUser(sql.FieldEQ(FieldMobileType, v))
+78
View File
@@ -113,6 +113,20 @@ func (_c *CpUserCreate) SetNillableCookie(v *string) *CpUserCreate {
return _c
}
// SetTgc sets the "tgc" field.
func (_c *CpUserCreate) SetTgc(v string) *CpUserCreate {
_c.mutation.SetTgc(v)
return _c
}
// SetNillableTgc sets the "tgc" field if the given value is not nil.
func (_c *CpUserCreate) SetNillableTgc(v *string) *CpUserCreate {
if v != nil {
_c.SetTgc(*v)
}
return _c
}
// SetMobileType sets the "mobile_type" field.
func (_c *CpUserCreate) SetMobileType(v cpuser.MobileType) *CpUserCreate {
_c.mutation.SetMobileType(v)
@@ -357,6 +371,10 @@ func (_c *CpUserCreate) createSpec() (*CpUser, *sqlgraph.CreateSpec) {
_spec.SetField(cpuser.FieldCookie, field.TypeString, value)
_node.Cookie = value
}
if value, ok := _c.mutation.Tgc(); ok {
_spec.SetField(cpuser.FieldTgc, field.TypeString, value)
_node.Tgc = value
}
if value, ok := _c.mutation.MobileType(); ok {
_spec.SetField(cpuser.FieldMobileType, field.TypeEnum, value)
_node.MobileType = value
@@ -624,6 +642,24 @@ func (u *CpUserUpsert) ClearCookie() *CpUserUpsert {
return u
}
// SetTgc sets the "tgc" field.
func (u *CpUserUpsert) SetTgc(v string) *CpUserUpsert {
u.Set(cpuser.FieldTgc, v)
return u
}
// UpdateTgc sets the "tgc" field to the value that was provided on create.
func (u *CpUserUpsert) UpdateTgc() *CpUserUpsert {
u.SetExcluded(cpuser.FieldTgc)
return u
}
// ClearTgc clears the value of the "tgc" field.
func (u *CpUserUpsert) ClearTgc() *CpUserUpsert {
u.SetNull(cpuser.FieldTgc)
return u
}
// SetMobileType sets the "mobile_type" field.
func (u *CpUserUpsert) SetMobileType(v cpuser.MobileType) *CpUserUpsert {
u.Set(cpuser.FieldMobileType, v)
@@ -833,6 +869,27 @@ func (u *CpUserUpsertOne) ClearCookie() *CpUserUpsertOne {
})
}
// SetTgc sets the "tgc" field.
func (u *CpUserUpsertOne) SetTgc(v string) *CpUserUpsertOne {
return u.Update(func(s *CpUserUpsert) {
s.SetTgc(v)
})
}
// UpdateTgc sets the "tgc" field to the value that was provided on create.
func (u *CpUserUpsertOne) UpdateTgc() *CpUserUpsertOne {
return u.Update(func(s *CpUserUpsert) {
s.UpdateTgc()
})
}
// ClearTgc clears the value of the "tgc" field.
func (u *CpUserUpsertOne) ClearTgc() *CpUserUpsertOne {
return u.Update(func(s *CpUserUpsert) {
s.ClearTgc()
})
}
// SetMobileType sets the "mobile_type" field.
func (u *CpUserUpsertOne) SetMobileType(v cpuser.MobileType) *CpUserUpsertOne {
return u.Update(func(s *CpUserUpsert) {
@@ -1212,6 +1269,27 @@ func (u *CpUserUpsertBulk) ClearCookie() *CpUserUpsertBulk {
})
}
// SetTgc sets the "tgc" field.
func (u *CpUserUpsertBulk) SetTgc(v string) *CpUserUpsertBulk {
return u.Update(func(s *CpUserUpsert) {
s.SetTgc(v)
})
}
// UpdateTgc sets the "tgc" field to the value that was provided on create.
func (u *CpUserUpsertBulk) UpdateTgc() *CpUserUpsertBulk {
return u.Update(func(s *CpUserUpsert) {
s.UpdateTgc()
})
}
// ClearTgc clears the value of the "tgc" field.
func (u *CpUserUpsertBulk) ClearTgc() *CpUserUpsertBulk {
return u.Update(func(s *CpUserUpsert) {
s.ClearTgc()
})
}
// SetMobileType sets the "mobile_type" field.
func (u *CpUserUpsertBulk) SetMobileType(v cpuser.MobileType) *CpUserUpsertBulk {
return u.Update(func(s *CpUserUpsert) {
+52
View File
@@ -163,6 +163,26 @@ func (_u *CpUserUpdate) ClearCookie() *CpUserUpdate {
return _u
}
// SetTgc sets the "tgc" field.
func (_u *CpUserUpdate) SetTgc(v string) *CpUserUpdate {
_u.mutation.SetTgc(v)
return _u
}
// SetNillableTgc sets the "tgc" field if the given value is not nil.
func (_u *CpUserUpdate) SetNillableTgc(v *string) *CpUserUpdate {
if v != nil {
_u.SetTgc(*v)
}
return _u
}
// ClearTgc clears the value of the "tgc" field.
func (_u *CpUserUpdate) ClearTgc() *CpUserUpdate {
_u.mutation.ClearTgc()
return _u
}
// SetMobileType sets the "mobile_type" field.
func (_u *CpUserUpdate) SetMobileType(v cpuser.MobileType) *CpUserUpdate {
_u.mutation.SetMobileType(v)
@@ -438,6 +458,12 @@ func (_u *CpUserUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if _u.mutation.CookieCleared() {
_spec.ClearField(cpuser.FieldCookie, field.TypeString)
}
if value, ok := _u.mutation.Tgc(); ok {
_spec.SetField(cpuser.FieldTgc, field.TypeString, value)
}
if _u.mutation.TgcCleared() {
_spec.ClearField(cpuser.FieldTgc, field.TypeString)
}
if value, ok := _u.mutation.MobileType(); ok {
_spec.SetField(cpuser.FieldMobileType, field.TypeEnum, value)
}
@@ -802,6 +828,26 @@ func (_u *CpUserUpdateOne) ClearCookie() *CpUserUpdateOne {
return _u
}
// SetTgc sets the "tgc" field.
func (_u *CpUserUpdateOne) SetTgc(v string) *CpUserUpdateOne {
_u.mutation.SetTgc(v)
return _u
}
// SetNillableTgc sets the "tgc" field if the given value is not nil.
func (_u *CpUserUpdateOne) SetNillableTgc(v *string) *CpUserUpdateOne {
if v != nil {
_u.SetTgc(*v)
}
return _u
}
// ClearTgc clears the value of the "tgc" field.
func (_u *CpUserUpdateOne) ClearTgc() *CpUserUpdateOne {
_u.mutation.ClearTgc()
return _u
}
// SetMobileType sets the "mobile_type" field.
func (_u *CpUserUpdateOne) SetMobileType(v cpuser.MobileType) *CpUserUpdateOne {
_u.mutation.SetMobileType(v)
@@ -1107,6 +1153,12 @@ func (_u *CpUserUpdateOne) sqlSave(ctx context.Context) (_node *CpUser, err erro
if _u.mutation.CookieCleared() {
_spec.ClearField(cpuser.FieldCookie, field.TypeString)
}
if value, ok := _u.mutation.Tgc(); ok {
_spec.SetField(cpuser.FieldTgc, field.TypeString, value)
}
if _u.mutation.TgcCleared() {
_spec.ClearField(cpuser.FieldTgc, field.TypeString)
}
if value, ok := _u.mutation.MobileType(); ok {
_spec.SetField(cpuser.FieldMobileType, field.TypeEnum, value)
}
+12
View File
@@ -146,6 +146,7 @@ var schemaGraph = func() *sqlgraph.Schema {
cptask.FieldExecuteHour: {Type: field.TypeInt, Column: cptask.FieldExecuteHour},
cptask.FieldExecuteMinute: {Type: field.TypeInt, Column: cptask.FieldExecuteMinute},
cptask.FieldNextExecuteTime: {Type: field.TypeInt64, Column: cptask.FieldNextExecuteTime},
cptask.FieldExecuteDays: {Type: field.TypeJSON, Column: cptask.FieldExecuteDays},
},
}
graph.Nodes[5] = &sqlgraph.Node{
@@ -189,6 +190,7 @@ var schemaGraph = func() *sqlgraph.Schema {
cpuser.FieldEmail: {Type: field.TypeString, Column: cpuser.FieldEmail},
cpuser.FieldMobileCode: {Type: field.TypeString, Column: cpuser.FieldMobileCode},
cpuser.FieldCookie: {Type: field.TypeString, Column: cpuser.FieldCookie},
cpuser.FieldTgc: {Type: field.TypeString, Column: cpuser.FieldTgc},
cpuser.FieldMobileType: {Type: field.TypeEnum, Column: cpuser.FieldMobileType},
cpuser.FieldLoginType: {Type: field.TypeEnum, Column: cpuser.FieldLoginType},
},
@@ -1381,6 +1383,11 @@ func (f *CpTaskFilter) WhereNextExecuteTime(p entql.Int64P) {
f.Where(p.Field(cptask.FieldNextExecuteTime))
}
// WhereExecuteDays applies the entql json.RawMessage predicate on the execute_days field.
func (f *CpTaskFilter) WhereExecuteDays(p entql.BytesP) {
f.Where(p.Field(cptask.FieldExecuteDays))
}
// WhereHasCpUser applies a predicate to check if query has an edge cp_user.
func (f *CpTaskFilter) WhereHasCpUser() {
f.Where(entql.HasEdge("cp_user"))
@@ -1630,6 +1637,11 @@ func (f *CpUserFilter) WhereCookie(p entql.StringP) {
f.Where(p.Field(cpuser.FieldCookie))
}
// WhereTgc applies the entql string predicate on the tgc field.
func (f *CpUserFilter) WhereTgc(p entql.StringP) {
f.Where(p.Field(cpuser.FieldTgc))
}
// WhereMobileType applies the entql string predicate on the mobile_type field.
func (f *CpUserFilter) WhereMobileType(p entql.StringP) {
f.Where(p.Field(cpuser.FieldMobileType))
File diff suppressed because one or more lines are too long
+6 -4
View File
@@ -134,6 +134,7 @@ var (
{Name: "execute_hour", Type: field.TypeInt, Nullable: true},
{Name: "execute_minute", Type: field.TypeInt, Nullable: true},
{Name: "next_execute_time", Type: field.TypeInt64, Nullable: true},
{Name: "execute_days", Type: field.TypeJSON, Nullable: true},
{Name: "cp_user_id", Type: field.TypeInt64},
{Name: "user_id", Type: field.TypeInt64},
}
@@ -145,13 +146,13 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "cp_tasks_cp_users_cptasks",
Columns: []*schema.Column{CpTasksColumns[15]},
Columns: []*schema.Column{CpTasksColumns[16]},
RefColumns: []*schema.Column{CpUsersColumns[0]},
OnDelete: schema.Cascade,
},
{
Symbol: "cp_tasks_sys_user_cp_task",
Columns: []*schema.Column{CpTasksColumns[16]},
Columns: []*schema.Column{CpTasksColumns[17]},
RefColumns: []*schema.Column{SysUserColumns[0]},
OnDelete: schema.Cascade,
},
@@ -205,6 +206,7 @@ var (
{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: "tgc", 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},
@@ -218,13 +220,13 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "cp_users_cp_schools_cpusers",
Columns: []*schema.Column{CpUsersColumns[10]},
Columns: []*schema.Column{CpUsersColumns[11]},
RefColumns: []*schema.Column{CpSchoolsColumns[0]},
OnDelete: schema.NoAction,
},
{
Symbol: "cp_users_sys_user_cpuser",
Columns: []*schema.Column{CpUsersColumns[11]},
Columns: []*schema.Column{CpUsersColumns[12]},
RefColumns: []*schema.Column{SysUserColumns[0]},
OnDelete: schema.Cascade,
},
+165 -2
View File
@@ -3766,6 +3766,8 @@ type CpTaskMutation struct {
addexecute_minute *int
next_execute_time *int64
addnext_execute_time *int64
execute_days *[]int
appendexecute_days []int
clearedFields map[string]struct{}
cp_user *int64
clearedcp_user bool
@@ -4621,6 +4623,71 @@ func (m *CpTaskMutation) ResetNextExecuteTime() {
delete(m.clearedFields, cptask.FieldNextExecuteTime)
}
// SetExecuteDays sets the "execute_days" field.
func (m *CpTaskMutation) SetExecuteDays(i []int) {
m.execute_days = &i
m.appendexecute_days = nil
}
// ExecuteDays returns the value of the "execute_days" field in the mutation.
func (m *CpTaskMutation) ExecuteDays() (r []int, exists bool) {
v := m.execute_days
if v == nil {
return
}
return *v, true
}
// OldExecuteDays returns the old "execute_days" field's value of the CpTask entity.
// If the CpTask 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 *CpTaskMutation) OldExecuteDays(ctx context.Context) (v []int, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldExecuteDays is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldExecuteDays requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldExecuteDays: %w", err)
}
return oldValue.ExecuteDays, nil
}
// AppendExecuteDays adds i to the "execute_days" field.
func (m *CpTaskMutation) AppendExecuteDays(i []int) {
m.appendexecute_days = append(m.appendexecute_days, i...)
}
// AppendedExecuteDays returns the list of values that were appended to the "execute_days" field in this mutation.
func (m *CpTaskMutation) AppendedExecuteDays() ([]int, bool) {
if len(m.appendexecute_days) == 0 {
return nil, false
}
return m.appendexecute_days, true
}
// ClearExecuteDays clears the value of the "execute_days" field.
func (m *CpTaskMutation) ClearExecuteDays() {
m.execute_days = nil
m.appendexecute_days = nil
m.clearedFields[cptask.FieldExecuteDays] = struct{}{}
}
// ExecuteDaysCleared returns if the "execute_days" field was cleared in this mutation.
func (m *CpTaskMutation) ExecuteDaysCleared() bool {
_, ok := m.clearedFields[cptask.FieldExecuteDays]
return ok
}
// ResetExecuteDays resets all changes to the "execute_days" field.
func (m *CpTaskMutation) ResetExecuteDays() {
m.execute_days = nil
m.appendexecute_days = nil
delete(m.clearedFields, cptask.FieldExecuteDays)
}
// ClearCpUser clears the "cp_user" edge to the CpUser entity.
func (m *CpTaskMutation) ClearCpUser() {
m.clearedcp_user = true
@@ -4776,7 +4843,7 @@ func (m *CpTaskMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *CpTaskMutation) Fields() []string {
fields := make([]string, 0, 16)
fields := make([]string, 0, 17)
if m.create_time != nil {
fields = append(fields, cptask.FieldCreateTime)
}
@@ -4825,6 +4892,9 @@ func (m *CpTaskMutation) Fields() []string {
if m.next_execute_time != nil {
fields = append(fields, cptask.FieldNextExecuteTime)
}
if m.execute_days != nil {
fields = append(fields, cptask.FieldExecuteDays)
}
return fields
}
@@ -4865,6 +4935,8 @@ func (m *CpTaskMutation) Field(name string) (ent.Value, bool) {
return m.ExecuteMinute()
case cptask.FieldNextExecuteTime:
return m.NextExecuteTime()
case cptask.FieldExecuteDays:
return m.ExecuteDays()
}
return nil, false
}
@@ -4906,6 +4978,8 @@ func (m *CpTaskMutation) OldField(ctx context.Context, name string) (ent.Value,
return m.OldExecuteMinute(ctx)
case cptask.FieldNextExecuteTime:
return m.OldNextExecuteTime(ctx)
case cptask.FieldExecuteDays:
return m.OldExecuteDays(ctx)
}
return nil, fmt.Errorf("unknown CpTask field %s", name)
}
@@ -5027,6 +5101,13 @@ func (m *CpTaskMutation) SetField(name string, value ent.Value) error {
}
m.SetNextExecuteTime(v)
return nil
case cptask.FieldExecuteDays:
v, ok := value.([]int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetExecuteDays(v)
return nil
}
return fmt.Errorf("unknown CpTask field %s", name)
}
@@ -5135,6 +5216,9 @@ func (m *CpTaskMutation) ClearedFields() []string {
if m.FieldCleared(cptask.FieldNextExecuteTime) {
fields = append(fields, cptask.FieldNextExecuteTime)
}
if m.FieldCleared(cptask.FieldExecuteDays) {
fields = append(fields, cptask.FieldExecuteDays)
}
return fields
}
@@ -5164,6 +5248,9 @@ func (m *CpTaskMutation) ClearField(name string) error {
case cptask.FieldNextExecuteTime:
m.ClearNextExecuteTime()
return nil
case cptask.FieldExecuteDays:
m.ClearExecuteDays()
return nil
}
return fmt.Errorf("unknown CpTask nullable field %s", name)
}
@@ -5220,6 +5307,9 @@ func (m *CpTaskMutation) ResetField(name string) error {
case cptask.FieldNextExecuteTime:
m.ResetNextExecuteTime()
return nil
case cptask.FieldExecuteDays:
m.ResetExecuteDays()
return nil
}
return fmt.Errorf("unknown CpTask field %s", name)
}
@@ -6251,6 +6341,7 @@ type CpUserMutation struct {
email *string
mobile_code *string
cookie *string
tgc *string
mobile_type *cpuser.MobileType
login_type *cpuser.LoginType
clearedFields map[string]struct{}
@@ -6746,6 +6837,55 @@ func (m *CpUserMutation) ResetCookie() {
delete(m.clearedFields, cpuser.FieldCookie)
}
// SetTgc sets the "tgc" field.
func (m *CpUserMutation) SetTgc(s string) {
m.tgc = &s
}
// Tgc returns the value of the "tgc" field in the mutation.
func (m *CpUserMutation) Tgc() (r string, exists bool) {
v := m.tgc
if v == nil {
return
}
return *v, true
}
// OldTgc returns the old "tgc" 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) OldTgc(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldTgc is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldTgc requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldTgc: %w", err)
}
return oldValue.Tgc, nil
}
// ClearTgc clears the value of the "tgc" field.
func (m *CpUserMutation) ClearTgc() {
m.tgc = nil
m.clearedFields[cpuser.FieldTgc] = struct{}{}
}
// TgcCleared returns if the "tgc" field was cleared in this mutation.
func (m *CpUserMutation) TgcCleared() bool {
_, ok := m.clearedFields[cpuser.FieldTgc]
return ok
}
// ResetTgc resets all changes to the "tgc" field.
func (m *CpUserMutation) ResetTgc() {
m.tgc = nil
delete(m.clearedFields, cpuser.FieldTgc)
}
// SetMobileType sets the "mobile_type" field.
func (m *CpUserMutation) SetMobileType(ct cpuser.MobileType) {
m.mobile_type = &ct
@@ -7081,7 +7221,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, 11)
fields := make([]string, 0, 12)
if m.create_time != nil {
fields = append(fields, cpuser.FieldCreateTime)
}
@@ -7109,6 +7249,9 @@ func (m *CpUserMutation) Fields() []string {
if m.cookie != nil {
fields = append(fields, cpuser.FieldCookie)
}
if m.tgc != nil {
fields = append(fields, cpuser.FieldTgc)
}
if m.mobile_type != nil {
fields = append(fields, cpuser.FieldMobileType)
}
@@ -7141,6 +7284,8 @@ func (m *CpUserMutation) Field(name string) (ent.Value, bool) {
return m.MobileCode()
case cpuser.FieldCookie:
return m.Cookie()
case cpuser.FieldTgc:
return m.Tgc()
case cpuser.FieldMobileType:
return m.MobileType()
case cpuser.FieldLoginType:
@@ -7172,6 +7317,8 @@ func (m *CpUserMutation) OldField(ctx context.Context, name string) (ent.Value,
return m.OldMobileCode(ctx)
case cpuser.FieldCookie:
return m.OldCookie(ctx)
case cpuser.FieldTgc:
return m.OldTgc(ctx)
case cpuser.FieldMobileType:
return m.OldMobileType(ctx)
case cpuser.FieldLoginType:
@@ -7248,6 +7395,13 @@ func (m *CpUserMutation) SetField(name string, value ent.Value) error {
}
m.SetCookie(v)
return nil
case cpuser.FieldTgc:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetTgc(v)
return nil
case cpuser.FieldMobileType:
v, ok := value.(cpuser.MobileType)
if !ok {
@@ -7307,6 +7461,9 @@ func (m *CpUserMutation) ClearedFields() []string {
if m.FieldCleared(cpuser.FieldCookie) {
fields = append(fields, cpuser.FieldCookie)
}
if m.FieldCleared(cpuser.FieldTgc) {
fields = append(fields, cpuser.FieldTgc)
}
return fields
}
@@ -7333,6 +7490,9 @@ func (m *CpUserMutation) ClearField(name string) error {
case cpuser.FieldCookie:
m.ClearCookie()
return nil
case cpuser.FieldTgc:
m.ClearTgc()
return nil
}
return fmt.Errorf("unknown CpUser nullable field %s", name)
}
@@ -7368,6 +7528,9 @@ func (m *CpUserMutation) ResetField(name string) error {
case cpuser.FieldCookie:
m.ResetCookie()
return nil
case cpuser.FieldTgc:
m.ResetTgc()
return nil
case cpuser.FieldMobileType:
m.ResetMobileType()
return nil
+1
View File
@@ -31,6 +31,7 @@ func (CpTask) Fields() []ent.Field {
field.Int("execute_hour").Optional().Nillable(),
field.Int("execute_minute").Optional().Nillable(),
field.Int64("next_execute_time").Optional().Nillable(),
field.JSON("execute_days", []int{}).Optional().Comment("执行星期 1-7,1=周一,7=周日"),
}
}
+1
View File
@@ -24,6 +24,7 @@ func (CpUser) Fields() []ent.Field {
field.String("email").Optional(),
field.String("mobile_code").Optional(),
field.Text("cookie").Optional(),
field.Text("tgc").Optional(),
field.Enum("mobile_type").Values("random", "authorize"),
field.Enum("login_type").Values("account", "phone").Default("account"),
}
+80
View File
@@ -0,0 +1,80 @@
package logger
import (
"os"
"path/filepath"
"strings"
"github.com/shirou/gopsutil/v3/process"
)
func IsDebug() bool {
return isGolandDebug() || isDebugByPath()
}
func isDebugByPath() bool {
exePath := strings.ToLower(os.Args[0])
// 获取绝对路径
absPath, err := filepath.Abs(exePath)
if err != nil {
absPath = exePath
}
// 检查路径中是否包含常见的临时目录或"go-build"字样
tmpDir := strings.ToLower(getTempDir())
isTemp := strings.Contains(absPath, tmpDir) || strings.Contains(absPath, "go-build") || strings.Contains(absPath, "go_build_")
return isTemp
}
func getTempDir() string {
tempDir := os.TempDir()
// 尝试转换为长路径
if longPath, err := filepath.EvalSymlinks(tempDir); err == nil {
if absPath, err := filepath.Abs(longPath); err == nil {
return absPath
}
return longPath
}
return filepath.Dir(tempDir)
}
func isGolandDebug() bool {
// 获取当前进程ID
pid := os.Getpid()
// 获取当前进程信息
currentProcess, err := process.NewProcess(int32(pid))
if err != nil {
return false
}
// 获取父进程
parent, err := currentProcess.Parent()
if err != nil {
return false
}
// 获取父进程名称
name, err := parent.Name()
if err != nil {
return false
}
// 检查父进程是否是dlv(Go调试器)或GoLand相关进程
name = strings.ToLower(name)
debugProcessNames := []string{
"dlv.exe", "dlv",
"goland.exe", "goland64.exe",
"goland", "idea.exe",
}
for _, debugName := range debugProcessNames {
if strings.Contains(name, debugName) {
return true
}
}
return false
}
+5
View File
@@ -13,6 +13,11 @@ var Logger *zap.Logger
func init() {
// Create a production config and customize the time encoder
config := zap.NewProductionConfig()
if IsDebug() {
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel) // Enable debug logs
} else {
config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
}
config.EncoderConfig.EncodeTime = BeijingTimeEncoder
// Build the logger with the custom config
+36 -10
View File
@@ -4,10 +4,12 @@ import (
"bytes"
"context"
"encoding/gob"
"likedream-goframe/internal/application/cpdaily/imageuploader"
"likedream-goframe/internal/application/cpdaily/login"
"likedream-goframe/internal/application/cpdaily/school"
task2 "likedream-goframe/internal/application/cpdaily/task"
"likedream-goframe/internal/application/cpdaily/task/formtype"
"likedream-goframe/internal/application/cpdaily/types"
"likedream-goframe/internal/common/db"
errors2 "likedream-goframe/internal/common/errors"
"likedream-goframe/internal/ent"
@@ -22,6 +24,7 @@ import (
"time"
"github.com/bytedance/sonic"
"github.com/google/uuid"
"github.com/hibiken/asynq"
"github.com/samber/oops"
"go.uber.org/zap"
@@ -43,15 +46,18 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
// 先注册错误处理,确保任何阶段的错误都能被捕获并重置状态
var targetTask *ent.CpTask
defer utils.HandleError(func(err error) {
// 使用独立的 context,避免 asynq 超时 cancel ctx 后数据库操作失败导致状态无法更新
bgCtx := context.Background()
// 如果 targetTask 还没查询到,只能重置状态为 not_start,让任务重新入队
if targetTask == nil {
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusNotStart).Exec(ctx)
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusNotStart).Exec(bgCtx)
panic(err)
}
// 执行失败应该更新下次执行时间
executeTime := utils.GetCpdailyNextExecuteTime(targetTask)
e := db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetNextExecuteTime(executeTime).SetTaskStatus(cptask.TaskStatusFail).Exec(ctx)
e := db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetNextExecuteTime(executeTime).SetTaskStatus(cptask.TaskStatusFail).Exec(bgCtx)
utils.LogError(ctx, "设置任务状态失败", oops.With("task_id", taskPayload.CpTaskID).Wrap(e))
var errorMessage string
@@ -97,15 +103,19 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
// 查询学校详情
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, targetTask.Edges.CpUser.LoginType) {
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
loginExecutor.Login(ctx, &schoolDetail, targetTask.Edges.CpUser.Username, targetTask.Edges.CpUser.Password)
cookie = loginExecutor.GetCookieString()
db.Global.CpUser.UpdateOneID(targetTask.Edges.CpUser.ID).SetCookie(cookie).ExecX(ctx)
restyClient = login.GetClient(ctx, cookie)
refreshResult := login.EnsureCookieValid(ctx, &login.RefreshCookieParams{
ID: targetTask.Edges.CpUser.ID,
Cookie: targetTask.Edges.CpUser.Cookie,
Tgc: targetTask.Edges.CpUser.Tgc,
LoginType: targetTask.Edges.CpUser.LoginType,
Username: targetTask.Edges.CpUser.Username,
Password: targetTask.Edges.CpUser.Password,
SchoolID: targetTask.Edges.CpUser.SchoolID,
}, &schoolDetail)
if refreshResult.Cookie != targetTask.Edges.CpUser.Cookie {
db.Global.CpUser.UpdateOneID(targetTask.Edges.CpUser.ID).SetCookie(refreshResult.Cookie).SetTgc(refreshResult.Tgc).ExecX(ctx)
}
restyClient := refreshResult.Client
// 获取任务执行handler
taskExecutor := task2.GetCpdailyTaskService(ctx, targetTask.TaskType, restyClient, schoolDetail)
@@ -142,6 +152,22 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
deviceID := targetTask.Edges.CpUser.MobileCode
// 固定获取图片id
imgId := GetNextImgId(customFormItems, targetTask.UsedImg)
// 兼容旧数据:旧数据存的是 S3 的 UUID,新数据存的是今日校园 previewUrl
if imgId != "" && !strings.Contains(imgId, "/") && !strings.Contains(imgId, ":") {
// 不含 / 和 : 说明不是 URL,可能是旧的 UUID,尝试从 S3 上传到今日校园
if parsedUUID, uuidErr := uuid.Parse(imgId); uuidErr == nil {
var imgTaskType types.TaskType
switch targetTask.TaskType {
case cptask.TaskTypeSign:
imgTaskType = types.TaskTypeSign
case cptask.TaskTypeSleepCheck:
imgTaskType = types.TaskTypeSleepCheck
case cptask.TaskTypeCollection:
imgTaskType = types.TaskTypeCollection
}
imgId = imageuploader.UploadImageFromStorage(ctx, schoolDetail.WecloudUrl, parsedUUID, imgTaskType, restyClient)
}
}
submitForm := taskExecutor.FillCustomForm(ctx, cpdailyRealTask, customFormItems, deviceID, imgId)
taskExecutor.Submit(ctx, submitForm, targetTask.Edges.CpUser.Username, deviceID)
+2 -1
View File
@@ -61,11 +61,12 @@ func cleanupStuckTasks(ctx context.Context) {
g.Log().Infof(ctx, "已清理 %d 个 archived 状态的 asynq 任务", len(archivedTasks))
}
// 重置数据库中卡住的任务状态
// 重置数据库中卡住的任务状态(queue 和 doing 都需要重置)
affected, err := db.Global.CpTask.Update().
Where(
cptask.Or(
cptask.TaskStatusEQ(cptask.TaskStatusQueue),
cptask.TaskStatusEQ(cptask.TaskStatusDoing),
),
).
SetTaskStatus(cptask.TaskStatusNotStart).
+16 -1
View File
@@ -112,12 +112,27 @@ func (ts *TaskScanner) scanCpdailyTask() {
func(selector *sql.Selector) {
selector.Where(sql.ExprP("success_count < frequency"))
}).
Select(cptask.FieldID, cptask.FieldExecuteHour, cptask.FieldExecuteMinute, cptask.FieldNextExecuteTime).
Select(cptask.FieldID, cptask.FieldExecuteHour, cptask.FieldExecuteMinute, cptask.FieldNextExecuteTime, cptask.FieldExecuteDays).
AllX(ts.ctx)
var failedIDS []int64
// 当前星期几(ISO: 周一=1, 周日=7)
todayWeekday := int(currentTime.Weekday())
if todayWeekday == 0 {
todayWeekday = 7
}
for _, task := range allTasks {
// 检查今天是否在执行日内
if len(task.ExecuteDays) > 0 && !slices.Contains(task.ExecuteDays, todayWeekday) {
// 今天不执行,重置到下一个执行日
nextExecuteAt := utils.GetCpdailyNextExecuteTime(task)
db.Global.CpTask.UpdateOneID(task.ID).Where(cptask.NextExecuteTimeNotNil()).SetNextExecuteTime(nextExecuteAt).Exec(ts.ctx)
failedIDS = append(failedIDS, task.ID)
continue
}
// 30分钟之前的任务不再执行,但是要重置下次执行时间
if *task.NextExecuteTime < minExecuteTime {
originExecuteTime := time.Unix(*task.NextExecuteTime, 0).In(cst2.TimeZoneShangHai).Format("2006-01-02 15:04:05")
@@ -78,10 +78,6 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
currentMinutes := currentHour*60 + currentMinute
if currentMinutes < startTime || currentMinutes >= endTime {
// logger.Logger.Debug("非暴力模式,当前时间不在执行时间范围内(8:30-17:30),跳过执行",
// zap.Int64("courseID", targetCourse.ID),
// zap.Int("currentHour", currentHour),
// zap.Int("currentMinute", currentMinute))
return nil
}
}
@@ -134,8 +130,12 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
startTime := 8*60 + 30 // 8:30
endTime := 17*60 + 30 // 17:30
currentMinutes := currentHour*60 + currentMinute
return currentMinutes >= startTime && currentMinutes < endTime
needStudy := currentMinutes >= startTime && currentMinutes < endTime
if !needStudy {
// 更新状态为等待中
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).SetStatus(courseentity.StatusWaiting).ExecX(ctx)
}
return needStudy
})
}
@@ -192,7 +192,8 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
// 执行刷课(带AI配置)
err = courseService.AutoStudyByCourseIDWithOptions(ctx, targetCourse.CourseID, &xuexitong.AutoStudyOptions{
AIConfig: aiConfig,
AutoSubmit: false, // 默认不自动提交,仅保存
AutoSubmit: true, // 默认不自动提交,仅保存
Thread: 1,
})
if err != nil {
// 如果是时间限制导致的停止,不算失败
-13
View File
@@ -59,19 +59,6 @@ func (s CourseServiceV1) List(ctx context.Context, req *v1.CourseListReq) *utils
func (s CourseServiceV1) AddCourse(ctx context.Context, req *v1.AddCourseReq) *utils.Response[any] {
user := utils.GetUserInfoFromCtx(ctx)
// 检查是否已经存在相同的刷课任务
exists := db2.Global.CourseEntity.Query().
Where(
courseentity.HasSysUserWith(sysuser.ID(user.ID)),
courseentity.Username(req.Username),
courseentity.CourseID(req.CourseID),
).
ExistX(ctx)
if exists {
utils.OkOrPanic(errors2.NewBusinessError(400, "该课程已存在刷课任务"))
}
// 登录学习通获取节点信息
loginClient := xuexitong.NewXuexitongLogin(ctx)
err := loginClient.Login(ctx, req.Username, req.Password, 3)
+45 -29
View File
@@ -2,15 +2,18 @@ package cpdaily
import (
"context"
"io"
v1 "likedream-goframe/internal/api/v1"
"likedream-goframe/internal/application/filestorage"
"likedream-goframe/internal/application/cpdaily/imageuploader"
"likedream-goframe/internal/application/cpdaily/login"
"likedream-goframe/internal/application/cpdaily/school"
"likedream-goframe/internal/application/cpdaily/types"
"likedream-goframe/internal/common/db"
"likedream-goframe/internal/ent/cpimage"
errors2 "likedream-goframe/internal/common/errors"
"likedream-goframe/internal/ent/cptask"
"likedream-goframe/internal/ent/cpuser"
"likedream-goframe/internal/ent/sysuser"
"likedream-goframe/internal/utils"
"github.com/gogf/gf/v2/frame/g"
"github.com/google/uuid"
"github.com/samber/oops"
)
@@ -19,34 +22,47 @@ type CpImageServiceV1 struct {
func (v CpImageServiceV1) Upload(ctx context.Context, req *v1.UploadImageReq) *utils.Response[any] {
user := utils.GetUserInfoFromCtx(ctx)
file := req.File
open, err := file.Open()
utils.OkOrPanic(err, oops.WithContext(ctx).Wrap)
key := utils.GenerateFileName("cpdaily/")
filestorage.GlobalS3.UploadFile(ctx, key, open)
img := db.Global.CpImage.Create().
SetUserID(user.ID).
SetKey(key).
SetContentType(file.Header.Get("Content-Type")).
SetContentLength(file.Size).
SaveX(ctx)
// 校验 cpUser 归属
cpUser := db.Global.CpUser.Query().
Where(cpuser.ID(req.CpUserId), cpuser.HasSysuserWith(sysuser.ID(user.ID))).
WithSchool().
OnlyX(ctx)
if cpUser == nil {
utils.OkOrPanic(errors2.NewBusinessError(400, "非法的今日校园账号"))
}
schoolDetail := school.GetSchoolDetail(ctx, cpUser.SchoolID)
refreshResult := login.EnsureCookieValid(ctx, &login.RefreshCookieParams{
ID: cpUser.ID, Cookie: cpUser.Cookie, Tgc: cpUser.Tgc,
LoginType: cpUser.LoginType, Username: cpUser.Username,
Password: cpUser.Password, SchoolID: cpUser.SchoolID,
}, &schoolDetail)
if refreshResult.Cookie != cpUser.Cookie {
db.Global.CpUser.UpdateOneID(cpUser.ID).SetCookie(refreshResult.Cookie).SetTgc(refreshResult.Tgc).ExecX(ctx)
}
client := refreshResult.Client
taskType := convertTaskType(ctx, req.TaskType)
previewUrl := imageuploader.UploadImageFromFile(ctx, schoolDetail.WecloudUrl, req.File, taskType, client)
return utils.SuccessWithData[any](v1.UploadImageRes{
FileKey: img.ImageID.String(),
FileSize: &file.Size,
PreviewUrl: previewUrl,
})
}
func (v CpImageServiceV1) Preview(ctx context.Context, req *v1.PreviewImageReq) *utils.Response[any] {
id, err := uuid.Parse(req.Key)
utils.OkOrPanic(err, oops.WithContext(ctx).Wrap)
cpImage := db.Global.CpImage.Query().Where(cpimage.ImageID(id)).OnlyX(ctx)
file := filestorage.GlobalS3.GetFile(ctx, cpImage.Key)
r := g.RequestFromCtx(ctx)
r.Response.Header().Set("Content-Type", "image/png")
// 将文件写入
io.Copy(r.Response.Writer, file)
r.Exit()
return nil
// convertTaskType 将 ent 的 TaskType 转换为 cpdaily types.TaskType
func convertTaskType(ctx context.Context, tt cptask.TaskType) types.TaskType {
switch tt {
case cptask.TaskTypeCollection:
return types.TaskTypeCollection
case cptask.TaskTypeSign:
return types.TaskTypeSign
case cptask.TaskTypeSleepCheck:
return types.TaskTypeSleepCheck
default:
utils.OkOrPanic(errors2.NewBusinessError(400, "不支持的任务类型"), oops.WithContext(ctx).Wrap)
panic("unreachable")
}
}
func NewCpImageServiceV1() *CpImageServiceV1 {
+24 -25
View File
@@ -41,17 +41,16 @@ func (v CpLeaveServiceV1) GetLeaveListFromSystem(ctx context.Context, req *v1.Ge
// 获取学校详情
schoolDetail := school.GetSchoolDetail(ctx, cpUser.SchoolID)
cookie := cpUser.Cookie
client := login.GetClient(ctx, cookie)
// 检查cookie是否有效
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, client, cpUser.LoginType) {
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
loginExecutor.Login(ctx, &schoolDetail, cpUser.Username, cpUser.Password)
cookie = loginExecutor.GetCookieString()
db2.Global.CpUser.UpdateOneID(cpUser.ID).SetCookie(cookie).ExecX(ctx)
client = login.GetClient(ctx, cookie)
refreshResult := login.EnsureCookieValid(ctx, &login.RefreshCookieParams{
ID: cpUser.ID, Cookie: cpUser.Cookie, Tgc: cpUser.Tgc,
LoginType: cpUser.LoginType, Username: cpUser.Username,
Password: cpUser.Password, SchoolID: cpUser.SchoolID,
}, &schoolDetail)
if refreshResult.Cookie != cpUser.Cookie {
db2.Global.CpUser.UpdateOneID(cpUser.ID).SetCookie(refreshResult.Cookie).SetTgc(refreshResult.Tgc).ExecX(ctx)
}
client := refreshResult.Client
// 创建销假服务
reportApp := leave.NewCpdailyReportApp(client, schoolDetail.WecloudUrl)
@@ -149,17 +148,16 @@ func (v CpLeaveServiceV1) Report(ctx context.Context, req *v1.CpLeaveReportReq)
// 获取学校详情
schoolDetail := school.GetSchoolDetail(ctx, cpUser.SchoolID)
cookie := cpUser.Cookie
client := login.GetClient(ctx, cookie)
// 检查cookie是否有效
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, client, cpUser.LoginType) {
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
loginExecutor.Login(ctx, &schoolDetail, cpUser.Username, cpUser.Password)
cookie = loginExecutor.GetCookieString()
db2.Global.CpUser.UpdateOneID(cpUser.ID).SetCookie(cookie).ExecX(ctx)
client = login.GetClient(ctx, cookie)
refreshResult := login.EnsureCookieValid(ctx, &login.RefreshCookieParams{
ID: cpUser.ID, Cookie: cpUser.Cookie, Tgc: cpUser.Tgc,
LoginType: cpUser.LoginType, Username: cpUser.Username,
Password: cpUser.Password, SchoolID: cpUser.SchoolID,
}, &schoolDetail)
if refreshResult.Cookie != cpUser.Cookie {
db2.Global.CpUser.UpdateOneID(cpUser.ID).SetCookie(refreshResult.Cookie).SetTgc(refreshResult.Tgc).ExecX(ctx)
}
client := refreshResult.Client
// 创建销假服务
reportApp := leave.NewCpdailyReportApp(client, schoolDetail.WecloudUrl)
@@ -191,14 +189,15 @@ func (v CpLeaveServiceV1) Upload(ctx context.Context, req *v1.CpLeaveUploadReq)
utils.OkOrPanic(errors2.NewBusinessError(400, "非法的今日校园账号"))
}
schoolDetail := school.GetSchoolDetail(ctx, cpUser.SchoolID)
client := login.GetClient(ctx, cpUser.Cookie)
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, client, cpUser.LoginType) {
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
loginExecutor.Login(ctx, &schoolDetail, cpUser.Username, cpUser.Password)
cookie := loginExecutor.GetCookieString()
db2.Global.CpUser.UpdateOne(cpUser).SetCookie(cookie).ExecX(ctx)
client = login.GetClient(ctx, cookie)
refreshResult := login.EnsureCookieValid(ctx, &login.RefreshCookieParams{
ID: cpUser.ID, Cookie: cpUser.Cookie, Tgc: cpUser.Tgc,
LoginType: cpUser.LoginType, Username: cpUser.Username,
Password: cpUser.Password, SchoolID: cpUser.SchoolID,
}, &schoolDetail)
if refreshResult.Cookie != cpUser.Cookie {
db2.Global.CpUser.UpdateOne(cpUser).SetCookie(refreshResult.Cookie).SetTgc(refreshResult.Tgc).ExecX(ctx)
}
client := refreshResult.Client
// 创建销假服务
reportApp := leave.NewCpdailyReportApp(client, schoolDetail.WecloudUrl)
+19 -15
View File
@@ -43,14 +43,15 @@ func (v CpTaskServiceV1) GetRecentTasks(ctx context.Context, req *v1.GetRecentTa
utils.OkOrPanic(errors2.NewBusinessError(400, "非法的今日校园账号"))
}
schoolDetail := school.GetSchoolDetail(ctx, cpUser.SchoolID)
client := login.GetClient(ctx, cpUser.Cookie)
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, client, cpUser.LoginType) {
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
loginExecutor.Login(ctx, &schoolDetail, cpUser.Username, cpUser.Password)
cookie := loginExecutor.GetCookieString()
db2.Global.CpUser.UpdateOne(cpUser).SetCookie(cookie).ExecX(ctx)
client = login.GetClient(ctx, cookie)
refreshResult := login.EnsureCookieValid(ctx, &login.RefreshCookieParams{
ID: cpUser.ID, Cookie: cpUser.Cookie, Tgc: cpUser.Tgc,
LoginType: cpUser.LoginType, Username: cpUser.Username,
Password: cpUser.Password, SchoolID: cpUser.SchoolID,
}, &schoolDetail)
if refreshResult.Cookie != cpUser.Cookie {
db2.Global.CpUser.UpdateOne(cpUser).SetCookie(refreshResult.Cookie).SetTgc(refreshResult.Tgc).ExecX(ctx)
}
client := refreshResult.Client
taskService := task.GetCpdailyTaskService(ctx, req.Type, client, schoolDetail)
titleList := taskService.GetRecentTaskTitle(ctx)
return utils.SuccessWithData[[]string](titleList)
@@ -118,17 +119,19 @@ 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, cpUser.LoginType) {
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
loginExecutor.Login(ctx, &schoolDetail, cpUser.Username, cpUser.Password)
cookie = loginExecutor.GetCookieString()
db2.Global.CpUser.UpdateOneID(cpUser.ID).SetCookie(cookie).ExecX(ctx)
client = login.GetClient(ctx, cookie)
refreshResult := login.EnsureCookieValid(ctx, &login.RefreshCookieParams{
ID: cpUser.ID, Cookie: cookie, Tgc: cpUser.Tgc,
LoginType: cpUser.LoginType, Username: cpUser.Username,
Password: cpUser.Password, SchoolID: cpUser.SchoolID,
}, &schoolDetail)
if refreshResult.Cookie != cookie {
db2.Global.CpUser.UpdateOneID(cpUser.ID).SetCookie(refreshResult.Cookie).SetTgc(refreshResult.Tgc).ExecX(ctx)
}
client := refreshResult.Client
taskService := task.GetCpdailyTaskService(ctx, cpTask.TaskType, client, schoolDetail)
formMap := taskService.QueryFormByTitle(ctx, cpTask.Title)
formDetail := taskService.ParseFormJson(ctx, formMap)
return utils.SuccessWithData[any](formDetail)
}
@@ -196,7 +199,8 @@ func (v CpTaskServiceV1) PostExecute(ctx context.Context, req *v1.PostCpTaskExec
SetTaskStatus(cptask.TaskStatusNotStart).
SetNotificationType(req.NotificationType).
SetExecuteHour(req.ExecuteHour).
SetExecuteMinute(req.ExecuteMinute)
SetExecuteMinute(req.ExecuteMinute).
SetExecuteDays(req.ExecuteDays)
if cpTask.Step == cptask.StepExecute {
update.SetTaskStatus(cptask.TaskStatusNotStart)
}
+8 -3
View File
@@ -94,6 +94,7 @@ func (v CpUserServiceV1) List(ctx context.Context, req *v1.CpUserListReq) *utils
for _, v := range data {
v.Password = ""
v.Cookie = ""
v.Tgc = ""
}
return utils.SuccessWithTotal[[]*ent.CpUser](data, total)
}
@@ -169,14 +170,14 @@ func (v CpUserServiceV1) AuthorizeConfirm(ctx context.Context, req *v1.CpUserAut
return utils.SuccessWithMsg("已确认授权")
}
func (v CpUserServiceV1) AuthorizeRandom(ctx context.Context, req *v1.CpUserAuthorizeRandomReq) *utils.Response[any] {
func (v CpUserServiceV1) CustomDevice(ctx context.Context, req *v1.CpUserCustomDeviceReq) *utils.Response[any] {
user := utils.GetUserInfoFromCtx(ctx)
cpUser := db.Global.CpUser.Query().Where(cpuser.ID(req.Id), cpuser.HasSysuserWith(sysuser.ID(user.ID))).FirstX(ctx)
if cpUser == nil {
return utils.FailParam("用户不存在")
}
db.Global.CpUser.UpdateOne(cpUser).SetMobileCode(utils.UUID()).SetMobileType(cpuser.MobileTypeRandom).ExecX(ctx)
return utils.SuccessWithMsg("已改为随机设备码")
db.Global.CpUser.UpdateOne(cpUser).SetMobileCode(req.MobileCode).SetMobileType(cpuser.MobileTypeAuthorize).ExecX(ctx)
return utils.SuccessWithMsg("自定义设备码设置成功")
}
func (v CpUserServiceV1) LoginGetCode(ctx context.Context, req *v1.LoginGetCode) *utils.Response[any] {
@@ -203,6 +204,8 @@ func (v CpUserServiceV1) AddByCode(ctx context.Context, req *v1.AddByCode) *util
loginExecutor := login.NewSmsWithoutDeviceID()
data := loginExecutor.LoginByCode(context.Background(), req.Mobile, req.Code, jar)
loginExecutor.SetLoginCookie(context.Background(), jar, data)
// SetLoginCookie 之后保存完整 cookie 作为 TGC(包含 CASTGC,有效期10年)
tgc := loginExecutor.GetCookieString()
// 扣费
//basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst.ConfigBasePointCpdailyPhone).String())
basePoint, err := decimal.NewFromString(db.Global.SysConfig.Query().Where(sysconfig.KeyEQ(cst.ConfigRedisCpdailyPhone)).OnlyX(ctx).Value)
@@ -217,6 +220,7 @@ func (v CpUserServiceV1) AddByCode(ctx context.Context, req *v1.AddByCode) *util
SetEmail(req.Email).
SetSchoolID(req.SchoolId).
SetCookie(loginExecutor.GetCookieString()).
SetTgc(tgc).
SetMobileCode(utils.UUID()).
SetMobileType(cpuser.MobileTypeRandom).
SetSysuserID(user.ID).
@@ -226,6 +230,7 @@ func (v CpUserServiceV1) AddByCode(ctx context.Context, req *v1.AddByCode) *util
} else {
db.Global.CpUser.UpdateOneID(req.Id).
SetCookie(loginExecutor.GetCookieString()).
SetTgc(tgc).
ExecX(ctx)
return utils.SuccessWithMsg("更新成功")
}
+35 -8
View File
@@ -11,6 +11,7 @@ import (
// 如果任务的执行时间为nil,则返回 math.MaxInt64
// 如果任务的执行时间不为nil,则返回下次执行时间的Unix时间
// 注意:如果任务的执行时间在当前时间之前,则返回明天的执行时间
// 同时考虑 execute_days(周几执行),跳过不在执行日的日期
func GetCpdailyNextExecuteTime(task *ent.CpTask) int64 {
executeHour, executeMinute := task.ExecuteHour, task.ExecuteMinute
if executeHour == nil || executeMinute == nil {
@@ -18,15 +19,41 @@ func GetCpdailyNextExecuteTime(task *ent.CpTask) int64 {
}
current := time.Now().In(cst.TimeZoneShangHai)
// 如果当天执行时间未过,返回当天执行时间
if current.Hour() < *executeHour || (current.Hour() == *executeHour && current.Minute() < *executeMinute) {
executeTime := time.Date(current.Year(), current.Month(), current.Day(), *executeHour, *executeMinute, 0, 0, cst.TimeZoneShangHai)
return executeTime.Unix()
// 先确定候选日期:如果当天时间未过用当天,否则从明天开始
candidate := time.Date(current.Year(), current.Month(), current.Day(), *executeHour, *executeMinute, 0, 0, cst.TimeZoneShangHai)
if !current.Before(candidate) {
candidate = candidate.AddDate(0, 0, 1)
}
// 如果当天执行时间已过,返回明天的执行时间
executeTime := time.Date(current.Year(), current.Month(), current.Day(), *executeHour, *executeMinute, 0, 0, cst.TimeZoneShangHai)
executeTime = executeTime.AddDate(0, 0, 1)
// 如果没有设置 execute_days 或为空,视为每天执行
if len(task.ExecuteDays) == 0 {
return candidate.Unix()
}
return executeTime.Unix()
// 构建快速查找 set
daySet := make(map[int]bool, len(task.ExecuteDays))
for _, d := range task.ExecuteDays {
daySet[d] = true
}
// 最多查找7天,找到第一个匹配的执行日
for i := 0; i < 7; i++ {
weekday := isoWeekday(candidate)
if daySet[weekday] {
return candidate.Unix()
}
candidate = candidate.AddDate(0, 0, 1)
}
// 理论上不会到这里(7天内必有匹配),兜底返回
return math.MaxInt64
}
// isoWeekday 返回 ISO 星期几:周一=1, 周日=7
func isoWeekday(t time.Time) int {
d := int(t.Weekday())
if d == 0 {
return 7 // Sunday
}
return d
}
+7
View File
@@ -0,0 +1,7 @@
package utils
import "time"
func GetCurrentTimeMillis() int64 {
return time.Now().UnixMilli()
}