Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af7fcc4cb4 | |||
| c206a0863e | |||
| b068e178da |
@@ -24,6 +24,6 @@ type ConfigListReq struct {
|
||||
}
|
||||
|
||||
type ConfigPublicGetReq struct {
|
||||
g.Meta `path:"/config/public/{key}" method:"get" tags:"Config" summary:"获取公开配置(无需认证)"`
|
||||
g.Meta `path:"/config/public/{key}" method:"get" tags:"Config" summary:"获取公开配置(无需认证)" auth:"false"`
|
||||
Key string `json:"key" v:"required#配置键不能为空"`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package v1
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
type CpLeaveListReq struct {
|
||||
@@ -26,4 +27,11 @@ type CpLeaveReportReq struct {
|
||||
Longitude string `json:"longitude"` // 经度(可选,不传则使用系统默认)
|
||||
Latitude string `json:"latitude"` // 纬度(可选,不传则使用系统默认)
|
||||
Address string `json:"address"` // 地址(可选,不传则使用系统默认)
|
||||
ImageUrl string `json:"imageUrl"`
|
||||
}
|
||||
|
||||
type CpLeaveUploadReq struct {
|
||||
g.Meta `path:"/leave/upload" method:"post" tags:"CpLeave" summary:"上传销假图片"`
|
||||
File *ghttp.UploadFile `json:"file"`
|
||||
CpUserId int64 `json:"cpUserId"`
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/google/uuid"
|
||||
"github.com/samber/oops"
|
||||
"resty.dev/v3"
|
||||
@@ -80,11 +81,54 @@ func UploadImageFromStorage(ctx context.Context, host string, imageId uuid.UUID,
|
||||
return previewUrl
|
||||
}
|
||||
|
||||
func UploadImageFromFile(ctx context.Context, host string, file *ghttp.UploadFile, taskType types.TaskType, client *resty.Client) string {
|
||||
|
||||
imageUploadPolicy := GetUploadPolicyAsync(ctx, host, taskType, client)
|
||||
open, _ := file.Open()
|
||||
resp, e := client.R().
|
||||
SetMultipartFields(&resty.MultipartField{
|
||||
Name: "x-obs-acl",
|
||||
Values: []string{"public-read"},
|
||||
}, &resty.MultipartField{
|
||||
Name: "AccessKeyId",
|
||||
Values: []string{imageUploadPolicy.AccessId},
|
||||
}, &resty.MultipartField{
|
||||
Name: "key",
|
||||
Values: []string{imageUploadPolicy.FileName},
|
||||
}, &resty.MultipartField{
|
||||
Name: "signature",
|
||||
Values: []string{imageUploadPolicy.Signature},
|
||||
}, &resty.MultipartField{
|
||||
Name: "policy",
|
||||
Values: []string{imageUploadPolicy.Policy},
|
||||
}, &resty.MultipartField{
|
||||
Name: "content-type",
|
||||
Values: []string{file.Header.Get("Content-Type")},
|
||||
}, &resty.MultipartField{
|
||||
Name: "file",
|
||||
FileName: "file",
|
||||
Reader: open,
|
||||
}).
|
||||
Post(imageUploadPolicy.Host)
|
||||
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
if resp.StatusCode() > 299 {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "上传图片失败: "+utils.ReadString(resp.Body)), oops.WithContext(ctx).With("response", utils.ReadString(resp.Body)).Wrap)
|
||||
}
|
||||
|
||||
previewUrl := GetPreviewUrlAsync(ctx, host, imageUploadPolicy.FileName, taskType, client)
|
||||
|
||||
return previewUrl
|
||||
}
|
||||
|
||||
// GetPreviewUrlAsync 用于获取图片预览地址
|
||||
func GetPreviewUrlAsync(ctx context.Context, host string, fileKey string, taskType types.TaskType, client *resty.Client) string {
|
||||
var apiPath string
|
||||
if taskType == types.TaskTypeSleepCheck {
|
||||
apiPath = "/previewAttachment"
|
||||
} else if taskType == types.TaskTypeLeave {
|
||||
apiPath = "/obs/previewAttachment"
|
||||
} else {
|
||||
apiPath = "/sign/previewAttachment"
|
||||
}
|
||||
@@ -164,6 +208,8 @@ func GetImageUploadUrlPrefix(ctx context.Context, taskType types.TaskType) strin
|
||||
return "/wec-counselor-sign-apps/stu"
|
||||
case types.TaskTypeCollection:
|
||||
return "/wec-counselor-collector-apps/student/collector"
|
||||
case types.TaskTypeLeave:
|
||||
return "/wec-counselor-leave-apps/leave"
|
||||
default:
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "未知的任务类型"), oops.WithContext(ctx).Wrap)
|
||||
panic("unreachable")
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type Attach struct {
|
||||
Type int `json:"type"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
@@ -3,15 +3,19 @@ package leave
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/cpdaily"
|
||||
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||
"likedream-goframe/internal/application/cpdaily/imageuploader"
|
||||
"likedream-goframe/internal/application/cpdaily/leave/model"
|
||||
"likedream-goframe/internal/application/cpdaily/task/leavetype"
|
||||
"likedream-goframe/internal/application/cpdaily/taskhelper"
|
||||
"likedream-goframe/internal/application/cpdaily/types"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/google/uuid"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -67,21 +71,24 @@ func (c *CpdailyReportApp) GetLeaveList() []leavetype.LeaveListOne {
|
||||
if row.Get("status").String() == "4" {
|
||||
continue
|
||||
}
|
||||
id := row.Get("id").String()
|
||||
detail := c.LeaveDetail(id)
|
||||
leaveOne := leavetype.LeaveListOne{
|
||||
Id: row.Get("id").String(),
|
||||
LeaveType: row.Get("leaveType").String(),
|
||||
CreateTime: row.Get("createTime").String(),
|
||||
Reason: row.Get("leaveReason").String(),
|
||||
AceEndTime: row.Get("actEndTime").String(),
|
||||
Status: row.Get("status").String(),
|
||||
}
|
||||
Id: row.Get("id").String(),
|
||||
LeaveType: row.Get("leaveType").String(),
|
||||
CreateTime: row.Get("createTime").String(),
|
||||
Reason: row.Get("leaveReason").String(),
|
||||
AceEndTime: row.Get("actEndTime").String(),
|
||||
Status: row.Get("status").String(),
|
||||
ChooseAttendance: detail.Get("leaveType.chooseAttendance").Int(),
|
||||
//Longitude: detail.Get("detail.applyLocation.longitude").String(),
|
||||
//Latitude: detail.Get("detail.applyLocation.latitude").String(),
|
||||
//Address: detail.Get("detail.applyLocation.address").String(),
|
||||
|
||||
// 获取详情以填充位置信息
|
||||
detail := c.LeaveDetail(leaveOne.Id)
|
||||
lon, lat := c.GetLocation(detail)
|
||||
leaveOne.Longitude = lon
|
||||
leaveOne.Latitude = lat
|
||||
leaveOne.Address = detail.Get("detail.applyLocation.address").String()
|
||||
Longitude: detail.Get("leaveExtends.0.longitude").String(),
|
||||
Latitude: detail.Get("leaveExtends.0.latitude").String(),
|
||||
Address: detail.Get("leaveExtends.0.address").String(),
|
||||
}
|
||||
|
||||
listList = append(listList, leaveOne)
|
||||
}
|
||||
@@ -113,26 +120,29 @@ func (c *CpdailyReportApp) LeaveDetail(leaveId string) gjson.Result {
|
||||
return datas
|
||||
}
|
||||
|
||||
type Attach struct {
|
||||
Type string `json:"type"`
|
||||
Url string `json:"url"`
|
||||
func (c *CpdailyReportApp) UploadImage(ctx context.Context, file *ghttp.UploadFile) string {
|
||||
return imageuploader.UploadImageFromFile(ctx, c.host, file, types.TaskTypeLeave, c.client)
|
||||
}
|
||||
|
||||
func (c *CpdailyReportApp) FillReportForm(detail gjson.Result) map[string]*gvar.Var {
|
||||
func (c *CpdailyReportApp) FillReportForm(detail gjson.Result, req *v1.CpLeaveReportReq) map[string]*gvar.Var {
|
||||
resultForm := make(map[string]*gvar.Var)
|
||||
resultForm["id"] = gvar.New(detail.Get("detail.id").String())
|
||||
resultForm["needApproval"] = gvar.New(1)
|
||||
resultForm["leaveType"] = gvar.New(detail.Get("leaveType.code").String())
|
||||
resultForm["reportLocation"] = gvar.New(detail.Get("detail.applyLocation.address").String())
|
||||
if req.Address == "" {
|
||||
resultForm["reportLocation"] = gvar.New(detail.Get("leaveExtends.0.address").String())
|
||||
} else {
|
||||
resultForm["reportLocation"] = gvar.New(req.Address)
|
||||
}
|
||||
resultForm["approverWid"] = gvar.New("")
|
||||
resultForm["reportApplyExplain"] = gvar.New(nil)
|
||||
//resultForm["reportAttach"] = gvar.New(.Array())
|
||||
resultForm["reportAttach"] = gvar.New([]string{})
|
||||
resultForm["uaIsCpadaily"] = gvar.New(true)
|
||||
attachStr := detail.Get("detail.applyAttach").String()
|
||||
if attachStr != "" {
|
||||
var attach []Attach
|
||||
_ = sonic.Unmarshal([]byte(attachStr), &attach)
|
||||
if req.ImageUrl != "" {
|
||||
var attach []model.Attach
|
||||
attach = append(attach, model.Attach{
|
||||
Type: 1,
|
||||
Url: req.ImageUrl,
|
||||
})
|
||||
resultForm["reportAttach"] = gvar.New(attach)
|
||||
} else {
|
||||
resultForm["reportAttach"] = gvar.New([]string{})
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,7 +8,8 @@ type LeaveListOne struct {
|
||||
Reason string `json:"reason"`
|
||||
Status string `json:"status"`
|
||||
// 销假时填充的位置信息
|
||||
Longitude string `json:"longitude"` // 经度
|
||||
Latitude string `json:"latitude"` // 纬度
|
||||
Address string `json:"address"` // 地址
|
||||
Longitude string `json:"longitude"` // 经度
|
||||
Latitude string `json:"latitude"` // 纬度
|
||||
Address string `json:"address"` // 地址
|
||||
ChooseAttendance int64 `json:"chooseAttendance"` // 是否需要图片 1需要 0 不需要
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ const (
|
||||
TaskTypeCollection = TaskType(iota)
|
||||
TaskTypeSign
|
||||
TaskTypeSleepCheck
|
||||
TaskTypeLeave
|
||||
)
|
||||
|
||||
@@ -50,10 +50,14 @@ var ErrTimeRestriction = errors2.NewBusinessError(499, "当前时间不在允许
|
||||
// TimeCheckFunc 时间检查回调函数类型,返回 true 表示可以继续执行
|
||||
type TimeCheckFunc func() bool
|
||||
|
||||
// TaskCompleteCallback 任务点完成回调函数类型
|
||||
type TaskCompleteCallback func()
|
||||
|
||||
type XuexitongCourse struct {
|
||||
client *resty.Client
|
||||
userID string
|
||||
timeCheckFunc TimeCheckFunc // 时间检查回调,每个任务点完成后调用
|
||||
client *resty.Client
|
||||
userID string
|
||||
timeCheckFunc TimeCheckFunc // 时间检查回调,每个任务点完成后调用
|
||||
taskCompleteCallback TaskCompleteCallback // 任务点完成回调
|
||||
}
|
||||
|
||||
func NewXuexitongCourse(client *resty.Client) *XuexitongCourse {
|
||||
@@ -67,6 +71,18 @@ func (c *XuexitongCourse) SetTimeCheckFunc(fn TimeCheckFunc) {
|
||||
c.timeCheckFunc = fn
|
||||
}
|
||||
|
||||
// SetTaskCompleteCallback 设置任务点完成回调函数
|
||||
func (c *XuexitongCourse) SetTaskCompleteCallback(fn TaskCompleteCallback) {
|
||||
c.taskCompleteCallback = fn
|
||||
}
|
||||
|
||||
// onTaskComplete 任务点完成时调用
|
||||
func (c *XuexitongCourse) onTaskComplete() {
|
||||
if c.taskCompleteCallback != nil {
|
||||
c.taskCompleteCallback()
|
||||
}
|
||||
}
|
||||
|
||||
// checkTimeRestriction 检查时间限制,如果设置了回调且返回 false,则返回时间限制错误
|
||||
func (c *XuexitongCourse) checkTimeRestriction() error {
|
||||
if c.timeCheckFunc != nil && !c.timeCheckFunc() {
|
||||
@@ -138,7 +154,13 @@ func (c *XuexitongCourse) PullCourseList(ctx context.Context) []entity.XueXiTCou
|
||||
courseID = strings.Split(strings.Split(sqUrl, "courseId=")[1], "&")[0]
|
||||
}
|
||||
if strings.Contains(sqUrl, "userId=") {
|
||||
c.userID = strings.Split(sqUrl, "userId=")[1]
|
||||
userIDPart := strings.Split(sqUrl, "userId=")[1]
|
||||
// 处理 userId 后面可能还有其他参数的情况
|
||||
if idx := strings.Index(userIDPart, "&"); idx != -1 {
|
||||
c.userID = userIDPart[:idx]
|
||||
} else {
|
||||
c.userID = userIDPart
|
||||
}
|
||||
}
|
||||
|
||||
course := entity.XueXiTCourse{
|
||||
@@ -315,6 +337,13 @@ func (c *XuexitongCourse) FetchChapterPointStatus(ctx context.Context, course en
|
||||
"courseid": {strconv.Itoa(courseID)},
|
||||
}
|
||||
|
||||
logger.Logger.Debug("FetchChapterPointStatus 请求参数",
|
||||
zap.String("userID", c.userID),
|
||||
zap.Int("classID", classID),
|
||||
zap.Int("courseID", courseID),
|
||||
zap.Int("cpi", course.Cpi),
|
||||
zap.Int("nodesCount", len(nodes)))
|
||||
|
||||
var body string
|
||||
err := retry.Do(func() error {
|
||||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||||
@@ -332,12 +361,24 @@ func (c *XuexitongCourse) FetchChapterPointStatus(ctx context.Context, course en
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Logger.Debug("FetchChapterPointStatus 响应", zap.String("body", body))
|
||||
|
||||
var status entity.ChapterPointStatus
|
||||
err = json.Unmarshal([]byte(body), &status)
|
||||
if err != nil {
|
||||
logger.Logger.Error("FetchChapterPointStatus 解析失败", zap.Error(err), zap.String("body", body))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新章节的 PointTotal 和 PointFinish 字段
|
||||
for i := range chapters {
|
||||
chapterIDStr := strconv.Itoa(chapters[i].ID)
|
||||
if pointData, exists := status[chapterIDStr]; exists {
|
||||
chapters[i].PointTotal = pointData.UnFinishCount + pointData.FinishCount
|
||||
chapters[i].PointFinish = pointData.FinishCount
|
||||
}
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
@@ -432,6 +473,8 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
|
||||
continue
|
||||
}
|
||||
pointType, ok := iframe.Other["module"]
|
||||
// 调试日志:打印每个 iframe 的 module 类型
|
||||
logger.Logger.Debug("解析到任务点", zap.String("module", pointType), zap.Bool("hasModule", ok), zap.Any("data", iframe.Data))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
@@ -485,7 +528,7 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
|
||||
Type: "insertbook",
|
||||
})
|
||||
|
||||
case "insertwork":
|
||||
case "work", "insertwork":
|
||||
workID, _ := iframe.Data["workid"].(string)
|
||||
schoolID, _ := iframe.Data["schoolid"].(string)
|
||||
jobID, _ := iframe.Data["_jobid"].(string)
|
||||
@@ -506,7 +549,7 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
|
||||
JobID: jobID,
|
||||
})
|
||||
|
||||
case "insertlink":
|
||||
case "hyperlink", "insertlink":
|
||||
jobID, _ := iframe.Data["_jobid"].(string)
|
||||
if jobID == "" {
|
||||
continue
|
||||
@@ -1009,6 +1052,7 @@ func (c *XuexitongCourse) AutoPlayVideo(ctx context.Context, course entity.XueXi
|
||||
if err := json.Unmarshal([]byte(playReport), &result); err == nil {
|
||||
if isPassed, ok := result["isPassed"].(bool); ok && isPassed {
|
||||
logger.Logger.Info("视频播放完成", zap.String("jobID", p.JobID))
|
||||
c.onTaskComplete() // 任务点完成回调
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1025,6 +1069,7 @@ func (c *XuexitongCourse) AutoPlayVideo(ctx context.Context, course entity.XueXi
|
||||
if err := json.Unmarshal([]byte(playReport), &result); err == nil {
|
||||
if isPassed, ok := result["isPassed"].(bool); ok && isPassed {
|
||||
logger.Logger.Info("视频播放完成", zap.String("jobID", p.JobID))
|
||||
c.onTaskComplete() // 任务点完成回调
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1055,6 +1100,7 @@ func (c *XuexitongCourse) AutoReadDocument(ctx context.Context, course entity.Xu
|
||||
if err := json.Unmarshal([]byte(report), &result); err == nil {
|
||||
if status, ok := result["status"].(bool); ok && status {
|
||||
logger.Logger.Info("文档刷课完成", zap.String("jobID", p.JobID), zap.String("title", p.Title))
|
||||
c.onTaskComplete() // 任务点完成回调
|
||||
return nil
|
||||
}
|
||||
if msg, ok := result["msg"].(string); ok {
|
||||
@@ -1149,22 +1195,10 @@ func (c *XuexitongCourse) AutoStudyCourse(ctx context.Context, course entity.Xue
|
||||
|
||||
logger.Logger.Info(fmt.Sprintf("课程 %s 共有 %d 个章节", course.CourseName, len(chapters)))
|
||||
|
||||
// 获取章节任务点状态
|
||||
status, err := c.FetchChapterPointStatus(ctx, course, chapters)
|
||||
// 获取章节任务点状态(会自动更新 chapters 中每个章节的 PointTotal 和 PointFinish)
|
||||
_, err := c.FetchChapterPointStatus(ctx, course, chapters)
|
||||
if err != nil {
|
||||
logger.Logger.Warn("获取章节状态失败", zap.Error(err))
|
||||
} else {
|
||||
// 更新章节状态
|
||||
for i := range chapters {
|
||||
if pointData, exists := status[strconv.Itoa(chapters[i].ID)]; exists {
|
||||
if pointData.UnFinishCount != 0 && pointData.TotalCount == 0 {
|
||||
chapters[i].PointTotal = pointData.UnFinishCount
|
||||
} else {
|
||||
chapters[i].PointTotal = pointData.TotalCount
|
||||
}
|
||||
chapters[i].PointFinish = pointData.FinishCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历所有章节
|
||||
@@ -1501,6 +1535,7 @@ func (c *XuexitongCourse) AutoCompleteHyperlink(ctx context.Context, course enti
|
||||
if err := json.Unmarshal([]byte(report), &result); err == nil {
|
||||
if status, ok := result["status"].(bool); ok && status {
|
||||
logger.Logger.Info("外链刷课完成", zap.String("jobID", p.JobID), zap.String("title", p.Title))
|
||||
c.onTaskComplete() // 任务点完成回调
|
||||
return nil
|
||||
}
|
||||
if msg, ok := result["msg"].(string); ok {
|
||||
@@ -1592,6 +1627,7 @@ func (c *XuexitongCourse) AutoWatchLive(ctx context.Context, course entity.XueXi
|
||||
|
||||
if p.VideoCompletePercent >= 90 {
|
||||
logger.Logger.Info("直播刷课完成", zap.String("title", p.Title))
|
||||
c.onTaskComplete() // 任务点完成回调
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ const (
|
||||
ConfigBasePointCpdailyTask = "basePoint.cpdailyTask"
|
||||
ConfigBasePointCpdailyLeave = "basePoint.cpdailyLeave"
|
||||
ConfigBasePointCpdailyPhone = "basePoint.cpdailyPhone"
|
||||
|
||||
// 系统配置
|
||||
ConfigSystemAllowRegister = "system.allowRegister" // 是否允许注册
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -26,6 +26,11 @@ func (c CpLeaveCtrlV1) Report(ctx context.Context, req *v1.CpLeaveReportReq) (re
|
||||
return service.Global.CpLeave.Report(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c CpLeaveCtrlV1) Upload(ctx context.Context, req *v1.CpLeaveUploadReq) (res *utils.Response[string], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.CpLeave.Upload(ctx, req), nil
|
||||
}
|
||||
|
||||
func NewCpLeaveCtrlV1() ICpLeaveCtrlV1 {
|
||||
return &CpLeaveCtrlV1{}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ type ICpLeaveCtrlV1 interface {
|
||||
GetLeaveListFromSystem(ctx context.Context, req *v1.GetLeaveListFromSystemReq) (*utils.Response[any], error)
|
||||
List(ctx context.Context, req *v1.CpLeaveListReq) (*utils.Response[[]*ent.CpLeave], error)
|
||||
Report(ctx context.Context, req *v1.CpLeaveReportReq) (*utils.Response[any], error)
|
||||
Upload(ctx context.Context, req *v1.CpLeaveUploadReq) (*utils.Response[string], error)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/framework/captcha"
|
||||
"likedream-goframe/internal/framework/errors"
|
||||
"likedream-goframe/internal/framework/redis"
|
||||
"likedream-goframe/internal/service"
|
||||
"likedream-goframe/internal/utils"
|
||||
@@ -64,7 +63,6 @@ func (c *UserLoginV1) Register(ctx context.Context, req *v1.UserRegisterReq) (re
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
utils.OkOrPanic(errors.NewBusinessError(400, "注册功能暂时关闭,请等待重新开放"))
|
||||
return service.Global.UserLogin.Register(ctx, req), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -47,9 +47,9 @@ type CourseEntity struct {
|
||||
// Violence holds the value of the "violence" field.
|
||||
Violence courseentity.Violence `json:"violence,omitempty"`
|
||||
// Nodes holds the value of the "nodes" field.
|
||||
Nodes string `json:"nodes,omitempty"`
|
||||
Nodes string `json:"-"`
|
||||
// CompletedNodes holds the value of the "completed_nodes" field.
|
||||
CompletedNodes string `json:"completed_nodes,omitempty"`
|
||||
CompletedNodes string `json:"-"`
|
||||
// Desc holds the value of the "desc" field.
|
||||
Desc string `json:"desc,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
@@ -294,11 +294,9 @@ func (_m *CourseEntity) String() string {
|
||||
builder.WriteString("violence=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.Violence))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("nodes=")
|
||||
builder.WriteString(_m.Nodes)
|
||||
builder.WriteString("nodes=<sensitive>")
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("completed_nodes=")
|
||||
builder.WriteString(_m.CompletedNodes)
|
||||
builder.WriteString("completed_nodes=<sensitive>")
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("desc=")
|
||||
builder.WriteString(_m.Desc)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -29,8 +29,8 @@ func (CourseEntity) Fields() []ent.Field {
|
||||
field.Int("success_count").Default(0), // 成功数量
|
||||
field.Enum("exam_state").Values("not_start", "waiting", "doing", "done", "fail").Default("not_start"), // 考试状态
|
||||
field.Enum("violence").Values("no", "yes").Default("no"), // 是否暴力刷课
|
||||
field.Text("nodes").Optional(), // 所有节点信息(JSON格式)
|
||||
field.Text("completed_nodes").Optional(), // 已完成的节点ID列表(JSON格式)
|
||||
field.Text("nodes").Optional().Sensitive(), // 所有节点信息(JSON格式)
|
||||
field.Text("completed_nodes").Optional().Sensitive(), // 已完成的节点ID列表(JSON格式)
|
||||
field.String("desc").Optional(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,20 @@ import (
|
||||
var globalScanner *TaskScanner
|
||||
|
||||
func initScanner() {
|
||||
ctx := gctx.GetInitCtx()
|
||||
|
||||
// 应用启动时,将所有 StatusDoing(执行中)状态的学习通任务重置为 StatusWaiting
|
||||
// 这样可以确保应用重启后,之前正在执行的任务能够继续执行
|
||||
count, err := db.Global.CourseEntity.Update().
|
||||
Where(courseentity.StatusEQ(courseentity.StatusDoing)).
|
||||
SetStatus(courseentity.StatusWaiting).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "重置执行中的学习通任务失败: %v", err)
|
||||
} else if count > 0 {
|
||||
g.Log().Infof(ctx, "应用启动时重置了 %d 个执行中的学习通任务为等待状态", count)
|
||||
}
|
||||
|
||||
globalScanner = NewTaskScanner()
|
||||
go globalScanner.StartScan()
|
||||
}
|
||||
|
||||
@@ -112,6 +112,52 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
|
||||
})
|
||||
}
|
||||
|
||||
// 先获取课程列表找到目标课程
|
||||
courseList := courseService.PullCourseList(ctx)
|
||||
var course *entity.XueXiTCourse
|
||||
for _, c := range courseList {
|
||||
if c.CourseID == targetCourse.CourseID {
|
||||
course = &c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if course == nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(404, "未找到课程"))
|
||||
}
|
||||
|
||||
// 开始刷课前,先获取并更新任务点统计
|
||||
g.Log().Infof(ctx, "课程 %s 正在获取任务点统计...", targetCourse.CourseName)
|
||||
|
||||
chapters := courseService.PullCourseChapterList(ctx, *course)
|
||||
// FetchChapterPointStatus 会更新 chapters 中每个章节的 PointTotal 和 PointFinish
|
||||
_, statusErr := courseService.FetchChapterPointStatus(ctx, *course, chapters)
|
||||
if statusErr != nil {
|
||||
g.Log().Warningf(ctx, "获取章节任务点状态失败: %v", statusErr)
|
||||
} else {
|
||||
// 通过累加每个章节的 PointTotal 和 PointFinish 计算总数
|
||||
var totalPointCount, finishedPointCount int
|
||||
for _, chapter := range chapters {
|
||||
totalPointCount += chapter.PointTotal
|
||||
finishedPointCount += chapter.PointFinish
|
||||
}
|
||||
g.Log().Infof(ctx, "课程 %s 任务点统计: 总数 %d, 已完成 %d", targetCourse.CourseName, totalPointCount, finishedPointCount)
|
||||
|
||||
// 更新数据库
|
||||
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).
|
||||
SetTaskCount(totalPointCount).
|
||||
SetSuccessCount(finishedPointCount).
|
||||
ExecX(ctx)
|
||||
}
|
||||
|
||||
// 设置任务点完成回调,每完成一个任务点更新 success_count
|
||||
courseService.SetTaskCompleteCallback(func() {
|
||||
// 增加 success_count
|
||||
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).
|
||||
AddSuccessCount(1).
|
||||
ExecX(ctx)
|
||||
})
|
||||
|
||||
// 执行刷课
|
||||
err = courseService.AutoStudyByCourseID(ctx, targetCourse.CourseID)
|
||||
if err != nil {
|
||||
@@ -125,36 +171,32 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
|
||||
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "刷课失败", err))
|
||||
}
|
||||
|
||||
// 刷课完成后,统计任务情况
|
||||
g.Log().Infof(ctx, "刷课完成,正在统计任务情况...")
|
||||
// 刷课完成后,重新统计任务情况
|
||||
g.Log().Infof(ctx, "刷课完成,正在重新统计任务情况...")
|
||||
|
||||
// 先获取课程列表找到目标课程
|
||||
courseList := courseService.PullCourseList(ctx)
|
||||
var course *entity.XueXiTCourse
|
||||
for _, c := range courseList {
|
||||
if c.CourseID == targetCourse.CourseID {
|
||||
course = &c
|
||||
break
|
||||
// 重新获取章节列表和任务点状态
|
||||
chaptersAfter := courseService.PullCourseChapterList(ctx, *course)
|
||||
// FetchChapterPointStatus 会更新 chapters 中每个章节的 PointTotal 和 PointFinish
|
||||
_, statusErrAfter := courseService.FetchChapterPointStatus(ctx, *course, chaptersAfter)
|
||||
|
||||
var totalPointCountAfter, finishedPointCountAfter int
|
||||
if statusErrAfter != nil {
|
||||
g.Log().Warningf(ctx, "获取章节任务点状态失败: %v", statusErrAfter)
|
||||
} else {
|
||||
// 通过累加每个章节的 PointTotal 和 PointFinish 计算总数
|
||||
for _, chapter := range chaptersAfter {
|
||||
totalPointCountAfter += chapter.PointTotal
|
||||
finishedPointCountAfter += chapter.PointFinish
|
||||
}
|
||||
}
|
||||
|
||||
totalTasks := 0
|
||||
if course != nil {
|
||||
// 获取章节列表
|
||||
chapters := courseService.PullCourseChapterList(ctx, *course)
|
||||
g.Log().Infof(ctx, "课程 %s 统计完成: 总任务点 %d, 已完成 %d", targetCourse.CourseName, totalPointCountAfter, finishedPointCountAfter)
|
||||
|
||||
// 统计总任务数(累加所有章节的JobCount)
|
||||
for _, chapter := range chapters {
|
||||
totalTasks += chapter.JobCount
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "课程 %s 统计完成: 总任务点 %d", targetCourse.CourseName, totalTasks)
|
||||
|
||||
// 更新数据库(暂时不统计完成数,只统计总数)
|
||||
// 更新数据库
|
||||
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).
|
||||
SetStatus(courseentity.StatusDone).
|
||||
SetTaskCount(totalTasks).
|
||||
SetTaskCount(totalPointCountAfter).
|
||||
SetSuccessCount(finishedPointCountAfter).
|
||||
ExecX(ctx)
|
||||
|
||||
g.Log().Infof(ctx, "学习通刷课完成 - 课程: %s", targetCourse.CourseName)
|
||||
|
||||
@@ -36,7 +36,7 @@ func (s *ConfigServiceV1) Get(ctx context.Context, req *v1.ConfigGetReq) *utils.
|
||||
// PublicGet 获取公开配置(无需认证)
|
||||
func (s *ConfigServiceV1) PublicGet(ctx context.Context, req *v1.ConfigPublicGetReq) *utils.Response[*ent.SysConfig] {
|
||||
// 只允许获取特定的公开配置
|
||||
allowedKeys := []string{"system.announcement"}
|
||||
allowedKeys := []string{"system.announcement", "system.allowRegister"}
|
||||
|
||||
allowed := false
|
||||
for _, key := range allowedKeys {
|
||||
|
||||
@@ -13,9 +13,7 @@ import (
|
||||
"likedream-goframe/internal/ent/sysuser"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"strconv"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
@@ -91,109 +89,8 @@ func (s CourseServiceV1) AddCourse(ctx context.Context, req *v1.AddCourseReq) *u
|
||||
if targetCourse == nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "未找到该课程"))
|
||||
}
|
||||
chapters := courseService.PullCourseChapterList(ctx, *targetCourse)
|
||||
|
||||
// 定义节点信息结构
|
||||
type NodeInfo struct {
|
||||
ChapterID string `json:"chapter_id"`
|
||||
ChapterName string `json:"chapter_name"`
|
||||
CardID string `json:"card_id"`
|
||||
CardTitle string `json:"card_title"`
|
||||
CardType string `json:"card_type"`
|
||||
}
|
||||
|
||||
var allNodes []NodeInfo
|
||||
var completedNodes []string
|
||||
|
||||
// 递归获取所有章节的任务点
|
||||
var collectNodes func(chapters []entity.XueXiTCourseChapter)
|
||||
collectNodes = func(chapters []entity.XueXiTCourseChapter) {
|
||||
for _, chapter := range chapters {
|
||||
// 如果有任务点,获取卡片信息
|
||||
if chapter.JobCount > 0 {
|
||||
cards, points, err := courseService.FetchChapterCards(ctx, *targetCourse, chapter.ID)
|
||||
if err != nil {
|
||||
logger.Logger.Warn("获取章节任务点失败", zap.String("chapter", chapter.Name), zap.Error(err))
|
||||
} else if points != nil {
|
||||
chapterIDStr := strconv.Itoa(chapter.ID)
|
||||
// 统计视频任务点
|
||||
for _, video := range points.Videos {
|
||||
nodeInfo := NodeInfo{
|
||||
ChapterID: chapterIDStr,
|
||||
ChapterName: chapter.Name,
|
||||
CardID: video.JobID,
|
||||
CardTitle: video.Title,
|
||||
CardType: "video",
|
||||
}
|
||||
allNodes = append(allNodes, nodeInfo)
|
||||
if video.IsPassed {
|
||||
completedNodes = append(completedNodes, chapterIDStr+":"+video.JobID)
|
||||
}
|
||||
}
|
||||
// 统计文档任务点
|
||||
for _, doc := range points.Documents {
|
||||
nodeInfo := NodeInfo{
|
||||
ChapterID: chapterIDStr,
|
||||
ChapterName: chapter.Name,
|
||||
CardID: doc.JobID,
|
||||
CardTitle: doc.Title,
|
||||
CardType: "document",
|
||||
}
|
||||
allNodes = append(allNodes, nodeInfo)
|
||||
}
|
||||
// 统计作业任务点
|
||||
for _, work := range points.Works {
|
||||
nodeInfo := NodeInfo{
|
||||
ChapterID: chapterIDStr,
|
||||
ChapterName: chapter.Name,
|
||||
CardID: work.JobID,
|
||||
CardTitle: work.Title,
|
||||
CardType: "work",
|
||||
}
|
||||
allNodes = append(allNodes, nodeInfo)
|
||||
if work.IsPassed {
|
||||
completedNodes = append(completedNodes, chapterIDStr+":"+work.JobID)
|
||||
}
|
||||
}
|
||||
// 统计外链任务点
|
||||
for _, link := range points.Hyperlinks {
|
||||
nodeInfo := NodeInfo{
|
||||
ChapterID: chapterIDStr,
|
||||
ChapterName: chapter.Name,
|
||||
CardID: link.JobID,
|
||||
CardTitle: link.Title,
|
||||
CardType: "hyperlink",
|
||||
}
|
||||
allNodes = append(allNodes, nodeInfo)
|
||||
}
|
||||
// 统计直播任务点
|
||||
for _, live := range points.Lives {
|
||||
nodeInfo := NodeInfo{
|
||||
ChapterID: chapterIDStr,
|
||||
ChapterName: chapter.Name,
|
||||
CardID: live.JobID,
|
||||
CardTitle: live.Title,
|
||||
CardType: "live",
|
||||
}
|
||||
allNodes = append(allNodes, nodeInfo)
|
||||
}
|
||||
_ = cards // cards 暂时不使用
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理子章节
|
||||
if len(chapter.Knowledge) > 0 {
|
||||
collectNodes(chapter.Knowledge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectNodes(chapters)
|
||||
|
||||
// 将节点信息序列化为JSON
|
||||
nodesJSON, _ := sonic.MarshalString(allNodes)
|
||||
completedNodesJSON, _ := sonic.MarshalString(completedNodes)
|
||||
|
||||
// 添加课程时不获取任务点统计,由定时任务处理
|
||||
db2.Global.CourseEntity.Create().
|
||||
SetUserID(user.ID).
|
||||
SetUsername(req.Username).
|
||||
@@ -205,10 +102,8 @@ func (s CourseServiceV1) AddCourse(ctx context.Context, req *v1.AddCourseReq) *u
|
||||
SetViolence(req.Violence).
|
||||
SetNillableDesc(&req.Desc).
|
||||
SetStatus(courseentity.StatusNotStart).
|
||||
SetTaskCount(len(allNodes)).
|
||||
SetSuccessCount(len(completedNodes)).
|
||||
SetNodes(nodesJSON).
|
||||
SetCompletedNodes(completedNodesJSON).
|
||||
SetTaskCount(0).
|
||||
SetSuccessCount(0).
|
||||
SaveX(ctx)
|
||||
|
||||
return utils.SuccessWithMsg("添加成功")
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"likedream-goframe/internal/service/balance"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
@@ -168,24 +167,10 @@ func (v CpLeaveServiceV1) Report(ctx context.Context, req *v1.CpLeaveReportReq)
|
||||
detail := reportApp.LeaveDetail(req.LeaveId)
|
||||
|
||||
// 填充表单
|
||||
form := reportApp.FillReportForm(detail)
|
||||
|
||||
// 获取位置信息,优先使用前端传入的
|
||||
var lon, lat string
|
||||
if req.Longitude != "" && req.Latitude != "" {
|
||||
lon = req.Longitude
|
||||
lat = req.Latitude
|
||||
} else {
|
||||
lon, lat = reportApp.GetLocation(detail)
|
||||
}
|
||||
|
||||
// 如果前端传入了地址,使用前端的地址
|
||||
if req.Address != "" {
|
||||
form["reportLocation"] = gvar.New(req.Address)
|
||||
}
|
||||
form := reportApp.FillReportForm(detail, req)
|
||||
|
||||
// 提交销假
|
||||
reportApp.SubmitReport(ctx, form, lon, lat, cpUser.Username, cpUser.MobileCode)
|
||||
reportApp.SubmitReport(ctx, form, req.Longitude, req.Latitude, cpUser.Username, cpUser.MobileCode)
|
||||
|
||||
// 如果执行成功,更新记录状态
|
||||
db2.Global.CpLeave.UpdateOne(leaveRecord).
|
||||
@@ -196,6 +181,30 @@ func (v CpLeaveServiceV1) Report(ctx context.Context, req *v1.CpLeaveReportReq)
|
||||
return utils.SuccessWithMsg("销假成功")
|
||||
}
|
||||
|
||||
// Upload 上传图片并返回预览地址
|
||||
func (v CpLeaveServiceV1) Upload(ctx context.Context, req *v1.CpLeaveUploadReq) *utils.Response[string] {
|
||||
user := utils.GetUserInfoFromCtx(ctx)
|
||||
// 检测是否属于自己的cpuser
|
||||
cpUser := db2.Global.CpUser.Query().Where(cpuser.ID(req.CpUserId), cpuser.HasSysuserWith(sysuser.ID(user.ID))).WithSysuser().WithSchool().OnlyX(ctx)
|
||||
if cpUser.Edges.Sysuser == nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "非法的今日校园账号"))
|
||||
}
|
||||
schoolDetail := school.GetSchoolDetail(ctx, cpUser.SchoolID)
|
||||
client := login.GetClient(ctx, cpUser.Cookie)
|
||||
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, client, cpUser.LoginType) {
|
||||
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
|
||||
loginExecutor.Login(ctx, &schoolDetail, cpUser.Username, cpUser.Password)
|
||||
cookie := loginExecutor.GetCookieString()
|
||||
db2.Global.CpUser.UpdateOne(cpUser).SetCookie(cookie).ExecX(ctx)
|
||||
client = login.GetClient(ctx, cookie)
|
||||
}
|
||||
// 创建销假服务
|
||||
reportApp := leave.NewCpdailyReportApp(client, schoolDetail.WecloudUrl)
|
||||
|
||||
imageUrl := reportApp.UploadImage(ctx, req.File)
|
||||
return utils.SuccessWithData(imageUrl)
|
||||
}
|
||||
|
||||
func NewCpLeaveServiceV1() *CpLeaveServiceV1 {
|
||||
return &CpLeaveServiceV1{}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ func (s *EmailServiceV1) sendEmail(ctx context.Context, to, subject, body string
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
task := queue.AddTask(ctx, asynq.NewTask(cst2.TaskEmail, payload.Bytes()), asynq.Queue(cst2.TaskQueueEmail),
|
||||
asynq.MaxRetry(3),
|
||||
asynq.MaxRetry(1),
|
||||
asynq.Timeout(5*time.Minute),
|
||||
)
|
||||
logger.Logger.Info("添加任务: ", zap.String("task_id", task.ID), zap.String("邮箱", to))
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"likedream-goframe/internal/api/v1"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
cst2 "likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/db"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/ent/sysconfig"
|
||||
"likedream-goframe/internal/ent/sysrole"
|
||||
"likedream-goframe/internal/ent/sysuser"
|
||||
"likedream-goframe/internal/framework/redis"
|
||||
@@ -47,6 +48,12 @@ func (s *UserLoginServiceV1) Login(ctx context.Context, req *v1.UserLoginReq) *u
|
||||
}
|
||||
|
||||
func (s *UserLoginServiceV1) Register(ctx context.Context, req *v1.UserRegisterReq) *utils.Response[any] {
|
||||
// 检查是否允许注册
|
||||
allowRegister := s.IsRegistrationAllowed(ctx)
|
||||
if !allowRegister {
|
||||
return utils.FailParam("系统当前不允许注册")
|
||||
}
|
||||
|
||||
//邀请码查询
|
||||
var parent *ent.SysUser
|
||||
if req.InviteCode != "" {
|
||||
@@ -255,3 +262,18 @@ func (s *UserLoginServiceV1) GetResetPasswordInfo(ctx context.Context, req *v1.R
|
||||
Email: user.Email,
|
||||
})
|
||||
}
|
||||
|
||||
// IsRegistrationAllowed 检查是否允许注册
|
||||
func (s *UserLoginServiceV1) IsRegistrationAllowed(ctx context.Context) bool {
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ(cst2.ConfigSystemAllowRegister)).
|
||||
Only(ctx)
|
||||
|
||||
// 如果配置不存在或查询出错,默认允许注册
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查配置值,"true" 或 "1" 表示允许注册
|
||||
return config.Value == "true" || config.Value == "1"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user