Compare commits

...

5 Commits

Author SHA1 Message Date
sugarBecomer b857d55e25 🐛 漏掉的文件 2026-03-16 12:14:05 +08:00
sugarBecomer ca20be8b7e 🐛 学校信息缓存 2026-03-16 11:20:19 +08:00
sugarBecomer ce98ce4afe 🐛 授权方式修复 2026-03-13 21:37:35 +08:00
sugarBecomer c65318c545 🐛 修复短信登录 2026-03-13 21:21:09 +08:00
sugarBecomer 63b91629fa :bug 修复问题 2026-03-13 18:04:15 +08:00
12 changed files with 124 additions and 32 deletions
+1
View File
@@ -89,4 +89,5 @@ type CpUserCustomDeviceReq struct {
g.Meta `path:"/user/customDevice" method:"post" tags:"CpUser" summary:"自定义设备码"`
Id int64 `json:"id" v:"required#请指定用户"`
MobileCode string `json:"mobileCode" v:"required#请输入设备码"`
MobileType string `json:"mobileType" v:"required|in:random,manual#请指定设备码类型|设备码类型无效"`
}
+2 -16
View File
@@ -48,22 +48,8 @@ func (p *PersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
delete(globalCookies, cookie.Name)
}
// 查找是否存在相同 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)
}
// 同一 host 下同名 cookie 直接覆盖,避免多个同名 cookie 导致旧值被选中
p.hostMap[host][cookie.Name] = []*http.Cookie{cookie}
}
}
+69 -5
View File
@@ -15,6 +15,7 @@ import (
"likedream-goframe/internal/utils"
"net/http"
"net/url"
"strings"
"github.com/bytedance/sonic"
"github.com/samber/oops"
@@ -284,25 +285,88 @@ func (s *SmsLoginHandler) GetCookieString() string {
// RefreshByTgc 使用保存的完整 cookie(包含 CASTGC 等长期凭证)重新去学校换取 CAS cookie。
// tgc 参数是 SetLoginCookie 之后序列化的完整 cookie jar,包含 CASTGC(有效期10年)、
// sessionToken 等认证信息,可以直接用来重新激活 session。
// sessionToken 等认证信息。流程:先用 sessionToken 调用 getImUserToken 换取新的 CAS cookie,
// 再激活 session。
func (s *SmsLoginHandler) RefreshByTgc(ctx context.Context, tgc string, schoolId string) (cookie string, newTgc string) {
jar := cookiejar.NewPersistCookieJar()
jar.Deserialize(tgc)
// 合并所有 host 的 cookie 到 global,消除因多 host 存储导致的重复 cookie
jar.RemoveCookieInfo()
s.client = httpclient.NewCpdailyClient()
s.client.SetCookieJar(jar)
detail := school.GetSchoolDetail(ctx, schoolId)
logger.Logger.Info("[TGC] RefreshByTgc 开始用CASTGC重新激活session",
logger.Logger.Info("[TGC] RefreshByTgc 开始用TGC换取CAS cookie",
zap.String("schoolId", schoolId),
zap.String("wecloudUrl", detail.WecloudUrl),
zap.Int("tgcLen", len(tgc)),
)
// 用保存的 cookie(含 CASTGC)重新激活 session
ActivateSession(ctx, detail.WecloudUrl, s.client)
// 从 cookie jar 中提取 sessionToken,用于换取新的 CAS cookie
schoolUrl, _ := url.Parse(detail.WecloudUrl)
sessionToken := ""
for _, c := range jar.Cookies(schoolUrl) {
if c.Name == "sessionToken" {
sessionToken = c.Value
break
}
}
logger.Logger.Info("[TGC] RefreshByTgc ActivateSession完成")
if sessionToken != "" {
// 换取新的 CAS cookie(与 SetLoginCookie 相同的流程)
getCookieUrl := fmt.Sprintf("%s%s", detail.WecloudUrl, "/wec-im-group/group/imUserToken/getImUserToken")
ampCookieMap := map[string]any{
"AMP2": []map[string]any{
{"path": "/", "value": sessionToken, "domain": "", "name": "sessionToken"},
},
"AMP1": []map[string]any{
{"path": "/", "value": sessionToken, "domain": "", "name": "sessionToken"},
},
}
ampTokenString, e := sonic.Marshal(ampCookieMap)
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
ampTokenEncrypted := cryptoutil.DesEncrypt(ampTokenString, cryptoutil.Normal)
ampToken := base64.StdEncoding.EncodeToString(ampTokenEncrypted)
ctx = utils.NewCpdailySmsContextBuilder().
WithDeviceID(s.deviceID).
WithTenantID(detail.Id).
WithSessionToken(sessionToken).
Build(ctx)
// 使用不复制 Cookie 头的重定向策略,避免旧 Cookie 覆盖 jar 中更新的值
s.client.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
if len(via) >= 50 {
return fmt.Errorf("resty: stopped after 50 redirects")
}
// 只复制非 Cookie 头,让 jar 自动管理 Cookie
for key, val := range via[0].Header {
if !strings.EqualFold(key, "Cookie") {
req.Header[key] = val
}
}
return nil
}))
req := s.client.R().
SetHeader("AmpCookies", ampToken).
SetContext(ctx)
setSmsRequestHeaders(ctx, req)
resp, e := req.Get(getCookieUrl)
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
respJson := utils.ReadJson[cpdailySmsCommonResponse[any]](resp.Body)
respJson.EnsureSuccess()
logger.Logger.Info("[TGC] RefreshByTgc 换取CAS cookie完成")
} else {
logger.Logger.Warn("[TGC] RefreshByTgc 未找到sessionToken,跳过换取CAS cookie步骤")
}
newTgc = tgc // TGC 保持不变(CASTGC 有效期10年)
cookie = s.GetCookieString()
+1 -1
View File
@@ -17,5 +17,5 @@ var (
RedisBalanceMigrateExpire = int64(3600) // 积分转移链接1小时有效
RedisCpUserLoginExpire = int64(180)
RedisCpUserAuthorizeExpire = int64(1800)
RedisSchoolInfoExpire = int64(3600) // 学校信息1小时有效
RedisSchoolInfoExpire = int64(3600 * 24 * 30) // 学校信息30天有效
)
+6 -5
View File
@@ -1,11 +1,12 @@
package cst
const (
TaskCpdaily = "task:cpdaily"
TaskEmail = "task:email"
TaskResetDaily = "task:reset:daily"
TaskXuexitong = "task:xuexitong"
TaskXuexitongVio = "task:xuexitong:violence"
TaskCpdaily = "task:cpdaily"
TaskEmail = "task:email"
TaskResetDaily = "task:reset:daily"
TaskRefreshSchoolDetail = "task:refresh:school:detail"
TaskXuexitong = "task:xuexitong"
TaskXuexitongVio = "task:xuexitong:violence"
TaskCpdailyId = "cpdaily:task:schedule:%d"
+2 -1
View File
@@ -140,6 +140,7 @@ type MobileType string
const (
MobileTypeRandom MobileType = "random"
MobileTypeAuthorize MobileType = "authorize"
MobileTypeManual MobileType = "manual"
)
func (mt MobileType) String() string {
@@ -149,7 +150,7 @@ func (mt MobileType) String() string {
// MobileTypeValidator is a validator for the "mobile_type" field enum values. It is called by the builders before save.
func MobileTypeValidator(mt MobileType) error {
switch mt {
case MobileTypeRandom, MobileTypeAuthorize:
case MobileTypeRandom, MobileTypeAuthorize, MobileTypeManual:
return nil
default:
return fmt.Errorf("cpuser: invalid enum value for mobile_type field: %q", mt)
+1 -1
View File
@@ -207,7 +207,7 @@ var (
{Name: "mobile_code", Type: field.TypeString, Nullable: true},
{Name: "cookie", Type: field.TypeString, Nullable: true, Size: 2147483647},
{Name: "tgc", Type: field.TypeString, Nullable: true, Size: 2147483647},
{Name: "mobile_type", Type: field.TypeEnum, Enums: []string{"random", "authorize"}},
{Name: "mobile_type", Type: field.TypeEnum, Enums: []string{"random", "authorize", "manual"}},
{Name: "login_type", Type: field.TypeEnum, Enums: []string{"account", "phone"}, Default: "account"},
{Name: "school_id", Type: field.TypeString},
{Name: "user_id", Type: field.TypeInt64},
+1 -1
View File
@@ -25,7 +25,7 @@ func (CpUser) Fields() []ent.Field {
field.String("mobile_code").Optional(),
field.Text("cookie").Optional(),
field.Text("tgc").Optional(),
field.Enum("mobile_type").Values("random", "authorize"),
field.Enum("mobile_type").Values("random", "authorize", "manual"),
field.Enum("login_type").Values("account", "phone").Default("account"),
}
}
+9 -1
View File
@@ -36,7 +36,6 @@ type periodicTaskConfigProvider struct{}
func (p *periodicTaskConfigProvider) GetConfigs() ([]*asynq.PeriodicTaskConfig, error) {
return []*asynq.PeriodicTaskConfig{
{
//Cronspec: "0 1 * * *", // 每天凌晨1点执行
Cronspec: "0 1 * * * ",
Task: asynq.NewTask(cst.TaskResetDaily, nil),
Opts: []asynq.Option{
@@ -45,5 +44,14 @@ func (p *periodicTaskConfigProvider) GetConfigs() ([]*asynq.PeriodicTaskConfig,
asynq.Timeout(0), // 使用默认超时
},
},
{
Cronspec: "0 12 * * *",
Task: asynq.NewTask(cst.TaskRefreshSchoolDetail, nil),
Opts: []asynq.Option{
asynq.Queue(cst.TaskQueueDefault),
asynq.MaxRetry(1),
asynq.Timeout(0),
},
},
}, nil
}
@@ -0,0 +1,30 @@
package schedule
import (
"context"
"likedream-goframe/internal/application/cpdaily/school"
"likedream-goframe/internal/framework/logger"
"likedream-goframe/internal/utils"
"github.com/hibiken/asynq"
"go.uber.org/zap"
)
type RefreshSchoolDetailTaskHandler struct{}
// ProcessTask 每天中午刷新一次全部学校详情缓存
func (h *RefreshSchoolDetailTaskHandler) ProcessTask(ctx context.Context, _ *asynq.Task) (err error) {
defer utils.Pass(&err)
logger.Logger.Info("开始批量刷新学校详情缓存信息")
schools := school.GetSchoolList(ctx)
for _, item := range schools {
func() {
defer utils.HandleError(func(err error) {
logger.Logger.Warn("刷新学校详情缓存失败", zap.String("school_id", item.Id), zap.Error(err))
})
school.GetSchoolDetail(ctx, item.Id)
}()
}
return
}
+1
View File
@@ -68,6 +68,7 @@ func initServer(ctx context.Context) {
auxMux := asynq.NewServeMux()
auxMux.Handle(cst2.TaskEmail, &EmailTaskHandler{})
auxMux.Handle(cst2.TaskResetDaily, &ResetTaskHandler{})
auxMux.Handle(cst2.TaskRefreshSchoolDetail, &RefreshSchoolDetailTaskHandler{})
e := globalServer.Start(mainMux)
utils.OkOrPanic(e)
+1 -1
View File
@@ -176,7 +176,7 @@ func (v CpUserServiceV1) CustomDevice(ctx context.Context, req *v1.CpUserCustomD
if cpUser == nil {
return utils.FailParam("用户不存在")
}
db.Global.CpUser.UpdateOne(cpUser).SetMobileCode(req.MobileCode).SetMobileType(cpuser.MobileTypeAuthorize).ExecX(ctx)
db.Global.CpUser.UpdateOne(cpUser).SetMobileCode(req.MobileCode).SetMobileType(cpuser.MobileType(req.MobileType)).ExecX(ctx)
return utils.SuccessWithMsg("自定义设备码设置成功")
}