Compare commits

...

21 Commits

Author SHA1 Message Date
sugarBecomer 7469813e37 🐛 浅浅的加个锁 2025-11-18 15:53:57 +08:00
sugarBecomer 8eb4e40b57 🐛 缓存高频访问的学校信息 2025-11-18 15:37:08 +08:00
sugarBecomer 311cf18708 🐛 每次直接创建全新client试试效果 2025-11-18 14:57:37 +08:00
sugarBecomer 1bde0af01d 🐛 小问题修复 2025-11-18 14:34:43 +08:00
sugarBecomer 765ec6fb32 🐛 修复cookie问题 2025-11-17 17:14:49 +08:00
sugarBecomer 5cd8329519 🐛 修改定时任务信息 2025-11-17 12:39:05 +08:00
sugarBecomer 1ea9170d89 Merge remote-tracking branch 'origin/main' 2025-11-17 12:05:37 +08:00
sugarBecomer 29fc69d022 🐛 尝试解决cookie jar的问题 2025-11-17 12:03:47 +08:00
RecordingTheSmile 09e7a82882 fix: iap login failed message 2025-11-16 19:51:35 +08:00
sugarBecomer 1fff983d37 🐛 尝试干掉submitData的userId看看是否解决 2025-11-16 19:14:32 +08:00
RecordingTheSmile 836e61b8b0 fix: duplicate asynq.Unique option. 2025-11-14 22:36:01 +08:00
sugarBecomer 96044b8a88 Merge remote-tracking branch 'origin/main' 2025-11-14 22:35:32 +08:00
sugarBecomer acb865596e 🐛 取消定时任务cookie检测,强制登录 2025-11-14 22:35:15 +08:00
RecordingTheSmile 59559f4201 fix: task id unique 2025-11-14 22:26:10 +08:00
sugarBecomer 4d516adea8 ✨ 新增适配安徽建筑大学登录 2025-11-14 21:48:56 +08:00
sugarBecomer a37368ddcf ✨ 新增适配山东华宇工学院滑块 2025-11-14 20:38:58 +08:00
sugarBecomer 26dcfaf82d 🐛 增加日志信息 2025-11-14 13:09:00 +08:00
sugarBecomer e55a8866b1 🐛 schedule不要任务id唯一 2025-11-13 23:28:32 +08:00
sugarBecomer 0702135db2 🐛 iap新版本登录 2025-11-13 22:39:01 +08:00
sugarBecomer c668a76a99 🐛 关联关系bug 2025-11-13 21:42:26 +08:00
sugarBecomer 2a1cb90766 🐛 紧急修复漏洞 2025-11-13 21:20:07 +08:00
30 changed files with 731 additions and 82 deletions
+5 -1
View File
@@ -44,4 +44,8 @@ bucket = "likedream"
[cpdaily]
appVersion = "9.7.2"
systemVersion = "iOS 17"
calVersion = "firstv"
calVersion = "firstv"
[task]
concurrency = 20
+132 -10
View File
@@ -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
}
+156 -13
View File
@@ -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)
}
@@ -12,14 +12,14 @@ import (
)
func TestDesDecrypt(t *testing.T) {
decodeString, _ := base64.StdEncoding.DecodeString("X3/auV/FUP3Fj9iou6jNosFm9+6oiZ08pQlSt2qUlEhBEZXchcrXZJ96OsPIH4tfc6oRqdvlnObFx+uJfKA9oEuQQURvvDcDLz/1uAsQHXQIXNaW1Bwt9vdo4e1Sxzvt2sBxcHZXlsAPnxRw8UCM6DnoU4kHXF45A2i9wQpzH1cgKsoN/tWLRPYOBSMsPfbsch+wFgFpkQGRWNM9E0CVmpwZJWn946McO4HbMJngTeT5Df8YOUgpkFdccMKrJyPqE7UMOc56YLkPjb/wzmFyd1/HFCUbJwwdhOueH70GSpsH5LrWEhw7jvf2VBmnm669E2uvQjeCtPI1SvVuf7WP9Srp1BXpMtfK")
decodeString, _ := base64.StdEncoding.DecodeString("64JITpWPkKuGMhi1m7Ikxu1ekL6VQOdjmJvaQPI07MWbLETtJsxzkk2bCUVNaWgUULa3wS9LH+ANO5CUBeOXi02CcS/8lN/tYDFYCyuK7dLN+q0fD0gQlUsuHwy/78EkwZa9VrLh4e519vM9AxR2GnK4WGpumeH7TGJjDkLH0AitKfUYQnil9AKZEVgSSkHkg9ko64qj0QJvzmo76zOkMpIb1+1Pn2cO+5soTSl8BCsP/8hOP5HdOMVBCYH++iSK3O5sYz/cwZq/G8aJrqmEFcDHbgTouXyD5kxa4lb72Vw3Ku+eH+3qDwYMYxWrIcAm")
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==")
d2, _ := base64.StdEncoding.DecodeString("86JzX4MBsorI5KasKBHUj6orO1qcPWbo758Ux6G2NUoRuGCoZnE+cJW5slZvsflBrKITPeitF7GTisMNRJxRqlA8cLXAqyo9ZlBPPx0MM3KjILHiA/EvrRrmamo7vi1d/F0ChQc8jLfMgkkcOg4FBmM1H8Yg6KyYdMNN7anIbIdJ+6RPEsakjAIw0r4eawofSh5E0QclDry0v36GK3UA3GDBBfqSub8V5PRL7fQty/R0lx7+mfc2AIXXjUVsv85WQKoyyUwg3siWpQL5S1cbtQNUXClSQP1IAGROnBftvdrib01PR1h7Cp+kvE3HK0+/8l0FoGJ1aijff1t9T9MaVXM/jSUFwkA7tmZIjodrUDhZcCMNm5YLm1ZO4UVBQO118FyOBjSLet0nPc0Wg0/dTO0XZoo9e2bCQwjIQC2G8TdzHDxvDjGEc6hX++nZrN23n19bAgqdvzZt58KftznE9s5t4CEjiHx3ykyidGEGr/VEi/05uOtm8/EsFUVp8+MyVgaTxoaq/4LBu28L59U6+6mZjOFUt49STR5p3UPI3ZJQLe2lmnx9BdqrIHbdXgYNu5oNzrDM8m3z3ns9M2HEGg==")
d := AesDecryptForCat(d2)
t.Log(d)
@@ -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[:])
}
@@ -2,6 +2,9 @@ package cryptoutil
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"fmt"
"math/big"
@@ -36,6 +39,34 @@ func NewRSAKeyPair(exponent, privateExponent, modulus string) (*RSAKeyPair, erro
}, nil
}
// NewRSAKeyPairFromPublicKey 从 X.509 格式的 base64 公钥字符串创建 RSA 密钥对
// publicKeyBase64: X.509 DER 格式的 base64 编码公钥字符串
func NewRSAKeyPairFromPublicKey(publicKeyBase64 string) (*RSAKeyPair, error) {
// 解码 base64
derBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
if err != nil {
return nil, fmt.Errorf("failed to decode base64: %w", err)
}
// 解析 X.509 格式的公钥
pubKey, err := x509.ParsePKIXPublicKey(derBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse public key: %w", err)
}
// 转换为 RSA 公钥
rsaPubKey, ok := pubKey.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("not an RSA public key")
}
// 创建密钥对
return &RSAKeyPair{
n: rsaPubKey.N,
e: big.NewInt(int64(rsaPubKey.E)),
}, nil
}
// Encrypt 使用公钥加密数据,返回十六进制字符串
func (k *RSAKeyPair) Encrypt(plaintext string) (string, error) {
ciphertext, err := k.EncryptBytes([]byte(plaintext))
@@ -52,3 +52,36 @@ func TestRSAKeyPair_EncryptBytes(t *testing.T) {
t.Logf("密文长度: %d bytes", len(ciphertext))
}
func TestNewRSAKeyPairFromPublicKey(t *testing.T) {
// 使用你提供的 X.509 格式的 base64 公钥
publicKey := "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCpbRy8ZoyQvRPpUDIXycglTwVDcYrLcv7P9HI4E7/TAPZiI3GN0ckTUVVWRvoo/re3uCOOJK+F+Ufh19XiRIdoPYCULsSyEjcLLiyAS09mVOzhAQco84E/lM24T7rRTVID7LIgWPewyN8Nd5vzHET4Q3qTthoL8n82BwC1iXuVQIDAQAB"
keyPair, err := NewRSAKeyPairFromPublicKey(publicKey)
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("密文为空")
}
// 验证密文长度(1024位密钥应该产生128字节 = 256个十六进制字符)
expectedHexLen := (keyPair.GetModulus().BitLen() + 7) / 8 * 2
if len(ciphertext) != expectedHexLen {
t.Errorf("密文长度不正确: 期望 %d, 实际 %d", expectedHexLen, len(ciphertext))
}
t.Logf("密钥长度: %d bits", keyPair.GetModulus().BitLen())
}
+209
View File
@@ -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)
+10 -8
View File
@@ -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
}
+2 -1
View File
@@ -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
}
+41 -9
View File
@@ -5,25 +5,29 @@ import (
"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/samber/oops"
"github.com/tidwall/gjson"
"resty.dev/v3"
)
const iapRsaPubKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCpbRy8ZoyQvRPpUDIXycglTwVDcYrLcv7P9HI4E7/TAPZiI3GN0ckTUVVWRvoo/re3uCOOJK+F+Ufh19XiRIdoPYCULsSyEjcLLiyAS09mVOzhAQco84E/lM24T7rRTVID7LIgWPewyN8Nd5vzHET4Q3qTthoL8n82BwC1iXuVQIDAQAB"
type CpdailyIapLogin struct {
client *resty.Client
}
func NewIap(ctx context.Context) *CpdailyIapLogin {
return &CpdailyIapLogin{
client: httpclient.Cpdaily.Clone(ctx),
client: httpclient.NewCpdailyClient(),
}
}
@@ -37,11 +41,10 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
c.client.SetCookieJar(jar).
SetRedirectPolicy(resty.NoRedirectPolicy())
ltInfoResponse, err := c.client.R().
WithContext(ctx).
SetBody(map[string]any{}).
Post(fmt.Sprintf("%s/iap/security/lt", loginUrl))
// 获取基础lt
preLoginUrl := loginUrl + "/portal/login"
c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
loginRedirectResponse, err := c.client.R().Get(preLoginUrl)
utils.OkOrPanic(err, func(err error) error {
return oops.
In("CpdailyIapLogin.Login").
@@ -50,6 +53,25 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
With("password", password).
Wrap(err)
})
u := loginRedirectResponse.RawResponse.Request.URL
oldLt := strings.Split(u.String(), "=")[1]
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
ltInfoResponse, err := c.client.R().
WithContext(ctx).
SetFormData(map[string]string{
"lt": oldLt,
}).
Post(fmt.Sprintf("%s/iap/security/lt", loginUrl))
rsa, _ := cryptoutil.NewRSAKeyPairFromPublicKey(iapRsaPubKey)
password, _ = rsa.Encrypt(password)
utils.OkOrPanic(err, func(err error) error {
return oops.
In("CpdailyIapLogin.Login").
With("schoolDetail", schoolDetail).
With("username", username).
With("password", "{rsa}"+password).
Wrap(err)
})
defer ltInfoResponse.Body.Close()
@@ -64,8 +86,9 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
"rememberMe": "true",
"dllt": "",
"mobile": "",
"captcha": "",
"username": username,
"password": password,
"password": "{rsa}" + password,
}
if isIapNeedCaptcha(ctx, loginUrl, username, c.client) {
@@ -142,9 +165,18 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
case "CAPTCHA_NOTMATCH":
utils.OkOrPanic(errors.NewBusinessError(503, "Iap登录失败, 验证码识别失败"))
case "FAIL_UPNOTMATCH":
utils.OkOrPanic(errors.NewBusinessError(503, "Iap登录失败, 用户名或密码错误"))
utils.OkOrPanic(errors.NewBusinessError(503, "Iap登录失败, 用户名或密码错误"), func(err error) error {
return oops.With("username", username).
With("password", password).
With("userId", utils.GetUserIdNotPanic(ctx)).
In("Cpdaily.Login").New("Iap登录失败, 用户名或密码错误")
})
default:
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("Iap登录失败, 状态识别码: %s", resultCode)))
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").Wrap)
}
}
}
+1 -1
View File
@@ -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 {
@@ -7,16 +7,22 @@ import (
"encoding/json"
cryptoutil2 "likedream-goframe/internal/application/cpdaily/cryptoutil"
"likedream-goframe/internal/application/httpclient"
cst2 "likedream-goframe/internal/common/cst"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/framework/redis"
"likedream-goframe/internal/utils"
"sync"
"time"
"github.com/bytedance/sonic"
"github.com/samber/oops"
"github.com/tidwall/gjson"
)
var lock = sync.RWMutex{}
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 +58,17 @@ func GetSchoolList(ctx context.Context) []CpdailySchoolBriefInfo {
}
func GetSchoolDetail(ctx context.Context, tenantId string) SchoolDetailInfo {
response, err := httpclient.Cpdaily.R().Clone(ctx).
lock.Lock()
defer lock.Unlock()
client := redis.GetClient()
redisResult := client.Get(ctx, cst2.RedisSchoolInfo+tenantId)
if redisResult.Err() == nil {
var ret SchoolDetailInfo
err := sonic.Unmarshal([]byte(redisResult.Val()), &ret)
utils.OkOrPanic(err)
return ret
}
response, err := httpclient.Cpdaily.Clone(ctx).R().
SetHeaders(map[string]string{
"deviceType": "2",
"CpdailyClientType": "CPDAILY",
@@ -89,6 +105,6 @@ func GetSchoolDetail(ctx context.Context, tenantId string) SchoolDetailInfo {
WithContext(ctx).
Wrap(errors.NewBusinessError(503, "学校ID不存在").Decorate("empty school detail")))
}
client.Set(ctx, cst2.RedisSchoolInfo+tenantId, ret[0], time.Duration(cst2.RedisSchoolInfoExpire)*time.Second)
return ret[0]
}
@@ -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"],
+25 -2
View File
@@ -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
}
+2
View File
@@ -7,6 +7,7 @@ const (
RedisBalanceMigrate = "balancemigrate:" // 积分转移token
RedisCpUserLogin = "cpuserlogin:"
RedisCpUserAuthorize = "cpuser:authorize:"
RedisSchoolInfo = "cpuser:schoolinfo:"
)
var (
@@ -16,4 +17,5 @@ var (
RedisBalanceMigrateExpire = int64(3600) // 积分转移链接1小时有效
RedisCpUserLoginExpire = int64(180)
RedisCpUserAuthorizeExpire = int64(1800)
RedisSchoolInfoExpire = int64(3600) // 学校信息1小时有效
)
+1 -1
View File
@@ -27,7 +27,7 @@ func InitDB() {
client, err := ent.Open(dbType.String(), dbDsn.String())
utils.OkOrPanic(err)
// 自动迁移
if err := client.Debug().Schema.Create(context.Background()); err != nil {
if err := client.Schema.Create(context.Background()); err != nil {
logger.Logger.Fatal("failed creating schema resources: %v", zap.Error(err))
}
Global = client
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -79,7 +79,7 @@ var (
Symbol: "cp_tasks_cp_users_cptasks",
Columns: []*schema.Column{CpTasksColumns[15]},
RefColumns: []*schema.Column{CpUsersColumns[0]},
OnDelete: schema.NoAction,
OnDelete: schema.Cascade,
},
{
Symbol: "cp_tasks_sys_user_cp_task",
+1 -1
View File
@@ -31,7 +31,7 @@ func (CpUser) Fields() []ent.Field {
// Edges of the CpUser.
func (CpUser) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cptasks", CpTask.Type),
edge.To("cptasks", CpTask.Type).Annotations(entsql.OnDelete(entsql.Cascade)),
edge.From("sysuser", SysUser.Type).Unique().Required().Field("user_id").Ref("cpuser"),
edge.To("cp_task_logs", CpTaskLog.Type).Annotations(entsql.OnDelete(entsql.Cascade)), // 用户拥有的cp任务日志
edge.From("school", CpSchool.Type).
+12 -9
View File
@@ -55,7 +55,9 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusDoing).ExecX(ctx)
defer utils.HandleError(func(err error) {
e := db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusFail).Exec(ctx)
// 执行失败应该更新下次执行时间
executeTime := utils.GetCpdailyNextExecuteTime(targetTask)
e := db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetNextExecuteTime(executeTime).SetTaskStatus(cptask.TaskStatusFail).Exec(ctx)
utils.LogError(ctx, "设置任务状态失败", oops.With("task_id", taskPayload.CpTaskID).Wrap(e))
var errorMessage string
@@ -65,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().
@@ -94,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)
+2 -4
View File
@@ -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,11 +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.Unique(10*time.Second),
asynq.ProcessAt(execTime),
asynq.Deadline(execTime.Add(12*time.Minute)),
asynq.Timeout(10*time.Minute),
asynq.Queue(cst2.TaskQueueCpdaily),
)
// 更新未开始的任务状态为队列中
+1 -1
View File
@@ -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,
+3 -2
View File
@@ -16,8 +16,8 @@ var BalanceService = &BalanceServiceV1{}
type BalanceServiceV1 struct {
}
func (v *BalanceServiceV1) ChangeUserBalance(ctx context.Context, userId int64, balance decimal.Decimal) error {
return db.WithTx(ctx, func(c context.Context, tx *ent.Tx) {
func (v *BalanceServiceV1) ChangeUserBalance(ctx context.Context, userId int64, balance decimal.Decimal) {
err := db.WithTx(ctx, func(c context.Context, tx *ent.Tx) {
// 使用 SELECT ... FOR UPDATE 锁定用户记录
user, err := tx.SysUser.Query().
Where(sysuser.ID(userId)).
@@ -39,5 +39,6 @@ func (v *BalanceServiceV1) ChangeUserBalance(ctx context.Context, userId int64,
ExecX(ctx)
tx.Commit()
}, nil)
utils.OkOrPanic(err)
}
+3 -3
View File
@@ -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)),
@@ -294,6 +293,7 @@ func (v CpTaskServiceV1) PauseTask(ctx context.Context, req *v1.PauseTaskReq) *u
return utils.SuccessWithMsg("恢复成功")
} else {
go schedule.StopTask(context.Background(), cst2.TaskQueueCpdaily, fmt.Sprintf(cst2.TaskCpdailyId, req.Id))
db2.Global.CpTask.UpdateOne(cpTask).SetTaskStatus(cptask.TaskStatusPause).SetNextExecuteTime(-1).ExecX(ctx)
return utils.SuccessWithMsg("暂停成功")
}
@@ -364,10 +364,8 @@ func (s *UserInfoServiceV1) BalanceMigrate(ctx context.Context, req *v1.BalanceM
if newUser.Edges.Agent != nil {
b = utils.TransformBalance(b, newUser.Edges.Agent.Rate)
}
err = balance.BalanceService.ChangeUserBalance(ctx, newUser.ID, b)
if err != nil {
return utils.FailParam("积分转移失败,请重新尝试")
}
balance.BalanceService.ChangeUserBalance(ctx, newUser.ID, b)
// 清空旧系统积分
err = db.GlobalGorm.Table("AspNetUsers").Where("email = ?", oldSystemEmail).Update("balance", 0).Error
if err != nil {
+8
View File
@@ -19,6 +19,14 @@ func GetUserInfoFromCtx(ctx context.Context) *ent.SysUser {
return user
}
func GetUserIdNotPanic(ctx context.Context) int64 {
if ctx.Value(cst.CtxUserInfo) == nil {
return 0
}
user := ctx.Value(cst.CtxUserInfo).(*ent.SysUser)
return user.ID
}
// CalculateBalance 获取给下级充值后剩余余额
func CalculateBalance(balance decimal.Decimal, rate decimal.Decimal, amount decimal.Decimal, amountRate decimal.Decimal) decimal.Decimal {
// 如果当前级别费率为0(超级管理员)
+1
View File
@@ -22,6 +22,7 @@ func main() {
if err != nil {
log.Panic(err)
}
time.Local = cst.TimeZoneShangHai
args := ""
if len(os.Args) > 1 {
args = os.Args[1]