Compare commits

...

16 Commits

Author SHA1 Message Date
sugarBecomer ce536eb5bc 🐛 cookie问题 2025-11-13 16:53:59 +08:00
sugarBecomer 0244e330fd 🐛 cookie问题 2025-11-13 16:49:36 +08:00
sugarBecomer 035bac10cc 🐛 尝试调整登录cookie校验 2025-11-13 11:46:44 +08:00
sugarBecomer cb808f9c68 ✨ 适配南昌理工学院 2025-11-13 00:17:49 +08:00
sugarBecomer 22735aeaae ✨ 随机图片改为顺序图片 2025-11-12 19:59:02 +08:00
sugarBecomer dd6eeb5dfe 🐛 opencv 2025-11-12 15:54:52 +08:00
sugarBecomer ac6409f3e2 🐛 opencv 2025-11-12 15:39:20 +08:00
sugarBecomer 36a6932056 🐛 opencv 2025-11-12 15:19:15 +08:00
sugarBecomer 72dab0e968 🐛 opencv 2025-11-12 15:01:33 +08:00
sugarBecomer b716434f62 🐛 opencv 2025-11-12 14:45:25 +08:00
sugarBecomer 8ed7dd77e4 🐛 opencv 2025-11-12 14:30:03 +08:00
sugarBecomer ae1c9c695e 🐛 签到问题 & opencv 2025-11-12 14:12:47 +08:00
sugarBecomer 66995c0c78 🐛 cas滑块验证码 2025-11-12 13:35:17 +08:00
sugarBecomer 120e690795 🐛 cas滑块验证码 2025-11-12 13:31:54 +08:00
sugarBecomer bc2bb25578 🐛 签到bug 2025-11-11 22:34:06 +08:00
RecordingTheSmile b37fc5cb24 fix: 签到额外信息填写问题 2025-11-11 22:29:01 +08:00
36 changed files with 1218 additions and 167 deletions
+8 -7
View File
@@ -1,16 +1,17 @@
FROM golang:latest AS backend-builder
RUN apt update
RUN apt install -y build-essential
FROM gocv/opencv:latest AS backend-builder
RUN echo "开始安装依赖包..."
RUN apt update && apt install -y upx-ucl
WORKDIR /build
COPY . .
ENV CGO_ENABLED=1
RUN echo "开始打包二进制文件"
RUN go build -ldflags="-s -w" -o bin/server
RUN apt install -y upx-ucl
RUN echo "开始压缩二进制文件"
RUN upx --best --lzma bin/server
FROM golang:latest
FROM gocv/opencv:latest
COPY --from=backend-builder /build/bin/server /app/server
#COPY static /app/static
#COPY static/*.so /app/
+2 -2
View File
@@ -42,6 +42,6 @@ baseUrl = "http://s3.out"
bucket = "likedream"
[cpdaily]
appVersion = "5.7.0"
appVersion = "9.7.2"
systemVersion = "iOS 17"
calVersion = "3.0.0"
calVersion = "firstv"
+1
View File
@@ -33,6 +33,7 @@ require (
github.com/yalue/onnxruntime_go v1.12.1
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.0
gocv.io/x/gocv v0.42.0
golang.org/x/crypto v0.43.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
+2
View File
@@ -244,6 +244,8 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
gocv.io/x/gocv v0.42.0 h1:AAsrFJH2aIsQHukkCovWqj0MCGZleQpVyf5gNVRXjQI=
gocv.io/x/gocv v0.42.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -0,0 +1,76 @@
package cryptoutil
import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"net/url"
"strings"
"testing"
"github.com/gogf/gf/v2/container/gvar"
)
func TestDesDecrypt(t *testing.T) {
decodeString, _ := base64.StdEncoding.DecodeString("X3/auV/FUP3Fj9iou6jNosFm9+6oiZ08pQlSt2qUlEhBEZXchcrXZJ96OsPIH4tfc6oRqdvlnObFx+uJfKA9oEuQQURvvDcDLz/1uAsQHXQIXNaW1Bwt9vdo4e1Sxzvt2sBxcHZXlsAPnxRw8UCM6DnoU4kHXF45A2i9wQpzH1cgKsoN/tWLRPYOBSMsPfbsch+wFgFpkQGRWNM9E0CVmpwZJWn946McO4HbMJngTeT5Df8YOUgpkFdccMKrJyPqE7UMOc56YLkPjb/wzmFyd1/HFCUbJwwdhOueH70GSpsH5LrWEhw7jvf2VBmnm669E2uvQjeCtPI1SvVuf7WP9Srp1BXpMtfK")
decrypt := DesDecrypt(decodeString, Cat)
str := "{\"lon\":\"113.045246\",\"lat\":\"28.077659\",\"model\":\"847b5b9c-2d97-41ec-8a5d-e9ac79da090d\",\"appVersion\":\"9.7.2\",\"userId\":\"430181200504150038\",\"systemName\":\"android\",\"deviceId\":\"09E46EC9-57C1-4724-BE53-5879CA80A2A0\",\"cpdailyVersion\":\"9.7.2\",\"systemVersion\":\"iOS 17\"}"
encrypt := DesEncrypt([]byte(str), Cat)
toString := base64.StdEncoding.EncodeToString(encrypt)
t.Log(toString)
t.Log(string(decrypt))
d2, _ := base64.StdEncoding.DecodeString("0u7hBd+6O11ZaGsT51FUAVfB72waqtTx5oQQLVEEAuCM29x8lQc41i9/dF6+9wjQNC05jdJTBmHlwFoLD09hfsnQWSyjl/ys124RUQL3r/Um5AwNdKiJE24YQsyQlP3ZamUIFlrNSxmn/DfZ/jR9oZhiVKZSlygA2LNLxtNOyk4rETfqKpZKFOnThbXy5Hf779jUDmPNwn1ZrdWN8UHm6wO+EahrrX60vC02VUHs7r3rMrDWZjBJ57eSgD0QwZabcIb/u9kx1AD8OAxINar3X+N+uAhfrxjHd6v6OVvnc2I9iPLEGNFRKZ94jq64hnATOSH6S/IR/QbDoxebWpGUXm3ALYZeoni09uyhG7IE5NtRw/OmqYtdknrs+c0qyOOBkvxPlqKo+SV4qq7xtrn9RWeZIigiyM8mVtdovN1T/x3DnHxd/AYoSS3/cGkg+2QHzWey/Xf5c4g0EvhaGlE0C7ejKIn5wp0XeHyvAjm4ZVyUhgTN2dZVCQ6ChxXOpLnVDWPNeeZ9vysqVl1ZSkQdCz20gy3p9RfYuArluQqjwIoWQe632Cc/Ep++99V5Tc9iIlLr93PfxFaJJfov3avLKw==")
d := AesDecryptForCat(d2)
t.Log(d)
form := map[string]*gvar.Var{
"appVersion": gvar.New("9.7.2"),
"bodyString": gvar.New("0u7hBd+6O11ZaGsT51FUAVfB72waqtTx5oQQLVEEAuCM29x8lQc41i9/dF6+9wjQNC05jdJTBmHlwFoLD09hfsnQWSyjl/ys124RUQL3r/Um5AwNdKiJE24YQsyQlP3ZamUIFlrNSxmn/DfZ/jR9oZhiVKZSlygA2LNLxtNOyk4rETfqKpZKFOnThbXy5Hf779jUDmPNwn1ZrdWN8UHm6wO+EahrrX60vC02VUHs7r3rMrDWZjBJ57eSgD0QwZabcIb/u9kx1AD8OAxINar3X+N+uAhfrxjHd6v6OVvnc2I9iPLEGNFRKZ94jq64hnATOSH6S/IR/QbDoxebWpGUXm3ALYZeoni09uyhG7IE5NtRw/OmqYtdknrs+c0qyOOBkvxPlqKo+SV4qq7xtrn9RWeZIigiyM8mVtdovN1T/x3DnHxd/AYoSS3/cGkg+2QHzWey/Xf5c4g0EvhaGlE0C7ejKIn5wp0XeHyvAjm4ZVyUhgTN2dZVCQ6ChxXOpLnVDWPNeeZ9vysqVl1ZSkQdCz20gy3p9RfYuArluQqjwIoWQe632Cc/Ep++99V5Tc9iIlLr93PfxFaJJfov3avLKw=="),
"calVersion": gvar.New("firstv"),
"deviceId": gvar.New("09E46EC9-57C1-4724-BE53-5879CA80A2A0"),
"lat": gvar.New("28.077659"),
"lon": gvar.New("113.045246"),
"model": gvar.New("847b5b9c-2d97-41ec-8a5d-e9ac79da090d"),
"systemName": gvar.New("android"),
"systemVersion": gvar.New("iOS 17"),
"userId": gvar.New("430181200504150038"),
"version": gvar.New("first_v4"),
}
sign := GetSubmitSign(form)
t.Log(sign)
}
const submitSignSalt = "hru89rwr87Nthifa" // 请根据实际情况替换为真实盐值
func GetSubmitSign(form map[string]*gvar.Var) string {
// 按照指定顺序构建哈希数据
keys := []string{
"appVersion",
"bodyString",
"deviceId",
"lat",
"lon",
"model",
"systemName",
"systemVersion",
"userId",
}
var sb strings.Builder
for _, key := range keys {
val := ""
if v, ok := form[key]; ok && v != nil {
val = v.String()
}
// 这里是 key=UrlEncode(key)&,注意是对 key 进行 url encode
sb.WriteString(key)
sb.WriteString("=")
sb.WriteString(url.QueryEscape(val))
sb.WriteString("&")
}
sb.WriteString(submitSignSalt)
// 计算MD5
hash := md5.Sum([]byte(sb.String()))
return hex.EncodeToString(hash[:])
}
@@ -0,0 +1,122 @@
package cryptoutil
import (
"crypto/rand"
"encoding/hex"
"fmt"
"math/big"
)
// RSAKeyPair RSA密钥对结构体
type RSAKeyPair struct {
n *big.Int // 模数
e *big.Int // 公钥指数
}
// NewRSAKeyPair 创建RSA密钥对
// exponent: 公钥指数的十六进制字符串,通常是 "10001" (65537)
// privateExponent: 私钥指数的十六进制字符串,加密时可以为空
// modulus: 模数的十六进制字符串
func NewRSAKeyPair(exponent, privateExponent, modulus string) (*RSAKeyPair, error) {
// 解析公钥指数
e := new(big.Int)
if _, ok := e.SetString(exponent, 16); !ok {
return nil, fmt.Errorf("invalid exponent: %s", exponent)
}
// 解析模数
n := new(big.Int)
if _, ok := n.SetString(modulus, 16); !ok {
return nil, fmt.Errorf("invalid modulus: %s", modulus)
}
return &RSAKeyPair{
n: n,
e: e,
}, nil
}
// Encrypt 使用公钥加密数据,返回十六进制字符串
func (k *RSAKeyPair) Encrypt(plaintext string) (string, error) {
ciphertext, err := k.EncryptBytes([]byte(plaintext))
if err != nil {
return "", err
}
// 返回十六进制编码的密文
return hex.EncodeToString(ciphertext), nil
}
// EncryptBytes 使用公钥加密字节数据,返回加密后的字节
// 使用 PKCS1v15 填充方式,兼容 JavaScript RSA 实现
func (k *RSAKeyPair) EncryptBytes(plaintext []byte) ([]byte, error) {
// 计算密钥长度(字节)
keySize := (k.n.BitLen() + 7) / 8
// PKCS1v15 填充
padded, err := pkcs1v15Pad(plaintext, keySize)
if err != nil {
return nil, err
}
// 将填充后的数据转换为大整数
m := new(big.Int).SetBytes(padded)
// 执行 RSA 加密: c = m^e mod n
c := new(big.Int).Exp(m, k.e, k.n)
// 将结果转换为字节数组,确保长度与密钥长度一致
ciphertext := c.Bytes()
if len(ciphertext) < keySize {
// 如果结果长度不足,在前面填充零
result := make([]byte, keySize)
copy(result[keySize-len(ciphertext):], ciphertext)
return result, nil
}
return ciphertext, nil
}
// pkcs1v15Pad 实现 PKCS#1 v1.5 填充
func pkcs1v15Pad(data []byte, keySize int) ([]byte, error) {
if len(data) > keySize-11 {
return nil, fmt.Errorf("data too long: %d bytes (max %d)", len(data), keySize-11)
}
// PKCS1v15 格式: 0x00 || 0x02 || PS || 0x00 || data
// 其中 PS 是随机非零字节,长度至少为 8 字节
padLen := keySize - len(data) - 3
padded := make([]byte, keySize)
padded[0] = 0x00
padded[1] = 0x02
// 生成随机非零字节作为填充
ps := make([]byte, padLen)
_, err := rand.Read(ps)
if err != nil {
return nil, fmt.Errorf("failed to generate random padding: %w", err)
}
// 确保所有填充字节都是非零
for i := 0; i < padLen; i++ {
for ps[i] == 0 {
rand.Read(ps[i : i+1])
}
padded[2+i] = ps[i]
}
padded[2+padLen] = 0x00
copy(padded[3+padLen:], data)
return padded, nil
}
// GetModulus 获取模数
func (k *RSAKeyPair) GetModulus() *big.Int {
return k.n
}
// GetExponent 获取公钥指数
func (k *RSAKeyPair) GetExponent() *big.Int {
return k.e
}
@@ -0,0 +1,54 @@
package cryptoutil
import (
"testing"
)
func TestRSAKeyPair(t *testing.T) {
// 使用你提供的参数创建 RSA 密钥对
exponent := "10001"
modulus := "a1155dc73de04614604cbed62cc3b3198a639eb7b7d19cc2dfe3e787fe435f1464beee42a9a3cab6f1d3bfe66cd00317220ee770fd779b24c12c7698e251e875"
keyPair, err := NewRSAKeyPair(exponent, "", modulus)
if err != nil {
t.Fatalf("创建 RSA 密钥对失败: %v", err)
}
// 测试加密
plaintext := "241703070163"
ciphertext, err := keyPair.Encrypt(plaintext)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
t.Logf("明文: %s", plaintext)
t.Logf("密文(十六进制): %s", ciphertext)
// 验证密文不为空
if len(ciphertext) == 0 {
t.Error("密文为空")
}
}
func TestRSAKeyPair_EncryptBytes(t *testing.T) {
exponent := "10001"
modulus := "a1155dc73de04614604cbed62cc3b3198a639eb7b7d19cc2dfe3e787fe435f1464beee42a9a3cab6f1d3bfe66cd00317220ee770fd779b24c12c7698e251e875"
keyPair, err := NewRSAKeyPair(exponent, "", modulus)
if err != nil {
t.Fatalf("创建 RSA 密钥对失败: %v", err)
}
// 测试字节加密
plaintext := []byte("Test data 测试数据")
ciphertext, err := keyPair.EncryptBytes(plaintext)
if err != nil {
t.Fatalf("字节加密失败: %v", err)
}
if len(ciphertext) == 0 {
t.Error("密文字节为空")
}
t.Logf("密文长度: %d bytes", len(ciphertext))
}
@@ -7,6 +7,9 @@ import (
func GetCpdailyLoginBySchoolInfo(ctx context.Context, info *school.SchoolDetailInfo) CpdailyLogin {
switch info.Id {
// 南昌理工学院
case "d6e8f2ca-10b2-4599-bdb8-4b33d419658d":
return NewNut(ctx)
default:
if info.IsIap() {
return NewIap(ctx)
+39 -13
View File
@@ -16,10 +16,12 @@ import (
"net/http"
"net/url"
"regexp"
"slices"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/avast/retry-go/v4"
"github.com/deatil/go-cryptobin/cryptobin/crypto"
"github.com/samber/oops"
"github.com/tidwall/gjson"
@@ -56,7 +58,7 @@ func (c *CpdailyCasLogin) GetClient() *resty.Client {
func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
jar := cookiejar.NewPersistCookieJar()
c.client = c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
// 获取合法的登录URL
loginUrl := getCasValidLoginUrl(schoolDetail)
@@ -76,7 +78,7 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
// 关闭client的重定向
c.client = c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
// 解析登录页面
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
@@ -188,20 +190,35 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
} else {
allInputs["password"] = encryptCasPassword(password, salt)
}
var SliderCaptchaIds = []string{"1018764144934965"}
// 判断是否需要验证码
if isCasNeedCaptcha(ctx, loginResponseUrl, loginType, username, c.client) {
// 解决验证码
captchaUrl := getCasCaptchaUrl(loginResponseUrl, loginType)
// 如果是滑块验证码
if slices.Contains(SliderCaptchaIds, schoolDetail.Id) {
e := retry.Do(func() error {
return utils.Catch(func() {
captchaUrl := getCasSliderCaptchaUrl(loginResponseUrl)
verifyUrl := getCasSliderVerifyUrl(loginResponseUrl)
ResolveCasSliderCaptcha(ctx, captchaUrl, verifyUrl, c.client)
})
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
if e != nil {
utils.OkOrPanic(errors.NewBusinessError(503, "滑块验证码解析失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
}
} else {
// 解决常规图片验证码
captchaUrl := getCasCaptchaUrl(loginResponseUrl, loginType)
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
switch loginType {
case 0:
allInputs["captchaResponse"] = captchaResult
default:
allInputs["captcha"] = captchaResult
switch loginType {
case 0:
allInputs["captchaResponse"] = captchaResult
default:
allInputs["captcha"] = captchaResult
}
}
}
loginPostResponse, err := c.client.R().
@@ -334,7 +351,7 @@ func handleCasLoginSuccess(ctx context.Context, response *resty.Response, school
jumpUrl, err := response.RawResponse.Location()
utils.OkOrPanic(err)
client = client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
verifyResponse, err := client.R().
WithContext(ctx).
@@ -399,7 +416,7 @@ func submitCasKickOut(ctx context.Context, requestUrl string, doc *goquery.Docum
submitForm[name] = value
}
client = client.SetRedirectPolicy(resty.NoRedirectPolicy())
client.SetRedirectPolicy(resty.NoRedirectPolicy())
response, err := client.R().
SetFormData(submitForm).
@@ -529,3 +546,12 @@ func getCasCaptchaUrl(url *url.URL, loginType int) string {
return fmt.Sprintf("%s://%s/authserver/getCaptcha.htl", url.Scheme, url.Host)
}
}
func getCasSliderCaptchaUrl(url *url.URL) string {
// /authserver/common/openSliderCaptcha.htl?_=1762873725346
return fmt.Sprintf("%s://%s/authserver/common/openSliderCaptcha.htl", url.Scheme, url.Host)
}
func getCasSliderVerifyUrl(url *url.URL) string {
return fmt.Sprintf("%s://%s/authserver/common/verifySliderCaptcha.htl", url.Scheme, url.Host)
}
+5 -5
View File
@@ -14,7 +14,7 @@ import (
)
func ActivateSession(ctx context.Context, host string, client *resty.Client) {
client = client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
host = strings.TrimSuffix(host, "/")
@@ -78,19 +78,19 @@ func ActivateSession(ctx context.Context, host string, client *resty.Client) {
activateStep1()
activateStep2()
client = client.SetRedirectPolicy(resty.NoRedirectPolicy())
client.SetRedirectPolicy(resty.NoRedirectPolicy())
}
func CookieEnabled(ctx context.Context, host string, client *resty.Client) bool {
client = client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
defer func() {
client = client.SetRedirectPolicy(resty.NoRedirectPolicy())
client.SetRedirectPolicy(resty.NoRedirectPolicy())
}()
host = strings.TrimSuffix(host, "/")
sessionResponse, err := client.R().
WithContext(ctx).
SetBody(map[string]any{}).
Post(fmt.Sprintf("%s/wec-counselor-attendance-apps/student/attendance/getStuSignInfosByWeekMonth", host))
Post(fmt.Sprintf("%s/wec-counselor-attendance-apps/student/attendance/getStuAttendacesInOneDay", host))
utils.OkOrPanic(err)
defer sessionResponse.Body.Close()
+3 -3
View File
@@ -34,7 +34,7 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
loginUrl := getIapValidLoginUrl(schoolDetail)
jar := cookiejar.NewPersistCookieJar()
c.client = c.client.SetCookieJar(jar).
c.client.SetCookieJar(jar).
SetRedirectPolicy(resty.NoRedirectPolicy())
ltInfoResponse, err := c.client.R().
@@ -103,7 +103,7 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
Wrap(err)
})
c.client = c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
finalLoginResponse, err := c.client.R().
WithContext(ctx).
@@ -123,7 +123,7 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
ActivateSession(ctx, schoolDetail.WecloudUrl, c.client)
c.client = c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
default:
loginResponseBytes, err := io.ReadAll(loginResponse.Body)
utils.OkOrPanic(err, func(err error) error {
+213
View File
@@ -0,0 +1,213 @@
package login
import (
"bytes"
"context"
"fmt"
"io"
"likedream-goframe/internal/application/cookiejar"
"likedream-goframe/internal/application/cpdaily/cryptoutil"
"likedream-goframe/internal/application/cpdaily/school"
"likedream-goframe/internal/application/httpclient"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/utils"
"net/http"
"net/url"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/bytedance/sonic"
"github.com/samber/oops"
"resty.dev/v3"
)
// 南阳理工学院登录实现 RSA加密 & 验证码必出
type CpdailyNutLogin struct {
client *resty.Client
}
func (c CpdailyNutLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
jar := cookiejar.NewPersistCookieJar()
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
// 获取合法的登录URL
loginUrl := getCasValidLoginUrl(schoolDetail)
// 请求登录页面并获取真正的登录URL
loginPageResponse, err := c.client.R().
WithContext(ctx).
Get(loginUrl)
utils.OkOrPanic(err)
defer loginPageResponse.Body.Close()
loginPageBytes, err := io.ReadAll(loginPageResponse.Body)
utils.OkOrPanic(err)
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
// 关闭client的重定向
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
// 解析登录页面
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
utils.OkOrPanic(err)
// 判断登录类型
var (
allInputs = map[string]string{}
fm1Node = loginPage.Find("#fm1").First()
)
inputs := fm1Node.Find("input")
for _, input := range inputs.EachIter() {
name, exists := input.Attr("name")
if !exists {
continue
}
value, _ := input.Attr("value")
allInputs[name] = value
}
allInputs["username"] = username
// 处理密码
type NutPubKeyResponse struct {
Modulus string `json:"modulus"`
Exponent string `json:"exponent"`
}
var pubKey NutPubKeyResponse
pubKeyResponse, err := c.client.R().Get(getNutPubKey(loginResponseUrl))
utils.OkOrPanic(err)
defer pubKeyResponse.Body.Close()
pubKeyBytes, err := io.ReadAll(pubKeyResponse.Body)
utils.OkOrPanic(err)
sonic.Unmarshal(pubKeyBytes, &pubKey)
pair, err := cryptoutil.NewRSAKeyPair(pubKey.Exponent, "", pubKey.Modulus)
utils.OkOrPanic(err)
password, err = pair.Encrypt(reverseStringASCII(password))
utils.OkOrPanic(err)
allInputs["password"] = password
if isNutNeedCaptcha(ctx, loginResponseUrl, c.client) {
// 解决常规图片验证码
captchaUrl := getNutCaptchaUrl(loginResponseUrl)
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
allInputs["authcode"] = captchaResult
} else {
allInputs["authcode"] = ""
}
// 登录提交
loginPostResponse, err := c.client.R().
SetFormData(allInputs).
WithContext(ctx).
Post(loginResponseUrl.String())
utils.OkOrPanic(err, func(err error) error {
return oops.
WithContext(ctx).
With("loginResponseUrl", loginResponseUrl).
With("allInputs", allInputs).
Wrap(err)
})
defer loginPostResponse.Body.Close()
switch loginPostResponse.StatusCode() {
case 302:
handleCasLoginSuccess(ctx, loginPostResponse, schoolDetail, username, password, c.client)
case 200:
fallthrough
case 401:
doc, err := goquery.NewDocumentFromReader(loginPostResponse.Body)
utils.OkOrPanic(err, func(err error) error {
return oops.
WithContext(ctx).
With("loginResponseUrl", loginResponseUrl).
With("allInputs", allInputs).
Wrap(err)
})
if isCasNeedKickOut(doc) {
kickoutResponse := submitCasKickOut(ctx, loginResponseUrl.String(), doc, c.client)
handleCasLoginSuccess(ctx, kickoutResponse, schoolDetail, username, password, c.client)
return
}
message := getCasErrorMessage(doc)
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("今日校园登录失败: %s", message)), func(err error) error {
return oops.
WithContext(ctx).
With("loginResponseUrl", loginResponseUrl).
With("allInputs", allInputs).
With("errorMessage", message).
Wrap(errors.NewBusinessError(503, message))
})
}
}
func getNutCaptchaUrl(url *url.URL) string {
return fmt.Sprintf("%s://%s/cas/kaptcha", url.Scheme, url.Host)
}
func isNutNeedCaptcha(ctx context.Context, url *url.URL, client *resty.Client) bool {
requestURL := getNutNeedCaptchaUrl(url)
captchaResponse, err := client.R().
WithContext(ctx).
Get(requestURL)
utils.OkOrPanic(oops.With().
With("requestURL", requestURL).
WithContext(ctx).
Wrap(err))
defer captchaResponse.Body.Close()
if captchaResponse.IsError() {
utils.OkOrPanic(oops.With().
With("requestURL", requestURL).
WithContext(ctx).
Wrap(errors.NewBusinessError(503, "请求验证码失败")))
}
responseBytes, err := io.ReadAll(captchaResponse.Body)
utils.OkOrPanic(err)
return strings.ToLower(strings.TrimSpace(string(responseBytes))) != "false"
}
func getNutPubKey(url *url.URL) string {
return fmt.Sprintf("%s://%s/cas/v2/getPubKey", url.Scheme, url.Host)
}
func getNutNeedCaptchaUrl(url *url.URL) string {
return fmt.Sprintf("%s://%s/cas/v2/getKaptchaStatus", url.Scheme, url.Host)
}
func (c CpdailyNutLogin) GetCookieJar() http.CookieJar {
return c.client.CookieJar()
}
func (c CpdailyNutLogin) GetCookieString() string {
cookieJar := c.GetCookieJar()
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
return serializableJar.Serialize()
}
func (c CpdailyNutLogin) GetClient() *resty.Client {
return c.client
}
func NewNut(ctx context.Context) *CpdailyNutLogin {
return &CpdailyNutLogin{
client: httpclient.Cpdaily.Clone(ctx),
}
}
func reverseStringASCII(s string) string {
bytes := []byte(s)
for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
bytes[i], bytes[j] = bytes[j], bytes[i]
}
return string(bytes)
}
+37 -53
View File
@@ -1,72 +1,56 @@
package login
import (
"bytes"
"context"
"image"
"io"
"likedream-goframe/internal/application/httpclient"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/application/recognizer/ddddocr"
"likedream-goframe/internal/utils"
"github.com/gogf/gf/v2/frame/g"
"github.com/samber/oops"
"github.com/tidwall/gjson"
"resty.dev/v3"
)
func ResolveOcrCaptcha(ctx context.Context, url string, client *resty.Client) string {
captchaResponse, err := client.R().
WithContext(ctx).
Get(url)
byteData := DownCode2Byte(ctx, url, client)
img, err := BytesToImage(byteData)
utils.OkOrPanic(err, func(err error) error {
return oops.
With("captchaUrl", url).
With("BytesToImage", url).
WithContext(ctx).
Wrap(err)
})
defer func() {
_ = captchaResponse.Body.Close()
}()
ocrUrl := g.Cfg().MustGetWithEnv(ctx, cst.ConfigOcrServerUrl).String()
ocrResponse, err := httpclient.Common.R().
SetMultipartFields(&resty.MultipartField{
Name: "file",
FileName: "captcha.jpg",
ContentType: captchaResponse.Header().Get("Content-Type"),
Reader: captchaResponse.Body,
}).
WithContext(ctx).
Post(ocrUrl + "/ocr")
utils.OkOrPanic(err, func(err error) error {
return oops.
With("captchaUrl", url).
WithContext(ctx).
Wrap(err)
})
defer func() {
_ = ocrResponse.Body.Close()
}()
responseBytes, err := io.ReadAll(ocrResponse.Body)
utils.OkOrPanic(err)
responseJSON := gjson.ParseBytes(responseBytes)
data := responseJSON.Get("data").String()
if data == "" {
utils.OkOrPanic(oops.
In("ResolveOcrCaptcha").
With("captchaUrl", url).
With("message", responseJSON.Get("message").String()).
WithContext(ctx).
New("OCR服务返回数据为空"))
}
data := ddddocr.AutoOCRVerification(img)
return data
}
func DownCode2Byte(ctx context.Context, url string, client *resty.Client) []byte {
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()
all, err := io.ReadAll(res.Body)
utils.OkOrPanic(err, func(err error) error {
return oops.
With("captchaUrl", url).
WithContext(ctx).
Wrap(err)
})
return all
}
// 通用解码函数,自动检测格式
func BytesToImage(imgBytes []byte) (image.Image, error) {
img, _, err := image.Decode(bytes.NewReader(imgBytes))
if err != nil {
return nil, err
}
return img, nil
}
@@ -0,0 +1,67 @@
package login
import (
"context"
"fmt"
"io"
"likedream-goframe/internal/application/recognizer/slider"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/utils"
"github.com/bytedance/sonic"
"github.com/samber/oops"
"resty.dev/v3"
)
type CasSliderCaptchaReponse struct {
BigImage string `json:"bigImage"`
SmallImage string `json:"smallImage"`
TagWidth int `json:"tagWidth"`
YHeight int `json:"yHeight"`
}
type CasSliderResult struct {
ErrorCode int `json:"errorCode"`
ErrorMsg string `json:"errorMsg"`
}
func ResolveCasSliderCaptcha(ctx context.Context, captchaUrl string, verifyUrl string, client *resty.Client) {
res, err := client.R().
WithContext(ctx).
Get(captchaUrl)
utils.OkOrPanic(err, func(err error) error {
return oops.
With("captchaUrl", captchaUrl).
WithContext(ctx).
Wrap(err)
})
defer res.Body.Close()
bytes, _ := io.ReadAll(res.Body)
var captcha CasSliderCaptchaReponse
sonic.Unmarshal(bytes, &captcha)
width := slider.VerifySliderWidthByBase64(captcha.BigImage, captcha.SmallImage)
res2, err2 := client.R().
WithContext(ctx).
SetFormData(map[string]string{
"canvasLength": "280",
"moveLength": fmt.Sprintf("%d", width),
}).
Post(verifyUrl)
utils.OkOrPanic(err2, func(err error) error {
return oops.
With("verifyUrl", verifyUrl).
WithContext(ctx).
Wrap(err)
})
defer res2.Body.Close()
bytes, _ = io.ReadAll(res2.Body)
str := string(bytes)
if len(str) > 0 {
var result CasSliderResult
sonic.Unmarshal(bytes, &result)
if result.ErrorCode != 1 {
utils.OkOrPanic(oops.WithContext(ctx).Wrap(errors.NewBusinessError(500, "滑块解析错误")))
}
}
}
+6 -11
View File
@@ -2,27 +2,22 @@ package task
import (
"context"
"likedream-goframe/internal/application/cookiejar"
"likedream-goframe/internal/application/cpdaily/school"
"likedream-goframe/internal/application/httpclient"
errors2 "likedream-goframe/internal/common/errors"
"likedream-goframe/internal/ent/cptask"
"likedream-goframe/internal/utils"
"resty.dev/v3"
)
func GetCpdailyTaskService(ctx context.Context, Type cptask.TaskType, cookie string, schoolId string) CpdailyTask {
detail := school.GetSchoolDetail(ctx, schoolId)
cookieJar := cookiejar.NewPersistCookieJar()
cookieJar.Deserialize(cookie)
client := httpclient.Cpdaily.Clone(ctx)
client.SetCookieJar(cookieJar)
func GetCpdailyTaskService(ctx context.Context, Type cptask.TaskType, client *resty.Client, schoolDetail school.SchoolDetailInfo) CpdailyTask {
switch Type {
case cptask.TaskTypeCollection:
return NewCollectionTask(client, detail.WecloudUrl)
return NewCollectionTask(client, schoolDetail.WecloudUrl)
case cptask.TaskTypeSign:
return NewAutoSignTask(client, detail.WecloudUrl)
return NewAutoSignTask(client, schoolDetail.WecloudUrl)
case cptask.TaskTypeSleepCheck:
return NewSleepCheckTask(client, detail.WecloudUrl)
return NewSleepCheckTask(client, schoolDetail.WecloudUrl)
default:
utils.OkOrPanic(errors2.NewBusinessError(400, "不支持的任务类型"))
}
@@ -354,7 +354,7 @@ func (c *CpdailyCollectionTask) ParseFormJson(ctx context.Context, rawJson map[s
return list
}
func (c *CpdailyCollectionTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var {
func (c *CpdailyCollectionTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var {
taskInfo := form["taskInfo"].MapStrVar()
rows := form["rows"].Array()
filledForm := make([]map[string]*gvar.Var, 0)
@@ -12,6 +12,6 @@ type CpdailyTask interface {
QueryFormByTitle(ctx context.Context, titleGlob string) map[string]*gvar.Var
QueryFormByYearMonth(ctx context.Context, titleGlob string, year, month string) map[string]*gvar.Var
ParseFormJson(ctx context.Context, rawJson map[string]*gvar.Var) []*formtype.CpTaskFormItem
FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var
FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var
Submit(ctx context.Context, form map[string]*gvar.Var, username, deviceId string)
}
+10 -25
View File
@@ -12,7 +12,6 @@ import (
"likedream-goframe/internal/application/cpdaily/types"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/utils"
"math/rand/v2"
"slices"
"strings"
"time"
@@ -467,7 +466,7 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
return list
}
func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var {
func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var {
var resultForm map[string]*gvar.Var
// 填充必要变量
@@ -508,27 +507,11 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
utils.OkOrPanic(errors.NewBusinessError(400, "签到需要图片,但是当前图片为空"), oops.WithContext(ctx).Wrap)
}
imageUuidSeq := strings.SplitSeq(targetParam.Content, ",")
var imageUuids []string
for imageUuid := range imageUuidSeq {
imageUuid = strings.TrimSpace(imageUuid)
if imageUuid == "" {
continue
}
imageUuids = append(imageUuids, imageUuid)
}
if len(imageUuids) == 0 {
if imgId == "" {
utils.OkOrPanic(errors.NewBusinessError(400, "签到照片未上传"), oops.WithContext(ctx).Wrap)
}
// 随机选择一张照片
imageUuid := imageUuids[rand.IntN(len(imageUuids))]
imageId, e := uuid.Parse(imageUuid)
imageId, e := uuid.Parse(imgId)
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSign, c.client)
@@ -548,7 +531,9 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
ticket = submitCaptchaResolver.GetCaptchaTicket(ctx, captchaInfo)
})
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
if e != nil {
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
}
resultForm["ticket"] = gvar.New(ticket)
} else {
@@ -571,7 +556,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
var extraFieldItemValues []map[string]*gvar.Var
for i, field := range extraField {
current := uint(i) + 1
current := uint(i + 1)
// 检查数字是否在范围内
found := slices.Contains(numbers, current)
if !found {
@@ -581,7 +566,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
// 找到对应的参数
var targetParam *formtype.CpTaskFormItem
for _, v := range paramList {
if v.Number == uint(i) {
if v.Number == current {
targetParam = v
break
}
@@ -591,7 +576,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
continue
}
fieldObj := field.(map[string]*gvar.Var)
fieldObj := gvar.New(field).MapStrVar()
fieldTitle := fieldObj["title"].String()
if targetParam.Title != fieldTitle {
@@ -602,7 +587,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
var extraFieldItemValue map[string]*gvar.Var
for _, item := range extraFieldItems {
itemObj := item.(map[string]*gvar.Var)
itemObj := gvar.New(item).MapStrVar()
content := itemObj["content"].String()
if content == targetParam.Content {
@@ -13,7 +13,6 @@ import (
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/utils"
"math/rand/v2"
"strconv"
"strings"
"time"
@@ -190,11 +189,7 @@ func (c *CpdailySleepCheckTask) QueryFormByTitle(ctx context.Context, titleGlob
SetBody(map[string]any{}).
WithContext(ctx).
Post(c.getReqUrl("getStuAttendacesInOneDay"))
//
response, err = c.client.R().
SetBody(map[string]any{}).
WithContext(ctx).
Post(c.getReqUrl("getStuAttendacesInOneDay"))
utils.OkOrPanic(err, func(err error) error {
return oops.With("host", c.host).
In("CpdailySleepCheckTask.QueryFormByTitle").
@@ -501,7 +496,7 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
return list
}
func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var {
func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var {
var resultForm map[string]*gvar.Var
// 填充必要变量
@@ -561,27 +556,11 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
utils.OkOrPanic(errors.NewBusinessError(503, "未找到查寝照片参数,请上传照片"), oops.WithContext(ctx).Wrap)
}
imageUuidSeq := strings.SplitSeq(targetParam.Content, ",")
var imageUuids []string
for imageUuid := range imageUuidSeq {
imageUuid = strings.TrimSpace(imageUuid)
if imageUuid == "" {
continue
}
imageUuids = append(imageUuids, imageUuid)
}
if len(imageUuids) == 0 {
if imgId == "" {
utils.OkOrPanic(errors.NewBusinessError(503, "未找到查寝照片参数,请上传照片"), oops.WithContext(ctx).Wrap)
}
// 随机选择一张照片
imageUuid := imageUuids[rand.IntN(len(imageUuids))]
imageId, e := uuid.Parse(imageUuid)
imageId, e := uuid.Parse(imgId)
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSleepCheck, c.client)
@@ -11,7 +11,7 @@ import (
"github.com/gogf/gf/v2/container/gvar"
)
const submitSignSalt = "HashSalt" // 请根据实际情况替换为真实盐值
const submitSignSalt = "hru89rwr87Nthifa" // 请根据实际情况替换为真实盐值
func GetSubmitSign(form map[string]*gvar.Var) string {
// 按照指定顺序构建哈希数据
File diff suppressed because one or more lines are too long
@@ -0,0 +1,192 @@
package slider
import (
"encoding/base64"
"os"
"gocv.io/x/gocv"
)
func VerifySliderWidth(bgImgPath, sliderImgPath string) int {
// 检查背景图片文件是否存在
if _, err := os.Stat(bgImgPath); os.IsNotExist(err) {
return -1
}
// 检查滑块图片文件是否存在
if _, err := os.Stat(sliderImgPath); os.IsNotExist(err) {
return -1
}
// 读取背景图片
bgImg := gocv.IMRead(bgImgPath, gocv.IMReadColor)
if bgImg.Empty() {
return -1
}
defer bgImg.Close()
// 读取滑块图片(包含 alpha 通道)
sliderImgWithAlpha := gocv.IMRead(sliderImgPath, gocv.IMReadUnchanged)
if sliderImgWithAlpha.Empty() {
return -1
}
defer sliderImgWithAlpha.Close()
// 分离滑块图片的通道
channels := gocv.Split(sliderImgWithAlpha)
defer func() {
for i := range channels {
channels[i].Close()
}
}()
var sliderImg gocv.Mat
var mask gocv.Mat
// 如果有 alpha 通道(4通道),提取 mask
if len(channels) == 4 {
// 前3个通道是 BGR
sliderImg = gocv.NewMat()
defer sliderImg.Close()
gocv.Merge(channels[:3], &sliderImg)
// 使用 alpha 通道作为 mask
mask = channels[3].Clone()
defer mask.Close()
} else if len(channels) == 3 {
// 没有透明通道,直接使用原图
sliderImg = sliderImgWithAlpha.Clone()
defer sliderImg.Close()
mask = gocv.NewMat() // 空 mask
defer mask.Close()
} else {
return -1
}
// 创建结果矩阵用于存储匹配结果
result := gocv.NewMat()
defer result.Close()
// 直接在原图上使用模板匹配找到滑块位置
if mask.Empty() {
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, gocv.NewMat())
} else {
// 带 mask 的模板匹配
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, mask)
}
// 找到最佳匹配位置
_, maxVal, _, maxLoc := gocv.MinMaxLoc(result)
// 如果匹配度太低,返回 -1 表示失败
if maxVal < 0.6 {
return -1
}
// 获取原图上的 x 坐标,然后按比例缩放到目标尺寸
originalWidth := bgImg.Cols()
targetWidth := 280
scale := float64(targetWidth) / float64(originalWidth)
scaledX := int(float64(maxLoc.X) * scale)
// 返回缩放后的 x 坐标(滑块需要移动的距离)
return scaledX
}
// VerifySliderWidthByBase64 通过 base64 编码的图片计算滑块验证码需要移动的距离
// bgImgBase64: 背景图片的 base64 编码
// sliderImgBase64: 滑块图片的 base64 编码
// 返回: 滑块需要移动的 x 距离(已缩放到 280 宽度),失败返回 -1
func VerifySliderWidthByBase64(bgImgBase64, sliderImgBase64 string) int {
// 解码背景图片的 base64
bgImgData, err := base64.StdEncoding.DecodeString(bgImgBase64)
if err != nil {
return -1
}
// 解码滑块图片的 base64
sliderImgData, err := base64.StdEncoding.DecodeString(sliderImgBase64)
if err != nil {
return -1
}
// 从内存中读取背景图片
bgImg, err := gocv.IMDecode(bgImgData, gocv.IMReadColor)
if err != nil {
return -1
}
if bgImg.Empty() {
return -1
}
defer bgImg.Close()
// 从内存中读取滑块图片(包含 alpha 通道)
sliderImgWithAlpha, err := gocv.IMDecode(sliderImgData, gocv.IMReadUnchanged)
if err != nil {
return -1
}
if sliderImgWithAlpha.Empty() {
return -1
}
defer sliderImgWithAlpha.Close()
// 分离滑块图片的通道
channels := gocv.Split(sliderImgWithAlpha)
defer func() {
for i := range channels {
channels[i].Close()
}
}()
var sliderImg gocv.Mat
var mask gocv.Mat
// 如果有 alpha 通道(4通道),提取 mask
if len(channels) == 4 {
// 前3个通道是 BGR
sliderImg = gocv.NewMat()
defer sliderImg.Close()
gocv.Merge(channels[:3], &sliderImg)
// 使用 alpha 通道作为 mask
mask = channels[3].Clone()
defer mask.Close()
} else if len(channels) == 3 {
// 没有透明通道,直接使用原图
sliderImg = sliderImgWithAlpha.Clone()
defer sliderImg.Close()
mask = gocv.NewMat() // 空 mask
defer mask.Close()
} else {
return -1
}
// 创建结果矩阵用于存储匹配结果
result := gocv.NewMat()
defer result.Close()
// 直接在原图上使用模板匹配找到滑块位置
if mask.Empty() {
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, gocv.NewMat())
} else {
// 带 mask 的模板匹配
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, mask)
}
// 找到最佳匹配位置
_, maxVal, _, maxLoc := gocv.MinMaxLoc(result)
// 如果匹配度太低,返回 -1 表示失败
if maxVal < 0.6 {
return -1
}
// 获取原图上的 x 坐标,然后按比例缩放到目标尺寸
originalWidth := bgImg.Cols()
targetWidth := 280
scale := float64(targetWidth) / float64(originalWidth)
scaledX := int(float64(maxLoc.X) * scale)
// 返回缩放后的 x 坐标(滑块需要移动的距离)
return scaledX
}
+12 -1
View File
@@ -39,6 +39,8 @@ type CpTask struct {
SuccessCount int `json:"success_count,omitempty"`
// FormData holds the value of the "form_data" field.
FormData string `json:"form_data,omitempty"`
// UsedImg holds the value of the "used_img" field.
UsedImg string `json:"used_img,omitempty"`
// Step holds the value of the "step" field.
Step cptask.Step `json:"step,omitempty"`
// NotificationType holds the value of the "notification_type" field.
@@ -109,7 +111,7 @@ func (*CpTask) scanValues(columns []string) ([]any, error) {
values[i] = new(ent2.CustomTime)
case cptask.FieldID, cptask.FieldCpUserID, cptask.FieldUserID, cptask.FieldFrequency, cptask.FieldSuccessCount, cptask.FieldExecuteHour, cptask.FieldExecuteMinute, cptask.FieldNextExecuteTime:
values[i] = new(sql.NullInt64)
case cptask.FieldTaskType, cptask.FieldTitle, cptask.FieldTaskStatus, cptask.FieldFormData, cptask.FieldStep, cptask.FieldNotificationType:
case cptask.FieldTaskType, cptask.FieldTitle, cptask.FieldTaskStatus, cptask.FieldFormData, cptask.FieldUsedImg, cptask.FieldStep, cptask.FieldNotificationType:
values[i] = new(sql.NullString)
default:
values[i] = new(sql.UnknownType)
@@ -192,6 +194,12 @@ func (_m *CpTask) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.FormData = value.String
}
case cptask.FieldUsedImg:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field used_img", values[i])
} else if value.Valid {
_m.UsedImg = value.String
}
case cptask.FieldStep:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field step", values[i])
@@ -306,6 +314,9 @@ func (_m *CpTask) String() string {
builder.WriteString("form_data=")
builder.WriteString(_m.FormData)
builder.WriteString(", ")
builder.WriteString("used_img=")
builder.WriteString(_m.UsedImg)
builder.WriteString(", ")
builder.WriteString("step=")
builder.WriteString(fmt.Sprintf("%v", _m.Step))
builder.WriteString(", ")
+8
View File
@@ -36,6 +36,8 @@ const (
FieldSuccessCount = "success_count"
// FieldFormData holds the string denoting the form_data field in the database.
FieldFormData = "form_data"
// FieldUsedImg holds the string denoting the used_img field in the database.
FieldUsedImg = "used_img"
// FieldStep holds the string denoting the step field in the database.
FieldStep = "step"
// FieldNotificationType holds the string denoting the notification_type field in the database.
@@ -90,6 +92,7 @@ var Columns = []string{
FieldTaskStatus,
FieldSuccessCount,
FieldFormData,
FieldUsedImg,
FieldStep,
FieldNotificationType,
FieldExecuteHour,
@@ -284,6 +287,11 @@ func ByFormData(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldFormData, opts...).ToFunc()
}
// ByUsedImg orders the results by the used_img field.
func ByUsedImg(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldUsedImg, opts...).ToFunc()
}
// ByStep orders the results by the step field.
func ByStep(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldStep, opts...).ToFunc()
+80
View File
@@ -96,6 +96,11 @@ func FormData(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldEQ(FieldFormData, v))
}
// UsedImg applies equality check predicate on the "used_img" field. It's identical to UsedImgEQ.
func UsedImg(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldEQ(FieldUsedImg, v))
}
// ExecuteHour applies equality check predicate on the "execute_hour" field. It's identical to ExecuteHourEQ.
func ExecuteHour(v int) predicate.CpTask {
return predicate.CpTask(sql.FieldEQ(FieldExecuteHour, v))
@@ -551,6 +556,81 @@ func FormDataContainsFold(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldContainsFold(FieldFormData, v))
}
// UsedImgEQ applies the EQ predicate on the "used_img" field.
func UsedImgEQ(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldEQ(FieldUsedImg, v))
}
// UsedImgNEQ applies the NEQ predicate on the "used_img" field.
func UsedImgNEQ(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldNEQ(FieldUsedImg, v))
}
// UsedImgIn applies the In predicate on the "used_img" field.
func UsedImgIn(vs ...string) predicate.CpTask {
return predicate.CpTask(sql.FieldIn(FieldUsedImg, vs...))
}
// UsedImgNotIn applies the NotIn predicate on the "used_img" field.
func UsedImgNotIn(vs ...string) predicate.CpTask {
return predicate.CpTask(sql.FieldNotIn(FieldUsedImg, vs...))
}
// UsedImgGT applies the GT predicate on the "used_img" field.
func UsedImgGT(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldGT(FieldUsedImg, v))
}
// UsedImgGTE applies the GTE predicate on the "used_img" field.
func UsedImgGTE(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldGTE(FieldUsedImg, v))
}
// UsedImgLT applies the LT predicate on the "used_img" field.
func UsedImgLT(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldLT(FieldUsedImg, v))
}
// UsedImgLTE applies the LTE predicate on the "used_img" field.
func UsedImgLTE(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldLTE(FieldUsedImg, v))
}
// UsedImgContains applies the Contains predicate on the "used_img" field.
func UsedImgContains(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldContains(FieldUsedImg, v))
}
// UsedImgHasPrefix applies the HasPrefix predicate on the "used_img" field.
func UsedImgHasPrefix(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldHasPrefix(FieldUsedImg, v))
}
// UsedImgHasSuffix applies the HasSuffix predicate on the "used_img" field.
func UsedImgHasSuffix(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldHasSuffix(FieldUsedImg, v))
}
// UsedImgIsNil applies the IsNil predicate on the "used_img" field.
func UsedImgIsNil() predicate.CpTask {
return predicate.CpTask(sql.FieldIsNull(FieldUsedImg))
}
// UsedImgNotNil applies the NotNil predicate on the "used_img" field.
func UsedImgNotNil() predicate.CpTask {
return predicate.CpTask(sql.FieldNotNull(FieldUsedImg))
}
// UsedImgEqualFold applies the EqualFold predicate on the "used_img" field.
func UsedImgEqualFold(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldEqualFold(FieldUsedImg, v))
}
// UsedImgContainsFold applies the ContainsFold predicate on the "used_img" field.
func UsedImgContainsFold(v string) predicate.CpTask {
return predicate.CpTask(sql.FieldContainsFold(FieldUsedImg, v))
}
// StepEQ applies the EQ predicate on the "step" field.
func StepEQ(v Step) predicate.CpTask {
return predicate.CpTask(sql.FieldEQ(FieldStep, v))
+78
View File
@@ -109,6 +109,20 @@ func (_c *CpTaskCreate) SetNillableFormData(v *string) *CpTaskCreate {
return _c
}
// SetUsedImg sets the "used_img" field.
func (_c *CpTaskCreate) SetUsedImg(v string) *CpTaskCreate {
_c.mutation.SetUsedImg(v)
return _c
}
// SetNillableUsedImg sets the "used_img" field if the given value is not nil.
func (_c *CpTaskCreate) SetNillableUsedImg(v *string) *CpTaskCreate {
if v != nil {
_c.SetUsedImg(*v)
}
return _c
}
// SetStep sets the "step" field.
func (_c *CpTaskCreate) SetStep(v cptask.Step) *CpTaskCreate {
_c.mutation.SetStep(v)
@@ -382,6 +396,10 @@ func (_c *CpTaskCreate) createSpec() (*CpTask, *sqlgraph.CreateSpec) {
_spec.SetField(cptask.FieldFormData, field.TypeString, value)
_node.FormData = value
}
if value, ok := _c.mutation.UsedImg(); ok {
_spec.SetField(cptask.FieldUsedImg, field.TypeString, value)
_node.UsedImg = value
}
if value, ok := _c.mutation.Step(); ok {
_spec.SetField(cptask.FieldStep, field.TypeEnum, value)
_node.Step = value
@@ -633,6 +651,24 @@ func (u *CpTaskUpsert) ClearFormData() *CpTaskUpsert {
return u
}
// SetUsedImg sets the "used_img" field.
func (u *CpTaskUpsert) SetUsedImg(v string) *CpTaskUpsert {
u.Set(cptask.FieldUsedImg, v)
return u
}
// UpdateUsedImg sets the "used_img" field to the value that was provided on create.
func (u *CpTaskUpsert) UpdateUsedImg() *CpTaskUpsert {
u.SetExcluded(cptask.FieldUsedImg)
return u
}
// ClearUsedImg clears the value of the "used_img" field.
func (u *CpTaskUpsert) ClearUsedImg() *CpTaskUpsert {
u.SetNull(cptask.FieldUsedImg)
return u
}
// SetStep sets the "step" field.
func (u *CpTaskUpsert) SetStep(v cptask.Step) *CpTaskUpsert {
u.Set(cptask.FieldStep, v)
@@ -921,6 +957,27 @@ func (u *CpTaskUpsertOne) ClearFormData() *CpTaskUpsertOne {
})
}
// SetUsedImg sets the "used_img" field.
func (u *CpTaskUpsertOne) SetUsedImg(v string) *CpTaskUpsertOne {
return u.Update(func(s *CpTaskUpsert) {
s.SetUsedImg(v)
})
}
// UpdateUsedImg sets the "used_img" field to the value that was provided on create.
func (u *CpTaskUpsertOne) UpdateUsedImg() *CpTaskUpsertOne {
return u.Update(func(s *CpTaskUpsert) {
s.UpdateUsedImg()
})
}
// ClearUsedImg clears the value of the "used_img" field.
func (u *CpTaskUpsertOne) ClearUsedImg() *CpTaskUpsertOne {
return u.Update(func(s *CpTaskUpsert) {
s.ClearUsedImg()
})
}
// SetStep sets the "step" field.
func (u *CpTaskUpsertOne) SetStep(v cptask.Step) *CpTaskUpsertOne {
return u.Update(func(s *CpTaskUpsert) {
@@ -1391,6 +1448,27 @@ func (u *CpTaskUpsertBulk) ClearFormData() *CpTaskUpsertBulk {
})
}
// SetUsedImg sets the "used_img" field.
func (u *CpTaskUpsertBulk) SetUsedImg(v string) *CpTaskUpsertBulk {
return u.Update(func(s *CpTaskUpsert) {
s.SetUsedImg(v)
})
}
// UpdateUsedImg sets the "used_img" field to the value that was provided on create.
func (u *CpTaskUpsertBulk) UpdateUsedImg() *CpTaskUpsertBulk {
return u.Update(func(s *CpTaskUpsert) {
s.UpdateUsedImg()
})
}
// ClearUsedImg clears the value of the "used_img" field.
func (u *CpTaskUpsertBulk) ClearUsedImg() *CpTaskUpsertBulk {
return u.Update(func(s *CpTaskUpsert) {
s.ClearUsedImg()
})
}
// SetStep sets the "step" field.
func (u *CpTaskUpsertBulk) SetStep(v cptask.Step) *CpTaskUpsertBulk {
return u.Update(func(s *CpTaskUpsert) {
+52
View File
@@ -171,6 +171,26 @@ func (_u *CpTaskUpdate) ClearFormData() *CpTaskUpdate {
return _u
}
// SetUsedImg sets the "used_img" field.
func (_u *CpTaskUpdate) SetUsedImg(v string) *CpTaskUpdate {
_u.mutation.SetUsedImg(v)
return _u
}
// SetNillableUsedImg sets the "used_img" field if the given value is not nil.
func (_u *CpTaskUpdate) SetNillableUsedImg(v *string) *CpTaskUpdate {
if v != nil {
_u.SetUsedImg(*v)
}
return _u
}
// ClearUsedImg clears the value of the "used_img" field.
func (_u *CpTaskUpdate) ClearUsedImg() *CpTaskUpdate {
_u.mutation.ClearUsedImg()
return _u
}
// SetStep sets the "step" field.
func (_u *CpTaskUpdate) SetStep(v cptask.Step) *CpTaskUpdate {
_u.mutation.SetStep(v)
@@ -460,6 +480,12 @@ func (_u *CpTaskUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if _u.mutation.FormDataCleared() {
_spec.ClearField(cptask.FieldFormData, field.TypeString)
}
if value, ok := _u.mutation.UsedImg(); ok {
_spec.SetField(cptask.FieldUsedImg, field.TypeString, value)
}
if _u.mutation.UsedImgCleared() {
_spec.ClearField(cptask.FieldUsedImg, field.TypeString)
}
if value, ok := _u.mutation.Step(); ok {
_spec.SetField(cptask.FieldStep, field.TypeEnum, value)
}
@@ -765,6 +791,26 @@ func (_u *CpTaskUpdateOne) ClearFormData() *CpTaskUpdateOne {
return _u
}
// SetUsedImg sets the "used_img" field.
func (_u *CpTaskUpdateOne) SetUsedImg(v string) *CpTaskUpdateOne {
_u.mutation.SetUsedImg(v)
return _u
}
// SetNillableUsedImg sets the "used_img" field if the given value is not nil.
func (_u *CpTaskUpdateOne) SetNillableUsedImg(v *string) *CpTaskUpdateOne {
if v != nil {
_u.SetUsedImg(*v)
}
return _u
}
// ClearUsedImg clears the value of the "used_img" field.
func (_u *CpTaskUpdateOne) ClearUsedImg() *CpTaskUpdateOne {
_u.mutation.ClearUsedImg()
return _u
}
// SetStep sets the "step" field.
func (_u *CpTaskUpdateOne) SetStep(v cptask.Step) *CpTaskUpdateOne {
_u.mutation.SetStep(v)
@@ -1084,6 +1130,12 @@ func (_u *CpTaskUpdateOne) sqlSave(ctx context.Context) (_node *CpTask, err erro
if _u.mutation.FormDataCleared() {
_spec.ClearField(cptask.FieldFormData, field.TypeString)
}
if value, ok := _u.mutation.UsedImg(); ok {
_spec.SetField(cptask.FieldUsedImg, field.TypeString, value)
}
if _u.mutation.UsedImgCleared() {
_spec.ClearField(cptask.FieldUsedImg, field.TypeString)
}
if value, ok := _u.mutation.Step(); ok {
_spec.SetField(cptask.FieldStep, field.TypeEnum, value)
}
+6
View File
@@ -84,6 +84,7 @@ var schemaGraph = func() *sqlgraph.Schema {
cptask.FieldTaskStatus: {Type: field.TypeEnum, Column: cptask.FieldTaskStatus},
cptask.FieldSuccessCount: {Type: field.TypeInt, Column: cptask.FieldSuccessCount},
cptask.FieldFormData: {Type: field.TypeString, Column: cptask.FieldFormData},
cptask.FieldUsedImg: {Type: field.TypeString, Column: cptask.FieldUsedImg},
cptask.FieldStep: {Type: field.TypeEnum, Column: cptask.FieldStep},
cptask.FieldNotificationType: {Type: field.TypeEnum, Column: cptask.FieldNotificationType},
cptask.FieldExecuteHour: {Type: field.TypeInt, Column: cptask.FieldExecuteHour},
@@ -905,6 +906,11 @@ func (f *CpTaskFilter) WhereFormData(p entql.StringP) {
f.Where(p.Field(cptask.FieldFormData))
}
// WhereUsedImg applies the entql string predicate on the used_img field.
func (f *CpTaskFilter) WhereUsedImg(p entql.StringP) {
f.Where(p.Field(cptask.FieldUsedImg))
}
// WhereStep applies the entql string predicate on the step field.
func (f *CpTaskFilter) WhereStep(p entql.StringP) {
f.Where(p.Field(cptask.FieldStep))
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -60,6 +60,7 @@ var (
{Name: "task_status", Type: field.TypeEnum, Enums: []string{"not_start", "doing", "done", "fail", "pause", "queue", "unknown"}},
{Name: "success_count", Type: field.TypeInt, Default: 0},
{Name: "form_data", Type: field.TypeString, Nullable: true, Size: 2147483647},
{Name: "used_img", Type: field.TypeString, Nullable: true},
{Name: "step", Type: field.TypeEnum, Enums: []string{"form", "execute", "complete"}},
{Name: "notification_type", Type: field.TypeEnum, Enums: []string{"none", "cp_user_email", "system_user_email"}},
{Name: "execute_hour", Type: field.TypeInt, Nullable: true},
@@ -76,13 +77,13 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "cp_tasks_cp_users_cptasks",
Columns: []*schema.Column{CpTasksColumns[14]},
Columns: []*schema.Column{CpTasksColumns[15]},
RefColumns: []*schema.Column{CpUsersColumns[0]},
OnDelete: schema.NoAction,
},
{
Symbol: "cp_tasks_sys_user_cp_task",
Columns: []*schema.Column{CpTasksColumns[15]},
Columns: []*schema.Column{CpTasksColumns[16]},
RefColumns: []*schema.Column{SysUserColumns[0]},
OnDelete: schema.Cascade,
},
+74 -1
View File
@@ -1467,6 +1467,7 @@ type CpTaskMutation struct {
success_count *int
addsuccess_count *int
form_data *string
used_img *string
step *cptask.Step
notification_type *cptask.NotificationType
execute_hour *int
@@ -1999,6 +2000,55 @@ func (m *CpTaskMutation) ResetFormData() {
delete(m.clearedFields, cptask.FieldFormData)
}
// SetUsedImg sets the "used_img" field.
func (m *CpTaskMutation) SetUsedImg(s string) {
m.used_img = &s
}
// UsedImg returns the value of the "used_img" field in the mutation.
func (m *CpTaskMutation) UsedImg() (r string, exists bool) {
v := m.used_img
if v == nil {
return
}
return *v, true
}
// OldUsedImg returns the old "used_img" 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) OldUsedImg(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldUsedImg is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldUsedImg requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldUsedImg: %w", err)
}
return oldValue.UsedImg, nil
}
// ClearUsedImg clears the value of the "used_img" field.
func (m *CpTaskMutation) ClearUsedImg() {
m.used_img = nil
m.clearedFields[cptask.FieldUsedImg] = struct{}{}
}
// UsedImgCleared returns if the "used_img" field was cleared in this mutation.
func (m *CpTaskMutation) UsedImgCleared() bool {
_, ok := m.clearedFields[cptask.FieldUsedImg]
return ok
}
// ResetUsedImg resets all changes to the "used_img" field.
func (m *CpTaskMutation) ResetUsedImg() {
m.used_img = nil
delete(m.clearedFields, cptask.FieldUsedImg)
}
// SetStep sets the "step" field.
func (m *CpTaskMutation) SetStep(c cptask.Step) {
m.step = &c
@@ -2436,7 +2486,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, 15)
fields := make([]string, 0, 16)
if m.create_time != nil {
fields = append(fields, cptask.FieldCreateTime)
}
@@ -2467,6 +2517,9 @@ func (m *CpTaskMutation) Fields() []string {
if m.form_data != nil {
fields = append(fields, cptask.FieldFormData)
}
if m.used_img != nil {
fields = append(fields, cptask.FieldUsedImg)
}
if m.step != nil {
fields = append(fields, cptask.FieldStep)
}
@@ -2510,6 +2563,8 @@ func (m *CpTaskMutation) Field(name string) (ent.Value, bool) {
return m.SuccessCount()
case cptask.FieldFormData:
return m.FormData()
case cptask.FieldUsedImg:
return m.UsedImg()
case cptask.FieldStep:
return m.Step()
case cptask.FieldNotificationType:
@@ -2549,6 +2604,8 @@ func (m *CpTaskMutation) OldField(ctx context.Context, name string) (ent.Value,
return m.OldSuccessCount(ctx)
case cptask.FieldFormData:
return m.OldFormData(ctx)
case cptask.FieldUsedImg:
return m.OldUsedImg(ctx)
case cptask.FieldStep:
return m.OldStep(ctx)
case cptask.FieldNotificationType:
@@ -2638,6 +2695,13 @@ func (m *CpTaskMutation) SetField(name string, value ent.Value) error {
}
m.SetFormData(v)
return nil
case cptask.FieldUsedImg:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetUsedImg(v)
return nil
case cptask.FieldStep:
v, ok := value.(cptask.Step)
if !ok {
@@ -2769,6 +2833,9 @@ func (m *CpTaskMutation) ClearedFields() []string {
if m.FieldCleared(cptask.FieldFormData) {
fields = append(fields, cptask.FieldFormData)
}
if m.FieldCleared(cptask.FieldUsedImg) {
fields = append(fields, cptask.FieldUsedImg)
}
if m.FieldCleared(cptask.FieldExecuteHour) {
fields = append(fields, cptask.FieldExecuteHour)
}
@@ -2795,6 +2862,9 @@ func (m *CpTaskMutation) ClearField(name string) error {
case cptask.FieldFormData:
m.ClearFormData()
return nil
case cptask.FieldUsedImg:
m.ClearUsedImg()
return nil
case cptask.FieldExecuteHour:
m.ClearExecuteHour()
return nil
@@ -2842,6 +2912,9 @@ func (m *CpTaskMutation) ResetField(name string) error {
case cptask.FieldFormData:
m.ResetFormData()
return nil
case cptask.FieldUsedImg:
m.ResetUsedImg()
return nil
case cptask.FieldStep:
m.ResetStep()
return nil
+1
View File
@@ -25,6 +25,7 @@ func (CpTask) Fields() []ent.Field {
field.Enum("task_status").Values("not_start", "doing", "done", "fail", "pause", "queue", "unknown"),
field.Int("success_count").Default(0),
field.Text("form_data").Optional(),
field.String("used_img").Optional(),
field.Enum("step").Values("form", "execute", "complete"),
field.Enum("notification_type").Values("none", "cp_user_email", "system_user_email"),
field.Int("execute_hour").Optional().Nillable(),
+28 -5
View File
@@ -18,6 +18,8 @@ import (
"likedream-goframe/internal/framework/model/email"
email2 "likedream-goframe/internal/service/email"
"likedream-goframe/internal/utils"
"slices"
"strings"
"time"
"github.com/bytedance/sonic"
@@ -96,11 +98,11 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
loginExecutor.Login(ctx, &schoolDetail, targetTask.Edges.CpUser.Username, targetTask.Edges.CpUser.Password)
cookie = loginExecutor.GetCookieString()
db.Global.CpUser.UpdateOne(targetTask.Edges.CpUser).SetCookie(cookie).ExecX(ctx)
db.Global.CpUser.UpdateOneID(targetTask.Edges.CpUser.ID).SetCookie(cookie).ExecX(ctx)
restyClient = login.GetClient(ctx, cookie)
}
// 获取任务执行handler
taskExecutor := task2.GetCpdailyTaskService(ctx, targetTask.TaskType, cookie, targetTask.Edges.CpUser.SchoolID)
taskExecutor := task2.GetCpdailyTaskService(ctx, targetTask.TaskType, restyClient, schoolDetail)
cpdailyRealTask := taskExecutor.QueryFormByTitle(ctx, targetTask.Title)
@@ -131,13 +133,14 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
default:
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "不支持的设备类型", targetTask.Edges.CpUser.MobileType))
}
submitForm := taskExecutor.FillCustomForm(ctx, cpdailyRealTask, customFormItems, deviceID, targetTask.Edges.CpUser.Username)
// 固定获取图片id
imgId := GetNextImgId(customFormItems, targetTask.UsedImg)
submitForm := taskExecutor.FillCustomForm(ctx, cpdailyRealTask, customFormItems, deviceID, imgId)
taskExecutor.Submit(ctx, submitForm, targetTask.Edges.CpUser.Username, deviceID)
// 任务完成后重置状态为 TaskStatusDone,更新下次执行时间 以便下次继续执行
executeTime := utils.GetCpdailyNextExecuteTime(targetTask)
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusDone).SetNextExecuteTime(executeTime).AddSuccessCount(1).ExecX(ctx)
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusDone).SetUsedImg(imgId).SetNextExecuteTime(executeTime).AddSuccessCount(1).ExecX(ctx)
// 创建日志
db.Global.CpTaskLog.Create().
@@ -157,6 +160,26 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
return
}
func GetNextImgId(forms []*formtype.CpTaskFormItem, imgId string) string {
var allImgIds []string
for _, item := range forms {
if item.ColName == "signImg" {
allImgIds = strings.Split(item.Content, ",")
break
}
}
if len(allImgIds) == 0 {
return ""
}
if imgId == "" {
return allImgIds[0]
}
index := slices.Index(allImgIds, imgId)
if index == -1 || index == len(allImgIds)-1 {
return allImgIds[0]
}
return allImgIds[index+1]
}
// sendTaskNotificationEmail 发送任务通知邮件
func (c *CpdailyTaskHandler) sendTaskNotificationEmail(ctx context.Context, task *ent.CpTask, status email.TaskStatus, details map[string]string) {
+6 -3
View File
@@ -2,9 +2,9 @@ package schedule
import (
"context"
"likedream-goframe/internal/utils"
"likedream-goframe/internal/framework/logger"
"github.com/samber/oops"
"go.uber.org/zap"
)
// StopTask 让任务队列停止执行某任务
@@ -17,5 +17,8 @@ func StopTask(ctx context.Context, queue, taskID string) {
queue = "default"
}
e := globalInspector.DeleteTask(queue, taskID)
utils.OkOrPanic(e, oops.WithContext(ctx).With("queue", queue, "taskID", taskID).Wrap)
if e != nil {
logger.Logger.Error("删除定时任务失败", zap.Error(e))
}
//utils.OkOrPanic(e, oops.WithContext(ctx).With("queue", queue, "taskID", taskID).Wrap)
}
+7 -6
View File
@@ -51,7 +51,7 @@ func (v CpTaskServiceV1) GetRecentTasks(ctx context.Context, req *v1.GetRecentTa
db2.Global.CpUser.UpdateOne(cpUser).SetCookie(cookie).ExecX(ctx)
client = login.GetClient(ctx, cookie)
}
taskService := task.GetCpdailyTaskService(ctx, req.Type, cpUser.Cookie, cpUser.SchoolID)
taskService := task.GetCpdailyTaskService(ctx, req.Type, client, schoolDetail)
titleList := taskService.GetRecentTaskTitle(ctx)
return utils.SuccessWithData[[]string](titleList)
}
@@ -117,15 +117,16 @@ func (v CpTaskServiceV1) TaskFormData(ctx context.Context, req *v1.GetCpTaskForm
return utils.SuccessWithData[any](formData)
}
schoolDetail := school.GetSchoolDetail(ctx, cpTask.Edges.CpUser.SchoolID)
client := login.GetClient(ctx, cpUser.Cookie)
cookie := cpUser.Cookie
client := login.GetClient(ctx, cookie)
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, client) {
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
loginExecutor.Login(ctx, &schoolDetail, cpUser.Username, cpUser.Password)
cpUser.Cookie = loginExecutor.GetCookieString()
db2.Global.CpUser.UpdateOne(cpUser).SetCookie(cpUser.Cookie).ExecX(ctx)
client = login.GetClient(ctx, cpUser.Cookie)
cookie = loginExecutor.GetCookieString()
db2.Global.CpUser.UpdateOneID(cpUser.ID).SetCookie(cookie).ExecX(ctx)
client = login.GetClient(ctx, cookie)
}
taskService := task.GetCpdailyTaskService(ctx, cpTask.TaskType, cpUser.Cookie, cpUser.SchoolID)
taskService := task.GetCpdailyTaskService(ctx, cpTask.TaskType, client, schoolDetail)
formMap := taskService.QueryFormByTitle(ctx, cpTask.Title)
formDetail := taskService.ParseFormJson(ctx, formMap)
return utils.SuccessWithData[any](formDetail)
+3 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"image"
"image/png"
"likedream-goframe/internal/framework/logger"
"os"
)
@@ -13,10 +14,11 @@ func ReadImg(imgFile string) (image.Image, error) {
if err != nil {
return nil, err
}
img, err := png.Decode(f)
img, s, err := image.Decode(f)
if err != nil {
return nil, err
}
logger.Logger.Info(s)
f.Close()
return img, nil
}