Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 788f1f4de7 | |||
| 54d81b5ba8 | |||
| 4912a27348 | |||
| 085820892f | |||
| 391a4c2ccb |
@@ -457,8 +457,9 @@ func submitCasKickOut(ctx context.Context, requestUrl string, doc *goquery.Docum
|
||||
|
||||
func getCasValidLoginUrl(info *school.SchoolDetailInfo) string {
|
||||
// 特殊学校单独处理
|
||||
// 甘肃农业大学
|
||||
if info.Id == "gsau" {
|
||||
// 甘肃农业大学 西安体育学院
|
||||
ids := []string{"gsau", "ddf0639d-394d-48a2-a166-07ed84b7e6f0"}
|
||||
if slices.Contains(ids, info.Id) {
|
||||
return info.IdsUrl
|
||||
}
|
||||
if strings.Contains(info.AmpUrl, "campusphere") || strings.Contains(info.AmpUrl, "wec-portal-mobile") {
|
||||
|
||||
@@ -3,11 +3,17 @@ package httpclient
|
||||
import (
|
||||
"crypto/tls"
|
||||
"likedream-goframe/internal/utils"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
os.Setenv("GODEBUG", "tlsrsakex=1")
|
||||
}
|
||||
|
||||
var Cpdaily = resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30*time.Second).
|
||||
@@ -28,13 +34,18 @@ func init() {
|
||||
// NewCpdailyClient 创建一个全新的独立 client 实例,避免 Clone 可能的浅拷贝问题
|
||||
// 每个实例都是完全独立的,适用于并发场景,不会共享状态
|
||||
func NewCpdailyClient() *resty.Client {
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: tls.VersionTLS10,
|
||||
},
|
||||
TLSHandshakeTimeout: 15 * time.Second,
|
||||
}
|
||||
client := resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30*time.Second).
|
||||
SetDoNotParseResponse(true).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}).
|
||||
SetTransport(transport).
|
||||
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")
|
||||
|
||||
// 根据调试模式设置代理和调试选项
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"likedream-goframe/internal/utils"
|
||||
"time"
|
||||
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
func NewMogudingClient() *resty.Client {
|
||||
client := resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30*time.Second).
|
||||
SetDoNotParseResponse(false).
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
const (
|
||||
// AES 密钥
|
||||
Secret = "23DbtQHR2UMbH6mJ"
|
||||
// MD5 签名后缀
|
||||
SignSuffix = "3478cbbc33f84bd00d75d7dfa69e0daa"
|
||||
)
|
||||
|
||||
// Encrypt 使用 AES ECB PKCS7 加密并返回十六进制字符串
|
||||
func Encrypt(plaintext string) (string, error) {
|
||||
key := []byte(Secret)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// PKCS7 填充
|
||||
plainBytes := []byte(plaintext)
|
||||
padding := aes.BlockSize - len(plainBytes)%aes.BlockSize
|
||||
padText := make([]byte, len(plainBytes)+padding)
|
||||
copy(padText, plainBytes)
|
||||
for i := len(plainBytes); i < len(padText); i++ {
|
||||
padText[i] = byte(padding)
|
||||
}
|
||||
|
||||
// ECB 模式加密
|
||||
encrypted := make([]byte, len(padText))
|
||||
for i := 0; i < len(padText); i += aes.BlockSize {
|
||||
block.Encrypt(encrypted[i:i+aes.BlockSize], padText[i:i+aes.BlockSize])
|
||||
}
|
||||
|
||||
return hex.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
// Sign2 MD5 签名
|
||||
func Sign2(str string) string {
|
||||
str += SignSuffix
|
||||
hash := md5.Sum([]byte(str))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package entity
|
||||
|
||||
// MogudingUser 蘑菇钉用户实体
|
||||
type MogudingUser struct {
|
||||
Username string `json:"username"` // 账号
|
||||
Password string `json:"password"` // 密码
|
||||
UserId string `json:"userId"`
|
||||
Token string `json:"token"`
|
||||
UserType string `json:"userType"`
|
||||
PlanId string `json:"planId"`
|
||||
PlanName string `json:"planName"`
|
||||
}
|
||||
|
||||
// LoginDto 登录请求 DTO
|
||||
type LoginDto struct {
|
||||
Phone string `json:"phone"`
|
||||
Password string `json:"password"`
|
||||
Captcha string `json:"captcha"`
|
||||
LoginType string `json:"loginType"`
|
||||
Uuid string `json:"uuid"`
|
||||
Device string `json:"device"`
|
||||
Version string `json:"version"`
|
||||
T string `json:"t"`
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应
|
||||
type LoginResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
UserId string `json:"userId"`
|
||||
Token string `json:"token"`
|
||||
UserType string `json:"userType"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// PlanResponse 获取 PlanId 响应
|
||||
type PlanResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data []struct {
|
||||
PlanId string `json:"planId"`
|
||||
PlanName string `json:"planName"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// SignRequest 签到请求
|
||||
type SignRequest struct {
|
||||
Country string `json:"country"`
|
||||
Address string `json:"address"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
State string `json:"state"`
|
||||
Area string `json:"area"`
|
||||
Latitude string `json:"latitude"`
|
||||
Longitude string `json:"longitude"`
|
||||
Description string `json:"description"`
|
||||
PlanId string `json:"planId"`
|
||||
Type string `json:"type"` // START 或 END
|
||||
Device string `json:"device"`
|
||||
}
|
||||
|
||||
// SignResponse 签到响应
|
||||
type SignResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package moguding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/application/moguding/cryptoutil"
|
||||
"likedream-goframe/internal/application/moguding/entity"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
// API 基础地址
|
||||
ApiBase = "https://api.moguding.net:9000"
|
||||
// 登录接口
|
||||
ApiLogin = ApiBase + "/session/user/v6/login"
|
||||
// 获取实习计划接口
|
||||
ApiGetPlan = ApiBase + "/practice/plan/v3/getPlanByStu"
|
||||
// 打卡签到接口
|
||||
ApiSign = ApiBase + "/attendence/clock/v4/save"
|
||||
)
|
||||
|
||||
type MogudingClient struct {
|
||||
client *resty.Client
|
||||
user *entity.MogudingUser
|
||||
}
|
||||
|
||||
// NewMogudingClient 创建蘑菇钉客户端
|
||||
func NewMogudingClient(ctx context.Context) *MogudingClient {
|
||||
client := httpclient.NewMogudingClient()
|
||||
|
||||
return &MogudingClient{
|
||||
client: client,
|
||||
user: &entity.MogudingUser{},
|
||||
}
|
||||
}
|
||||
|
||||
// Login 登录
|
||||
func (c *MogudingClient) Login(ctx context.Context, username, password string) error {
|
||||
if username == "" || password == "" {
|
||||
return fmt.Errorf("账号或密码不能为空")
|
||||
}
|
||||
|
||||
// 加密账号密码
|
||||
encryptedPhone, err := cryptoutil.Encrypt(username)
|
||||
if err != nil {
|
||||
logger.Logger.Error("加密账号失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
encryptedPassword, err := cryptoutil.Encrypt(password)
|
||||
if err != nil {
|
||||
logger.Logger.Error("加密密码失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 加密时间戳
|
||||
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||
encryptedTime, err := cryptoutil.Encrypt(timestamp)
|
||||
if err != nil {
|
||||
logger.Logger.Error("加密时间戳失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 构造登录请求
|
||||
loginDto := entity.LoginDto{
|
||||
Phone: encryptedPhone,
|
||||
Password: encryptedPassword,
|
||||
T: encryptedTime,
|
||||
LoginType: "android",
|
||||
}
|
||||
|
||||
// 发送登录请求
|
||||
resp, err := c.client.R().
|
||||
SetBody(loginDto).
|
||||
Post(ApiLogin)
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.Error("登录请求失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var loginResp entity.LoginResponse
|
||||
if err := json.Unmarshal(resp.Bytes(), &loginResp); err != nil {
|
||||
logger.Logger.Error("解析登录响应失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if loginResp.Code != 200 {
|
||||
return fmt.Errorf("登录失败:%s", loginResp.Msg)
|
||||
}
|
||||
|
||||
// 保存用户信息
|
||||
c.user.Username = username
|
||||
c.user.Password = password
|
||||
c.user.UserId = loginResp.Data.UserId
|
||||
c.user.Token = loginResp.Data.Token
|
||||
c.user.UserType = loginResp.Data.UserType
|
||||
|
||||
logger.Logger.Info("登录成功",
|
||||
zap.String("userId", c.user.UserId),
|
||||
zap.String("userType", c.user.UserType))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlanId 获取实习计划 ID
|
||||
func (c *MogudingClient) GetPlanId(ctx context.Context) error {
|
||||
if c.user.Token == "" {
|
||||
return fmt.Errorf("请先登录")
|
||||
}
|
||||
|
||||
// 计算签名
|
||||
signStr := c.user.UserId + c.user.UserType
|
||||
sign := cryptoutil.Sign2(signStr)
|
||||
|
||||
// 发送请求
|
||||
resp, err := c.client.R().
|
||||
SetHeaders(map[string]string{
|
||||
"roleKey": c.user.UserType,
|
||||
"sign": sign,
|
||||
"authorization": c.user.Token,
|
||||
}).
|
||||
SetBody(map[string]string{"state": ""}).
|
||||
Post(ApiGetPlan)
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.Error("获取 PlanId 请求失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var planResp entity.PlanResponse
|
||||
if err := json.Unmarshal(resp.Bytes(), &planResp); err != nil {
|
||||
logger.Logger.Error("解析 PlanId 响应失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if planResp.Code != 200 {
|
||||
return fmt.Errorf("获取 PlanId 失败:%s", planResp.Msg)
|
||||
}
|
||||
|
||||
if len(planResp.Data) == 0 {
|
||||
return fmt.Errorf("未找到实习计划")
|
||||
}
|
||||
|
||||
// 保存第一个计划信息
|
||||
c.user.PlanId = planResp.Data[0].PlanId
|
||||
c.user.PlanName = planResp.Data[0].PlanName
|
||||
|
||||
logger.Logger.Info("获取 PlanId 成功",
|
||||
zap.String("planId", c.user.PlanId),
|
||||
zap.String("planName", c.user.PlanName))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign 打卡签到
|
||||
func (c *MogudingClient) Sign(ctx context.Context, signReq *entity.SignRequest) error {
|
||||
if c.user.Token == "" {
|
||||
return fmt.Errorf("请先登录")
|
||||
}
|
||||
|
||||
if c.user.PlanId == "" {
|
||||
return fmt.Errorf("请先获取实习计划")
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if signReq.PlanId == "" {
|
||||
signReq.PlanId = c.user.PlanId
|
||||
}
|
||||
if signReq.Device == "" {
|
||||
signReq.Device = "Android"
|
||||
}
|
||||
if signReq.Type == "" {
|
||||
signReq.Type = "START"
|
||||
}
|
||||
|
||||
// 计算签名
|
||||
signStr := signReq.Device + signReq.Type + signReq.PlanId + c.user.UserId + signReq.Address
|
||||
sign := cryptoutil.Sign2(signStr)
|
||||
|
||||
// 发送请求
|
||||
resp, err := c.client.R().
|
||||
SetHeaders(map[string]string{
|
||||
"roleKey": c.user.UserType,
|
||||
"sign": sign,
|
||||
"authorization": c.user.Token,
|
||||
}).
|
||||
SetBody(signReq).
|
||||
Post(ApiSign)
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.Error("签到请求失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var signResp entity.SignResponse
|
||||
if err := json.Unmarshal(resp.Bytes(), &signResp); err != nil {
|
||||
logger.Logger.Error("解析签到响应失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if signResp.Code != 200 {
|
||||
return fmt.Errorf("签到失败:%s", signResp.Msg)
|
||||
}
|
||||
|
||||
logger.Logger.Info("签到成功",
|
||||
zap.String("type", signReq.Type),
|
||||
zap.String("address", signReq.Address))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUser 获取用户信息
|
||||
func (c *MogudingClient) GetUser() *entity.MogudingUser {
|
||||
return c.user
|
||||
}
|
||||
|
||||
// SetUser 设置用户信息(用于恢复登录状态)
|
||||
func (c *MogudingClient) SetUser(user *entity.MogudingUser) {
|
||||
c.user = user
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package moguding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/application/moguding/entity"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMogudingLogin(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMogudingClient(ctx)
|
||||
|
||||
// 替换为实际的测试账号
|
||||
username := "13307425047"
|
||||
password := "wgz2021322L"
|
||||
|
||||
err := client.Login(ctx, username, password)
|
||||
if err != nil {
|
||||
t.Fatalf("登录失败: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("登录成功,UserId: %s, Token: %s", client.user.UserId, client.user.Token)
|
||||
}
|
||||
|
||||
func TestMogudingGetPlanId(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMogudingClient(ctx)
|
||||
|
||||
// 先登录
|
||||
username := "your_username"
|
||||
password := "your_password"
|
||||
|
||||
err := client.Login(ctx, username, password)
|
||||
if err != nil {
|
||||
t.Fatalf("登录失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取 PlanId
|
||||
err = client.GetPlanId(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("获取 PlanId 失败: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("获取 PlanId 成功,PlanId: %s, PlanName: %s", client.user.PlanId, client.user.PlanName)
|
||||
}
|
||||
|
||||
func TestMogudingSign(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMogudingClient(ctx)
|
||||
|
||||
// 先登录
|
||||
username := "your_username"
|
||||
password := "your_password"
|
||||
|
||||
err := client.Login(ctx, username, password)
|
||||
if err != nil {
|
||||
t.Fatalf("登录失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取 PlanId
|
||||
err = client.GetPlanId(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("获取 PlanId 失败: %v", err)
|
||||
}
|
||||
|
||||
// 打卡签到
|
||||
signReq := &entity.SignRequest{
|
||||
Country: "中国",
|
||||
Address: "四川省 · 成都市 · 武侯区 · 咕噜咕噜",
|
||||
Province: "四川省",
|
||||
City: "成都市",
|
||||
State: "1",
|
||||
Area: "武侯区",
|
||||
Latitude: "33.100039",
|
||||
Longitude: "101.987848",
|
||||
Description: "测试打卡",
|
||||
Type: "START", // START 表示上班打卡,END 表示下班打卡
|
||||
Device: "Android",
|
||||
}
|
||||
|
||||
err = client.Sign(ctx, signReq)
|
||||
if err != nil {
|
||||
t.Fatalf("打卡失败: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("打卡成功")
|
||||
}
|
||||
|
||||
func TestMogudingFullProcess(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMogudingClient(ctx)
|
||||
|
||||
// 1. 登录
|
||||
username := "your_username"
|
||||
password := "your_password"
|
||||
|
||||
t.Log("步骤1: 登录")
|
||||
err := client.Login(ctx, username, password)
|
||||
if err != nil {
|
||||
t.Fatalf("登录失败: %v", err)
|
||||
}
|
||||
t.Logf("登录成功,UserId: %s", client.user.UserId)
|
||||
|
||||
// 2. 获取 PlanId
|
||||
t.Log("步骤2: 获取实习计划")
|
||||
err = client.GetPlanId(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("获取 PlanId 失败: %v", err)
|
||||
}
|
||||
t.Logf("获取 PlanId 成功,PlanId: %s, PlanName: %s", client.user.PlanId, client.user.PlanName)
|
||||
|
||||
// 3. 上班打卡
|
||||
t.Log("步骤3: 上班打卡")
|
||||
signInReq := &entity.SignRequest{
|
||||
Country: "中国",
|
||||
Address: "四川省 · 成都市 · 武侯区 · 咕噜咕噜",
|
||||
Province: "四川省",
|
||||
City: "成都市",
|
||||
State: "1",
|
||||
Area: "武侯区",
|
||||
Latitude: "33.100039",
|
||||
Longitude: "101.987848",
|
||||
Description: "上班打卡",
|
||||
Type: "START",
|
||||
Device: "Android",
|
||||
}
|
||||
|
||||
err = client.Sign(ctx, signInReq)
|
||||
if err != nil {
|
||||
t.Fatalf("上班打卡失败: %v", err)
|
||||
}
|
||||
t.Log("上班打卡成功")
|
||||
|
||||
// 4. 下班打卡(可选)
|
||||
t.Log("步骤4: 下班打卡")
|
||||
signOutReq := &entity.SignRequest{
|
||||
Country: "中国",
|
||||
Address: "四川省 · 成都市 · 武侯区 · 咕噜咕噜",
|
||||
Province: "四川省",
|
||||
City: "成都市",
|
||||
State: "1",
|
||||
Area: "武侯区",
|
||||
Latitude: "33.100039",
|
||||
Longitude: "101.987848",
|
||||
Description: "下班打卡",
|
||||
Type: "END",
|
||||
Device: "Android",
|
||||
}
|
||||
|
||||
err = client.Sign(ctx, signOutReq)
|
||||
if err != nil {
|
||||
t.Fatalf("下班打卡失败: %v", err)
|
||||
}
|
||||
t.Log("下班打卡成功")
|
||||
|
||||
t.Log("完整流程测试通过!")
|
||||
}
|
||||
Reference in New Issue
Block a user