签入脚本
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gcron"
|
||||
|
||||
"easygoadmin/api/v1"
|
||||
"easygoadmin/internal/consts"
|
||||
"easygoadmin/internal/dao"
|
||||
"easygoadmin/internal/service"
|
||||
)
|
||||
|
||||
// ============================== 定时任务业务实现 ==============================
|
||||
|
||||
// sJob 定时任务业务实现
|
||||
// 实现 service.IJob 接口,接口定义见 api/v1/job.go
|
||||
type sJob struct{}
|
||||
|
||||
// jobSerializeMaps 枚举字段到字典编码的映射
|
||||
// {field}Text 由基座自动填充
|
||||
var jobSerializeMaps = map[string]string{
|
||||
"status": "job_status",
|
||||
"execute_policy": "job_execute_policy",
|
||||
"is_sync": "job_is_sync",
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterJob(&sJob{})
|
||||
// 调度器启动加载(cmd 启动流程会调用 DaemonInit,此处保证 init 顺序无关)
|
||||
}
|
||||
|
||||
// jobSpec 定时任务模块声明
|
||||
// 调度器挂载与摘除通过钩子承载
|
||||
var jobSpec = ModuleSpec[v1.JobItem, v1.JobItem, v1.JobDetailRes]{
|
||||
Model: dao.Job.Ctx,
|
||||
LikeFields: []string{"job_name", "job_alias"},
|
||||
EqFields: []string{"status"},
|
||||
OrderBy: "id DESC",
|
||||
ListOrderBy: "id",
|
||||
SerializeMaps: jobSerializeMaps,
|
||||
ToData: jobToData,
|
||||
BeforeAdd: jobDefaultStatus, // 新增固定 status=0(未发布)
|
||||
BeforeUpdate: jobExistGuard, // 存在性检查
|
||||
AfterUpdate: jobRemount, // 运行中任务重挂新配置
|
||||
BeforeDelete: jobDeleteGuard, // 存在性 + 摘除调度
|
||||
}
|
||||
|
||||
// jobToData 全量表单语义转换
|
||||
// 新增与修改均提交全部字段,status 不在表单内
|
||||
func jobToData(req any) map[string]any {
|
||||
switch r := req.(type) {
|
||||
case *v1.JobAddReq:
|
||||
return map[string]any{
|
||||
"job_name": r.JobName, "job_alias": r.JobAlias, "job_group": r.JobGroup,
|
||||
"job_trigger": r.JobTrigger, "cron_expression": r.CronExpression,
|
||||
"execute_policy": r.ExecutePolicy, "is_sync": r.IsSync,
|
||||
"url": r.Url, "params": r.Params, "note": r.Note,
|
||||
}
|
||||
case *v1.JobUpdateReq:
|
||||
return map[string]any{
|
||||
"job_name": r.JobName, "job_alias": r.JobAlias, "job_group": r.JobGroup,
|
||||
"job_trigger": r.JobTrigger, "cron_expression": r.CronExpression,
|
||||
"execute_policy": r.ExecutePolicy, "is_sync": r.IsSync,
|
||||
"url": r.Url, "params": r.Params, "note": r.Note,
|
||||
}
|
||||
}
|
||||
panic("jobToData: 不支持的入参类型")
|
||||
}
|
||||
|
||||
// jobDefaultStatus 新增时固定状态为未发布
|
||||
// 发布操作走 Status 或 Resume 方法
|
||||
func jobDefaultStatus(ctx context.Context, data map[string]any, req any) error {
|
||||
data["status"] = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// jobExistGuard 更新前存在性检查
|
||||
func jobExistGuard(ctx context.Context, data map[string]any, id int64, req any) error {
|
||||
if n, err := dao.Job.Ctx(ctx).Where("id", id).Count(); err != nil {
|
||||
return err
|
||||
} else if n == 0 {
|
||||
return gerror.NewCode(consts.CodeRecordNotFound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// jobRemount 更新后重挂调度
|
||||
// 运行中任务先摘除再按新配置挂载,挂载失败时降级为暂停
|
||||
func jobRemount(ctx context.Context, id int64, req any) error {
|
||||
r := mustAs[*v1.JobUpdateReq](req)
|
||||
one, err := dao.Job.Ctx(ctx).Where("id", id).One()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(one) == 0 || one["status"].Int() != 1 {
|
||||
return nil
|
||||
}
|
||||
removeJobFromScheduler(id)
|
||||
if err = addJobToScheduler(id, r.CronExpression, r.ExecutePolicy, r.IsSync); err != nil {
|
||||
// 挂载失败降级为暂停
|
||||
_, _ = dao.Job.Ctx(ctx).Where("id", id).Data(map[string]any{"status": 2}).Update()
|
||||
return gerror.NewCodef(consts.CodeParamError, "Cron表达式无效: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// jobDeleteGuard 删除守卫
|
||||
// 校验存在性并摘除调度
|
||||
func jobDeleteGuard(ctx context.Context, id int64) error {
|
||||
if n, err := dao.Job.Ctx(ctx).Where("id", id).Count(); err != nil {
|
||||
return err
|
||||
} else if n == 0 {
|
||||
return gerror.NewCode(consts.CodeRecordNotFound)
|
||||
}
|
||||
removeJobFromScheduler(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------- 标准方法:3行委托 ----------
|
||||
|
||||
// Page 任务分页
|
||||
// 按查询条件分页返回定时任务列表
|
||||
func (s *sJob) Page(ctx context.Context, req *v1.JobPageReq) (res *v1.JobPageRes, err error) {
|
||||
list, total, err := jobSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil
|
||||
}
|
||||
|
||||
// List 任务列表
|
||||
// 按关键词、状态等条件返回定时任务列表
|
||||
func (s *sJob) List(ctx context.Context, req *v1.JobListReq) (res v1.JobListRes, err error) {
|
||||
list, err := jobSpec.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return derefSlice(list), nil
|
||||
}
|
||||
|
||||
// Detail 任务详情
|
||||
//
|
||||
// 因 JobDetailRes 为嵌入结构,基座 FillTextFields 的反射计划要求顶层字段,故手写实现
|
||||
func (s *sJob) Detail(ctx context.Context, req *v1.JobDetailReq) (res *v1.JobDetailRes, err error) {
|
||||
one, err := dao.Job.Ctx(ctx).Where("id", req.Id).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(one) == 0 {
|
||||
return nil, gerror.NewCode(consts.CodeRecordNotFound)
|
||||
}
|
||||
var item v1.JobItem
|
||||
if err = one.Struct(&item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
FillTextFields(ctx, &item, jobSerializeMaps)
|
||||
return &v1.JobDetailRes{JobItem: item}, nil
|
||||
}
|
||||
|
||||
// Add 新增任务
|
||||
// 默认未发布状态,发布操作走 Status 或 Resume 方法
|
||||
func (s *sJob) Add(ctx context.Context, req *v1.JobAddReq) (res *v1.IdRes, err error) {
|
||||
id, err := jobSpec.Add(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.IdRes{Id: id}, nil
|
||||
}
|
||||
|
||||
// Update 修改任务
|
||||
// 运行中任务先摘除再重挂
|
||||
func (s *sJob) Update(ctx context.Context, req *v1.JobUpdateReq) (res *v1.JobUpdateRes, err error) {
|
||||
if err = jobSpec.Update(ctx, req.Id, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.JobUpdateRes{}, nil
|
||||
}
|
||||
|
||||
// Delete 删除任务
|
||||
// 软删记录并摘除调度
|
||||
func (s *sJob) Delete(ctx context.Context, req *v1.JobDeleteReq) (res *v1.JobDeleteRes, err error) {
|
||||
if err = jobSpec.Delete(ctx, req.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.JobDeleteRes{}, nil
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除任务
|
||||
// 按任务ID列表批量删除记录
|
||||
func (s *sJob) BatchDelete(ctx context.Context, req *v1.JobBatchDeleteReq) (res *v1.JobBatchDeleteRes, err error) {
|
||||
if err = jobSpec.BatchDelete(ctx, req.Ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.JobBatchDeleteRes{}, nil
|
||||
}
|
||||
|
||||
// ---------- 非标准方法(手写实现) ----------
|
||||
|
||||
// RunOnce 立即执行一次
|
||||
// 异步触发指定任务,未发布任务也可执行
|
||||
func (s *sJob) RunOnce(ctx context.Context, req *v1.JobRunOnceReq) (res *v1.JobRunOnceRes, err error) {
|
||||
one, err := dao.Job.Ctx(ctx).Where("id", req.Id).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(one) == 0 {
|
||||
return nil, gerror.NewCode(consts.CodeRecordNotFound)
|
||||
}
|
||||
go executeJob(context.Background(), req.Id, true)
|
||||
return &v1.JobRunOnceRes{}, nil
|
||||
}
|
||||
|
||||
// Pause 暂停任务
|
||||
// 摘除调度并将状态置为暂停
|
||||
func (s *sJob) Pause(ctx context.Context, req *v1.JobPauseReq) (res *v1.JobPauseRes, err error) {
|
||||
one, err := dao.Job.Ctx(ctx).Where("id", req.Id).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(one) == 0 {
|
||||
return nil, gerror.NewCode(consts.CodeRecordNotFound)
|
||||
}
|
||||
removeJobFromScheduler(req.Id)
|
||||
if _, err = dao.Job.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": 2}).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.JobPauseRes{}, nil
|
||||
}
|
||||
|
||||
// Resume 恢复任务
|
||||
// 重新挂载调度并将状态置为运行中
|
||||
func (s *sJob) Resume(ctx context.Context, req *v1.JobResumeReq) (res *v1.JobResumeRes, err error) {
|
||||
one, err := dao.Job.Ctx(ctx).Where("id", req.Id).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(one) == 0 {
|
||||
return nil, gerror.NewCode(consts.CodeRecordNotFound)
|
||||
}
|
||||
if err = addJobToScheduler(req.Id, one["cron_expression"].String(),
|
||||
one["execute_policy"].Int(), one["is_sync"].Int()); err != nil {
|
||||
return nil, gerror.NewCodef(consts.CodeParamError, "Cron表达式无效: %v", err)
|
||||
}
|
||||
if _, err = dao.Job.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": 1}).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.JobResumeRes{}, nil
|
||||
}
|
||||
|
||||
// Status 设置任务状态
|
||||
// 运行中状态挂载调度,暂停与未发布状态摘除调度
|
||||
func (s *sJob) Status(ctx context.Context, req *v1.JobStatusReq) (res *v1.JobStatusRes, err error) {
|
||||
one, err := dao.Job.Ctx(ctx).Where("id", req.Id).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(one) == 0 {
|
||||
return nil, gerror.NewCode(consts.CodeRecordNotFound)
|
||||
}
|
||||
removeJobFromScheduler(req.Id)
|
||||
if req.Status == 1 {
|
||||
if err = addJobToScheduler(req.Id, one["cron_expression"].String(),
|
||||
one["execute_policy"].Int(), one["is_sync"].Int()); err != nil {
|
||||
return nil, gerror.NewCodef(consts.CodeParamError, "Cron表达式无效: %v", err)
|
||||
}
|
||||
}
|
||||
if _, err = dao.Job.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": req.Status}).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.JobStatusRes{}, nil
|
||||
}
|
||||
|
||||
// DaemonStatus 查询调度器状态
|
||||
// 返回调度器中运行的任务数、任务键与启动时间
|
||||
func (s *sJob) DaemonStatus(ctx context.Context, req *v1.JobDaemonStatusReq) (res *v1.JobDaemonStatusRes, err error) {
|
||||
entries := gcron.Entries()
|
||||
ids := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
ids = append(ids, e.Name)
|
||||
}
|
||||
startText := ""
|
||||
if !daemonStart.IsZero() {
|
||||
startText = daemonStart.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
return &v1.JobDaemonStatusRes{
|
||||
Running: len(entries),
|
||||
JobIds: ids,
|
||||
StartTime: startText,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user