Compare commits

...

6 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
sugarBecomer 658a9c2b3e 🐛 图片预览 2026-03-12 08:07:33 +08:00
17 changed files with 178 additions and 33 deletions
+5
View File
@@ -13,3 +13,8 @@ type UploadImageReq struct {
CpUserId int64 `json:"cpUserId" v:"required#请指定今日校园用户"`
TaskType cptask.TaskType `json:"taskType" v:"required#请指定任务类型"`
}
type PreviewImageReq struct {
g.Meta `path:"/image/preview" method:"get" tags:"Image" summary:"预览图片"`
Key string `json:"key" v:"required#请指定图片key" in:"query"`
}
+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"
@@ -8,4 +8,5 @@ import (
type ICpImageCtrlV1 interface {
Upload(ctx context.Context, req *v1.UploadImageReq) (res *utils.Response[any], err error)
Preview(ctx context.Context, req *v1.PreviewImageReq) (res *utils.Response[any], err error)
}
@@ -15,6 +15,11 @@ func (c CpImageCtrlV1) Upload(ctx context.Context, req *v1.UploadImageReq) (res
return service.Global.CpImage.Upload(ctx, req), nil
}
func (c CpImageCtrlV1) Preview(ctx context.Context, req *v1.PreviewImageReq) (res *utils.Response[any], err error) {
defer utils.Pass(&err)
return service.Global.CpImage.Preview(ctx, req), nil
}
func NewCpImageCtrlV1() ICpImageCtrlV1 {
return &CpImageCtrlV1{}
}
+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
@@ -26,7 +26,7 @@ func ExceptionHandler(r *ghttp.Request) {
if err == nil && r.Response.BufferLength() > 0 {
return
}
if r.Response.Header().Get("Content-Type") == "image/png" {
if ct := r.Response.Header().Get("Content-Type"); ct != "" && len(ct) > 6 && ct[:6] == "image/" {
return
}
+42
View File
@@ -2,18 +2,23 @@ package cpdaily
import (
"context"
"io"
v1 "likedream-goframe/internal/api/v1"
"likedream-goframe/internal/application/cpdaily/imageuploader"
"likedream-goframe/internal/application/cpdaily/login"
"likedream-goframe/internal/application/cpdaily/school"
"likedream-goframe/internal/application/cpdaily/types"
"likedream-goframe/internal/application/filestorage"
"likedream-goframe/internal/common/db"
errors2 "likedream-goframe/internal/common/errors"
"likedream-goframe/internal/ent/cpimage"
"likedream-goframe/internal/ent/cptask"
"likedream-goframe/internal/ent/cpuser"
"likedream-goframe/internal/ent/sysuser"
"likedream-goframe/internal/utils"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/google/uuid"
"github.com/samber/oops"
)
@@ -50,6 +55,43 @@ func (v CpImageServiceV1) Upload(ctx context.Context, req *v1.UploadImageReq) *u
})
}
func (v CpImageServiceV1) Preview(ctx context.Context, req *v1.PreviewImageReq) *utils.Response[any] {
// 解析 key,可能是 UUID(旧数据)或直接的 S3 key
parsedUUID, uuidErr := uuid.Parse(req.Key)
var s3Key string
var contentType string
if uuidErr == nil {
// 是 UUID,从 cpimage 表查询对应的 S3 key
targetImage, e := db.Global.CpImage.Query().
Where(cpimage.ImageID(parsedUUID)).
Select(cpimage.FieldKey, cpimage.FieldContentType).
Only(ctx)
if e != nil {
return utils.Fail(404, "图片不存在")
}
s3Key = targetImage.Key
contentType = targetImage.ContentType
} else {
// 不是 UUID,直接当作 S3 key 使用
s3Key = req.Key
contentType = "image/jpeg"
}
// 从 S3 获取图片
file := filestorage.GlobalS3.GetFile(ctx, s3Key)
defer file.Close()
// 直接写入 HTTP 响应流
r := ghttp.RequestFromCtx(ctx)
r.Response.Header().Set("Content-Type", contentType)
r.Response.Header().Set("Cache-Control", "public, max-age=86400")
_, err := io.Copy(r.Response.Writer, file)
utils.OkOrPanic(err, oops.WithContext(ctx).Wrap)
return nil
}
// convertTaskType 将 ent 的 TaskType 转换为 cpdaily types.TaskType
func convertTaskType(ctx context.Context, tt cptask.TaskType) types.TaskType {
switch tt {
+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("自定义设备码设置成功")
}