Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 311cf18708 | |||
| 1bde0af01d | |||
| 765ec6fb32 | |||
| 5cd8329519 | |||
| 1ea9170d89 | |||
| 29fc69d022 | |||
| 09e7a82882 | |||
| 1fff983d37 | |||
| 836e61b8b0 | |||
| 96044b8a88 | |||
| acb865596e | |||
| 59559f4201 | |||
| 4d516adea8 | |||
| a37368ddcf |
+5
-1
@@ -44,4 +44,8 @@ bucket = "likedream"
|
||||
[cpdaily]
|
||||
appVersion = "9.7.2"
|
||||
systemVersion = "iOS 17"
|
||||
calVersion = "firstv"
|
||||
calVersion = "firstv"
|
||||
|
||||
|
||||
[task]
|
||||
concurrency = 20
|
||||
@@ -67,7 +67,23 @@ func (a *AllSavePersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie)
|
||||
if a.domainMap[mainDomain] == nil {
|
||||
a.domainMap[mainDomain] = make(cookieMap)
|
||||
}
|
||||
a.domainMap[mainDomain][cookie.Name] = cookie
|
||||
|
||||
// 查找是否存在相同 name, path 和 domain 的 cookie
|
||||
existingCookies := a.domainMap[mainDomain][cookie.Name]
|
||||
found := false
|
||||
for i, existing := range existingCookies {
|
||||
if existing.Path == cookie.Path && existing.Domain == cookie.Domain {
|
||||
// 找到完全匹配的 cookie,替换它
|
||||
existingCookies[i] = cookie
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// 没有找到匹配的,添加新的 cookie
|
||||
a.domainMap[mainDomain][cookie.Name] = append(a.domainMap[mainDomain][cookie.Name], cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,10 +94,43 @@ func (a *AllSavePersistCookieJar) Cookies(u *url.URL) []*http.Cookie {
|
||||
// 提取主域名
|
||||
mainDomain := extractMainDomain(u.Host)
|
||||
|
||||
if cookies, exists := a.domainMap[mainDomain]; exists {
|
||||
result := make([]*http.Cookie, 0, len(cookies))
|
||||
for _, cookie := range cookies {
|
||||
result = append(result, cookie)
|
||||
if cookiesList, exists := a.domainMap[mainDomain]; exists {
|
||||
result := make([]*http.Cookie, 0, len(cookiesList))
|
||||
requestPath := u.Path
|
||||
if requestPath == "" {
|
||||
requestPath = "/"
|
||||
}
|
||||
|
||||
for _, cookies := range cookiesList {
|
||||
// 如果只有一个 cookie,直接返回
|
||||
if len(cookies) == 1 {
|
||||
result = append(result, cookies[0])
|
||||
continue
|
||||
}
|
||||
|
||||
// 有多个同名 cookie,选择最匹配的(path 最长且匹配请求路径的)
|
||||
var bestMatch *http.Cookie
|
||||
bestMatchLen := -1
|
||||
|
||||
for _, cookie := range cookies {
|
||||
cookiePath := cookie.Path
|
||||
if cookiePath == "" {
|
||||
cookiePath = "/"
|
||||
}
|
||||
|
||||
// 检查 path 是否匹配
|
||||
if pathMatch(requestPath, cookiePath) {
|
||||
if len(cookiePath) > bestMatchLen {
|
||||
bestMatch = cookie
|
||||
bestMatchLen = len(cookiePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到匹配的,添加到结果中
|
||||
if bestMatch != nil {
|
||||
result = append(result, bestMatch)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -109,7 +158,30 @@ func (a *AllSavePersistCookieJar) Deserialize(content string) {
|
||||
|
||||
var domainMap domainMap
|
||||
err := sonic.Unmarshal([]byte(content), &domainMap)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldDomainMap map[string]oldCookieMap
|
||||
|
||||
var oldMap oldDomainMap
|
||||
oldErr := sonic.Unmarshal([]byte(content), &oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
domainMap = make(map[string]cookieMap)
|
||||
for domain, cookies := range oldMap {
|
||||
domainMap[domain] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
domainMap[domain][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.domainMap = domainMap
|
||||
}
|
||||
@@ -142,7 +214,31 @@ func (a *AllSavePersistCookieJar) DeserializeBytes(content []byte) {
|
||||
var domainMap domainMap
|
||||
err := gob.NewDecoder(retReader).
|
||||
Decode(&domainMap)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldDomainMap map[string]oldCookieMap
|
||||
|
||||
retReader = bytes.NewBuffer(content)
|
||||
var oldMap oldDomainMap
|
||||
oldErr := gob.NewDecoder(retReader).Decode(&oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
domainMap = make(map[string]cookieMap)
|
||||
for domain, cookies := range oldMap {
|
||||
domainMap[domain] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
domainMap[domain][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.domainMap = domainMap
|
||||
}
|
||||
@@ -151,10 +247,36 @@ func (a *AllSavePersistCookieJar) DeserializeReader(reader io.Reader) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
var domainMap domainMap
|
||||
err := gob.NewDecoder(reader).
|
||||
Decode(&domainMap)
|
||||
// 先读取所有数据,以便在失败时可以重试
|
||||
data, err := io.ReadAll(reader)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
var domainMap domainMap
|
||||
err = gob.NewDecoder(bytes.NewBuffer(data)).Decode(&domainMap)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldDomainMap map[string]oldCookieMap
|
||||
|
||||
var oldMap oldDomainMap
|
||||
oldErr := gob.NewDecoder(bytes.NewBuffer(data)).Decode(&oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
domainMap = make(map[string]cookieMap)
|
||||
for domain, cookies := range oldMap {
|
||||
domainMap[domain] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
domainMap[domain][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.domainMap = domainMap
|
||||
}
|
||||
|
||||
@@ -3,17 +3,19 @@ package cookiejar
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"github.com/bytedance/sonic"
|
||||
"io"
|
||||
"likedream-goframe/internal/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type hostMap map[string]cookieMap
|
||||
|
||||
type cookieMap map[string]*http.Cookie
|
||||
type cookieMap map[string][]*http.Cookie
|
||||
type PersistCookieJar struct {
|
||||
hostMap hostMap
|
||||
*sync.RWMutex
|
||||
@@ -33,14 +35,30 @@ func (p *PersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
|
||||
host := u.Host
|
||||
|
||||
if _, exists := p.hostMap[host]; !exists {
|
||||
p.hostMap = make(hostMap)
|
||||
p.hostMap[host] = make(cookieMap)
|
||||
}
|
||||
|
||||
for _, cookie := range cookies {
|
||||
if p.hostMap[host] == nil {
|
||||
p.hostMap[host] = make(cookieMap)
|
||||
}
|
||||
p.hostMap[host][cookie.Name] = cookie
|
||||
|
||||
// 查找是否存在相同 name, path 和 domain 的 cookie
|
||||
existingCookies := p.hostMap[host][cookie.Name]
|
||||
found := false
|
||||
for i, existing := range existingCookies {
|
||||
if existing.Path == cookie.Path && existing.Domain == cookie.Domain {
|
||||
// 找到完全匹配的 cookie,替换它
|
||||
existingCookies[i] = cookie
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// 没有找到匹配的,添加新的 cookie
|
||||
p.hostMap[host][cookie.Name] = append(p.hostMap[host][cookie.Name], cookie)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,16 +68,69 @@ func (p *PersistCookieJar) Cookies(u *url.URL) []*http.Cookie {
|
||||
defer p.RUnlock()
|
||||
|
||||
host := u.Host
|
||||
if cookies, exists := p.hostMap[host]; exists {
|
||||
result := make([]*http.Cookie, 0, len(cookies))
|
||||
for _, cookie := range cookies {
|
||||
result = append(result, cookie)
|
||||
if cookiesList, exists := p.hostMap[host]; exists {
|
||||
result := make([]*http.Cookie, 0, len(cookiesList))
|
||||
requestPath := u.Path
|
||||
if requestPath == "" {
|
||||
requestPath = "/"
|
||||
}
|
||||
|
||||
for _, cookies := range cookiesList {
|
||||
// 如果只有一个 cookie,直接返回
|
||||
if len(cookies) == 1 {
|
||||
result = append(result, cookies[0])
|
||||
continue
|
||||
}
|
||||
|
||||
// 有多个同名 cookie,选择最匹配的(path 最长且匹配请求路径的)
|
||||
var bestMatch *http.Cookie
|
||||
bestMatchLen := -1
|
||||
|
||||
for _, cookie := range cookies {
|
||||
cookiePath := cookie.Path
|
||||
if cookiePath == "" {
|
||||
cookiePath = "/"
|
||||
}
|
||||
|
||||
// 检查 path 是否匹配
|
||||
if pathMatch(requestPath, cookiePath) {
|
||||
if len(cookiePath) > bestMatchLen {
|
||||
bestMatch = cookie
|
||||
bestMatchLen = len(cookiePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到匹配的,添加到结果中
|
||||
if bestMatch != nil {
|
||||
result = append(result, bestMatch)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pathMatch 检查请求路径是否匹配 cookie 的 path
|
||||
// 根据 RFC 6265 的规则
|
||||
func pathMatch(requestPath, cookiePath string) bool {
|
||||
if requestPath == cookiePath {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(requestPath, cookiePath) {
|
||||
// cookiePath 以 / 结尾,或者 requestPath 的下一个字符是 /
|
||||
if strings.HasSuffix(cookiePath, "/") {
|
||||
return true
|
||||
}
|
||||
if len(requestPath) > len(cookiePath) && requestPath[len(cookiePath)] == '/' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *PersistCookieJar) Serialize() string {
|
||||
p.RLock()
|
||||
defer p.RUnlock()
|
||||
@@ -83,7 +154,29 @@ func (p *PersistCookieJar) Deserialize(content string) {
|
||||
var hostMap hostMap
|
||||
err := sonic.Unmarshal([]byte(content), &hostMap)
|
||||
|
||||
utils.OkOrPanic(err)
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldHostMap map[string]oldCookieMap
|
||||
|
||||
var oldMap oldHostMap
|
||||
oldErr := sonic.Unmarshal([]byte(content), &oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
hostMap = make(map[string]cookieMap)
|
||||
for host, cookies := range oldMap {
|
||||
hostMap[host] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
hostMap[host][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.hostMap = hostMap
|
||||
}
|
||||
@@ -116,7 +209,31 @@ func (p *PersistCookieJar) DeserializeBytes(content []byte) {
|
||||
var hostMap hostMap
|
||||
err := gob.NewDecoder(retReader).
|
||||
Decode(&hostMap)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldHostMap map[string]oldCookieMap
|
||||
|
||||
retReader = bytes.NewBuffer(content)
|
||||
var oldMap oldHostMap
|
||||
oldErr := gob.NewDecoder(retReader).Decode(&oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
hostMap = make(map[string]cookieMap)
|
||||
for host, cookies := range oldMap {
|
||||
hostMap[host] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
hostMap[host][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.hostMap = hostMap
|
||||
}
|
||||
@@ -125,10 +242,36 @@ func (p *PersistCookieJar) DeserializeReader(reader io.Reader) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
var hostMap hostMap
|
||||
err := gob.NewDecoder(reader).
|
||||
Decode(&hostMap)
|
||||
// 先读取所有数据,以便在失败时可以重试
|
||||
data, err := io.ReadAll(reader)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
var hostMap hostMap
|
||||
err = gob.NewDecoder(bytes.NewBuffer(data)).Decode(&hostMap)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldHostMap map[string]oldCookieMap
|
||||
|
||||
var oldMap oldHostMap
|
||||
oldErr := gob.NewDecoder(bytes.NewBuffer(data)).Decode(&oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
hostMap = make(map[string]cookieMap)
|
||||
for host, cookies := range oldMap {
|
||||
hostMap[host] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
hostMap[host][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.hostMap = hostMap
|
||||
}
|
||||
|
||||
@@ -114,3 +114,10 @@ func TestPersistCookieJar_DeserializeReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeseralizeCookie(t *testing.T) {
|
||||
var str = `{"cdpc.campusphere.net":{"MOD_AUTH_CAS":{"Name":"MOD_AUTH_CAS","Value":"ST-iap:1021988086918169:ST:8d7fe60b-dc7d-4fcd-b26b-5517e3ca1926:20251113162807","Quoted":false,"Path":"/","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":true,"SameSite":0,"Partitioned":false,"Raw":"MOD_AUTH_CAS=ST-iap:1021988086918169:ST:8d7fe60b-dc7d-4fcd-b26b-5517e3ca1926:20251113162807; path=/; Httponly","Unparsed":null},"HWWAFSESID":{"Name":"HWWAFSESID","Value":"88d84731cf506df0bf","Quoted":false,"Path":"/","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":false,"SameSite":0,"Partitioned":false,"Raw":"HWWAFSESID=88d84731cf506df0bf; path=/","Unparsed":null},"HWWAFSESTIME":{"Name":"HWWAFSESTIME","Value":"1763022486728","Quoted":false,"Path":"/","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":false,"SameSite":0,"Partitioned":false,"Raw":"HWWAFSESTIME=1763022486728; path=/","Unparsed":null},"CONVERSATION":{"Name":"CONVERSATION","Value":"iap-1021988086918169-CONV-83bee566-7010-4ef6-ad17-4b9eebcccdf8","Quoted":false,"Path":"/iap","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":true,"SameSite":0,"Partitioned":false,"Raw":"CONVERSATION=iap-1021988086918169-CONV-83bee566-7010-4ef6-ad17-4b9eebcccdf8; Path=/iap; HttpOnly","Unparsed":null},"CASTGC":{"Name":"CASTGC","Value":"iap-1021988086918169-TGT-717aa5db-92cb-46ac-a178-64b630946f43","Quoted":false,"Path":"/iap","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":true,"HttpOnly":true,"SameSite":4,"Partitioned":false,"Raw":"CASTGC=iap-1021988086918169-TGT-717aa5db-92cb-46ac-a178-64b630946f43; Path=/iap; HttpOnly; Secure; SameSite=None","Unparsed":null}}}`
|
||||
jar := NewPersistCookieJar()
|
||||
jar.Deserialize(str)
|
||||
t.Log(jar)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// MD5 对输入字符串进行 MD5 加密,返回 32 位小写十六进制字符串
|
||||
func MD5(input string) string {
|
||||
hash := md5.Sum([]byte(input))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// 南阳理工学院登录实现 RSA加密 & 验证码必出
|
||||
type CpdailyAhjzuLogin struct {
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func (c CpdailyAhjzuLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
|
||||
jar := cookiejar.NewPersistCookieJar()
|
||||
|
||||
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
|
||||
// 获取合法的登录URL
|
||||
loginUrl := getCasValidLoginUrl(schoolDetail)
|
||||
|
||||
// 请求登录页面并获取真正的登录URL
|
||||
loginPageResponse := getAhjzuLoginPageResponse(ctx, c.client, loginUrl)
|
||||
|
||||
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{}
|
||||
fm1Node = loginPage.Find("#credentials").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
|
||||
|
||||
// 处理密码
|
||||
allInputs["password"] = cryptoutil.MD5(password)
|
||||
|
||||
// 解决常规图片验证码
|
||||
captchaUrl := getAhjzuCaptchaUrl(loginResponseUrl)
|
||||
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||
|
||||
allInputs["validateCode"] = captchaResult
|
||||
|
||||
// 登录提交
|
||||
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 getAhjzuLoginPageResponse(ctx context.Context, client *resty.Client, loginUrl string) *resty.Response {
|
||||
res1, err := client.R().Get(loginUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("loginUrl", loginUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
if res1.StatusCode() != http.StatusFound && res1.StatusCode() != http.StatusMovedPermanently {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||
return oops.
|
||||
With("loginUrl", loginUrl).
|
||||
With("statusCode", res1.StatusCode()).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
}
|
||||
location := res1.RawResponse.Header.Get("Location")
|
||||
res2, err := client.R().Get(location)
|
||||
if res2.StatusCode() != http.StatusFound && res2.StatusCode() != http.StatusMovedPermanently {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||
return oops.
|
||||
With("loginUrl", loginUrl).
|
||||
With("statusCode", res2.StatusCode()).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
}
|
||||
location = res2.RawResponse.Header.Get("Location")
|
||||
res3, err := client.R().Get(location)
|
||||
if res3.StatusCode() != http.StatusFound && res3.StatusCode() != http.StatusMovedPermanently {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||
return oops.
|
||||
With("loginUrl", loginUrl).
|
||||
With("statusCode", res3.StatusCode()).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
}
|
||||
location = res3.RawResponse.Header.Get("Location")
|
||||
res4, err := client.R().Get(location)
|
||||
if res4.StatusCode() != http.StatusFound && res3.StatusCode() != http.StatusMovedPermanently {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||
return oops.
|
||||
With("loginUrl", loginUrl).
|
||||
With("statusCode", res4.StatusCode()).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
}
|
||||
location = res4.RawResponse.Header.Get("Location")
|
||||
res5, err := client.R().Get(location)
|
||||
if err != nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||
return oops.
|
||||
With("loginUrl", loginUrl).
|
||||
With("statusCode", res4.StatusCode()).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
}
|
||||
return res5
|
||||
}
|
||||
func getAhjzuCaptchaUrl(url *url.URL) string {
|
||||
return fmt.Sprintf("%s://%s/sso/code/code.jsp", url.Scheme, url.Host)
|
||||
}
|
||||
|
||||
func (c CpdailyAhjzuLogin) GetCookieJar() http.CookieJar {
|
||||
return c.client.CookieJar()
|
||||
}
|
||||
|
||||
func (c CpdailyAhjzuLogin) GetCookieString() string {
|
||||
cookieJar := c.GetCookieJar()
|
||||
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||
return serializableJar.Serialize()
|
||||
}
|
||||
|
||||
func (c CpdailyAhjzuLogin) GetClient() *resty.Client {
|
||||
return c.client
|
||||
}
|
||||
|
||||
func NewAhjzu(ctx context.Context) *CpdailyAhjzuLogin {
|
||||
return &CpdailyAhjzuLogin{
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ func GetCpdailyLoginBySchoolInfo(ctx context.Context, info *school.SchoolDetailI
|
||||
// 南昌理工学院
|
||||
case "d6e8f2ca-10b2-4599-bdb8-4b33d419658d":
|
||||
return NewNut(ctx)
|
||||
// 安徽建筑大学
|
||||
case "ahjzu":
|
||||
return NewAhjzu(ctx)
|
||||
default:
|
||||
if info.IsIap() {
|
||||
return NewIap(ctx)
|
||||
|
||||
@@ -39,7 +39,7 @@ type CpdailyCasLogin struct {
|
||||
|
||||
func NewCas(ctx context.Context) CpdailyLogin {
|
||||
return &CpdailyCasLogin{
|
||||
client: httpclient.Cpdaily.Clone(ctx),
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,11 +157,11 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
allInputs[name] = value
|
||||
}
|
||||
} else {
|
||||
utils.OkOrPanic(oops.With("schoolDetail", schoolDetail).
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "页面中未找到登录表单"), oops.With("schoolDetail", schoolDetail).
|
||||
With("username", username).
|
||||
With("password", password).
|
||||
WithContext(ctx).
|
||||
Wrap(errors.NewBusinessError(503, "页面中未找到登录表单")))
|
||||
Wrap)
|
||||
}
|
||||
|
||||
// 修改部分参数
|
||||
@@ -190,7 +190,9 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
} else {
|
||||
allInputs["password"] = encryptCasPassword(password, salt)
|
||||
}
|
||||
var SliderCaptchaIds = []string{"1018764144934965"}
|
||||
|
||||
// 声明需要滑块验证码的学校id
|
||||
var SliderCaptchaIds = []string{"1018764144934965", "sdhyxy"}
|
||||
// 判断是否需要验证码
|
||||
if isCasNeedCaptcha(ctx, loginResponseUrl, loginType, username, c.client) {
|
||||
// 如果是滑块验证码
|
||||
@@ -287,7 +289,7 @@ func (c *CpdailyCasLogin) ConfirmAdd(ctx context.Context, sc *school.SchoolDetai
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 提交确认添加
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
client := httpclient.NewCpdailyClient()
|
||||
client.SetCookieJar(cookieJar)
|
||||
post, err := client.R().SetFormData(map[string]string{
|
||||
"service": service,
|
||||
@@ -448,11 +450,11 @@ func submitCasKickOut(ctx context.Context, requestUrl string, doc *goquery.Docum
|
||||
}
|
||||
|
||||
func getCasValidLoginUrl(info *school.SchoolDetailInfo) string {
|
||||
if strings.Contains(info.AmpUrl, "campusphere") {
|
||||
if strings.Contains(info.AmpUrl, "campusphere") || strings.Contains(info.AmpUrl, "wec-portal-mobile") {
|
||||
return info.AmpUrl
|
||||
} else if strings.Contains(info.AmpUrl2, "campusphere") {
|
||||
} else if strings.Contains(info.AmpUrl2, "campusphere") || strings.Contains(info.AmpUrl2, "wec-portal-mobile") {
|
||||
return info.AmpUrl2
|
||||
} else if strings.Contains(info.AmpUrl3, "campusphere") {
|
||||
} else if strings.Contains(info.AmpUrl3, "campusphere") || strings.Contains(info.AmpUrl3, "wec-portal-mobile") {
|
||||
return info.AmpUrl3
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,8 @@ func CookieEnabled(ctx context.Context, host string, client *resty.Client) bool
|
||||
func GetClient(ctx context.Context, cookie string) *resty.Client {
|
||||
cookieJar := cookiejar.NewPersistCookieJar()
|
||||
cookieJar.Deserialize(cookie)
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
// 不使用 Clone,创建全新的独立 client 实例避免状态共享
|
||||
client := httpclient.NewCpdailyClient()
|
||||
client.SetCookieJar(cookieJar)
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ type CpdailyIapLogin struct {
|
||||
|
||||
func NewIap(ctx context.Context) *CpdailyIapLogin {
|
||||
return &CpdailyIapLogin{
|
||||
client: httpclient.Cpdaily.Clone(ctx),
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,12 +172,11 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
In("Cpdaily.Login").New("Iap登录失败, 用户名或密码错误")
|
||||
})
|
||||
default:
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("Iap登录失败, 状态识别码: %s", resultCode)), func(err error) error {
|
||||
return oops.With("username", username).
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("Iap登录失败, 状态识别码: %s", resultCode)),
|
||||
oops.With("username", username).
|
||||
With("password", password).
|
||||
With("userId", utils.GetUserIdNotPanic(ctx)).
|
||||
In("Cpdaily.Login").New("Iap登录失败, 状态识别码: %s" + resultCode)
|
||||
})
|
||||
In("Cpdaily.Login").Wrap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ func (c CpdailyNutLogin) GetClient() *resty.Client {
|
||||
|
||||
func NewNut(ctx context.Context) *CpdailyNutLogin {
|
||||
return &CpdailyNutLogin{
|
||||
client: httpclient.Cpdaily.Clone(ctx),
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
func reverseStringASCII(s string) string {
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
func GetSchoolList(ctx context.Context) []CpdailySchoolBriefInfo {
|
||||
response, err := httpclient.Cpdaily.R().
|
||||
response, err := httpclient.Cpdaily.Clone(ctx).R().
|
||||
WithContext(ctx).
|
||||
Get("https://static.campushoy.com/apicache/tenantListSort")
|
||||
|
||||
@@ -52,7 +52,7 @@ func GetSchoolList(ctx context.Context) []CpdailySchoolBriefInfo {
|
||||
}
|
||||
|
||||
func GetSchoolDetail(ctx context.Context, tenantId string) SchoolDetailInfo {
|
||||
response, err := httpclient.Cpdaily.R().Clone(ctx).
|
||||
response, err := httpclient.Cpdaily.Clone(ctx).R().
|
||||
SetHeaders(map[string]string{
|
||||
"deviceType": "2",
|
||||
"CpdailyClientType": "CPDAILY",
|
||||
|
||||
@@ -640,11 +640,11 @@ func (c *CpdailySleepCheckTask) Submit(ctx context.Context, form map[string]*gva
|
||||
|
||||
// 构造提交数据
|
||||
submitData := map[string]*gvar.Var{
|
||||
"lon": form["longitude"],
|
||||
"version": gvar.New(cryptoutil.EncryptVersion),
|
||||
"calVersion": gvar.New(cpdailyInfo.CalVersion),
|
||||
"deviceId": extensionForm["deviceId"],
|
||||
"userId": gvar.New(username),
|
||||
"lon": form["longitude"],
|
||||
"version": gvar.New(cryptoutil.EncryptVersion),
|
||||
"calVersion": gvar.New(cpdailyInfo.CalVersion),
|
||||
"deviceId": extensionForm["deviceId"],
|
||||
//"userId": gvar.New(username),
|
||||
"systemName": gvar.New("android"),
|
||||
"bodyString": gvar.New(bodyString),
|
||||
"lat": form["latitude"],
|
||||
|
||||
@@ -10,11 +10,12 @@ import (
|
||||
|
||||
var Cpdaily = resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30 * time.Second).
|
||||
SetTimeout(30*time.Second).
|
||||
SetDoNotParseResponse(true).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
}).
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
|
||||
|
||||
func init() {
|
||||
if !utils.IsDebug() {
|
||||
@@ -23,3 +24,25 @@ func init() {
|
||||
Cpdaily.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
}
|
||||
|
||||
// NewCpdailyClient 创建一个全新的独立 client 实例,避免 Clone 可能的浅拷贝问题
|
||||
// 每个实例都是完全独立的,适用于并发场景,不会共享状态
|
||||
func NewCpdailyClient() *resty.Client {
|
||||
client := resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30*time.Second).
|
||||
SetDoNotParseResponse(true).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}).
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
|
||||
|
||||
// 根据调试模式设置代理和调试选项
|
||||
if !utils.IsDebug() {
|
||||
client.SetDebug(false)
|
||||
} else {
|
||||
client.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
} else if e1, ok := utils.Of[*errors2.BusinessError](err); ok {
|
||||
errorMessage = e1.Message
|
||||
} else {
|
||||
errorMessage = "未知的错误导致任务执行失败"
|
||||
errorMessage = "未知的错误导致任务执行失败" + err.Error()
|
||||
}
|
||||
|
||||
e = db.Global.CpTaskLog.Create().
|
||||
@@ -96,13 +96,14 @@ 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) {
|
||||
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)
|
||||
}
|
||||
//if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, restyClient) {
|
||||
// 定时任务强制登录并更新cookie
|
||||
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)
|
||||
//}
|
||||
// 获取任务执行handler
|
||||
taskExecutor := task2.GetCpdailyTaskService(ctx, targetTask.TaskType, restyClient, schoolDetail)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
cst2 "likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/db"
|
||||
"likedream-goframe/internal/ent"
|
||||
@@ -124,10 +123,10 @@ func (ts *TaskScanner) scanCpdailyTask() {
|
||||
|
||||
e = utils.Catch(func() {
|
||||
queue.AddTask(ts.ctx, asynq.NewTask(cst2.TaskCpdaily, payloadWriter.Bytes()),
|
||||
asynq.TaskID(fmt.Sprintf(cst2.TaskCpdailyId, task.ID)),
|
||||
asynq.Unique(5*time.Minute),
|
||||
asynq.MaxRetry(4),
|
||||
asynq.ProcessAt(execTime),
|
||||
asynq.Deadline(execTime.Add(10*time.Minute)),
|
||||
asynq.Timeout(10*time.Minute),
|
||||
asynq.Queue(cst2.TaskQueueCpdaily),
|
||||
)
|
||||
// 更新未开始的任务状态为队列中
|
||||
|
||||
@@ -26,7 +26,7 @@ func initServer(ctx context.Context) {
|
||||
RetryDelayFunc: func(n int, _ error, _ *asynq.Task) time.Duration {
|
||||
return time.Duration(n) * 2 * time.Second
|
||||
},
|
||||
DelayedTaskCheckInterval: 10 * time.Second,
|
||||
DelayedTaskCheckInterval: 2 * time.Second,
|
||||
ErrorHandler: taskErrorHandler{},
|
||||
Queues: map[string]int{
|
||||
cst2.TaskQueueEmail: 2,
|
||||
|
||||
@@ -260,11 +260,10 @@ func (v CpTaskServiceV1) ExecuteNow(ctx context.Context, req *v1.ExecuteNowReq)
|
||||
|
||||
// 使用时间戳生成唯一的 TaskID,避免与自动扫描的任务冲突,也避免 Unique 限制
|
||||
// 由于已经检查了任务状态,不需要使用 Unique 来防重复
|
||||
taskID := fmt.Sprintf("cpdaily:task:manual:%d:%d", req.Id, time.Now().UnixNano())
|
||||
//taskID := fmt.Sprintf("cpdaily:task:manual:%d:%d", req.Id, time.Now().UnixNano())
|
||||
e = utils.Catch(func() {
|
||||
queue.AddTask(ctx, asynq.NewTask(cst2.TaskCpdaily, payloadWriter.Bytes()),
|
||||
asynq.TaskID(taskID),
|
||||
asynq.Unique(5*time.Second),
|
||||
asynq.Unique(2*time.Minute),
|
||||
asynq.MaxRetry(0),
|
||||
asynq.Timeout(10*time.Minute),
|
||||
asynq.Deadline(time.Now().Add(10*time.Minute)),
|
||||
|
||||
Reference in New Issue
Block a user