Compare commits

..

5 Commits

Author SHA1 Message Date
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
14 changed files with 187 additions and 18 deletions
+61 -2
View File
@@ -3,17 +3,47 @@ package cookiejar
import (
"bytes"
"encoding/gob"
"fmt"
"github.com/bytedance/sonic"
"io"
"likedream-goframe/internal/utils"
"net/http"
"net/url"
"strings"
"sync"
)
type hostMap map[string]cookieMap
type cookieMap map[string]*http.Cookie
// cookieKey 生成 cookie 的唯一标识 key,使用 name 和 path 的组合
func cookieKey(name, path string) string {
return fmt.Sprintf("%s|%s", name, path)
}
// pathMatch 检查请求路径是否匹配 cookie 的 path
// 根据 RFC 6265 Section 5.1.4
func pathMatch(requestPath, cookiePath string) bool {
// 如果路径完全相同
if requestPath == cookiePath {
return true
}
// 如果 cookie path 是请求 path 的前缀
if strings.HasPrefix(requestPath, cookiePath) {
// cookie path 以 / 结尾,或者请求 path 在 cookie path 后紧跟 /
if strings.HasSuffix(cookiePath, "/") {
return true
}
if len(requestPath) > len(cookiePath) && requestPath[len(cookiePath)] == '/' {
return true
}
}
return false
}
type PersistCookieJar struct {
hostMap hostMap
*sync.RWMutex
@@ -40,7 +70,23 @@ func (p *PersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
if p.hostMap[host] == nil {
p.hostMap[host] = make(cookieMap)
}
p.hostMap[host][cookie.Name] = cookie
// 如果 cookie 没有设置 Path,使用默认值 "/"
cookiePath := cookie.Path
if cookiePath == "" {
cookiePath = "/"
}
// 使用 name 和 path 的组合作为 key
key := cookieKey(cookie.Name, cookiePath)
// 如果 MaxAge=0 或 MaxAge<0,表示删除 cookie
// 只删除完全匹配的 cookie(name 和 path 都相同)
if cookie.MaxAge < 0 || (cookie.MaxAge == 0 && cookie.Value == "") {
delete(p.hostMap[host], key)
} else {
p.hostMap[host][key] = cookie
}
}
}
@@ -50,10 +96,23 @@ func (p *PersistCookieJar) Cookies(u *url.URL) []*http.Cookie {
defer p.RUnlock()
host := u.Host
requestPath := u.Path
if requestPath == "" {
requestPath = "/"
}
if cookies, exists := p.hostMap[host]; exists {
result := make([]*http.Cookie, 0, len(cookies))
for _, cookie := range cookies {
result = append(result, cookie)
// 检查 cookie 的 path 是否匹配请求的 path
cookiePath := cookie.Path
if cookiePath == "" {
cookiePath = "/"
}
if pathMatch(requestPath, cookiePath) {
result = append(result, cookie)
}
}
return result
}
@@ -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())
}
+3 -1
View File
@@ -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) {
// 如果是滑块验证码
+41 -8
View File
@@ -5,18 +5,22 @@ 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
}
@@ -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,19 @@ 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)), func(err error) error {
return oops.With("username", username).
With("password", password).
With("userId", utils.GetUserIdNotPanic(ctx)).
In("Cpdaily.Login").New("Iap登录失败, 状态识别码: %s" + resultCode)
})
}
}
}
+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).
@@ -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
+1 -2
View File
@@ -126,9 +126,8 @@ func (ts *TaskScanner) scanCpdailyTask() {
queue.AddTask(ts.ctx, asynq.NewTask(cst2.TaskCpdaily, payloadWriter.Bytes()),
asynq.TaskID(fmt.Sprintf(cst2.TaskCpdailyId, task.ID)),
asynq.MaxRetry(4),
asynq.Unique(10*time.Second),
asynq.ProcessAt(execTime),
asynq.Deadline(execTime.Add(12*time.Minute)),
asynq.Deadline(execTime.Add(10*time.Minute)),
asynq.Queue(cst2.TaskQueueCpdaily),
)
// 更新未开始的任务状态为队列中
+1
View File
@@ -294,6 +294,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("暂停成功")
}
+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]