committed by
GitHub
172 changed files with 8559 additions and 11 deletions
@ -0,0 +1,78 @@ |
|||
import { defAbpHttp } from '/@/utils/http/abp'; |
|||
import { |
|||
BackgroundJobInfo, |
|||
BackgroundJobInfoCreate, |
|||
BackgroundJobInfoUpdate, |
|||
BackgroundJobInfoGetListInput, |
|||
} from './model/backgroundJobInfoModel'; |
|||
import { format } from '/@/utils/strings'; |
|||
import { PagedResultDto } from '../model/baseModel'; |
|||
|
|||
enum Api { |
|||
GetById = '/api/task-management/background-jobs/{id}', |
|||
GetList = '/api/task-management/background-jobs', |
|||
Create = '/api/task-management/background-jobs', |
|||
Update = '/api/task-management/background-jobs/{id}', |
|||
Delete = '/api/task-management/background-jobs/{id}', |
|||
Pause = '/api/task-management/background-jobs/{id}/pause', |
|||
Resume = '/api/task-management/background-jobs/{id}/resume', |
|||
Trigger = '/api/task-management/background-jobs/{id}/trigger', |
|||
Stop = '/api/task-management/background-jobs/{id}/stop', |
|||
} |
|||
|
|||
export const getById = (id: string) => { |
|||
return defAbpHttp.get<BackgroundJobInfo>({ |
|||
url: format(Api.GetById, { id: id }), |
|||
}); |
|||
}; |
|||
|
|||
export const getList = (input: BackgroundJobInfoGetListInput) => { |
|||
return defAbpHttp.get<PagedResultDto<BackgroundJobInfo>>({ |
|||
url: Api.GetList, |
|||
params: input, |
|||
}); |
|||
}; |
|||
|
|||
export const create = (input: BackgroundJobInfoCreate) => { |
|||
return defAbpHttp.post<BackgroundJobInfo>({ |
|||
url: Api.Create, |
|||
data: input, |
|||
}); |
|||
}; |
|||
|
|||
export const update = (id: string, input: BackgroundJobInfoUpdate) => { |
|||
return defAbpHttp.put<BackgroundJobInfo>({ |
|||
url: format(Api.Update, { id: id }), |
|||
data: input, |
|||
}); |
|||
}; |
|||
|
|||
export const deleteById = (id: string) => { |
|||
return defAbpHttp.delete<void>({ |
|||
url: format(Api.Delete, { id: id }), |
|||
}); |
|||
}; |
|||
|
|||
export const pause = (id: string) => { |
|||
return defAbpHttp.put<void>({ |
|||
url: format(Api.Pause, { id: id }), |
|||
}); |
|||
}; |
|||
|
|||
export const resume = (id: string) => { |
|||
return defAbpHttp.put<void>({ |
|||
url: format(Api.Resume, { id: id }), |
|||
}); |
|||
}; |
|||
|
|||
export const trigger = (id: string) => { |
|||
return defAbpHttp.put<void>({ |
|||
url: format(Api.Trigger, { id: id }), |
|||
}); |
|||
}; |
|||
|
|||
export const stop = (id: string) => { |
|||
return defAbpHttp.put<void>({ |
|||
url: format(Api.Stop, { id: id }), |
|||
}); |
|||
}; |
|||
@ -0,0 +1,32 @@ |
|||
import { defAbpHttp } from '/@/utils/http/abp'; |
|||
import { |
|||
BackgroundJobLog, |
|||
BackgroundJobLogGetListInput, |
|||
} from './model/backgroundJobLogModel'; |
|||
import { format } from '/@/utils/strings'; |
|||
import { PagedResultDto } from '../model/baseModel'; |
|||
|
|||
enum Api { |
|||
GetById = '/api/task-management/background-jobs/logs/{id}', |
|||
GetList = '/api/task-management/background-jobs/logs', |
|||
Delete = '/api/task-management/background-jobs/logs/{id}', |
|||
} |
|||
|
|||
export const getById = (id: string) => { |
|||
return defAbpHttp.get<BackgroundJobLog>({ |
|||
url: format(Api.GetById, { id: id }), |
|||
}); |
|||
}; |
|||
|
|||
export const getList = (input: BackgroundJobLogGetListInput) => { |
|||
return defAbpHttp.get<PagedResultDto<BackgroundJobLog>>({ |
|||
url: Api.GetList, |
|||
params: input, |
|||
}); |
|||
}; |
|||
|
|||
export const deleteById = (id: string) => { |
|||
return defAbpHttp.delete<void>({ |
|||
url: format(Api.Delete, { id: id }), |
|||
}); |
|||
}; |
|||
@ -0,0 +1,89 @@ |
|||
import { ExtensibleAuditedEntity, IHasConcurrencyStamp, PagedAndSortedResultRequestDto } from "../../model/baseModel"; |
|||
|
|||
export enum JobStatus { |
|||
None = -1, |
|||
Completed = 0, |
|||
Running = 10, |
|||
FailedRetry = 15, |
|||
Paused = 20, |
|||
Stopped = 30, |
|||
} |
|||
|
|||
export enum JobType { |
|||
Once, |
|||
Period, |
|||
Persistent, |
|||
} |
|||
|
|||
export enum JobPriority { |
|||
Low = 5, |
|||
BelowNormal = 10, |
|||
Normal = 0xF, |
|||
AboveNormal = 20, |
|||
High = 25 |
|||
} |
|||
|
|||
export interface BackgroundJobInfo extends ExtensibleAuditedEntity<string>, IHasConcurrencyStamp { |
|||
isEnabled: boolean; |
|||
name: string; |
|||
group: string; |
|||
type: string; |
|||
result: string; |
|||
args: ExtraPropertyDictionary, |
|||
description?: string; |
|||
beginTime: Date; |
|||
endTime?: Date; |
|||
lastRunTime?: Date; |
|||
nextRunTime?: Date; |
|||
jobType: JobType; |
|||
cron: string; |
|||
triggerCount: number; |
|||
tryCount: number; |
|||
maxTryCount: number; |
|||
maxCount: number; |
|||
isAbandoned: boolean; |
|||
interval: number; |
|||
priority: JobPriority; |
|||
lockTimeOut: number; |
|||
} |
|||
|
|||
interface BackgroundJobInfoCreateOrUpdate { |
|||
isEnabled: boolean; |
|||
args: ExtraPropertyDictionary; |
|||
description?: string; |
|||
beginTime: Date; |
|||
endTime?: Date; |
|||
jobType: JobType; |
|||
cron: string; |
|||
maxCount: number; |
|||
interval: number; |
|||
priority: JobPriority; |
|||
lockTimeOut: number; |
|||
} |
|||
|
|||
export interface BackgroundJobInfoCreate extends BackgroundJobInfoCreateOrUpdate { |
|||
name: string; |
|||
group: string; |
|||
type: string; |
|||
} |
|||
|
|||
export interface BackgroundJobInfoUpdate extends BackgroundJobInfoCreateOrUpdate, IHasConcurrencyStamp { |
|||
|
|||
} |
|||
|
|||
export interface BackgroundJobInfoGetListInput extends PagedAndSortedResultRequestDto { |
|||
filter?: string; |
|||
name?: string; |
|||
group?: string; |
|||
type?: string; |
|||
status?: JobStatus; |
|||
beginTime?: Date; |
|||
endTime?: Date; |
|||
beginLastRunTime?: Date; |
|||
endLastRunTime?: Date; |
|||
beginCreationTime?: Date; |
|||
endCreationTime?: Date; |
|||
isAbandoned?: boolean; |
|||
jobType?: JobType; |
|||
priority?: JobPriority; |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
import { PagedAndSortedResultRequestDto } from '/@/api/model/baseModel'; |
|||
|
|||
export interface BackgroundJobLog { |
|||
id: number; |
|||
jobName: string; |
|||
jobGroup: string; |
|||
jobType: string; |
|||
message: string; |
|||
runTime: Date; |
|||
exception?: string; |
|||
} |
|||
|
|||
export interface BackgroundJobLogGetListInput extends PagedAndSortedResultRequestDto { |
|||
jobId?: string; |
|||
filter?: string; |
|||
hasExceptions?: boolean; |
|||
name?: string; |
|||
group?: string; |
|||
type?: string; |
|||
beginRunTime?: Date; |
|||
endRunTime?: Date; |
|||
} |
|||
@ -0,0 +1,338 @@ |
|||
<template> |
|||
<BasicModal |
|||
@register="registerModal" |
|||
:width="800" |
|||
:height="400" |
|||
:title="modalTitle" |
|||
:help-message="modalTips" |
|||
:mask-closable="false" |
|||
@ok="handleSubmit" |
|||
> |
|||
<Form |
|||
ref="formElRef" |
|||
:colon="false" |
|||
label-align="left" |
|||
layout="horizontal" |
|||
:model="modelRef" |
|||
:rules="modelRules" |
|||
> |
|||
<Tabs v-model:activeKey="activeKey"> |
|||
<TabPane key="basic" :tab="L('BasicInfo')"> |
|||
<FormItem name="isEnabled" :labelCol="{ span: 4 }" :wrapperCol="{ span: 18 }" :label="L('DisplayName:IsEnabled')"> |
|||
<Checkbox v-model:checked="modelRef.isEnabled">{{ L('DisplayName:IsEnabled') }}</Checkbox> |
|||
</FormItem> |
|||
<FormItem name="group" required :label="L('DisplayName:Group')"> |
|||
<Input :disabled="isEditModal" v-model:value="modelRef.group" autocomplete="off" /> |
|||
</FormItem> |
|||
<FormItem name="name" required :label="L('DisplayName:Name')"> |
|||
<Input :disabled="isEditModal" v-model:value="modelRef.name" autocomplete="off" /> |
|||
</FormItem> |
|||
<FormItem name="type" required :label="L('DisplayName:Type')" :extra="L('Description:Type')"> |
|||
<Textarea :disabled="isEditModal" v-model:value="modelRef.type" :auto-size="{ minRows: 3, maxRows: 6 }" /> |
|||
</FormItem> |
|||
<FormItem name="beginTime" :label="L('DisplayName:BeginTime')"> |
|||
<DatePicker style="width: 100%;" v-model:value="modelRef.beginTime" /> |
|||
</FormItem> |
|||
<FormItem name="endTime" :label="L('DisplayName:EndTime')"> |
|||
<DatePicker style="width: 100%;" v-model:value="modelRef.endTime" /> |
|||
</FormItem> |
|||
<FormItem name="jobType" :label="L('DisplayName:JobType')" :help="L('Description:JobType')"> |
|||
<Select :options="jopTypeOptions" v-model:value="modelRef.jobType" :default-value="JobType.Once" /> |
|||
</FormItem> |
|||
<FormItem name="cron" v-if="modelRef.jobType === JobType.Period" :label="L('DisplayName:Cron')" :help="L('Description:Cron')"> |
|||
<Input v-model:value="modelRef.cron" autocomplete="off" /> |
|||
</FormItem> |
|||
<FormItem name="interval" v-if="modelRef.jobType !== JobType.Period" :label="L('DisplayName:Interval')" :help="L('Description:Interval')"> |
|||
<InputNumber style="width: 100%;" v-model:value="modelRef.interval" /> |
|||
</FormItem> |
|||
<FormItem name="maxCount" :label="L('DisplayName:MaxCount')" :help="L('Description:MaxCount')"> |
|||
<InputNumber style="width: 100%;" v-model:value="modelRef.maxCount" /> |
|||
</FormItem> |
|||
<FormItem name="maxTryCount" :label="L('DisplayName:MaxTryCount')" :help="L('Description:MaxTryCount')"> |
|||
<InputNumber style="width: 100%;" v-model:value="modelRef.maxTryCount" /> |
|||
</FormItem> |
|||
<FormItem name="priority" :label="L('DisplayName:Priority')" :help="L('Description:Priority')"> |
|||
<Select :options="jobPriorityOptions" v-model:value="modelRef.priority" :default-value="JobPriority.Normal" /> |
|||
</FormItem> |
|||
<FormItem name="lockTimeOut" :label="L('DisplayName:LockTimeOut')" :help="L('Description:LockTimeOut')"> |
|||
<InputNumber style="width: 100%;" v-model:value="modelRef.lockTimeOut" /> |
|||
</FormItem> |
|||
<FormItem name="description" :label="L('DisplayName:Description')"> |
|||
<Textarea v-model:value="modelRef.description" :row="3" /> |
|||
</FormItem> |
|||
<FormItem :label="L('DisplayName:Result')"> |
|||
<Textarea readonly v-model:value="modelRef.result" :auto-size="{ minRows: 6, maxRows: 12 }" /> |
|||
</FormItem> |
|||
</TabPane> |
|||
<TabPane key="paramters" :tab="L('Paramters')"> |
|||
<BasicTable @register="registerTable" :data-source="jobArgs"> |
|||
<template #toolbar> |
|||
<Button type="primary" @click="handleAddNewArg">{{ L('BackgroundJobs:AddNewArg') }} |
|||
</Button> |
|||
</template> |
|||
<template #action="{ record }"> |
|||
<TableAction |
|||
:stop-button-propagation="true" |
|||
:actions="[ |
|||
{ |
|||
label: L('Edit'), |
|||
icon: 'ant-design:edit-outlined', |
|||
onClick: handleEditParam.bind(null, record), |
|||
}, |
|||
{ |
|||
color: 'error', |
|||
label: L('Delete'), |
|||
icon: 'ant-design:delete-outlined', |
|||
onClick: handleDeleteParam.bind(null, record), |
|||
}, |
|||
]" |
|||
/> |
|||
</template> |
|||
</BasicTable> |
|||
</TabPane> |
|||
</Tabs> |
|||
</Form> |
|||
<BasicModal |
|||
:title="L('BackgroundJobs:Paramter')" |
|||
@register="registerParamModal" |
|||
@ok="handleSaveParam" |
|||
> |
|||
<BasicForm @register="registerParamForm" /> |
|||
</BasicModal> |
|||
</BasicModal> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import { computed, ref, reactive, unref, nextTick } from 'vue'; |
|||
import { useLocalization } from '/@/hooks/abp/useLocalization'; |
|||
import { useValidation } from '/@/hooks/abp/useValidation'; |
|||
import { useMessage } from '/@/hooks/web/useMessage'; |
|||
import { |
|||
Button, |
|||
Checkbox, |
|||
DatePicker, |
|||
Form, |
|||
Select, |
|||
Tabs, |
|||
Input, |
|||
InputNumber, |
|||
Textarea, |
|||
} from 'ant-design-vue'; |
|||
import { BasicForm, useForm } from '/@/components/Form'; |
|||
import { BasicModal, useModal, useModalInner } from '/@/components/Modal'; |
|||
import { BasicTable, BasicColumn, TableAction, useTable } from '/@/components/Table'; |
|||
import { getById, create, update } from '/@/api/task-management/backgroundJobInfo'; |
|||
import { JobType, JobPriority, BackgroundJobInfo } from '/@/api/task-management/model/backgroundJobInfoModel'; |
|||
import { JobTypeMap, JobPriorityMap } from '../datas/typing'; |
|||
|
|||
const FormItem = Form.Item; |
|||
const TabPane = Tabs.TabPane; |
|||
|
|||
const emit = defineEmits(['change', 'register']); |
|||
const { L } = useLocalization('TaskManagement'); |
|||
const { ruleCreator } = useValidation(); |
|||
const { createMessage } = useMessage(); |
|||
const formElRef = ref<any>(); |
|||
const activeKey = ref('basic'); |
|||
const modelRef = ref<BackgroundJobInfo>({ |
|||
id: '', |
|||
isEnabled: true, |
|||
priority: JobPriority.Normal, |
|||
jobType: JobType.Once, |
|||
args: {}, |
|||
} as BackgroundJobInfo); |
|||
const [registerModal, { closeModal, changeOkLoading }] = useModalInner((model) => { |
|||
activeKey.value = 'basic'; |
|||
fetchModel(model.id); |
|||
}); |
|||
const [registerParamModal, { openModal: openParamModal, closeModal: closeParamModal }] = useModal(); |
|||
const [registerParamForm, { resetFields: resetParamFields, setFieldsValue: setParamFields, validate }] = useForm({ |
|||
labelAlign: 'left', |
|||
labelWidth: 120, |
|||
schemas: [ |
|||
{ |
|||
field: 'key', |
|||
component: 'Input', |
|||
label: L('DisplayName:Key'), |
|||
required: true, |
|||
colProps: { span: 24 }, |
|||
componentProps: { |
|||
autocomplete: "off" |
|||
}, |
|||
}, |
|||
{ |
|||
field: 'value', |
|||
component: 'InputTextArea', |
|||
label: L('DisplayName:Value'), |
|||
required: true, |
|||
colProps: { span: 24 }, |
|||
}, |
|||
], |
|||
showActionButtonGroup: false, |
|||
}); |
|||
const columns: BasicColumn[] = [ |
|||
{ |
|||
title: L('DisplayName:Key'), |
|||
dataIndex: 'key', |
|||
align: 'left', |
|||
width: 200, |
|||
sorter: true, |
|||
}, |
|||
{ |
|||
title: L('DisplayName:Value'), |
|||
dataIndex: 'value', |
|||
align: 'left', |
|||
width: 300, |
|||
sorter: true, |
|||
}, |
|||
]; |
|||
const [registerTable] = useTable({ |
|||
rowKey: 'key', |
|||
columns: columns, |
|||
pagination: false, |
|||
maxHeight: 300, |
|||
actionColumn: { |
|||
width: 180, |
|||
title: L('Actions'), |
|||
dataIndex: 'action', |
|||
slots: { customRender: 'action' }, |
|||
}, |
|||
}); |
|||
const isEditModal = computed(() => { |
|||
if (modelRef.value.id) { |
|||
return true; |
|||
} |
|||
return false; |
|||
}); |
|||
const modalTitle = computed(() => { |
|||
if (!isEditModal.value) { |
|||
return L('BackgroundJobs:AddNew'); |
|||
} |
|||
if (modelRef.value.isAbandoned) { |
|||
return `${L('BackgroundJobs:Edit')} - ${L('DisplayName:IsAbandoned')}`; |
|||
} |
|||
return L('BackgroundJobs:Edit'); |
|||
}); |
|||
const modalTips = computed(() => { |
|||
if (modelRef.value.isAbandoned) { |
|||
return L('Description:IsAbandoned'); |
|||
} |
|||
return ''; |
|||
}); |
|||
const modelRules = reactive({ |
|||
group: ruleCreator.fieldRequired({ |
|||
name: 'Group', |
|||
resourceName: 'TaskManagement', |
|||
prefix: 'DisplayName', |
|||
}), |
|||
name: ruleCreator.fieldRequired({ |
|||
name: 'Name', |
|||
resourceName: 'TaskManagement', |
|||
prefix: 'DisplayName', |
|||
}), |
|||
type: ruleCreator.fieldRequired({ |
|||
name: 'Type', |
|||
resourceName: 'TaskManagement', |
|||
prefix: 'DisplayName', |
|||
}), |
|||
beginTime: ruleCreator.fieldRequired({ |
|||
name: 'BeginTime', |
|||
resourceName: 'TaskManagement', |
|||
prefix: 'DisplayName', |
|||
type: 'date', |
|||
}), |
|||
}); |
|||
const jopTypeOptions = reactive([ |
|||
{ label: JobTypeMap[JobType.Once], value: JobType.Once }, |
|||
{ label: JobTypeMap[JobType.Period], value: JobType.Period }, |
|||
{ label: JobTypeMap[JobType.Persistent], value: JobType.Persistent }, |
|||
]); |
|||
const jobPriorityOptions = reactive([ |
|||
{ label: JobPriorityMap[JobPriority.Low], value: JobPriority.Low }, |
|||
{ label: JobPriorityMap[JobPriority.BelowNormal], value: JobPriority.BelowNormal }, |
|||
{ label: JobPriorityMap[JobPriority.Normal], value: JobPriority.Normal }, |
|||
{ label: JobPriorityMap[JobPriority.AboveNormal], value: JobPriority.AboveNormal }, |
|||
{ label: JobPriorityMap[JobPriority.High], value: JobPriority.High }, |
|||
]); |
|||
const jobArgs = computed(() => { |
|||
const model = unref(modelRef); |
|||
if (!model.args) return []; |
|||
return Object.keys(model.args).map((key) => { |
|||
return { |
|||
key: key, |
|||
value: model.args[key], |
|||
}; |
|||
}); |
|||
}); |
|||
|
|||
function fetchModel(id: string) { |
|||
if (!id) { |
|||
resetFields(); |
|||
return; |
|||
} |
|||
getById(id).then((res) => { |
|||
modelRef.value = res; |
|||
}); |
|||
} |
|||
|
|||
function resetFields() { |
|||
nextTick(() => { |
|||
modelRef.value = { |
|||
id: '', |
|||
isEnabled: true, |
|||
priority: JobPriority.Normal, |
|||
jobType: JobType.Once, |
|||
args: {}, |
|||
} as BackgroundJobInfo; |
|||
}); |
|||
} |
|||
|
|||
function handleSubmit() { |
|||
const formEl = unref(formElRef); |
|||
formEl?.validate().then(() => { |
|||
changeOkLoading(true); |
|||
const model = unref(modelRef); |
|||
const api = isEditModal.value |
|||
? update(model.id, Object.assign(model)) |
|||
: create(Object.assign(model)); |
|||
api.then(() => { |
|||
createMessage.success(L('Successful')); |
|||
formEl?.resetFields(); |
|||
closeModal(); |
|||
emit('change'); |
|||
}).finally(() => { |
|||
changeOkLoading(false); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
function handleAddNewArg() { |
|||
openParamModal(true); |
|||
nextTick(() => { |
|||
resetParamFields(); |
|||
}); |
|||
} |
|||
|
|||
function handleSaveParam() { |
|||
validate().then((input) => { |
|||
const model = unref(modelRef); |
|||
model.args ??= {}; |
|||
model.args[input.key] = input.value; |
|||
resetParamFields(); |
|||
closeParamModal(); |
|||
}); |
|||
} |
|||
|
|||
function handleEditParam(record) { |
|||
openParamModal(true); |
|||
nextTick(() => { |
|||
setParamFields(record); |
|||
}); |
|||
} |
|||
|
|||
function handleDeleteParam(record) { |
|||
const model = unref(modelRef); |
|||
model.args ??= {}; |
|||
delete model.args[record.key] |
|||
} |
|||
</script> |
|||
@ -0,0 +1,167 @@ |
|||
<template> |
|||
<div class="content"> |
|||
<BasicTable @register="registerTable"> |
|||
<template #toolbar> |
|||
<a-button |
|||
v-if="hasPermission('TaskManagement.BackgroundJobs.Create')" |
|||
type="primary" |
|||
@click="handleAddNew" |
|||
>{{ L('BackgroundJobs:AddNew') }}</a-button |
|||
> |
|||
</template> |
|||
<template #enable="{ record }"> |
|||
<Switch :checked="record.isEnabled" disabled /> |
|||
</template> |
|||
<template #status="{ record }"> |
|||
<Tooltip v-if="record.isAbandoned" color="orange" :title="L('Description:IsAbandoned')"> |
|||
<Tag :color="JobStatusColor[record.status]">{{ JobStatusMap[record.status] }}</Tag> |
|||
</Tooltip> |
|||
<Tag v-else :color="JobStatusColor[record.status]">{{ JobStatusMap[record.status] }}</Tag> |
|||
</template> |
|||
<template #type="{ record }"> |
|||
<Tag color="blue">{{ JobTypeMap[record.jobType] }}</Tag> |
|||
</template> |
|||
<template #priority="{ record }"> |
|||
<Tag :color="JobPriorityColor[record.priority]">{{ JobPriorityMap[record.priority] }}</Tag> |
|||
</template> |
|||
<template #action="{ record }"> |
|||
<TableAction |
|||
:stop-button-propagation="true" |
|||
:actions="[ |
|||
{ |
|||
auth: 'TaskManagement.BackgroundJobs.Update', |
|||
label: L('Edit'), |
|||
icon: 'ant-design:edit-outlined', |
|||
onClick: handleEdit.bind(null, record), |
|||
}, |
|||
{ |
|||
auth: 'TaskManagement.BackgroundJobs.Delete', |
|||
color: 'error', |
|||
label: L('Delete'), |
|||
icon: 'ant-design:delete-outlined', |
|||
onClick: handleDelete.bind(null, record), |
|||
}, |
|||
]" |
|||
:dropDownActions="[ |
|||
{ |
|||
auth: 'TaskManagement.BackgroundJobs.Pause', |
|||
label: L('BackgroundJobs:Pause'), |
|||
ifShow: [JobStatus.Running, JobStatus.FailedRetry].includes(record.status), |
|||
onClick: handlePause.bind(null, record), |
|||
}, |
|||
{ |
|||
auth: 'TaskManagement.BackgroundJobs.Resume', |
|||
label: L('BackgroundJobs:Resume'), |
|||
ifShow: [JobStatus.Paused, JobStatus.Stopped].includes(record.status), |
|||
onClick: handleResume.bind(null, record), |
|||
}, |
|||
{ |
|||
auth: 'TaskManagement.BackgroundJobs.Trigger', |
|||
label: L('BackgroundJobs:Trigger'), |
|||
ifShow: [JobStatus.Running, JobStatus.Completed, JobStatus.FailedRetry].includes(record.status), |
|||
onClick: handleTrigger.bind(null, record), |
|||
}, |
|||
{ |
|||
auth: 'TaskManagement.BackgroundJobs.Stop', |
|||
label: L('BackgroundJobs:Stop'), |
|||
ifShow: [JobStatus.Running, JobStatus.FailedRetry].includes(record.status), |
|||
onClick: handleStop.bind(null, record), |
|||
}, |
|||
]" |
|||
/> |
|||
</template> |
|||
</BasicTable> |
|||
<BackgroundJobInfoModal @change="handleChange" @register="registerModal" /> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import { Switch, Modal, Tag, Tooltip, message } from 'ant-design-vue'; |
|||
import { useLocalization } from '/@/hooks/abp/useLocalization'; |
|||
import { usePermission } from '/@/hooks/web/usePermission'; |
|||
import { useModal } from '/@/components/Modal'; |
|||
import { BasicTable, TableAction, useTable } from '/@/components/Table'; |
|||
import { formatPagedRequest } from '/@/utils/http/abp/helper'; |
|||
import { getList, deleteById, pause, resume, trigger, stop } from '/@/api/task-management/backgroundJobInfo'; |
|||
import { JobStatus } from '/@/api/task-management/model/backgroundJobInfoModel'; |
|||
import { getDataColumns } from '../datas/TableData'; |
|||
import { getSearchFormSchemas } from '../datas/ModalData'; |
|||
import { JobStatusMap, JobStatusColor, JobTypeMap, JobPriorityMap, JobPriorityColor } from '../datas/typing'; |
|||
import BackgroundJobInfoModal from './BackgroundJobInfoModal.vue'; |
|||
|
|||
const { L } = useLocalization('TaskManagement'); |
|||
const { hasPermission } = usePermission(); |
|||
const [registerModal, { openModal }] = useModal(); |
|||
const [registerTable, { reload }] = useTable({ |
|||
rowKey: 'id', |
|||
title: L('BackgroundJobs'), |
|||
columns: getDataColumns(), |
|||
api: getList, |
|||
beforeFetch: formatPagedRequest, |
|||
pagination: true, |
|||
striped: false, |
|||
useSearchForm: true, |
|||
showTableSetting: true, |
|||
bordered: true, |
|||
showIndexColumn: false, |
|||
canResize: false, |
|||
immediate: true, |
|||
rowSelection: { type: 'radio' }, |
|||
formConfig: getSearchFormSchemas(), |
|||
actionColumn: { |
|||
width: 220, |
|||
title: L('Actions'), |
|||
dataIndex: 'action', |
|||
slots: { customRender: 'action' }, |
|||
}, |
|||
}); |
|||
|
|||
function handleChange() { |
|||
reload(); |
|||
} |
|||
|
|||
function handleAddNew() { |
|||
openModal(true, { id: null }); |
|||
} |
|||
|
|||
function handleEdit(record) { |
|||
openModal(true, record); |
|||
} |
|||
|
|||
function handlePause(record) { |
|||
pause(record.id).then(() => { |
|||
message.success(L('Successful')); |
|||
}); |
|||
} |
|||
|
|||
function handleResume(record) { |
|||
resume(record.id).then(() => { |
|||
message.success(L('Successful')); |
|||
}); |
|||
} |
|||
|
|||
function handleTrigger(record) { |
|||
trigger(record.id).then(() => { |
|||
message.success(L('Successful')); |
|||
}); |
|||
} |
|||
|
|||
function handleStop(record) { |
|||
stop(record.id).then(() => { |
|||
message.success(L('Successful')); |
|||
}); |
|||
} |
|||
|
|||
function handleDelete(record) { |
|||
Modal.warning({ |
|||
title: L('AreYouSure'), |
|||
content: L('ItemWillBeDeletedMessage'), |
|||
okCancel: true, |
|||
onOk: () => { |
|||
deleteById(record.id).then(() => { |
|||
reload(); |
|||
}); |
|||
}, |
|||
}); |
|||
} |
|||
</script> |
|||
@ -0,0 +1,154 @@ |
|||
import { useLocalization } from '/@/hooks/abp/useLocalization'; |
|||
import { FormProps } from '/@/components/Form'; |
|||
import { JobStatus, JobType, JobPriority } from '/@/api/task-management/model/backgroundJobInfoModel'; |
|||
import { JobStatusMap, JobTypeMap, JobPriorityMap } from './typing'; |
|||
|
|||
const { L } = useLocalization('TaskManagement', 'AbpUi'); |
|||
|
|||
export function getSearchFormSchemas(): Partial<FormProps> { |
|||
return { |
|||
labelWidth: 100, |
|||
schemas: [ |
|||
{ |
|||
field: 'group', |
|||
component: 'Input', |
|||
label: L('DisplayName:Group'), |
|||
colProps: { span: 6 }, |
|||
}, |
|||
{ |
|||
field: 'name', |
|||
component: 'Input', |
|||
label: L('DisplayName:Name'), |
|||
colProps: { span: 6 }, |
|||
}, |
|||
{ |
|||
field: 'type', |
|||
component: 'Input', |
|||
label: L('DisplayName:Type'), |
|||
colProps: { span: 12 }, |
|||
}, |
|||
{ |
|||
field: 'status', |
|||
component: 'Select', |
|||
label: L('DisplayName:Status'), |
|||
colProps: { span: 6 }, |
|||
componentProps: { |
|||
options: [ |
|||
{ label: JobStatusMap[JobStatus.None], value: JobStatus.None }, |
|||
{ label: JobStatusMap[JobStatus.Running], value: JobStatus.Running }, |
|||
{ label: JobStatusMap[JobStatus.Completed], value: JobStatus.Completed }, |
|||
{ label: JobStatusMap[JobStatus.Paused], value: JobStatus.Paused }, |
|||
{ label: JobStatusMap[JobStatus.Stopped], value: JobStatus.Stopped }, |
|||
], |
|||
}, |
|||
}, |
|||
{ |
|||
field: 'beginTime', |
|||
component: 'DatePicker', |
|||
label: L('DisplayName:BeginTime'), |
|||
colProps: { span: 9 }, |
|||
componentProps: { |
|||
style: { |
|||
width: '100%', |
|||
}, |
|||
}, |
|||
}, |
|||
{ |
|||
field: 'endTime', |
|||
component: 'DatePicker', |
|||
label: L('DisplayName:EndTime'), |
|||
colProps: { span: 9 }, |
|||
componentProps: { |
|||
style: { |
|||
width: '100%', |
|||
}, |
|||
}, |
|||
}, |
|||
{ |
|||
field: 'jobType', |
|||
component: 'Select', |
|||
label: L('DisplayName:JobType'), |
|||
colProps: { span: 6 }, |
|||
componentProps: { |
|||
options: [ |
|||
{ label: JobTypeMap[JobType.Once], value: JobType.Once }, |
|||
{ label: JobTypeMap[JobType.Period], value: JobType.Period }, |
|||
{ label: JobTypeMap[JobType.Persistent], value: JobType.Persistent }, |
|||
], |
|||
}, |
|||
}, |
|||
{ |
|||
field: 'beginLastRunTime', |
|||
component: 'DatePicker', |
|||
label: L('DisplayName:BeginLastRunTime'), |
|||
colProps: { span: 9 }, |
|||
componentProps: { |
|||
style: { |
|||
width: '100%', |
|||
}, |
|||
}, |
|||
}, |
|||
{ |
|||
field: 'endLastRunTime', |
|||
component: 'DatePicker', |
|||
label: L('DisplayName:EndLastRunTime'), |
|||
colProps: { span: 9 }, |
|||
componentProps: { |
|||
style: { |
|||
width: '100%', |
|||
}, |
|||
}, |
|||
}, |
|||
{ |
|||
field: 'priority', |
|||
component: 'Select', |
|||
label: L('DisplayName:Priority'), |
|||
colProps: { span: 6 }, |
|||
componentProps: { |
|||
options: [ |
|||
{ label: JobPriorityMap[JobPriority.Low], value: JobPriority.Low }, |
|||
{ label: JobPriorityMap[JobPriority.BelowNormal], value: JobPriority.BelowNormal }, |
|||
{ label: JobPriorityMap[JobPriority.Normal], value: JobPriority.Normal }, |
|||
{ label: JobPriorityMap[JobPriority.AboveNormal], value: JobPriority.AboveNormal }, |
|||
{ label: JobPriorityMap[JobPriority.High], value: JobPriority.High }, |
|||
], |
|||
}, |
|||
}, |
|||
{ |
|||
field: 'beginCreationTime', |
|||
component: 'DatePicker', |
|||
label: L('DisplayName:BeginCreationTime'), |
|||
colProps: { span: 9 }, |
|||
componentProps: { |
|||
style: { |
|||
width: '100%', |
|||
}, |
|||
}, |
|||
}, |
|||
{ |
|||
field: 'endCreationTime', |
|||
component: 'DatePicker', |
|||
label: L('DisplayName:EndCreationTime'), |
|||
colProps: { span: 9 }, |
|||
componentProps: { |
|||
style: { |
|||
width: '100%', |
|||
}, |
|||
}, |
|||
}, |
|||
{ |
|||
field: 'isAbandoned', |
|||
component: 'Checkbox', |
|||
label: L('DisplayName:IsAbandoned'), |
|||
colProps: { span: 4 }, |
|||
renderComponentContent: L('DisplayName:IsAbandoned'), |
|||
}, |
|||
{ |
|||
field: 'filter', |
|||
component: 'Input', |
|||
label: L('Search'), |
|||
colProps: { span: 20 }, |
|||
}, |
|||
], |
|||
}; |
|||
} |
|||
@ -0,0 +1,125 @@ |
|||
import { useLocalization } from '/@/hooks/abp/useLocalization'; |
|||
import { BasicColumn } from '/@/components/Table'; |
|||
import { formatToDateTime } from '/@/utils/dateUtil'; |
|||
|
|||
const { L } = useLocalization('TaskManagement'); |
|||
|
|||
export function getDataColumns(): BasicColumn[] { |
|||
return [ |
|||
{ |
|||
title: 'id', |
|||
dataIndex: 'id', |
|||
width: 1, |
|||
ifShow: false, |
|||
}, |
|||
{ |
|||
title: L('DisplayName:Group'), |
|||
dataIndex: 'group', |
|||
align: 'left', |
|||
width: 150, |
|||
sorter: true, |
|||
}, |
|||
{ |
|||
title: L('DisplayName:Name'), |
|||
dataIndex: 'name', |
|||
align: 'left', |
|||
width: 300, |
|||
sorter: true, |
|||
}, |
|||
{ |
|||
title: L('DisplayName:Type'), |
|||
dataIndex: 'type', |
|||
align: 'left', |
|||
width: 350, |
|||
sorter: true, |
|||
}, |
|||
{ |
|||
title: L('DisplayName:CreationTime'), |
|||
dataIndex: 'creationTime', |
|||
align: 'left', |
|||
width: 150, |
|||
sorter: true, |
|||
format: (text) => { |
|||
return formatToDateTime(text); |
|||
}, |
|||
}, |
|||
{ |
|||
title: L('DisplayName:Status'), |
|||
dataIndex: 'status', |
|||
align: 'left', |
|||
width: 100, |
|||
sorter: true, |
|||
slots: { |
|||
customRender: 'status', |
|||
} |
|||
}, |
|||
{ |
|||
title: L('DisplayName:Result'), |
|||
dataIndex: 'result', |
|||
align: 'left', |
|||
width: 200, |
|||
sorter: true, |
|||
}, |
|||
{ |
|||
title: L('DisplayName:LastRunTime'), |
|||
dataIndex: 'lastRunTime', |
|||
align: 'left', |
|||
width: 150, |
|||
sorter: true, |
|||
format: (text) => { |
|||
return text ? formatToDateTime(text) : ''; |
|||
}, |
|||
}, |
|||
{ |
|||
title: L('DisplayName:NextRunTime'), |
|||
dataIndex: 'nextRunTime', |
|||
align: 'left', |
|||
width: 150, |
|||
sorter: true, |
|||
format: (text) => { |
|||
return text ? formatToDateTime(text) : ''; |
|||
}, |
|||
}, |
|||
{ |
|||
title: L('DisplayName:JobType'), |
|||
dataIndex: 'jobType', |
|||
align: 'left', |
|||
width: 150, |
|||
sorter: true, |
|||
slots: { |
|||
customRender: 'type', |
|||
} |
|||
}, |
|||
{ |
|||
title: L('DisplayName:Priority'), |
|||
dataIndex: 'priority', |
|||
align: 'left', |
|||
width: 150, |
|||
sorter: true, |
|||
slots: { |
|||
customRender: 'priority', |
|||
} |
|||
}, |
|||
{ |
|||
title: L('DisplayName:Cron'), |
|||
dataIndex: 'cron', |
|||
align: 'left', |
|||
width: 150, |
|||
sorter: true, |
|||
}, |
|||
{ |
|||
title: L('DisplayName:TriggerCount'), |
|||
dataIndex: 'triggerCount', |
|||
align: 'left', |
|||
width: 100, |
|||
sorter: true, |
|||
}, |
|||
{ |
|||
title: L('DisplayName:TryCount'), |
|||
dataIndex: 'tryCount', |
|||
align: 'left', |
|||
width: 100, |
|||
sorter: true, |
|||
}, |
|||
]; |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
import { JobStatus, JobType, JobPriority } from '/@/api/task-management/model/backgroundJobInfoModel'; |
|||
import { useLocalization } from '/@/hooks/abp/useLocalization'; |
|||
|
|||
const { L } = useLocalization('TaskManagement'); |
|||
|
|||
export const JobStatusMap = { |
|||
[JobStatus.None]: L('DisplayName:None'), |
|||
[JobStatus.Completed]: L('DisplayName:Completed'), |
|||
[JobStatus.Running]: L('DisplayName:Running'), |
|||
[JobStatus.FailedRetry]: L('DisplayName:FailedRetry'), |
|||
[JobStatus.Paused]: L('DisplayName:Paused'), |
|||
[JobStatus.Stopped]: L('DisplayName:Stopped'), |
|||
} |
|||
export const JobStatusColor = { |
|||
[JobStatus.None]: '', |
|||
[JobStatus.Completed]: '#339933', |
|||
[JobStatus.Running]: '#3399CC', |
|||
[JobStatus.FailedRetry]: '#FF6600', |
|||
[JobStatus.Paused]: '#CC6633', |
|||
[JobStatus.Stopped]: '#F00000', |
|||
} |
|||
|
|||
export const JobTypeMap = { |
|||
[JobType.Once]: L('DisplayName:Once'), |
|||
[JobType.Period]: L('DisplayName:Period'), |
|||
[JobType.Persistent]: L('DisplayName:Persistent'), |
|||
} |
|||
|
|||
export const JobPriorityMap = { |
|||
[JobPriority.Low]: L('DisplayName:Low'), |
|||
[JobPriority.BelowNormal]: L('DisplayName:BelowNormal'), |
|||
[JobPriority.Normal]: L('DisplayName:Normal'), |
|||
[JobPriority.AboveNormal]: L('DisplayName:AboveNormal'), |
|||
[JobPriority.High]: L('DisplayName:High'), |
|||
} |
|||
|
|||
export const JobPriorityColor = { |
|||
[JobPriority.Low]: 'purple', |
|||
[JobPriority.BelowNormal]: 'cyan', |
|||
[JobPriority.Normal]: 'blue', |
|||
[JobPriority.AboveNormal]: 'orange', |
|||
[JobPriority.High]: 'red', |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
<template> |
|||
<BackgroundJobInfoTable /> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue'; |
|||
|
|||
import BackgroundJobInfoTable from './components/BackgroundJobInfoTable.vue'; |
|||
export default defineComponent({ |
|||
components: { |
|||
BackgroundJobInfoTable, |
|||
}, |
|||
setup() {}, |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,111 @@ |
|||
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00 |
|||
# Visual Studio Version 17 |
|||
VisualStudioVersion = 17.0.31903.59 |
|||
MinimumVisualStudioVersion = 10.0.40219.1 |
|||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "host", "host", "{8DA8A2EE-0B26-487E-A6C4-518906E92B1B}" |
|||
EndProject |
|||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C38EB7EF-BAE9-4129-862A-71C652B81775}" |
|||
EndProject |
|||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{77341F31-F54C-436A-AF8D-F78D91303C45}" |
|||
EndProject |
|||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.TaskManagement.Domain.Shared", "modules\task-management\LINGYUN.Abp.TaskManagement.Domain.Shared\LINGYUN.Abp.TaskManagement.Domain.Shared.csproj", "{691CD138-9FFA-4988-BAC4-A32F0DAE1090}" |
|||
EndProject |
|||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.TaskManagement.Domain", "modules\task-management\LINGYUN.Abp.TaskManagement.Domain\LINGYUN.Abp.TaskManagement.Domain.csproj", "{8873A651-4F83-43B3-A34E-7AAA8A3ED4CF}" |
|||
EndProject |
|||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.TaskManagement.Application.Contracts", "modules\task-management\LINGYUN.Abp.TaskManagement.Application.Contracts\LINGYUN.Abp.TaskManagement.Application.Contracts.csproj", "{D5EC4CA0-7E16-4D17-B08E-E162EF332C77}" |
|||
EndProject |
|||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.TaskManagement.Application", "modules\task-management\LINGYUN.Abp.TaskManagement.Application\LINGYUN.Abp.TaskManagement.Application.csproj", "{09039DDF-5B0E-4670-8055-CC0BE82D4D2C}" |
|||
EndProject |
|||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.TaskManagement.HttpApi", "modules\task-management\LINGYUN.Abp.TaskManagement.HttpApi\LINGYUN.Abp.TaskManagement.HttpApi.csproj", "{283B0039-F67C-41F7-B554-DF5EE9178C4A}" |
|||
EndProject |
|||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.TaskManagement.EntityFrameworkCore", "modules\task-management\LINGYUN.Abp.TaskManagement.EntityFrameworkCore\LINGYUN.Abp.TaskManagement.EntityFrameworkCore.csproj", "{B8AB5E9B-9711-470E-8072-7444579EC5F6}" |
|||
EndProject |
|||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C99728F6-FB3C-4D26-8917-1D30725209B9}" |
|||
ProjectSection(SolutionItems) = preProject |
|||
.editorconfig = .editorconfig |
|||
EndProjectSection |
|||
EndProject |
|||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "provider", "provider", "{385578CC-C0F1-4377-A7A2-682B8F416234}" |
|||
EndProject |
|||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.BackgroundTasks", "modules\task-management\LINGYUN.Abp.BackgroundTasks\LINGYUN.Abp.BackgroundTasks.csproj", "{AC0B4342-9C9B-41E6-9646-E505C763EE77}" |
|||
EndProject |
|||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.BackgroundTasks.Quartz", "modules\task-management\LINGYUN.Abp.BackgroundTasks.Quartz\LINGYUN.Abp.BackgroundTasks.Quartz.csproj", "{7051C251-11D0-4971-B13E-F6929AE6DE89}" |
|||
EndProject |
|||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LY.MicroService.TaskManagement.HttpApi.Host", "services\LY.MicroService.TaskManagement.HttpApi.Host\LY.MicroService.TaskManagement.HttpApi.Host.csproj", "{E8022994-A19F-4540-B9D1-7EF4AA85D18A}" |
|||
EndProject |
|||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.BackgroundTasks.Abstractions", "modules\task-management\LINGYUN.Abp.BackgroundTasks.Abstractions\LINGYUN.Abp.BackgroundTasks.Abstractions.csproj", "{4A049C32-55F2-4A5F-954A-C8A977C2D87F}" |
|||
EndProject |
|||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.BackgroundTasks.Jobs", "modules\task-management\LINGYUN.Abp.BackgroundTasks.Jobs\LINGYUN.Abp.BackgroundTasks.Jobs.csproj", "{4C59F590-AA6C-4C46-8060-DA65D8305980}" |
|||
EndProject |
|||
Global |
|||
GlobalSection(SolutionConfigurationPlatforms) = preSolution |
|||
Debug|Any CPU = Debug|Any CPU |
|||
Release|Any CPU = Release|Any CPU |
|||
EndGlobalSection |
|||
GlobalSection(ProjectConfigurationPlatforms) = postSolution |
|||
{691CD138-9FFA-4988-BAC4-A32F0DAE1090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
|||
{691CD138-9FFA-4988-BAC4-A32F0DAE1090}.Debug|Any CPU.Build.0 = Debug|Any CPU |
|||
{691CD138-9FFA-4988-BAC4-A32F0DAE1090}.Release|Any CPU.ActiveCfg = Release|Any CPU |
|||
{691CD138-9FFA-4988-BAC4-A32F0DAE1090}.Release|Any CPU.Build.0 = Release|Any CPU |
|||
{8873A651-4F83-43B3-A34E-7AAA8A3ED4CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
|||
{8873A651-4F83-43B3-A34E-7AAA8A3ED4CF}.Debug|Any CPU.Build.0 = Debug|Any CPU |
|||
{8873A651-4F83-43B3-A34E-7AAA8A3ED4CF}.Release|Any CPU.ActiveCfg = Release|Any CPU |
|||
{8873A651-4F83-43B3-A34E-7AAA8A3ED4CF}.Release|Any CPU.Build.0 = Release|Any CPU |
|||
{D5EC4CA0-7E16-4D17-B08E-E162EF332C77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
|||
{D5EC4CA0-7E16-4D17-B08E-E162EF332C77}.Debug|Any CPU.Build.0 = Debug|Any CPU |
|||
{D5EC4CA0-7E16-4D17-B08E-E162EF332C77}.Release|Any CPU.ActiveCfg = Release|Any CPU |
|||
{D5EC4CA0-7E16-4D17-B08E-E162EF332C77}.Release|Any CPU.Build.0 = Release|Any CPU |
|||
{09039DDF-5B0E-4670-8055-CC0BE82D4D2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
|||
{09039DDF-5B0E-4670-8055-CC0BE82D4D2C}.Debug|Any CPU.Build.0 = Debug|Any CPU |
|||
{09039DDF-5B0E-4670-8055-CC0BE82D4D2C}.Release|Any CPU.ActiveCfg = Release|Any CPU |
|||
{09039DDF-5B0E-4670-8055-CC0BE82D4D2C}.Release|Any CPU.Build.0 = Release|Any CPU |
|||
{283B0039-F67C-41F7-B554-DF5EE9178C4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
|||
{283B0039-F67C-41F7-B554-DF5EE9178C4A}.Debug|Any CPU.Build.0 = Debug|Any CPU |
|||
{283B0039-F67C-41F7-B554-DF5EE9178C4A}.Release|Any CPU.ActiveCfg = Release|Any CPU |
|||
{283B0039-F67C-41F7-B554-DF5EE9178C4A}.Release|Any CPU.Build.0 = Release|Any CPU |
|||
{B8AB5E9B-9711-470E-8072-7444579EC5F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
|||
{B8AB5E9B-9711-470E-8072-7444579EC5F6}.Debug|Any CPU.Build.0 = Debug|Any CPU |
|||
{B8AB5E9B-9711-470E-8072-7444579EC5F6}.Release|Any CPU.ActiveCfg = Release|Any CPU |
|||
{B8AB5E9B-9711-470E-8072-7444579EC5F6}.Release|Any CPU.Build.0 = Release|Any CPU |
|||
{AC0B4342-9C9B-41E6-9646-E505C763EE77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
|||
{AC0B4342-9C9B-41E6-9646-E505C763EE77}.Debug|Any CPU.Build.0 = Debug|Any CPU |
|||
{AC0B4342-9C9B-41E6-9646-E505C763EE77}.Release|Any CPU.ActiveCfg = Release|Any CPU |
|||
{AC0B4342-9C9B-41E6-9646-E505C763EE77}.Release|Any CPU.Build.0 = Release|Any CPU |
|||
{7051C251-11D0-4971-B13E-F6929AE6DE89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
|||
{7051C251-11D0-4971-B13E-F6929AE6DE89}.Debug|Any CPU.Build.0 = Debug|Any CPU |
|||
{7051C251-11D0-4971-B13E-F6929AE6DE89}.Release|Any CPU.ActiveCfg = Release|Any CPU |
|||
{7051C251-11D0-4971-B13E-F6929AE6DE89}.Release|Any CPU.Build.0 = Release|Any CPU |
|||
{E8022994-A19F-4540-B9D1-7EF4AA85D18A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
|||
{E8022994-A19F-4540-B9D1-7EF4AA85D18A}.Debug|Any CPU.Build.0 = Debug|Any CPU |
|||
{E8022994-A19F-4540-B9D1-7EF4AA85D18A}.Release|Any CPU.ActiveCfg = Release|Any CPU |
|||
{E8022994-A19F-4540-B9D1-7EF4AA85D18A}.Release|Any CPU.Build.0 = Release|Any CPU |
|||
{4A049C32-55F2-4A5F-954A-C8A977C2D87F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
|||
{4A049C32-55F2-4A5F-954A-C8A977C2D87F}.Debug|Any CPU.Build.0 = Debug|Any CPU |
|||
{4A049C32-55F2-4A5F-954A-C8A977C2D87F}.Release|Any CPU.ActiveCfg = Release|Any CPU |
|||
{4A049C32-55F2-4A5F-954A-C8A977C2D87F}.Release|Any CPU.Build.0 = Release|Any CPU |
|||
{4C59F590-AA6C-4C46-8060-DA65D8305980}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
|||
{4C59F590-AA6C-4C46-8060-DA65D8305980}.Debug|Any CPU.Build.0 = Debug|Any CPU |
|||
{4C59F590-AA6C-4C46-8060-DA65D8305980}.Release|Any CPU.ActiveCfg = Release|Any CPU |
|||
{4C59F590-AA6C-4C46-8060-DA65D8305980}.Release|Any CPU.Build.0 = Release|Any CPU |
|||
EndGlobalSection |
|||
GlobalSection(SolutionProperties) = preSolution |
|||
HideSolutionNode = FALSE |
|||
EndGlobalSection |
|||
GlobalSection(NestedProjects) = preSolution |
|||
{691CD138-9FFA-4988-BAC4-A32F0DAE1090} = {C38EB7EF-BAE9-4129-862A-71C652B81775} |
|||
{8873A651-4F83-43B3-A34E-7AAA8A3ED4CF} = {C38EB7EF-BAE9-4129-862A-71C652B81775} |
|||
{D5EC4CA0-7E16-4D17-B08E-E162EF332C77} = {C38EB7EF-BAE9-4129-862A-71C652B81775} |
|||
{09039DDF-5B0E-4670-8055-CC0BE82D4D2C} = {C38EB7EF-BAE9-4129-862A-71C652B81775} |
|||
{283B0039-F67C-41F7-B554-DF5EE9178C4A} = {C38EB7EF-BAE9-4129-862A-71C652B81775} |
|||
{B8AB5E9B-9711-470E-8072-7444579EC5F6} = {C38EB7EF-BAE9-4129-862A-71C652B81775} |
|||
{AC0B4342-9C9B-41E6-9646-E505C763EE77} = {C38EB7EF-BAE9-4129-862A-71C652B81775} |
|||
{7051C251-11D0-4971-B13E-F6929AE6DE89} = {385578CC-C0F1-4377-A7A2-682B8F416234} |
|||
{E8022994-A19F-4540-B9D1-7EF4AA85D18A} = {8DA8A2EE-0B26-487E-A6C4-518906E92B1B} |
|||
{4A049C32-55F2-4A5F-954A-C8A977C2D87F} = {C38EB7EF-BAE9-4129-862A-71C652B81775} |
|||
{4C59F590-AA6C-4C46-8060-DA65D8305980} = {C38EB7EF-BAE9-4129-862A-71C652B81775} |
|||
EndGlobalSection |
|||
GlobalSection(ExtensibilityGlobals) = postSolution |
|||
SolutionGuid = {E1FD1F4C-D344-408B-97CF-B6F1F6D7D293} |
|||
EndGlobalSection |
|||
EndGlobal |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,15 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Core" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,42 @@ |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public class AbpBackgroundTaskConcurrentException : AbpJobExecutionException |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new <see cref="AbpBackgroundTaskConcurrentException"/> object.
|
|||
/// </summary>
|
|||
/// <param name="innerException">Inner exception</param>
|
|||
public AbpBackgroundTaskConcurrentException(Type jobType) |
|||
: base( |
|||
jobType, |
|||
$"This job {jobType.Name} cannot be performed because it has been locked by another performer", |
|||
null) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="AbpBackgroundTaskConcurrentException"/> object.
|
|||
/// </summary>
|
|||
/// <param name="jobType">Execute job type</param>
|
|||
/// <param name="innerException">Inner exception</param>
|
|||
public AbpBackgroundTaskConcurrentException(Type jobType, Exception innerException) |
|||
: base( |
|||
jobType, |
|||
$"This job {jobType.Name} cannot be performed because it has been locked by another performer", |
|||
innerException) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="AbpBackgroundTaskConcurrentException"/> object.
|
|||
/// </summary>
|
|||
/// <param name="jobType">Execute job type</param>
|
|||
/// <param name="message">Exception message</param>
|
|||
/// <param name="innerException">Inner exception</param>
|
|||
public AbpBackgroundTaskConcurrentException(Type jobType, string message, Exception innerException) |
|||
: base(jobType, message, innerException) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Reflection; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public class AbpBackgroundTasksAbstractionsModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
AutoAddJobMonitors(context.Services); |
|||
} |
|||
|
|||
private static void AutoAddJobMonitors(IServiceCollection services) |
|||
{ |
|||
var jobMonitors = new List<Type>(); |
|||
|
|||
services.OnRegistred(context => |
|||
{ |
|||
if (ReflectionHelper.IsAssignableToGenericType(context.ImplementationType, typeof(JobEventBase<>))) |
|||
{ |
|||
jobMonitors.Add(context.ImplementationType); |
|||
} |
|||
}); |
|||
|
|||
services.Configure<AbpBackgroundTasksOptions>(options => |
|||
{ |
|||
options.JobMonitors.AddIfNotContains(jobMonitors); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,84 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Collections; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public class AbpBackgroundTasksOptions |
|||
{ |
|||
/// <summary>
|
|||
/// 任务监听类型列表
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 用户可以实现事件监听实现自定义逻辑
|
|||
/// </remarks>
|
|||
public ITypeList<IJobEvent> JobMonitors { get; } |
|||
/// <summary>
|
|||
/// 作业提供者列表
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 用户实现的作业可以添加在集合中
|
|||
/// </remarks>
|
|||
public IDictionary<string, Type> JobProviders { get; } |
|||
/// <summary>
|
|||
/// 任务过期时间
|
|||
/// 默认: 15 days
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 任务过期时间,超出时间段清理
|
|||
/// </remarks>
|
|||
public TimeSpan JobExpiratime { get; set; } |
|||
/// <summary>
|
|||
/// 每次清理任务批次大小
|
|||
/// 默认: 1000
|
|||
/// </summary>
|
|||
public int MaxJobCleanCount { get; set; } |
|||
/// <summary>
|
|||
/// 清理过期任务批次Cron表达式
|
|||
/// 默认: 600秒(0 0/10 * * * ? * )
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Cron表达式
|
|||
/// </remarks>
|
|||
public string JobCleanCronExpression { get; set; } |
|||
/// <summary>
|
|||
/// 每次轮询任务批次大小
|
|||
/// 默认: 1000
|
|||
/// </summary>
|
|||
public int MaxJobFetchCount { get; set; } |
|||
/// <summary>
|
|||
/// 轮询任务批次Cron表达式
|
|||
/// 默认: 30秒(0/30 * * * * ? )
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Cron表达式
|
|||
/// </remarks>
|
|||
public string JobFetchCronExpression { get; set; } |
|||
/// <summary>
|
|||
/// 轮询任务批次时锁定任务超时时长(秒)
|
|||
/// 默认:120
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 轮询任务也属于一个后台任务, 需要对每一次轮询加锁,防止重复任务入库
|
|||
/// </remarks>
|
|||
public int JobFetchLockTimeOut { get; set; } |
|||
public AbpBackgroundTasksOptions() |
|||
{ |
|||
MaxJobFetchCount = 1000; |
|||
JobFetchLockTimeOut = 120; |
|||
JobFetchCronExpression = "0/30 * * * * ? "; |
|||
|
|||
MaxJobCleanCount = 1000; |
|||
JobExpiratime = TimeSpan.FromDays(15d); |
|||
JobCleanCronExpression = "0 0/10 * * * ? *"; |
|||
|
|||
JobMonitors = new TypeList<IJobEvent>(); |
|||
JobProviders = new Dictionary<string, Type>(); |
|||
} |
|||
|
|||
public void AddProvider<TJobRunnable>(string name) |
|||
where TJobRunnable : IJobRunnable |
|||
{ |
|||
JobProviders[name] = typeof(TJobRunnable); |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
using System; |
|||
using Volo.Abp; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public class AbpJobExecutionException : AbpException |
|||
{ |
|||
public Type JobType { get; } |
|||
/// <summary>
|
|||
/// Creates a new <see cref="AbpJobExecutionException"/> object.
|
|||
/// </summary>
|
|||
/// <param name="innerException">Inner exception</param>
|
|||
public AbpJobExecutionException(Type jobType) |
|||
: this( |
|||
jobType, |
|||
$"Unable to execute job {jobType.Name}.", |
|||
null) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="AbpJobExecutionException"/> object.
|
|||
/// </summary>
|
|||
/// <param name="jobType">Execute job type</param>
|
|||
/// <param name="innerException">Inner exception</param>
|
|||
public AbpJobExecutionException(Type jobType, Exception innerException) |
|||
: this( |
|||
jobType, |
|||
$"Unable to execute job {jobType.Name} because it: {innerException.Message}", |
|||
innerException) |
|||
{ |
|||
} |
|||
/// <summary>
|
|||
/// Creates a new <see cref="AbpJobExecutionException"/> object.
|
|||
/// </summary>
|
|||
/// <param name="jobType">Execute job type</param>
|
|||
/// <param name="message">Exception message</param>
|
|||
/// <param name="innerException">Inner exception</param>
|
|||
public AbpJobExecutionException(Type jobType, string message, Exception innerException) |
|||
: base(message, innerException) |
|||
{ |
|||
JobType = jobType; |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
/// <summary>
|
|||
/// 挂载任务事件接口
|
|||
/// </summary>
|
|||
public interface IJobEvent |
|||
{ |
|||
/// <summary>
|
|||
/// 任务启动前事件
|
|||
/// </summary>
|
|||
/// <param name="jobEventData"></param>
|
|||
/// <returns></returns>
|
|||
Task OnJobBeforeExecuted(JobEventContext context); |
|||
/// <summary>
|
|||
/// 任务完成后事件
|
|||
/// </summary>
|
|||
/// <param name="jobEventData"></param>
|
|||
/// <returns></returns>
|
|||
Task OnJobAfterExecuted(JobEventContext context); |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
/// <summary>
|
|||
/// 任务类需要实现此接口
|
|||
/// </summary>
|
|||
public interface IJobRunnable |
|||
{ |
|||
Task ExecuteAsync(JobRunnableContext context); |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public abstract class JobEventBase<TEvent> : IJobEvent |
|||
{ |
|||
public ILogger<TEvent> Logger { protected get; set; } |
|||
protected JobEventBase() |
|||
{ |
|||
Logger = NullLogger<TEvent>.Instance; |
|||
} |
|||
|
|||
public async Task OnJobAfterExecuted(JobEventContext context) |
|||
{ |
|||
try |
|||
{ |
|||
await OnJobAfterExecutedAsync(context); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Logger.LogError("Failed to execute event, error:" + GetSourceException(ex).Message); |
|||
} |
|||
} |
|||
|
|||
public async Task OnJobBeforeExecuted(JobEventContext context) |
|||
{ |
|||
try |
|||
{ |
|||
await OnJobBeforeExecutedAsync(context); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Logger.LogError("Failed to execute preprocessing event, error:" + GetSourceException(ex).Message); |
|||
} |
|||
} |
|||
|
|||
protected virtual Task OnJobAfterExecutedAsync(JobEventContext context) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
protected virtual Task OnJobBeforeExecutedAsync(JobEventContext context) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
protected virtual Exception GetSourceException(Exception exception) |
|||
{ |
|||
if (exception.InnerException != null) |
|||
{ |
|||
return GetSourceException(exception.InnerException); |
|||
} |
|||
return exception; |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public class JobEventContext |
|||
{ |
|||
public IServiceProvider ServiceProvider { get; } |
|||
public JobEventData EventData { get; } |
|||
|
|||
public JobEventContext( |
|||
IServiceProvider serviceProvider, |
|||
JobEventData jobEventData) |
|||
{ |
|||
ServiceProvider = serviceProvider; |
|||
EventData = jobEventData; |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public class JobEventData |
|||
{ |
|||
/// <summary>
|
|||
/// 任务类别
|
|||
/// </summary>
|
|||
public Type Type { get; } |
|||
/// <summary>
|
|||
/// 任务组别
|
|||
/// </summary>
|
|||
public string Group { get; } |
|||
/// <summary>
|
|||
/// 任务名称
|
|||
/// </summary>
|
|||
public string Name { get; } |
|||
/// <summary>
|
|||
/// 任务标识
|
|||
/// </summary>
|
|||
public Guid Key { get; } |
|||
/// <summary>
|
|||
/// 任务状态
|
|||
/// </summary>
|
|||
public JobStatus Status { get; set; } |
|||
/// <summary>
|
|||
/// 执行者租户
|
|||
/// </summary>
|
|||
public Guid? TenantId { get; set; } |
|||
/// <summary>
|
|||
/// 错误明细
|
|||
/// </summary>
|
|||
public Exception Exception { get; } |
|||
/// <summary>
|
|||
/// 任务描述
|
|||
/// </summary>
|
|||
public string Description { get; set; } |
|||
/// <summary>
|
|||
/// 返回参数
|
|||
/// </summary>
|
|||
public string Result { get; set; } |
|||
/// <summary>
|
|||
/// 触发次数
|
|||
/// </summary>
|
|||
public int Triggered { get; set; } |
|||
/// <summary>
|
|||
/// 最大可执行次数
|
|||
/// </summary>
|
|||
public int RepeatCount { get; set; } |
|||
/// <summary>
|
|||
/// 失败重试上限
|
|||
/// 默认:50
|
|||
/// </summary>
|
|||
public int TryCount { get; set; } |
|||
/// <summary>
|
|||
/// 最大执行次数
|
|||
/// 默认:0, 无限制
|
|||
/// </summary>
|
|||
public int MaxCount { get; set; } |
|||
/// <summary>
|
|||
/// 运行时间
|
|||
/// </summary>
|
|||
public DateTime RunTime { get; set; } |
|||
/// <summary>
|
|||
/// 上次运行时间
|
|||
/// </summary>
|
|||
public DateTime? LastRunTime { get; set; } |
|||
/// <summary>
|
|||
/// 下次运行时间
|
|||
/// </summary>
|
|||
public DateTime? NextRunTime { get; set; } |
|||
/// <summary>
|
|||
/// 连续失败且不会再次执行
|
|||
/// </summary>
|
|||
public bool IsAbandoned { get; set; } |
|||
public JobEventData( |
|||
Guid key, |
|||
Type type, |
|||
string group, |
|||
string name, |
|||
Exception exception = null) |
|||
{ |
|||
Key = key; |
|||
Type = type; |
|||
Group = group; |
|||
Name = name; |
|||
Exception = exception; |
|||
} |
|||
} |
|||
@ -0,0 +1,104 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public class JobInfo |
|||
{ |
|||
/// <summary>
|
|||
/// 任务标识
|
|||
/// </summary>
|
|||
public Guid Id { get; set; } |
|||
/// <summary>
|
|||
/// 任务名称
|
|||
/// </summary>
|
|||
public string Name { get; set; } |
|||
/// <summary>
|
|||
/// 任务分组
|
|||
/// </summary>
|
|||
public string Group { get; set; } |
|||
/// <summary>
|
|||
/// 任务类型
|
|||
/// </summary>
|
|||
public string Type { get; set; } |
|||
/// <summary>
|
|||
/// 返回参数
|
|||
/// </summary>
|
|||
public string Result { get; set; } |
|||
/// <summary>
|
|||
/// 任务参数
|
|||
/// </summary>
|
|||
public IDictionary<string, object> Args { get; set; } |
|||
/// <summary>
|
|||
/// 任务状态
|
|||
/// </summary>
|
|||
public JobStatus Status { get; set; } = JobStatus.None; |
|||
/// <summary>
|
|||
/// 描述
|
|||
/// </summary>
|
|||
public string Description { get; set; } |
|||
/// <summary>
|
|||
/// 创建时间
|
|||
/// </summary>
|
|||
public DateTime CreationTime { get; set; } |
|||
/// <summary>
|
|||
/// 开始时间
|
|||
/// </summary>
|
|||
public DateTime BeginTime { get; set; } |
|||
/// <summary>
|
|||
/// 结束时间
|
|||
/// </summary>
|
|||
public DateTime? EndTime { get; set; } |
|||
/// <summary>
|
|||
/// 上次运行时间
|
|||
/// </summary>
|
|||
public DateTime? LastRunTime { get; set; } |
|||
/// <summary>
|
|||
/// 下一次执行时间
|
|||
/// </summary>
|
|||
public DateTime? NextRunTime { get; set; } |
|||
/// <summary>
|
|||
/// 任务类别
|
|||
/// </summary>
|
|||
public JobType JobType { get; set; } = JobType.Once; |
|||
/// <summary>
|
|||
/// Cron表达式,如果是周期性任务需要指定
|
|||
/// </summary>
|
|||
public string Cron { get; set; } |
|||
/// <summary>
|
|||
/// 触发次数
|
|||
/// </summary>
|
|||
public int TriggerCount { get; set; } |
|||
/// <summary>
|
|||
/// 失败重试次数
|
|||
/// </summary>
|
|||
public int TryCount { get; set; } |
|||
/// <summary>
|
|||
/// 失败重试上限
|
|||
/// 默认:50
|
|||
/// </summary>
|
|||
public int MaxTryCount { get; set; } = 50; |
|||
/// <summary>
|
|||
/// 最大执行次数
|
|||
/// 默认:0, 无限制
|
|||
/// </summary>
|
|||
public int MaxCount { get; set; } |
|||
/// <summary>
|
|||
/// 连续失败且不会再次执行
|
|||
/// </summary>
|
|||
public bool IsAbandoned { get; set; } |
|||
/// <summary>
|
|||
/// 间隔时间,单位秒,与Cron表达式冲突
|
|||
/// 默认: 300
|
|||
/// </summary>
|
|||
public int Interval { get; set; } = 300; |
|||
/// <summary>
|
|||
/// 任务优先级
|
|||
/// </summary>
|
|||
public JobPriority Priority { get; set; } = JobPriority.Normal; |
|||
/// <summary>
|
|||
/// 任务独占超时时长(秒)
|
|||
/// 0或更小不生效
|
|||
/// </summary>
|
|||
public int LockTimeOut { get; set; } |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
/// <summary>
|
|||
/// 任务优先级
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 与框架保持一致
|
|||
/// </remarks>
|
|||
public enum JobPriority |
|||
{ |
|||
Low = 5, |
|||
BelowNormal = 10, |
|||
Normal = 0xF, |
|||
AboveNormal = 20, |
|||
High = 25 |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public class JobRunnableContext |
|||
{ |
|||
public Type JobType { get; } |
|||
public IServiceProvider ServiceProvider { get; } |
|||
public IReadOnlyDictionary<string, object> JobData { get; } |
|||
public object Result { get; private set; } |
|||
public JobRunnableContext( |
|||
Type jobType, |
|||
IServiceProvider serviceProvider, |
|||
IReadOnlyDictionary<string, object> jobData) |
|||
{ |
|||
JobType = jobType; |
|||
ServiceProvider = serviceProvider; |
|||
JobData = jobData; |
|||
} |
|||
|
|||
public void SetResult(object result) |
|||
{ |
|||
Result = result; |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public static class JobRunnableContextExtensions |
|||
{ |
|||
public static T GetService<T>(this JobRunnableContext context) |
|||
{ |
|||
return context.ServiceProvider.GetService<T>(); |
|||
} |
|||
|
|||
public static object GetService(this JobRunnableContext context, Type serviceType) |
|||
{ |
|||
return context.ServiceProvider.GetService(serviceType); |
|||
} |
|||
|
|||
public static T GetRequiredService<T>(this JobRunnableContext context) |
|||
{ |
|||
return context.ServiceProvider.GetRequiredService<T>(); |
|||
} |
|||
|
|||
public static object GetRequiredService(this JobRunnableContext context, Type serviceType) |
|||
{ |
|||
return context.ServiceProvider.GetRequiredService(serviceType); |
|||
} |
|||
|
|||
public static string GetString(this JobRunnableContext context, string key) |
|||
{ |
|||
return context.GetJobData(key).ToString(); |
|||
} |
|||
|
|||
public static bool TryGetString(this JobRunnableContext context, string key, out string value) |
|||
{ |
|||
if (context.TryGetJobData(key, out var data) && data != null) |
|||
{ |
|||
value = data.ToString(); |
|||
return true; |
|||
} |
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
public static T GetJobData<T>(this JobRunnableContext context, string key) where T : struct |
|||
{ |
|||
var value = context.GetJobData(key); |
|||
|
|||
return value.To<T>(); |
|||
} |
|||
|
|||
public static bool TryGetJobData<T>(this JobRunnableContext context, string key, out T value) where T : struct |
|||
{ |
|||
if (context.TryGetJobData(key, out var data) && data != null) |
|||
{ |
|||
try |
|||
{ |
|||
value = data.To<T>(); |
|||
return true; |
|||
} |
|||
catch |
|||
{ |
|||
} |
|||
} |
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
public static object GetJobData(this JobRunnableContext context, string key) |
|||
{ |
|||
if (context.TryGetJobData(key, out var value) && value != null) |
|||
{ |
|||
return value; |
|||
} |
|||
throw new ArgumentException(key + " not specified."); |
|||
} |
|||
|
|||
public static bool TryGetJobData(this JobRunnableContext context, string key, out object value) |
|||
{ |
|||
if (context.JobData.TryGetValue(key, out value)) |
|||
{ |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public enum JobStatus |
|||
{ |
|||
/// <summary>
|
|||
/// 未知
|
|||
/// </summary>
|
|||
None = -1, |
|||
/// <summary>
|
|||
/// 已完成
|
|||
/// </summary>
|
|||
Completed = 0, |
|||
/// <summary>
|
|||
/// 运行中
|
|||
/// </summary>
|
|||
Running = 10, |
|||
/// <summary>
|
|||
/// 失败重试
|
|||
/// </summary>
|
|||
FailedRetry = 15, |
|||
/// <summary>
|
|||
/// 已暂停
|
|||
/// </summary>
|
|||
Paused = 20, |
|||
/// <summary>
|
|||
/// 已停止
|
|||
/// </summary>
|
|||
Stopped = 30 |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
/// <summary>
|
|||
/// 任务类别
|
|||
/// </summary>
|
|||
public enum JobType |
|||
{ |
|||
/// <summary>
|
|||
/// 一次性
|
|||
/// </summary>
|
|||
Once, |
|||
/// <summary>
|
|||
/// 周期性
|
|||
/// </summary>
|
|||
Period, |
|||
/// <summary>
|
|||
/// 持续性
|
|||
/// </summary>
|
|||
Persistent |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,22 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Emailing" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Volo.Abp.Sms" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Volo.Abp.Http.Client" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.BackgroundTasks.Abstractions\LINGYUN.Abp.BackgroundTasks.Abstractions.csproj" /> |
|||
<ProjectReference Include="..\..\dapr\LINGYUN.Abp.Dapr.Client\LINGYUN.Abp.Dapr.Client.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,27 @@ |
|||
using LINGYUN.Abp.Dapr.Client; |
|||
using Volo.Abp.Emailing; |
|||
using Volo.Abp.Http.Client; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Sms; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
[DependsOn(typeof(AbpEmailingModule))] |
|||
[DependsOn(typeof(AbpSmsModule))] |
|||
[DependsOn(typeof(AbpHttpClientModule))] |
|||
[DependsOn(typeof(AbpDaprClientModule))] |
|||
public class AbpBackgroundTasksJobsModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpBackgroundTasksOptions>(options => |
|||
{ |
|||
options.AddProvider<ConsoleJob>(DefaultJobNames.ConsoleJob); |
|||
options.AddProvider<SendEmailJob>(DefaultJobNames.SendEmailJob); |
|||
options.AddProvider<SendSmsJob>(DefaultJobNames.SendSmsJob); |
|||
options.AddProvider<SleepJob>(DefaultJobNames.SleepJob); |
|||
options.AddProvider<ServiceInvocationJob>(DefaultJobNames.ServiceInvocationJob); |
|||
options.AddProvider<HttpRequestJob>(DefaultJobNames.HttpRequestJob); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public class ConsoleJob : IJobRunnable |
|||
{ |
|||
public const string PropertyMessage = "message"; |
|||
public Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
context.TryGetString(PropertyMessage, out var message); |
|||
Console.WriteLine($"This message: {message ?? "None"} comes from the job: {GetType()}"); |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public static class DefaultJobNames |
|||
{ |
|||
/// <summary>
|
|||
/// 发送邮件
|
|||
/// </summary>
|
|||
public const string SendEmailJob = "SendEmail"; |
|||
/// <summary>
|
|||
/// 发送短信
|
|||
/// </summary>
|
|||
public const string SendSmsJob = "SendSms"; |
|||
/// <summary>
|
|||
/// 控制台输出
|
|||
/// </summary>
|
|||
public const string ConsoleJob = "Console"; |
|||
/// <summary>
|
|||
/// 休眠
|
|||
/// </summary>
|
|||
public const string SleepJob = "Sleep"; |
|||
/// <summary>
|
|||
/// 服务间调用
|
|||
/// </summary>
|
|||
public const string ServiceInvocationJob = "ServiceInvocation"; |
|||
/// <summary>
|
|||
/// Http请求
|
|||
/// </summary>
|
|||
public const string HttpRequestJob = "HttpRequest"; |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Net.Http; |
|||
using System.Net.Http.Headers; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Http; |
|||
using Volo.Abp.Json; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public class HttpRequestJob : IJobRunnable |
|||
{ |
|||
public const string PropertyUrl = "url"; |
|||
public const string PropertyMethod = "method"; |
|||
public const string PropertyData = "data"; |
|||
public const string PropertyContentType = "contentType"; |
|||
public const string PropertyHeaders = "headers"; |
|||
public const string PropertyToken = "token"; |
|||
|
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
var clientFactory = context.GetRequiredService<IHttpClientFactory>(); |
|||
|
|||
var client = clientFactory.CreateClient(); |
|||
var requestMessage = BuildRequestMessage(context); |
|||
|
|||
var response = await client.SendAsync( |
|||
requestMessage, |
|||
HttpCompletionOption.ResponseHeadersRead); |
|||
|
|||
var stringContent = await response.Content.ReadAsStringAsync(); |
|||
|
|||
if (!response.IsSuccessStatusCode && stringContent.IsNullOrWhiteSpace()) |
|||
{ |
|||
context.SetResult($"HttpStatusCode: {(int)response.StatusCode}, Reason: {response.ReasonPhrase}"); |
|||
return; |
|||
} |
|||
context.SetResult(stringContent); |
|||
} |
|||
|
|||
protected virtual HttpRequestMessage BuildRequestMessage(JobRunnableContext context) |
|||
{ |
|||
var url = context.GetString(PropertyUrl); |
|||
var method = context.GetString(PropertyMethod); |
|||
context.TryGetJobData(PropertyData, out var data); |
|||
context.TryGetJobData(PropertyContentType, out var contentType); |
|||
|
|||
var jsonSerializer = context.GetRequiredService<IJsonSerializer>(); |
|||
|
|||
var httpRequestMesasge = new HttpRequestMessage(new HttpMethod(method), url); |
|||
if (data != null) |
|||
{ |
|||
// TODO: 需要支持表单类型
|
|||
|
|||
// application/json 支持
|
|||
httpRequestMesasge.Content = new StringContent( |
|||
jsonSerializer.Serialize(data), |
|||
Encoding.UTF8, |
|||
contentType?.ToString() ?? MimeTypes.Application.Json); |
|||
} |
|||
if (context.TryGetJobData(PropertyHeaders, out var headers)) |
|||
{ |
|||
var headersDic = new Dictionary<string, object>(); |
|||
if (headers is string headerString) |
|||
{ |
|||
try |
|||
{ |
|||
headersDic = jsonSerializer.Deserialize<Dictionary<string, object>>(headerString); |
|||
} |
|||
catch { } |
|||
} |
|||
foreach (var header in headersDic) |
|||
{ |
|||
httpRequestMesasge.Headers.Add(header.Key, header.Value.ToString()); |
|||
} |
|||
} |
|||
// TODO: 和 headers 一起?
|
|||
if (context.TryGetString(PropertyToken, out var accessToken)) |
|||
{ |
|||
httpRequestMesasge.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); |
|||
} |
|||
|
|||
return httpRequestMesasge; |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Emailing; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public class SendEmailJob : IJobRunnable |
|||
{ |
|||
public const string PropertyFrom = "from"; |
|||
public const string PropertyTo = "to"; |
|||
public const string PropertySubject = "subject"; |
|||
public const string PropertyBody = "body"; |
|||
public const string PropertyIsBodyHtml = "isBodyHtml"; |
|||
|
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
context.TryGetString(PropertyFrom, out var from); |
|||
var to = context.GetString(PropertyTo); |
|||
var subject = context.GetString(PropertySubject); |
|||
var body = context.GetString(PropertyBody); |
|||
context.TryGetJobData<bool>(PropertyIsBodyHtml, out var isBodyHtml); |
|||
|
|||
var emailSender = context.GetRequiredService<IEmailSender>(); |
|||
|
|||
await emailSender.QueueAsync(from, to, subject, body, isBodyHtml); |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Json; |
|||
using Volo.Abp.Sms; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public class SendSmsJob : IJobRunnable |
|||
{ |
|||
public const string PropertyPhoneNumber = "phoneNumber"; |
|||
public const string PropertyMessage = "message"; |
|||
public const string PropertyProperties = "properties"; |
|||
|
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
var phoneNumber = context.GetString(PropertyPhoneNumber); |
|||
var message = context.GetString(PropertyMessage); |
|||
|
|||
var smsMessage = new SmsMessage(phoneNumber, message); |
|||
if (context.TryGetString(PropertyProperties, out var data)) |
|||
{ |
|||
var properties = new Dictionary<string, object>(); |
|||
|
|||
try |
|||
{ |
|||
var jsonSerializer = context.GetRequiredService<IJsonSerializer>(); |
|||
properties = jsonSerializer.Deserialize<Dictionary<string, object>>(data); |
|||
} |
|||
catch { } |
|||
|
|||
smsMessage.Properties.AddIfNotContains(properties); |
|||
} |
|||
|
|||
var smsSender = context.GetRequiredService<ISmsSender>(); |
|||
|
|||
await smsSender.SendAsync(smsMessage); |
|||
} |
|||
} |
|||
@ -0,0 +1,261 @@ |
|||
using LINGYUN.Abp.Dapr.Client.DynamicProxying; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Http.Client; |
|||
using Volo.Abp.Http.Client.ClientProxying; |
|||
using Volo.Abp.Http.Client.DynamicProxying; |
|||
using Volo.Abp.Http.Client.Proxying; |
|||
using Volo.Abp.Http.Modeling; |
|||
using Volo.Abp.Json; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public class ServiceInvocationJob : IJobRunnable |
|||
{ |
|||
// 必须, 接口类型
|
|||
public const string PropertyService = "service"; |
|||
// 必须, 接口方法名称
|
|||
public const string PropertyMethod = "method"; |
|||
// 可选, 请求数据, 传递 Dictionary<string, object> 类型的字符串参数
|
|||
public const string PropertyData = "data"; |
|||
// 可选, 请求时指定区域性
|
|||
public const string PropertyCulture = "culture"; |
|||
// 可选, 请求时指定租户
|
|||
public const string PropertyTenant = "tenant"; |
|||
// 可选, 提供者名称, http、dapr, 无此参数默认 http
|
|||
public const string PropertyProvider = "provider"; |
|||
// Dapr 调用所必须的参数
|
|||
public const string PropertyAppId = "appId"; |
|||
|
|||
// TODO: support grpc
|
|||
private readonly static string[] Providers = new string[] { "http", "dapr" }; |
|||
|
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
// 获取参数列表
|
|||
var type = context.GetString(PropertyService); |
|||
var method = context.GetString(PropertyMethod); |
|||
var serviceType = Type.GetType(type, true); |
|||
var serviceMethod = serviceType.GetMethod(method); |
|||
context.TryGetString(PropertyCulture, out var culture); |
|||
context.TryGetString(PropertyProvider, out var provider); |
|||
provider ??= "http"; |
|||
|
|||
if (!Providers.Contains(provider)) |
|||
{ |
|||
throw new AbpJobExecutionException( |
|||
GetType(), |
|||
$"Invalid inter-service invocation provider: {provider}", |
|||
null); |
|||
} |
|||
|
|||
Guid? tenantId = null; |
|||
if (context.TryGetString(PropertyTenant, out var tenant) && |
|||
Guid.TryParse(tenant, out var converTenant)) |
|||
{ |
|||
tenantId = converTenant; |
|||
} |
|||
|
|||
var currentTenant = context.GetRequiredService<ICurrentTenant>(); |
|||
using (currentTenant.Change(tenantId)) |
|||
{ |
|||
using (CultureHelper.Use(culture ?? CultureInfo.CurrentCulture.Name)) |
|||
{ |
|||
switch (provider) |
|||
{ |
|||
case "http": |
|||
await ExecuteWithHttpProxy(context, serviceType, serviceMethod); |
|||
break; |
|||
case "dapr": |
|||
await ExecuteWithDaprProxy(context, serviceType, serviceMethod); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
#region HttpClient
|
|||
|
|||
protected readonly static string CallRequestMethod = nameof(DynamicHttpProxyInterceptorClientProxy<object>.CallRequestAsync); |
|||
protected readonly static MethodInfo ClientProxyMethod = typeof(DynamicHttpProxyInterceptorClientProxy<>) |
|||
.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) |
|||
.First(m => m.Name == CallRequestMethod && !m.IsGenericMethodDefinition); |
|||
protected readonly static MethodInfo CallRequestAsyncMethod = typeof(DynamicHttpProxyInterceptor<object>) |
|||
.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) |
|||
.First(m => m.Name == CallRequestMethod && m.IsGenericMethodDefinition); |
|||
|
|||
protected virtual async Task ExecuteWithHttpProxy( |
|||
JobRunnableContext context, |
|||
Type serviceType, |
|||
MethodInfo serviceMethod) |
|||
{ |
|||
// 反射所必须的参数
|
|||
var clientProxyType = typeof(DynamicHttpProxyInterceptorClientProxy<>).MakeGenericType(serviceType); |
|||
var clientProxy = context.GetRequiredService(clientProxyType); |
|||
|
|||
// 调用远程服务发现端点
|
|||
var actionApiDescription = await GetActionApiDescriptionModel(context, serviceType, serviceMethod); |
|||
|
|||
// 调用参数
|
|||
var invokeParameters = new Dictionary<string, object>(); |
|||
if (context.TryGetString(PropertyData, out var data)) |
|||
{ |
|||
var jsonSerializer = context.GetRequiredService<IJsonSerializer>(); |
|||
invokeParameters = jsonSerializer.Deserialize<Dictionary<string, object>>(data); |
|||
} |
|||
|
|||
// 构造服务代理上下文
|
|||
var clientProxyRequestContext = new ClientProxyRequestContext( |
|||
actionApiDescription, |
|||
invokeParameters, |
|||
serviceType); |
|||
|
|||
if (serviceMethod.ReturnType.GenericTypeArguments.IsNullOrEmpty()) |
|||
{ |
|||
// 直接调用
|
|||
var taskProxy = (Task)ClientProxyMethod |
|||
.Invoke(clientProxy, new object[] { clientProxyRequestContext }); |
|||
await taskProxy; |
|||
} |
|||
else |
|||
{ |
|||
// 有返回值的调用
|
|||
var returnType = serviceMethod.ReturnType.GenericTypeArguments[0]; |
|||
var result = (Task)CallRequestAsyncMethod |
|||
.MakeGenericMethod(returnType) |
|||
.Invoke(this, new object[] { context }); |
|||
|
|||
context.SetResult(await GetResultAsync(result, returnType)); |
|||
} |
|||
} |
|||
|
|||
protected virtual async Task<ActionApiDescriptionModel> GetActionApiDescriptionModel( |
|||
JobRunnableContext context, |
|||
Type serviceType, |
|||
MethodInfo serviceMethod) |
|||
{ |
|||
var clientOptions = context.GetRequiredService<IOptions<AbpHttpClientOptions>>().Value; |
|||
var remoteServiceConfigurationProvider = context.GetRequiredService<IRemoteServiceConfigurationProvider>(); |
|||
var proxyHttpClientFactory = context.GetRequiredService<IProxyHttpClientFactory>(); |
|||
var apiDescriptionFinder = context.GetRequiredService<IApiDescriptionFinder>(); |
|||
|
|||
var clientConfig = clientOptions.HttpClientProxies.GetOrDefault(serviceType) ?? |
|||
throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {serviceType.FullName}."); |
|||
var remoteServiceConfig = await remoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName); |
|||
var client = proxyHttpClientFactory.Create(clientConfig.RemoteServiceName); |
|||
|
|||
return await apiDescriptionFinder.FindActionAsync( |
|||
client, |
|||
remoteServiceConfig.BaseUrl, |
|||
serviceType, |
|||
serviceMethod |
|||
); |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
|
|||
#region DaprClient
|
|||
|
|||
protected readonly static string DaprCallRequestMethod = nameof(DynamicDaprProxyInterceptorClientProxy<object>.CallRequestAsync); |
|||
protected readonly static MethodInfo DaprClientProxyMethod = typeof(DynamicDaprProxyInterceptorClientProxy<>) |
|||
.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) |
|||
.First(m => m.Name == DaprCallRequestMethod && !m.IsGenericMethodDefinition); |
|||
protected readonly static MethodInfo DaprCallRequestAsyncMethod = typeof(DynamicDaprProxyInterceptorClientProxy<object>) |
|||
.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) |
|||
.First(m => m.Name == DaprCallRequestMethod && m.IsGenericMethodDefinition); |
|||
|
|||
protected virtual async Task ExecuteWithDaprProxy( |
|||
JobRunnableContext context, |
|||
Type serviceType, |
|||
MethodInfo serviceMethod) |
|||
{ |
|||
var appid = context.GetString(PropertyAppId); |
|||
var clientOptions = context.GetRequiredService<IOptions<AbpHttpClientOptions>>().Value; |
|||
var clientConfig = clientOptions.HttpClientProxies.GetOrDefault(serviceType) ?? |
|||
throw new AbpJobExecutionException( |
|||
GetType(), |
|||
$"Could not get DynamicHttpClientProxyConfig for {serviceType.FullName}.", |
|||
null); |
|||
|
|||
// 反射所必须的参数
|
|||
var clientProxyType = typeof(DynamicDaprProxyInterceptorClientProxy<>).MakeGenericType(serviceType); |
|||
var clientProxy = context.GetRequiredService(clientProxyType); |
|||
|
|||
// 调用远程服务发现端点
|
|||
var actionApiDescription = await GetActionApiDescriptionModel( |
|||
context, |
|||
clientConfig.RemoteServiceName, |
|||
appid, |
|||
serviceType, |
|||
serviceMethod); |
|||
|
|||
// 调用参数
|
|||
var invokeParameters = new Dictionary<string, object>(); |
|||
if (context.TryGetString(PropertyData, out var data)) |
|||
{ |
|||
var jsonSerializer = context.GetRequiredService<IJsonSerializer>(); |
|||
invokeParameters = jsonSerializer.Deserialize<Dictionary<string, object>>(data); |
|||
} |
|||
|
|||
// 构造服务代理上下文
|
|||
var clientProxyRequestContext = new ClientProxyRequestContext( |
|||
actionApiDescription, |
|||
invokeParameters, |
|||
serviceType); |
|||
|
|||
if (serviceMethod.ReturnType.GenericTypeArguments.IsNullOrEmpty()) |
|||
{ |
|||
// 直接调用
|
|||
var taskProxy = (Task)DaprClientProxyMethod |
|||
.Invoke(clientProxy, new object[] { clientProxyRequestContext }); |
|||
await taskProxy; |
|||
} |
|||
else |
|||
{ |
|||
// 有返回值的调用
|
|||
var returnType = serviceMethod.ReturnType.GenericTypeArguments[0]; |
|||
var result = (Task)DaprCallRequestAsyncMethod |
|||
.MakeGenericMethod(returnType) |
|||
.Invoke(this, new object[] { context }); |
|||
|
|||
context.SetResult(await GetResultAsync(result, returnType)); |
|||
} |
|||
} |
|||
|
|||
protected virtual async Task<ActionApiDescriptionModel> GetActionApiDescriptionModel( |
|||
JobRunnableContext context, |
|||
string service, |
|||
string appId, |
|||
Type serviceType, |
|||
MethodInfo serviceMethod) |
|||
{ |
|||
var apiDescriptionFinder = context.GetRequiredService<IDaprApiDescriptionFinder>(); |
|||
|
|||
return await apiDescriptionFinder.FindActionAsync( |
|||
service, |
|||
appId, |
|||
serviceType, |
|||
serviceMethod); |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
protected virtual async Task<object> GetResultAsync(Task task, Type resultType) |
|||
{ |
|||
await task; |
|||
var resultProperty = typeof(Task<>) |
|||
.MakeGenericType(resultType) |
|||
.GetProperty(nameof(Task<object>.Result), BindingFlags.Instance | BindingFlags.Public); |
|||
Check.NotNull(resultProperty, nameof(resultProperty)); |
|||
return resultProperty.GetValue(task); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Jobs; |
|||
|
|||
public class SleepJob : IJobRunnable |
|||
{ |
|||
public async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
context.JobData.TryGetValue("Delay", out var sleep); |
|||
|
|||
Console.WriteLine($"Sleep {sleep ?? 20000} milliseconds."); |
|||
|
|||
await Task.Delay(sleep?.To<int>() ?? 20000); |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Quartz" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.BackgroundTasks\LINGYUN.Abp.BackgroundTasks.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,19 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Quartz; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Quartz; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Quartz; |
|||
|
|||
[DependsOn(typeof(AbpBackgroundTasksModule))] |
|||
[DependsOn(typeof(AbpQuartzModule))] |
|||
public class AbpBackgroundTasksQuartzModule : AbpModule |
|||
{ |
|||
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
|||
{ |
|||
var _scheduler = context.ServiceProvider.GetRequiredService<IScheduler>(); |
|||
|
|||
_scheduler.ListenerManager.AddJobListener(context.ServiceProvider.GetRequiredService<QuartzJobListener>()); |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using Quartz; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Quartz; |
|||
|
|||
public interface IQuartzJobExecutorProvider |
|||
{ |
|||
#nullable enable |
|||
IJobDetail? CreateJob(JobInfo job); |
|||
|
|||
ITrigger CreateTrigger(JobInfo job); |
|||
#nullable disable |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
using Quartz; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Quartz; |
|||
|
|||
[DisallowConcurrentExecution] |
|||
public class QuartzJobConcurrentAdapter<TJobRunnable> : QuartzJobSimpleAdapter<TJobRunnable> |
|||
where TJobRunnable : IJobRunnable |
|||
{ |
|||
public QuartzJobConcurrentAdapter(IServiceProvider serviceProvider) |
|||
: base(serviceProvider) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,112 @@ |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Microsoft.Extensions.Options; |
|||
using Quartz; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Timing; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Quartz; |
|||
|
|||
public class QuartzJobExecutorProvider : IQuartzJobExecutorProvider, ISingletonDependency |
|||
{ |
|||
public ILogger<QuartzJobExecutorProvider> Logger { protected get; set; } |
|||
|
|||
protected IClock Clock { get; } |
|||
protected AbpBackgroundTasksOptions Options { get; } |
|||
|
|||
public QuartzJobExecutorProvider( |
|||
IClock clock, |
|||
IOptions<AbpBackgroundTasksOptions> options) |
|||
{ |
|||
Clock = clock; |
|||
Options = options.Value; |
|||
|
|||
Logger = NullLogger<QuartzJobExecutorProvider>.Instance; |
|||
} |
|||
|
|||
public IJobDetail CreateJob(JobInfo job) |
|||
{ |
|||
var jobType = Type.GetType(job.Type) ?? Options.JobProviders.GetOrDefault(job.Type); |
|||
if (jobType == null) |
|||
{ |
|||
Logger.LogWarning($"The task: {job.Group} - {job.Name}: {job.Type} is not registered and cannot create an instance of the performer type."); |
|||
return null; |
|||
} |
|||
|
|||
var adapterType = typeof(QuartzJobSimpleAdapter<>); |
|||
if (job.LockTimeOut > 0) |
|||
{ |
|||
adapterType = typeof(QuartzJobConcurrentAdapter<>); |
|||
} |
|||
|
|||
if (!typeof(IJob).IsAssignableFrom(jobType)) |
|||
{ |
|||
jobType = adapterType.MakeGenericType(jobType); |
|||
} |
|||
|
|||
var jobBuilder = JobBuilder.Create(jobType) |
|||
.WithIdentity(job.Name, job.Group) |
|||
.WithDescription(job.Description); |
|||
|
|||
jobBuilder.UsingJobData(nameof(JobInfo.Id), job.Id); |
|||
jobBuilder.UsingJobData(nameof(JobInfo.LockTimeOut), job.LockTimeOut); |
|||
jobBuilder.UsingJobData(new JobDataMap(job.Args)); |
|||
|
|||
return jobBuilder.Build(); |
|||
} |
|||
|
|||
public ITrigger CreateTrigger(JobInfo job) |
|||
{ |
|||
var triggerBuilder = TriggerBuilder.Create(); |
|||
|
|||
switch (job.JobType) |
|||
{ |
|||
case JobType.Period: |
|||
if (!CronExpression.IsValidExpression(job.Cron)) |
|||
{ |
|||
Logger.LogWarning($"The task: {job.Group} - {job.Name} periodic task Cron expression was invalid and the task trigger could not be created."); |
|||
return null; |
|||
} |
|||
triggerBuilder |
|||
.WithIdentity(job.Name, job.Group) |
|||
.WithDescription(job.Description) |
|||
.EndAt(job.EndTime) |
|||
.ForJob(job.Name, job.Group) |
|||
.WithPriority((int)job.Priority) |
|||
.WithCronSchedule(job.Cron); |
|||
if (job.BeginTime > Clock.Now) |
|||
{ |
|||
triggerBuilder = triggerBuilder.StartAt(job.BeginTime); |
|||
} |
|||
break; |
|||
case JobType.Once: |
|||
case JobType.Persistent: |
|||
default: |
|||
// Quartz 需要减一位
|
|||
var maxCount = job.MaxCount <= 0 ? -1 : job.MaxCount - 1; |
|||
if (job.JobType == JobType.Once) |
|||
{ |
|||
maxCount = 0; |
|||
} |
|||
if (job.Status == JobStatus.FailedRetry && job.TryCount < job.MaxTryCount) |
|||
{ |
|||
maxCount = job.MaxTryCount <= 0 ? -1 : job.MaxTryCount - 1; |
|||
} |
|||
triggerBuilder |
|||
.WithIdentity(job.Name, job.Group) |
|||
.WithDescription(job.Description) |
|||
.StartAt(Clock.Now.AddSeconds(job.Interval)) |
|||
.EndAt(job.EndTime) |
|||
.ForJob(job.Name, job.Group) |
|||
.WithPriority((int)job.Priority) |
|||
.WithSimpleSchedule(x => |
|||
x.WithIntervalInSeconds(job.Interval) |
|||
.WithRepeatCount(maxCount)); |
|||
break; |
|||
} |
|||
|
|||
return triggerBuilder.Build(); |
|||
} |
|||
} |
|||
@ -0,0 +1,149 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Quartz; |
|||
using Quartz.Listener; |
|||
using System; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Quartz; |
|||
|
|||
public class QuartzJobListener : JobListenerSupport, ISingletonDependency |
|||
{ |
|||
public ILogger<QuartzJobListener> Logger { protected get; set; } |
|||
|
|||
public override string Name => "QuartzJobListener"; |
|||
|
|||
protected IJobEventProvider EventProvider { get; } |
|||
protected IServiceProvider ServiceProvider { get; } |
|||
|
|||
public QuartzJobListener( |
|||
IServiceProvider serviceProvider, |
|||
IJobEventProvider eventProvider) |
|||
{ |
|||
ServiceProvider = serviceProvider; |
|||
EventProvider = eventProvider; |
|||
|
|||
Logger = NullLogger<QuartzJobListener>.Instance; |
|||
} |
|||
|
|||
public override Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default) |
|||
{ |
|||
var jobType = context.JobDetail.JobType; |
|||
if (jobType.IsGenericType) |
|||
{ |
|||
jobType = jobType.GetGenericTypeDefinition(); |
|||
} |
|||
Logger.LogInformation($"The task {jobType.Name} could not be performed..."); |
|||
|
|||
return Task.FromResult(-1); |
|||
} |
|||
|
|||
public override async Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default) |
|||
{ |
|||
var jobId = context.GetString(nameof(JobInfo.Id)); |
|||
if (Guid.TryParse(jobId, out var jobUUId)) |
|||
{ |
|||
try |
|||
{ |
|||
using var scope = ServiceProvider.CreateScope(); |
|||
var jobEventData = new JobEventData( |
|||
jobUUId, |
|||
context.JobDetail.JobType, |
|||
context.JobDetail.Key.Group, |
|||
context.JobDetail.Key.Name) |
|||
{ |
|||
Result = context.Result?.ToString() |
|||
}; |
|||
|
|||
var jobEventList = EventProvider.GetAll(); |
|||
var eventContext = new JobEventContext( |
|||
scope.ServiceProvider, |
|||
jobEventData); |
|||
|
|||
var index = 0; |
|||
var taskList = new Task[jobEventList.Count]; |
|||
foreach (var jobEvent in jobEventList) |
|||
{ |
|||
taskList[index] = jobEvent.OnJobBeforeExecuted(eventContext); |
|||
index++; |
|||
} |
|||
|
|||
await Task.WhenAll(taskList); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Logger.LogError($"The event before the task execution is abnormal:{ex}"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public override async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, CancellationToken cancellationToken = default) |
|||
{ |
|||
try |
|||
{ |
|||
using var scope = ServiceProvider.CreateScope(); |
|||
var jobId = context.GetString(nameof(JobInfo.Id)); |
|||
if (Guid.TryParse(jobId, out var jobUUId)) |
|||
{ |
|||
var jobType = context.JobDetail.JobType; |
|||
if (jobType.IsGenericType) |
|||
{ |
|||
jobType = jobType.GetGenericArguments()[0]; |
|||
} |
|||
|
|||
var jobEventData = new JobEventData( |
|||
jobUUId, |
|||
jobType, |
|||
context.JobDetail.Key.Group, |
|||
context.JobDetail.Key.Name, |
|||
jobException) |
|||
{ |
|||
Status = JobStatus.Running |
|||
}; |
|||
|
|||
if (context.Trigger is ISimpleTrigger simpleTrigger) |
|||
{ |
|||
jobEventData.Triggered = simpleTrigger.TimesTriggered; |
|||
jobEventData.RepeatCount = simpleTrigger.RepeatCount; |
|||
} |
|||
jobEventData.Description = context.JobDetail.Description; |
|||
jobEventData.RunTime = context.FireTimeUtc.LocalDateTime; |
|||
jobEventData.LastRunTime = context.PreviousFireTimeUtc?.LocalDateTime; |
|||
jobEventData.NextRunTime = context.NextFireTimeUtc?.LocalDateTime; |
|||
if (context.Result != null) |
|||
{ |
|||
jobEventData.Result = context.Result?.ToString(); |
|||
} |
|||
var tenantIdString = context.GetString(nameof(IMultiTenant.TenantId)); |
|||
if (Guid.TryParse(tenantIdString, out var tenantId)) |
|||
{ |
|||
jobEventData.TenantId = tenantId; |
|||
} |
|||
|
|||
var jobEventList = EventProvider.GetAll(); |
|||
var eventContext = new JobEventContext( |
|||
scope.ServiceProvider, |
|||
jobEventData); |
|||
|
|||
var index = 0; |
|||
var taskList = new Task[jobEventList.Count]; |
|||
foreach (var jobEvent in jobEventList) |
|||
{ |
|||
taskList[index] = jobEvent.OnJobAfterExecuted(eventContext); |
|||
index++; |
|||
} |
|||
|
|||
await Task.WhenAll(taskList); |
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Logger.LogError($"The event is abnormal after the task is executed:{ex}"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,163 @@ |
|||
using Quartz; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Quartz; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class QuartzJobScheduler : IJobScheduler, ISingletonDependency |
|||
{ |
|||
protected IJobStore JobStore { get; } |
|||
protected IScheduler Scheduler { get; } |
|||
protected IQuartzJobExecutorProvider QuartzJobExecutor { get; } |
|||
|
|||
public QuartzJobScheduler( |
|||
IJobStore jobStore, |
|||
IScheduler scheduler, |
|||
IQuartzJobExecutorProvider quartzJobExecutor) |
|||
{ |
|||
JobStore = jobStore; |
|||
Scheduler = scheduler; |
|||
QuartzJobExecutor = quartzJobExecutor; |
|||
} |
|||
|
|||
public virtual async Task<bool> ExistsAsync(JobInfo job) |
|||
{ |
|||
var jobKey = new JobKey(job.Name, job.Group); |
|||
return await Scheduler.CheckExists(jobKey); |
|||
} |
|||
|
|||
public virtual async Task PauseAsync(JobInfo job) |
|||
{ |
|||
var jobKey = new JobKey(job.Name, job.Group); |
|||
if (await Scheduler.CheckExists(jobKey)) |
|||
{ |
|||
var triggers = await Scheduler.GetTriggersOfJob(jobKey); |
|||
foreach (var trigger in triggers) |
|||
{ |
|||
await Scheduler.PauseTrigger(trigger.Key); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public virtual async Task<bool> QueueAsync(JobInfo job) |
|||
{ |
|||
var jobKey = new JobKey(job.Name, job.Group); |
|||
if (await Scheduler.CheckExists(jobKey)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var jobDetail = QuartzJobExecutor.CreateJob(job); |
|||
if (jobDetail == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var jobTrigger = QuartzJobExecutor.CreateTrigger(job); |
|||
if (jobTrigger == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
await Scheduler.ScheduleJob(jobDetail, jobTrigger); |
|||
|
|||
return await Scheduler.CheckExists(jobTrigger.Key); |
|||
} |
|||
|
|||
public virtual async Task QueuesAsync(IEnumerable<JobInfo> jobs) |
|||
{ |
|||
var jobDictionary = new Dictionary<IJobDetail, IReadOnlyCollection<ITrigger>>(); |
|||
foreach (var job in jobs) |
|||
{ |
|||
var jobDetail = QuartzJobExecutor.CreateJob(job); |
|||
if (jobDetail == null) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var jobTrigger = QuartzJobExecutor.CreateTrigger(job); |
|||
if (jobTrigger == null) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
jobDictionary.Add(jobDetail, new ITrigger[] { jobTrigger }); |
|||
} |
|||
|
|||
await Scheduler.ScheduleJobs(jobDictionary, false); |
|||
} |
|||
|
|||
public virtual async Task<bool> RemoveAsync(JobInfo job) |
|||
{ |
|||
var jobKey = new JobKey(job.Name, job.Group); |
|||
if (!await Scheduler.CheckExists(jobKey)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var triggers = await Scheduler.GetTriggersOfJob(jobKey); |
|||
foreach (var trigger in triggers) |
|||
{ |
|||
await Scheduler.PauseTrigger(trigger.Key); |
|||
} |
|||
await Scheduler.DeleteJob(jobKey); |
|||
|
|||
return !await Scheduler.CheckExists(jobKey); |
|||
} |
|||
|
|||
public virtual async Task ResumeAsync(JobInfo job) |
|||
{ |
|||
var jobKey = new JobKey(job.Name, job.Group); |
|||
if (await Scheduler.CheckExists(jobKey)) |
|||
{ |
|||
var triggers = await Scheduler.GetTriggersOfJob(jobKey); |
|||
foreach (var trigger in triggers) |
|||
{ |
|||
await Scheduler.ResumeTrigger(trigger.Key); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public virtual async Task<bool> ShutdownAsync() |
|||
{ |
|||
await StopAsync(); |
|||
|
|||
await Scheduler.Shutdown(true); |
|||
|
|||
return Scheduler.IsShutdown; |
|||
} |
|||
|
|||
public virtual async Task<bool> StartAsync() |
|||
{ |
|||
if (Scheduler.InStandbyMode) |
|||
{ |
|||
await Scheduler.Start(); |
|||
} |
|||
return Scheduler.InStandbyMode; |
|||
} |
|||
|
|||
public virtual async Task<bool> StopAsync() |
|||
{ |
|||
if (!Scheduler.InStandbyMode) |
|||
{ |
|||
//等待任务运行完成
|
|||
await Scheduler.Standby(); |
|||
} |
|||
return !Scheduler.InStandbyMode; |
|||
} |
|||
|
|||
public virtual async Task TriggerAsync(JobInfo job) |
|||
{ |
|||
var jobKey = new JobKey(job.Name, job.Group); |
|||
if (!await Scheduler.CheckExists(jobKey)) |
|||
{ |
|||
job.JobType = JobType.Once; |
|||
|
|||
await QueueAsync(job); |
|||
} |
|||
await Scheduler.TriggerJob(jobKey); |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Quartz; |
|||
using System; |
|||
using System.Collections.Immutable; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Quartz; |
|||
|
|||
public class QuartzJobSimpleAdapter<TJobRunnable> : IJob |
|||
where TJobRunnable : IJobRunnable |
|||
{ |
|||
protected IServiceProvider ServiceProvider { get; } |
|||
|
|||
public QuartzJobSimpleAdapter( |
|||
IServiceProvider serviceProvider) |
|||
{ |
|||
ServiceProvider = serviceProvider; |
|||
} |
|||
|
|||
public async virtual Task Execute(IJobExecutionContext context) |
|||
{ |
|||
// 任务已经在一个作用域中
|
|||
// using var scope = ServiceProvider.CreateScope();
|
|||
var jobExecuter = ServiceProvider.GetRequiredService<IJobRunnableExecuter>(); |
|||
|
|||
var jobContext = new JobRunnableContext( |
|||
typeof(TJobRunnable), |
|||
ServiceProvider, |
|||
context.MergedJobDataMap.ToImmutableDictionary()); |
|||
|
|||
await jobExecuter.ExecuteAsync(jobContext); |
|||
|
|||
context.Result = jobContext.Result; |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using System; |
|||
|
|||
namespace Quartz; |
|||
|
|||
public static class IJobExecutionContextExtensions |
|||
{ |
|||
public static TValue GetData<TValue>(this IJobExecutionContext context, string key) |
|||
{ |
|||
var value = context.MergedJobDataMap.GetString(key); |
|||
|
|||
return (TValue)Convert.ChangeType(value, typeof(TValue)); |
|||
} |
|||
|
|||
public static string GetString(this IJobExecutionContext context, string key) |
|||
{ |
|||
var value = context.MergedJobDataMap.Get(key); |
|||
|
|||
return value != null ? value.ToString() : ""; |
|||
} |
|||
|
|||
public static int GetInt(this IJobExecutionContext context, string key) |
|||
{ |
|||
var value = context.MergedJobDataMap.GetInt(key); |
|||
|
|||
return value; |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
# LINGYUN.Abp.BackgroundTasks.Quartz |
|||
|
|||
后台任务(队列)模块的Quartz实现. |
|||
并添加一个监听器用于通知管理者任务状态 |
|||
|
|||
## 配置使用 |
|||
|
|||
模块按需引用,具体配置参考Volo.Abp.Quartz模块 |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpBackgroundTasksQuartzModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
// other |
|||
} |
|||
``` |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,22 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Auditing" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Volo.Abp.BackgroundJobs.Abstractions" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Volo.Abp.DistributedLocking.Abstractions" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Volo.Abp.Guids" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.BackgroundTasks.Abstractions\LINGYUN.Abp.BackgroundTasks.Abstractions.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,23 @@ |
|||
using LINGYUN.Abp.BackgroundTasks.Internal; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.BackgroundJobs; |
|||
using Volo.Abp.DistributedLocking; |
|||
using Volo.Abp.Guids; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
[DependsOn(typeof(AbpAuditingModule))] |
|||
[DependsOn(typeof(AbpBackgroundTasksAbstractionsModule))] |
|||
[DependsOn(typeof(AbpBackgroundJobsAbstractionsModule))] |
|||
[DependsOn(typeof(AbpDistributedLockingAbstractionsModule))] |
|||
[DependsOn(typeof(AbpGuidsModule))] |
|||
public class AbpBackgroundTasksModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddTransient(typeof(BackgroundJobAdapter<>)); |
|||
context.Services.AddHostedService<DefaultBackgroundWorker>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.BackgroundJobs; |
|||
using Volo.Abp.Json; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public class BackgroundJobAdapter<TArgs> : IJobRunnable |
|||
{ |
|||
public ILogger<BackgroundJobAdapter<TArgs>> Logger { protected get; set; } |
|||
|
|||
protected AbpBackgroundJobOptions Options { get; } |
|||
protected IServiceScopeFactory ServiceScopeFactory { get; } |
|||
protected IBackgroundJobExecuter JobExecuter { get; } |
|||
|
|||
public BackgroundJobAdapter( |
|||
IOptions<AbpBackgroundJobOptions> options, |
|||
IBackgroundJobExecuter jobExecuter, |
|||
IServiceScopeFactory serviceScopeFactory) |
|||
{ |
|||
JobExecuter = jobExecuter; |
|||
ServiceScopeFactory = serviceScopeFactory; |
|||
Options = options.Value; |
|||
|
|||
Logger = NullLogger<BackgroundJobAdapter<TArgs>>.Instance; |
|||
} |
|||
|
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
using var scope = ServiceScopeFactory.CreateScope(); |
|||
object args = null; |
|||
if (context.TryGetString(nameof(TArgs), out var argsJson)) |
|||
{ |
|||
var jsonSerializer = context.GetRequiredService<IJsonSerializer>(); |
|||
args = jsonSerializer.Deserialize<TArgs>(argsJson); |
|||
} |
|||
|
|||
var jobType = Options.GetJob(typeof(TArgs)).JobType; |
|||
var jobContext = new JobExecutionContext(scope.ServiceProvider, jobType, args); |
|||
await JobExecuter.ExecuteAsync(jobContext); |
|||
} |
|||
} |
|||
@ -0,0 +1,94 @@ |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.BackgroundJobs; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Guids; |
|||
using Volo.Abp.Json; |
|||
using Volo.Abp.Timing; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class BackgroundJobManager : IBackgroundJobManager, ITransientDependency |
|||
{ |
|||
protected IClock Clock { get; } |
|||
protected IJobStore JobStore { get; } |
|||
protected IJobScheduler JobScheduler { get; } |
|||
protected IGuidGenerator GuidGenerator { get; } |
|||
protected IJsonSerializer JsonSerializer { get; } |
|||
protected AbpBackgroundJobOptions Options { get; } |
|||
public BackgroundJobManager( |
|||
IClock clock, |
|||
IJobStore jobStore, |
|||
IJobScheduler jobScheduler, |
|||
IGuidGenerator guidGenerator, |
|||
IJsonSerializer jsonSerializer, |
|||
IOptions<AbpBackgroundJobOptions> options) |
|||
{ |
|||
Clock = clock; |
|||
JobStore = jobStore; |
|||
JobScheduler = jobScheduler; |
|||
GuidGenerator = guidGenerator; |
|||
JsonSerializer = jsonSerializer; |
|||
Options = options.Value; |
|||
} |
|||
|
|||
public virtual async Task<string> EnqueueAsync<TArgs>( |
|||
TArgs args, |
|||
BackgroundJobPriority priority = BackgroundJobPriority.Normal, |
|||
TimeSpan? delay = null) |
|||
{ |
|||
var jobConfiguration = Options.GetJob<TArgs>(); |
|||
var interval = 0; |
|||
if (delay.HasValue) |
|||
{ |
|||
interval = delay.Value.Seconds; |
|||
} |
|||
var jobId = GuidGenerator.Create(); |
|||
var jobArgs = new Dictionary<string, object> |
|||
{ |
|||
{ nameof(TArgs), JsonSerializer.Serialize(args) }, |
|||
{ "ArgsType", jobConfiguration.ArgsType.AssemblyQualifiedName }, |
|||
{ "JobType", jobConfiguration.JobType.AssemblyQualifiedName }, |
|||
{ "JobName", jobConfiguration.JobName }, |
|||
}; |
|||
var jobInfo = new JobInfo |
|||
{ |
|||
Id = jobId, |
|||
Name = jobId.ToString(), |
|||
Group = "BackgroundJobs", |
|||
Priority = ConverForm(priority), |
|||
BeginTime = DateTime.Now, |
|||
Args = jobArgs, |
|||
Description = "From the framework background jobs", |
|||
JobType = JobType.Once, |
|||
Interval = interval, |
|||
CreationTime = Clock.Now, |
|||
// 确保不会被轮询入队
|
|||
Status = JobStatus.None, |
|||
Type = typeof(BackgroundJobAdapter<TArgs>).AssemblyQualifiedName, |
|||
}; |
|||
|
|||
// 存储状态
|
|||
await JobStore.StoreAsync(jobInfo); |
|||
|
|||
// 手动入队
|
|||
await JobScheduler.QueueAsync(jobInfo); |
|||
|
|||
return jobId.ToString(); |
|||
} |
|||
|
|||
private JobPriority ConverForm(BackgroundJobPriority priority) |
|||
{ |
|||
return priority switch |
|||
{ |
|||
BackgroundJobPriority.Low => JobPriority.Low, |
|||
BackgroundJobPriority.High => JobPriority.High, |
|||
BackgroundJobPriority.BelowNormal => JobPriority.BelowNormal, |
|||
BackgroundJobPriority.AboveNormal => JobPriority.AboveNormal, |
|||
_ => JobPriority.Normal, |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
/// <summary>
|
|||
/// 任务事件提供者
|
|||
/// </summary>
|
|||
public interface IJobEventProvider |
|||
{ |
|||
/// <summary>
|
|||
/// 返回所有任务事件注册接口
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
IReadOnlyCollection<IJobEvent> GetAll(); |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
/// <summary>
|
|||
/// 定义任务执行者接口
|
|||
/// 可以通过它实现一些限制(例如分布式锁)
|
|||
/// </summary>
|
|||
public interface IJobRunnableExecuter |
|||
{ |
|||
Task ExecuteAsync(JobRunnableContext context); |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
using System.Threading.Tasks; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
/// <summary>
|
|||
/// 作业调度接口
|
|||
/// </summary>
|
|||
public interface IJobScheduler |
|||
{ |
|||
/// <summary>
|
|||
/// 任务入队
|
|||
/// </summary>
|
|||
/// <param name="job"></param>
|
|||
/// <returns></returns>
|
|||
Task<bool> QueueAsync(JobInfo job); |
|||
/// <summary>
|
|||
/// 任务入队
|
|||
/// </summary>
|
|||
/// <param name="jobs"></param>
|
|||
/// <returns></returns>
|
|||
Task QueuesAsync(IEnumerable<JobInfo> jobs); |
|||
/// <summary>
|
|||
/// 任务是否存在
|
|||
/// </summary>
|
|||
/// <param name="group"></param>
|
|||
/// <param name="name"></param>
|
|||
/// <returns></returns>
|
|||
Task<bool> ExistsAsync(JobInfo job); |
|||
/// <summary>
|
|||
/// 触发任务
|
|||
/// </summary>
|
|||
/// <param name="group"></param>
|
|||
/// <param name="name"></param>
|
|||
/// <returns></returns>
|
|||
Task TriggerAsync(JobInfo job); |
|||
/// <summary>
|
|||
/// 暂停任务
|
|||
/// </summary>
|
|||
/// <param name="group"></param>
|
|||
/// <param name="name"></param>
|
|||
/// <returns></returns>
|
|||
Task PauseAsync(JobInfo job); |
|||
/// <summary>
|
|||
/// 恢复暂停的任务
|
|||
/// </summary>
|
|||
/// <param name="group"></param>
|
|||
/// <param name="name"></param>
|
|||
/// <returns></returns>
|
|||
Task ResumeAsync(JobInfo job); |
|||
/// <summary>
|
|||
/// 移除任务
|
|||
/// </summary>
|
|||
/// <param name="group"></param>
|
|||
/// <param name="name"></param>
|
|||
/// <returns></returns>
|
|||
Task<bool> RemoveAsync(JobInfo job); |
|||
/// <summary>
|
|||
/// 启动任务协调器
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
Task<bool> StartAsync(); |
|||
/// <summary>
|
|||
/// 停止任务协调器
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
Task<bool> StopAsync(); |
|||
/// <summary>
|
|||
/// 释放任务协调器
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
Task<bool> ShutdownAsync(); |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public interface IJobStore |
|||
{ |
|||
Task<List<JobInfo>> GetWaitingListAsync( |
|||
int maxResultCount, |
|||
CancellationToken cancellationToken = default); |
|||
|
|||
Task<List<JobInfo>> GetAllPeriodTasksAsync( |
|||
CancellationToken cancellationToken = default); |
|||
|
|||
Task<JobInfo> FindAsync(Guid jobId); |
|||
|
|||
Task StoreAsync(JobInfo jobInfo); |
|||
|
|||
Task StoreLogAsync(JobEventData eventData); |
|||
|
|||
Task CleanupAsync( |
|||
int maxResultCount, |
|||
TimeSpan jobExpiratime, |
|||
CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Auditing; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Internal; |
|||
|
|||
[DisableAuditing] |
|||
internal class BackgroundCleaningJob : IJobRunnable |
|||
{ |
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
var options = context.ServiceProvider.GetRequiredService<IOptions<AbpBackgroundTasksOptions>>().Value; |
|||
var store = context.ServiceProvider.GetRequiredService<IJobStore>(); |
|||
|
|||
await store.CleanupAsync( |
|||
options.MaxJobCleanCount, |
|||
options.JobExpiratime); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Auditing; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Internal; |
|||
|
|||
[DisableAuditing] |
|||
internal class BackgroundKeepAliveJob : IJobRunnable |
|||
{ |
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
var store = context.ServiceProvider.GetRequiredService<IJobStore>(); |
|||
|
|||
// TODO: 如果积压有大量周期性任务, 可能后面的队列无法被检索到
|
|||
var periodJobs = await store.GetAllPeriodTasksAsync(); |
|||
|
|||
if (!periodJobs.Any()) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var jobScheduler = context.ServiceProvider.GetRequiredService<IJobScheduler>(); |
|||
|
|||
await jobScheduler.QueuesAsync(periodJobs); |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Auditing; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Internal; |
|||
|
|||
[DisableAuditing] |
|||
internal class BackgroundPollingJob : IJobRunnable |
|||
{ |
|||
public virtual async Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
var options = context.ServiceProvider.GetRequiredService<IOptions<AbpBackgroundTasksOptions>>().Value; |
|||
var store = context.ServiceProvider.GetRequiredService<IJobStore>(); |
|||
|
|||
// TODO: 如果积压有大量持续性任务, 可能后面的队列无法被检索到
|
|||
// 尽量让任务重复次数在可控范围内
|
|||
// 需要借助队列提供者来持久化已入队任务
|
|||
var waitingJobs = await store.GetWaitingListAsync(options.MaxJobFetchCount); |
|||
|
|||
if (!waitingJobs.Any()) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var jobScheduler = context.ServiceProvider.GetRequiredService<IJobScheduler>(); |
|||
|
|||
foreach (var job in waitingJobs) |
|||
{ |
|||
await jobScheduler.QueueAsync(job); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
using Microsoft.Extensions.Hosting; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Internal; |
|||
|
|||
internal class DefaultBackgroundWorker : BackgroundService |
|||
{ |
|||
private readonly IJobStore _jobStore; |
|||
private readonly IJobScheduler _jobScheduler; |
|||
private readonly AbpBackgroundTasksOptions _options; |
|||
|
|||
public DefaultBackgroundWorker( |
|||
IJobStore jobStore, |
|||
IJobScheduler jobScheduler, |
|||
IOptions<AbpBackgroundTasksOptions> options) |
|||
{ |
|||
_jobStore = jobStore; |
|||
_jobScheduler = jobScheduler; |
|||
_options = options.Value; |
|||
} |
|||
|
|||
protected async override Task ExecuteAsync(CancellationToken stoppingToken) |
|||
{ |
|||
await QueuePollingJob(); |
|||
await QueueKeepAliveJob(); |
|||
await QueueCleaningJob(); |
|||
} |
|||
|
|||
private async Task QueueKeepAliveJob() |
|||
{ |
|||
var keepAliveJob = BuildKeepAliveJobInfo(); |
|||
await _jobScheduler.QueueAsync(keepAliveJob); |
|||
} |
|||
|
|||
private async Task QueuePollingJob() |
|||
{ |
|||
var pollingJob = BuildPollingJobInfo(); |
|||
await _jobScheduler.QueueAsync(pollingJob); |
|||
} |
|||
|
|||
private async Task QueueCleaningJob() |
|||
{ |
|||
var cleaningJob = BuildCleaningJobInfo(); |
|||
await _jobScheduler.QueueAsync(cleaningJob); |
|||
} |
|||
|
|||
private JobInfo BuildKeepAliveJobInfo() |
|||
{ |
|||
return new JobInfo |
|||
{ |
|||
Id = Guid.Parse("8F50C5D9-5691-4B99-A52B-CABD91D93C89"), |
|||
Name = nameof(BackgroundKeepAliveJob), |
|||
Group = "Default", |
|||
Description = "Add periodic tasks", |
|||
Args = new Dictionary<string, object>(), |
|||
Status = JobStatus.Running, |
|||
BeginTime = DateTime.Now, |
|||
CreationTime = DateTime.Now, |
|||
JobType = JobType.Once, |
|||
Priority = JobPriority.High, |
|||
MaxCount = 1, |
|||
Interval = 30, |
|||
Type = typeof(BackgroundKeepAliveJob).AssemblyQualifiedName, |
|||
}; |
|||
} |
|||
|
|||
private JobInfo BuildPollingJobInfo() |
|||
{ |
|||
return new JobInfo |
|||
{ |
|||
Id = Guid.Parse("C51152E9-F0B8-4252-8352-283BE46083CC"), |
|||
Name = nameof(BackgroundPollingJob), |
|||
Group = "Default", |
|||
Description = "Polling tasks to be executed", |
|||
Args = new Dictionary<string, object>(), |
|||
Status = JobStatus.Running, |
|||
BeginTime = DateTime.Now, |
|||
CreationTime = DateTime.Now, |
|||
Cron = _options.JobFetchCronExpression, |
|||
JobType = JobType.Period, |
|||
Priority = JobPriority.High, |
|||
LockTimeOut = _options.JobFetchLockTimeOut, |
|||
Type = typeof(BackgroundPollingJob).AssemblyQualifiedName, |
|||
}; |
|||
} |
|||
|
|||
private JobInfo BuildCleaningJobInfo() |
|||
{ |
|||
return new JobInfo |
|||
{ |
|||
Id = Guid.Parse("AAAF8783-FA06-4CF9-BDCA-11140FB2478F"), |
|||
Name = nameof(BackgroundCleaningJob), |
|||
Group = "Default", |
|||
Description = "Cleaning tasks to be executed", |
|||
Args = new Dictionary<string, object>(), |
|||
Status = JobStatus.Running, |
|||
BeginTime = DateTime.Now, |
|||
CreationTime = DateTime.Now, |
|||
Cron = _options.JobCleanCronExpression, |
|||
JobType = JobType.Period, |
|||
Priority = JobPriority.High, |
|||
Type = typeof(BackgroundCleaningJob).AssemblyQualifiedName, |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Internal; |
|||
|
|||
[Dependency(TryRegister = true)] |
|||
internal class InMemoryJobStore : IJobStore, ISingletonDependency |
|||
{ |
|||
private readonly List<JobInfo> _memoryJobStore; |
|||
|
|||
public InMemoryJobStore() |
|||
{ |
|||
_memoryJobStore = new List<JobInfo>(); |
|||
} |
|||
|
|||
public Task<List<JobInfo>> GetAllPeriodTasksAsync(CancellationToken cancellationToken = default) |
|||
{ |
|||
var jobs = _memoryJobStore |
|||
.Where(x => x.JobType == JobType.Period && x.Status == JobStatus.Running) |
|||
.OrderByDescending(x => x.Priority) |
|||
.ToList(); |
|||
|
|||
return Task.FromResult(jobs); |
|||
} |
|||
|
|||
public Task<List<JobInfo>> GetWaitingListAsync(int maxResultCount, CancellationToken cancellationToken = default) |
|||
{ |
|||
var now = DateTime.Now; |
|||
var jobs = _memoryJobStore |
|||
.Where(x => !x.IsAbandoned && x.JobType != JobType.Period && x.Status == JobStatus.Running) |
|||
.OrderByDescending(x => x.Priority) |
|||
.ThenBy(x => x.TryCount) |
|||
.ThenBy(x => x.NextRunTime) |
|||
.Take(maxResultCount) |
|||
.ToList(); |
|||
|
|||
return Task.FromResult(jobs); |
|||
} |
|||
|
|||
public Task<JobInfo> FindAsync(Guid jobId) |
|||
{ |
|||
var job = _memoryJobStore.FirstOrDefault(x => x.Id.Equals(jobId)); |
|||
return Task.FromResult(job); |
|||
} |
|||
|
|||
public Task StoreAsync(JobInfo jobInfo) |
|||
{ |
|||
var job = _memoryJobStore.FirstOrDefault(x => x.Id.Equals(jobInfo.Id)); |
|||
if (job != null) |
|||
{ |
|||
job.NextRunTime = jobInfo.NextRunTime; |
|||
job.LastRunTime = jobInfo.LastRunTime; |
|||
job.Status = jobInfo.Status; |
|||
job.TriggerCount = jobInfo.TriggerCount; |
|||
job.TryCount = jobInfo.TryCount; |
|||
job.IsAbandoned = jobInfo.IsAbandoned; |
|||
} |
|||
else |
|||
{ |
|||
_memoryJobStore.Add(jobInfo); |
|||
} |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task StoreLogAsync(JobEventData eventData) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task CleanupAsync(int maxResultCount, TimeSpan jobExpiratime, CancellationToken cancellationToken = default) |
|||
{ |
|||
var expiratime = DateTime.Now - jobExpiratime; |
|||
|
|||
var expriaJobs = _memoryJobStore.Where( |
|||
x => x.Status == JobStatus.Completed && |
|||
expiratime.CompareTo(x.LastRunTime ?? x.EndTime ?? x.CreationTime) <= 0) |
|||
.Take(maxResultCount); |
|||
|
|||
_memoryJobStore.RemoveAll(expriaJobs); |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Immutable; |
|||
using System.Linq; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Internal; |
|||
|
|||
internal class JobEventProvider : IJobEventProvider, ISingletonDependency |
|||
{ |
|||
private readonly Lazy<List<IJobEvent>> _lazyEvents; |
|||
private List<IJobEvent> _events => _lazyEvents.Value; |
|||
|
|||
private readonly IServiceProvider _serviceProvider; |
|||
private readonly AbpBackgroundTasksOptions _options; |
|||
public JobEventProvider( |
|||
IOptions<AbpBackgroundTasksOptions> options, |
|||
IServiceProvider serviceProvider) |
|||
{ |
|||
_options = options.Value; |
|||
_serviceProvider = serviceProvider; |
|||
|
|||
_lazyEvents = new Lazy<List<IJobEvent>>(CreateJobEvents); |
|||
} |
|||
public IReadOnlyCollection<IJobEvent> GetAll() |
|||
{ |
|||
return _events.ToImmutableList(); |
|||
} |
|||
|
|||
private List<IJobEvent> CreateJobEvents() |
|||
{ |
|||
var jobEvents = _options |
|||
.JobMonitors |
|||
.Select(p => _serviceProvider.GetRequiredService(p) as IJobEvent) |
|||
.ToList(); |
|||
|
|||
return jobEvents; |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Internal; |
|||
|
|||
public class JobExecutedEvent : JobEventBase<JobExecutedEvent>, ITransientDependency |
|||
{ |
|||
protected override async Task OnJobAfterExecutedAsync(JobEventContext context) |
|||
{ |
|||
var store = context.ServiceProvider.GetRequiredService<IJobStore>(); |
|||
|
|||
var job = await store.FindAsync(context.EventData.Key); |
|||
if (job != null) |
|||
{ |
|||
job.TriggerCount += 1; |
|||
job.LastRunTime = context.EventData.RunTime; |
|||
job.NextRunTime = context.EventData.NextRunTime; |
|||
job.Result = context.EventData.Result ?? "OK"; |
|||
job.Status = JobStatus.Running; |
|||
|
|||
// 一次性任务执行一次后标记为已完成
|
|||
if (job.JobType == JobType.Once) |
|||
{ |
|||
job.Status = JobStatus.Completed; |
|||
} |
|||
|
|||
// 任务异常后可重试
|
|||
if (context.EventData.Exception != null) |
|||
{ |
|||
job.TryCount += 1; |
|||
job.IsAbandoned = false; |
|||
// 将任务标记为运行中, 会被轮询重新进入队列
|
|||
job.Status = JobStatus.FailedRetry; |
|||
job.Result = context.EventData.Exception.Message; |
|||
|
|||
// 多次异常后需要重新计算优先级
|
|||
if (job.TryCount <= (job.MaxTryCount / 2) && |
|||
job.TryCount > (job.MaxTryCount / 3)) |
|||
{ |
|||
job.Priority = JobPriority.BelowNormal; |
|||
} |
|||
else if (job.TryCount > (job.MaxTryCount / 1.5)) |
|||
{ |
|||
job.Priority = JobPriority.Low; |
|||
} |
|||
|
|||
if (job.TryCount > job.MaxTryCount) |
|||
{ |
|||
job.Status = JobStatus.Stopped; |
|||
job.IsAbandoned = true; |
|||
job.NextRunTime = null; |
|||
|
|||
await RemoveJobAsync(context, job); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// 所有任务达到上限则标记已完成
|
|||
if (job.MaxCount > 0 && job.TriggerCount >= job.MaxCount) |
|||
{ |
|||
job.Status = JobStatus.Completed; |
|||
job.NextRunTime = null; |
|||
|
|||
await RemoveJobAsync(context, job); |
|||
} |
|||
} |
|||
|
|||
await store.StoreAsync(job); |
|||
} |
|||
} |
|||
|
|||
private async Task RemoveJobAsync(JobEventContext context, JobInfo jobInfo) |
|||
{ |
|||
var jobScheduler = context.ServiceProvider.GetRequiredService<IJobScheduler>(); |
|||
await jobScheduler.RemoveAsync(jobInfo); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks.Internal; |
|||
|
|||
/// <summary>
|
|||
/// 存储任务日志
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 任务类型标记了<see cref="DisableAuditingAttribute"/> 特性则不会记录日志
|
|||
/// </remarks>
|
|||
public class JobLogEvent : JobEventBase<JobLogEvent>, ITransientDependency |
|||
{ |
|||
protected async override Task OnJobAfterExecutedAsync(JobEventContext context) |
|||
{ |
|||
if (context.EventData.Type.IsDefined(typeof(DisableAuditingAttribute), true)) |
|||
{ |
|||
return; |
|||
} |
|||
var store = context.ServiceProvider.GetRequiredService<IJobStore>(); |
|||
|
|||
await store.StoreLogAsync(context.EventData); |
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.DistributedLocking; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundTasks; |
|||
|
|||
public class JobRunnableExecuter : IJobRunnableExecuter, ISingletonDependency |
|||
{ |
|||
protected const string LockKeyFormat = "p:{0},job:{1},key:{2}"; |
|||
|
|||
public async virtual Task ExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
Guid? tenantId = null; |
|||
if (context.JobData.TryGetValue(nameof(IMultiTenant.TenantId), out var tenant) && |
|||
Guid.TryParse(tenant?.ToString(), out var tid)) |
|||
{ |
|||
tenantId = tid; |
|||
} |
|||
|
|||
var currentTenant = context.ServiceProvider.GetRequiredService<ICurrentTenant>(); |
|||
using (currentTenant.Change(tenantId)) |
|||
{ |
|||
context.JobData.TryGetValue(nameof(JobInfo.LockTimeOut), out var lockTime); |
|||
|
|||
// 某些提供者如果无法保证锁一致性, 那么需要用分布式锁
|
|||
if (lockTime != null && (lockTime is int time && time > 0)) |
|||
{ |
|||
var jobId = context.JobData.GetOrDefault(nameof(JobInfo.Id)); |
|||
var jobLockKey = string.Format(LockKeyFormat, tenantId?.ToString() ?? "Default", context.JobType.Name, jobId); |
|||
var distributedLock = context.ServiceProvider.GetRequiredService<IAbpDistributedLock>(); |
|||
|
|||
var handle = await distributedLock.TryAcquireAsync(jobLockKey, TimeSpan.FromSeconds(time)); |
|||
if (handle == null) |
|||
{ |
|||
// 抛出异常 通过监听器使其重试
|
|||
throw new AbpBackgroundTaskConcurrentException(context.JobType); |
|||
} |
|||
|
|||
await using (handle) |
|||
{ |
|||
await InternalExecuteAsync(context); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
await InternalExecuteAsync(context); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private async Task InternalExecuteAsync(JobRunnableContext context) |
|||
{ |
|||
var jobRunnable = context.ServiceProvider.GetService(context.JobType); |
|||
if (jobRunnable == null) |
|||
{ |
|||
jobRunnable = Activator.CreateInstance(context.JobType); |
|||
} |
|||
await ((IJobRunnable)jobRunnable).ExecuteAsync(context); |
|||
} |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
# LINGYUN.Abp.BackgroundTasks |
|||
|
|||
后台任务(队列)模块,Abp提供的后台作业与后台工作者不支持Cron表达式, 提供可管理的后台任务(队列)功能. |
|||
|
|||
实现了**Volo.Abp.BackgroundJobs.IBackgroundJobManager**, 意味着您也能通过框架后台作业接口添加新作业. |
|||
|
|||
## 任务类别 |
|||
|
|||
* JobType.Once: 一次性任务, 此类型只会被执行一次, 适用于邮件通知等场景 |
|||
* JobType.Period: 周期性任务, 此类型任务会根据Cron表达式来决定运行方式, 适用于报表分析等场景 |
|||
* JobType.Persistent: 持续性任务, 此类型任务按照给定重复次数、重复间隔运行, 适用于接口压测等场景 |
|||
|
|||
## 配置使用 |
|||
|
|||
模块按需引用 |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpBackgroundTasksModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
// other |
|||
} |
|||
``` |
|||
|
|||
```csharp |
|||
public class DemoClass |
|||
{ |
|||
protected IServiceProvider ServiceProvider { get; } |
|||
|
|||
public DemoClass(IServiceProvider serviceProvider) |
|||
{ |
|||
ServiceProvider = serviceProvider; |
|||
} |
|||
|
|||
public async Task Some() |
|||
{ |
|||
var scheduler = ServiceProvider.GetRequiredService<IJobScheduler>(); |
|||
|
|||
// 将周期性(5秒一次)任务添加到队列 |
|||
await scheduler.QueueAsync(new JobInfo |
|||
{ |
|||
Type = typeof(ConsoleJob).AssemblyQualifiedName, |
|||
Args = new Dictionary<string, object>(), |
|||
Name = "Test-Console-Period", |
|||
Group = "Test", |
|||
Description = "Test-Console", |
|||
Id = Guid.NewGuid(), |
|||
JobType = JobType.Period, |
|||
Priority = Volo.Abp.BackgroundJobs.BackgroundJobPriority.Low, |
|||
Cron = "0/5 * * * * ? ", |
|||
TryCount = 10, |
|||
Status = JobStatus.Running, |
|||
// 定义此字段处理并发 |
|||
LockTimeOut = 120, |
|||
}); |
|||
|
|||
// 将一次性任务添加到队列, 将在10(Interval)秒后被执行 |
|||
await scheduler.QueueAsync(new JobInfo |
|||
{ |
|||
Type = typeof(ConsoleJob).AssemblyQualifiedName, |
|||
Args = new Dictionary<string, object>(), |
|||
Name = "Test-Console-Once", |
|||
Group = "Test", |
|||
Description = "Test-Console", |
|||
Id = Guid.NewGuid(), |
|||
JobType = JobType.Once, |
|||
Priority = Volo.Abp.BackgroundJobs.BackgroundJobPriority.Low, |
|||
Interval = 10, |
|||
TryCount = 10, |
|||
Status = JobStatus.Running, |
|||
}); |
|||
|
|||
// 将持续性任务添加到队列, 将在10(Interval)秒后被执行, 最大执行5(MaxCount)次 |
|||
await scheduler.QueueAsync(new JobInfo |
|||
{ |
|||
Type = typeof(ConsoleJob).AssemblyQualifiedName, |
|||
Args = new Dictionary<string, object>(), |
|||
Name = "Test-Console-Persistent", |
|||
Group = "Test", |
|||
Description = "Test-Console", |
|||
Id = Guid.NewGuid(), |
|||
JobType = JobType.Persistent, |
|||
Priority = Volo.Abp.BackgroundJobs.BackgroundJobPriority.Low, |
|||
Interval = 10, |
|||
TryCount = 10, |
|||
MaxCount = 5, |
|||
Status = JobStatus.Running, |
|||
}); |
|||
|
|||
// 同样可以把框架后台作业添加到作业调度器中, 不需要更改使用习惯 |
|||
var backgroundJobManager = ServiceProvider.GetRequiredService<IBackgroundJobManager>(); |
|||
await jobManager.EnqueueAsync( |
|||
new SmsJobArgs |
|||
{ |
|||
PhoneNumber = "13800138000", |
|||
Message = "来自框架的后台工作者" |
|||
}, |
|||
BackgroundJobPriority.High, |
|||
TimeSpan.FromSeconds(10)); |
|||
} |
|||
} |
|||
|
|||
public class SmsJobArgs |
|||
{ |
|||
public string PhoneNumber { get; set; } |
|||
public string Message { get; set; } |
|||
} |
|||
|
|||
public class SmsJob : AsyncBackgroundJob<SmsJobArgs>, ITransientDependency |
|||
{ |
|||
public override Task ExecuteAsync(SmsJobArgs args) |
|||
{ |
|||
Console.WriteLine($"Send sms message: {args.Message}"); |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
``` |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,20 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Authorization" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Volo.Abp.Ddd.Application.Contracts" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.TaskManagement.Domain.Shared\LINGYUN.Abp.TaskManagement.Domain.Shared.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,36 @@ |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Volo.Abp.Validation; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
public class BackgroundJobInfoCreateDto : BackgroundJobInfoCreateOrUpdateDto |
|||
{ |
|||
/// <summary>
|
|||
/// 任务名称
|
|||
/// </summary>
|
|||
[Required] |
|||
[DynamicStringLength(typeof(BackgroundJobInfoConsts), nameof(BackgroundJobInfoConsts.MaxNameLength))] |
|||
public string Name { get; set; } |
|||
/// <summary>
|
|||
/// 任务分组
|
|||
/// </summary>
|
|||
[Required] |
|||
[DynamicStringLength(typeof(BackgroundJobInfoConsts), nameof(BackgroundJobInfoConsts.MaxGroupLength))] |
|||
public string Group { get; set; } |
|||
/// <summary>
|
|||
/// 任务类型
|
|||
/// </summary>
|
|||
[Required] |
|||
[DynamicStringLength(typeof(BackgroundJobInfoConsts), nameof(BackgroundJobInfoConsts.MaxTypeLength))] |
|||
public string Type { get; set; } |
|||
/// <summary>
|
|||
/// 开始时间
|
|||
/// </summary>
|
|||
[Required] |
|||
public DateTime BeginTime { get; set; } |
|||
/// <summary>
|
|||
/// 结束时间
|
|||
/// </summary>
|
|||
public DateTime? EndTime { get; set; } |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
using LINGYUN.Abp.BackgroundTasks; |
|||
using System; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.Validation; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
public abstract class BackgroundJobInfoCreateOrUpdateDto |
|||
{ |
|||
/// <summary>
|
|||
/// 是否启用
|
|||
/// </summary>
|
|||
public bool IsEnabled { get; set; } |
|||
/// <summary>
|
|||
/// 任务参数
|
|||
/// </summary>
|
|||
public ExtraPropertyDictionary Args { get; set; } |
|||
/// <summary>
|
|||
/// 描述
|
|||
/// </summary>
|
|||
[DynamicStringLength(typeof(BackgroundJobInfoConsts), nameof(BackgroundJobInfoConsts.MaxDescriptionLength))] |
|||
public string Description { get; set; } |
|||
/// <summary>
|
|||
/// 任务类别
|
|||
/// </summary>
|
|||
public JobType JobType { get; set; } |
|||
/// <summary>
|
|||
/// Cron表达式,如果是持续任务需要指定
|
|||
/// </summary>
|
|||
[DynamicStringLength(typeof(BackgroundJobInfoConsts), nameof(BackgroundJobInfoConsts.MaxCronLength))] |
|||
public string Cron { get; set; } |
|||
/// <summary>
|
|||
/// 失败重试上限
|
|||
/// 默认:50
|
|||
/// </summary>
|
|||
public int MaxTryCount { get; set; } |
|||
/// <summary>
|
|||
/// 最大执行次数
|
|||
/// 默认:0, 无限制
|
|||
/// </summary>
|
|||
public int MaxCount { get; set; } |
|||
/// <summary>
|
|||
/// 间隔时间,单位秒,与Cron表达式冲突
|
|||
/// 默认: 300
|
|||
/// </summary>
|
|||
public int Interval { get; set; } = 300; |
|||
/// <summary>
|
|||
/// 任务优先级
|
|||
/// </summary>
|
|||
public JobPriority Priority { get; set; } |
|||
/// <summary>
|
|||
/// 任务独占超时时长(秒)
|
|||
/// 0或更小不生效
|
|||
/// </summary>
|
|||
public int LockTimeOut { get; set; } |
|||
} |
|||
@ -0,0 +1,104 @@ |
|||
using LINGYUN.Abp.BackgroundTasks; |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.Domain.Entities; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
public class BackgroundJobInfoDto : ExtensibleAuditedEntityDto<Guid>, IHasConcurrencyStamp |
|||
{ |
|||
public string ConcurrencyStamp { get; set; } |
|||
/// <summary>
|
|||
/// 任务名称
|
|||
/// </summary>
|
|||
public string Name { get; set; } |
|||
/// <summary>
|
|||
/// 任务分组
|
|||
/// </summary>
|
|||
public string Group { get; set; } |
|||
/// <summary>
|
|||
/// 任务类型
|
|||
/// </summary>
|
|||
public string Type { get; set; } |
|||
/// <summary>
|
|||
/// 返回参数
|
|||
/// </summary>
|
|||
public string Result { get; set; } |
|||
/// <summary>
|
|||
/// 任务参数
|
|||
/// </summary>
|
|||
public ExtraPropertyDictionary Args { get; set; } |
|||
/// <summary>
|
|||
/// 任务状态
|
|||
/// </summary>
|
|||
public JobStatus Status { get; set; } |
|||
/// <summary>
|
|||
/// 描述
|
|||
/// </summary>
|
|||
public string Description { get; set; } |
|||
/// <summary>
|
|||
/// 开始时间
|
|||
/// </summary>
|
|||
public DateTime BeginTime { get; set; } |
|||
/// <summary>
|
|||
/// 结束时间
|
|||
/// </summary>
|
|||
public DateTime? EndTime { get; set; } |
|||
/// <summary>
|
|||
/// 上次运行时间
|
|||
/// </summary>
|
|||
public DateTime? LastRunTime { get; set; } |
|||
/// <summary>
|
|||
/// 下一次执行时间
|
|||
/// </summary>
|
|||
public DateTime? NextRunTime { get; set; } |
|||
/// <summary>
|
|||
/// 任务类别
|
|||
/// </summary>
|
|||
public JobType JobType { get; set; } |
|||
/// <summary>
|
|||
/// Cron表达式,如果是持续任务需要指定
|
|||
/// </summary>
|
|||
public string Cron { get; set; } |
|||
/// <summary>
|
|||
/// 触发次数
|
|||
/// </summary>
|
|||
public int TriggerCount { get; set; } |
|||
/// <summary>
|
|||
/// 失败重试次数
|
|||
/// </summary>
|
|||
public int TryCount { get; set; } |
|||
/// <summary>
|
|||
/// 失败重试上限
|
|||
/// 默认:50
|
|||
/// </summary>
|
|||
public int MaxTryCount { get; set; } |
|||
/// <summary>
|
|||
/// 最大执行次数
|
|||
/// 默认:0, 无限制
|
|||
/// </summary>
|
|||
public int MaxCount { get; set; } |
|||
/// <summary>
|
|||
/// 连续失败且不会再次执行
|
|||
/// </summary>
|
|||
public bool IsAbandoned { get; set; } |
|||
/// <summary>
|
|||
/// 是否启用
|
|||
/// </summary>
|
|||
public bool IsEnabled { get; set; } |
|||
/// <summary>
|
|||
/// 间隔时间,单位秒,与Cron表达式冲突
|
|||
/// 默认: 300
|
|||
/// </summary>
|
|||
public int Interval { get; set; } = 300; |
|||
/// <summary>
|
|||
/// 任务优先级
|
|||
/// </summary>
|
|||
public JobPriority Priority { get; set; } |
|||
/// <summary>
|
|||
/// 任务独占超时时长(秒)
|
|||
/// 0或更小不生效
|
|||
/// </summary>
|
|||
public int LockTimeOut { get; set; } |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
using LINGYUN.Abp.BackgroundTasks; |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
public class BackgroundJobInfoGetListInput: PagedAndSortedResultRequestDto |
|||
{ |
|||
/// <summary>
|
|||
/// 其他过滤条件
|
|||
/// </summary>
|
|||
public string Filter { get; set; } |
|||
/// <summary>
|
|||
/// 任务名称
|
|||
/// </summary>
|
|||
public string Name { get; set; } |
|||
/// <summary>
|
|||
/// 任务分组
|
|||
/// </summary>
|
|||
public string Group { get; set; } |
|||
/// <summary>
|
|||
/// 任务类型
|
|||
/// </summary>
|
|||
public string Type { get; set; } |
|||
/// <summary>
|
|||
/// 任务状态
|
|||
/// </summary>
|
|||
public JobStatus? Status { get; set; } |
|||
/// <summary>
|
|||
/// 开始时间
|
|||
/// </summary>
|
|||
public DateTime? BeginTime { get; set; } |
|||
/// <summary>
|
|||
/// 结束时间
|
|||
/// </summary>
|
|||
public DateTime? EndTime { get; set; } |
|||
/// <summary>
|
|||
/// 上次起始触发时间
|
|||
/// </summary>
|
|||
public DateTime? BeginLastRunTime { get; set; } |
|||
/// <summary>
|
|||
/// 上次截止触发时间
|
|||
/// </summary>
|
|||
public DateTime? EndLastRunTime { get; set; } |
|||
/// <summary>
|
|||
/// 起始创建时间
|
|||
/// </summary>
|
|||
public DateTime? BeginCreationTime { get; set; } |
|||
/// <summary>
|
|||
/// 截止创建时间
|
|||
/// </summary>
|
|||
public DateTime? EndCreationTime { get; set; } |
|||
/// <summary>
|
|||
/// 是否已放弃任务
|
|||
/// </summary>
|
|||
public bool? IsAbandoned { get; set; } |
|||
/// <summary>
|
|||
/// 任务类型
|
|||
/// </summary>
|
|||
public JobType? JobType { get; set; } |
|||
/// <summary>
|
|||
/// 优先级
|
|||
/// </summary>
|
|||
public JobPriority? Priority { get; set; } |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Volo.Abp.Domain.Entities; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
public class BackgroundJobInfoUpdateDto : BackgroundJobInfoCreateOrUpdateDto, IHasConcurrencyStamp |
|||
{ |
|||
public string ConcurrencyStamp { get; set; } |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
public class BackgroundJobLogDto : EntityDto<long> |
|||
{ |
|||
public string JobName { get; set; } |
|||
public string JobGroup { get; set; } |
|||
public string JobType { get; set; } |
|||
public string Message { get; set; } |
|||
public DateTime RunTime { get; set; } |
|||
public string Exception { get; set; } |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
public class BackgroundJobLogGetListInput : PagedAndSortedResultRequestDto |
|||
{ |
|||
public Guid? JobId { get; set; } |
|||
/// <summary>
|
|||
/// 其他过滤条件
|
|||
/// </summary>
|
|||
public string Filter { get; set; } |
|||
/// <summary>
|
|||
/// 存在异常
|
|||
/// </summary>
|
|||
public bool? HasExceptions { get; set; } |
|||
/// <summary>
|
|||
/// 任务名称
|
|||
/// </summary>
|
|||
public string Name { get; set; } |
|||
/// <summary>
|
|||
/// 任务分组
|
|||
/// </summary>
|
|||
public string Group { get; set; } |
|||
/// <summary>
|
|||
/// 任务类型
|
|||
/// </summary>
|
|||
public string Type { get; set; } |
|||
/// <summary>
|
|||
/// 开始触发时间
|
|||
/// </summary>
|
|||
public DateTime? BeginRunTime { get; set; } |
|||
/// <summary>
|
|||
/// 结束触发时间
|
|||
/// </summary>
|
|||
public DateTime? EndRunTime { get; set; } |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
public interface IBackgroundJobInfoAppService : |
|||
ICrudAppService< |
|||
BackgroundJobInfoDto, |
|||
Guid, |
|||
BackgroundJobInfoGetListInput, |
|||
BackgroundJobInfoCreateDto, |
|||
BackgroundJobInfoUpdateDto> |
|||
{ |
|||
Task TriggerAsync(Guid id); |
|||
|
|||
Task PauseAsync(Guid id); |
|||
|
|||
Task ResumeAsync(Guid id); |
|||
|
|||
Task StopAsync(Guid id); |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
public interface IBackgroundJobLogAppService : |
|||
IReadOnlyAppService< |
|||
BackgroundJobLogDto, |
|||
long, |
|||
BackgroundJobLogGetListInput>, |
|||
IDeleteAppService<long> |
|||
{ |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
using LINGYUN.Abp.TaskManagement.Localization; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement.Permissions; |
|||
|
|||
public class TaskManagementPermissionDefinitionProvider : PermissionDefinitionProvider |
|||
{ |
|||
public override void Define(IPermissionDefinitionContext context) |
|||
{ |
|||
var group = context.AddGroup( |
|||
TaskManagementPermissions.GroupName, |
|||
L("Permissions:TaskManagement"), |
|||
MultiTenancySides.Host); |
|||
|
|||
var backgroundJobs = group.AddPermission( |
|||
TaskManagementPermissions.BackgroundJobs.Default, |
|||
L("Permissions:BackgroundJobs"), |
|||
MultiTenancySides.Host); |
|||
backgroundJobs.AddChild( |
|||
TaskManagementPermissions.BackgroundJobs.Create, |
|||
L("Permissions:CreateJob"), |
|||
MultiTenancySides.Host); |
|||
backgroundJobs.AddChild( |
|||
TaskManagementPermissions.BackgroundJobs.Update, |
|||
L("Permissions:UpdateJob"), |
|||
MultiTenancySides.Host); |
|||
backgroundJobs.AddChild( |
|||
TaskManagementPermissions.BackgroundJobs.Delete, |
|||
L("Permissions:DeleteJob"), |
|||
MultiTenancySides.Host); |
|||
backgroundJobs.AddChild( |
|||
TaskManagementPermissions.BackgroundJobs.Trigger, |
|||
L("Permissions:TriggerJob"), |
|||
MultiTenancySides.Host); |
|||
backgroundJobs.AddChild( |
|||
TaskManagementPermissions.BackgroundJobs.Pause, |
|||
L("Permissions:PauseJob"), |
|||
MultiTenancySides.Host); |
|||
backgroundJobs.AddChild( |
|||
TaskManagementPermissions.BackgroundJobs.Resume, |
|||
L("Permissions:ResumeJob"), |
|||
MultiTenancySides.Host); |
|||
backgroundJobs.AddChild( |
|||
TaskManagementPermissions.BackgroundJobs.Stop, |
|||
L("Permissions:StopJob"), |
|||
MultiTenancySides.Host); |
|||
|
|||
var backgroundJobLogs = group.AddPermission( |
|||
TaskManagementPermissions.BackgroundJobLogs.Default, |
|||
L("Permissions:BackgroundJobLogs"), |
|||
MultiTenancySides.Host); |
|||
backgroundJobLogs.AddChild( |
|||
TaskManagementPermissions.BackgroundJobLogs.Delete, |
|||
L("Permissions:DeleteJobLogs"), |
|||
MultiTenancySides.Host); |
|||
} |
|||
|
|||
private ILocalizableString L(string name) |
|||
{ |
|||
return LocalizableString.Create<TaskManagementResource>(name); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
namespace LINGYUN.Abp.TaskManagement.Permissions; |
|||
|
|||
public static class TaskManagementPermissions |
|||
{ |
|||
public const string GroupName = "TaskManagement"; |
|||
|
|||
public static class BackgroundJobs |
|||
{ |
|||
public const string Default = GroupName + ".BackgroundJobs"; |
|||
public const string Create = Default + ".Create"; |
|||
public const string Update = Default + ".Update"; |
|||
public const string Delete = Default + ".Delete"; |
|||
public const string Trigger = Default + ".Trigger"; |
|||
public const string Pause = Default + ".Pause"; |
|||
public const string Resume = Default + ".Resume"; |
|||
public const string Stop = Default + ".Stop"; |
|||
} |
|||
|
|||
public static class BackgroundJobLogs |
|||
{ |
|||
public const string Default = GroupName + ".BackgroundJobLogs"; |
|||
public const string Delete = Default + ".Delete"; |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using Volo.Abp.Application; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
[DependsOn(typeof(TaskManagementDomainSharedModule))] |
|||
[DependsOn(typeof(AbpDddApplicationContractsModule))] |
|||
public class TaskManagementApplicationContractsModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
public static class TaskManagementRemoteServiceConsts |
|||
{ |
|||
public const string RemoteServiceName = "TaskManagement"; |
|||
|
|||
public const string ModuleName = "task-management"; |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,20 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Ddd.Application" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.TaskManagement.Application.Contracts\LINGYUN.Abp.TaskManagement.Application.Contracts.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.TaskManagement.Domain\LINGYUN.Abp.TaskManagement.Domain.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,176 @@ |
|||
using LINGYUN.Abp.BackgroundTasks; |
|||
using LINGYUN.Abp.TaskManagement.Permissions; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Data; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
[Authorize(TaskManagementPermissions.BackgroundJobs.Default)] |
|||
public class BackgroundJobInfoAppService : TaskManagementApplicationService, IBackgroundJobInfoAppService |
|||
{ |
|||
protected BackgroundJobManager BackgroundJobManager { get; } |
|||
protected IBackgroundJobInfoRepository BackgroundJobInfoRepository { get; } |
|||
|
|||
public BackgroundJobInfoAppService( |
|||
BackgroundJobManager backgroundJobManager, |
|||
IBackgroundJobInfoRepository backgroundJobInfoRepository) |
|||
{ |
|||
BackgroundJobManager = backgroundJobManager; |
|||
BackgroundJobInfoRepository = backgroundJobInfoRepository; |
|||
} |
|||
|
|||
[Authorize(TaskManagementPermissions.BackgroundJobs.Create)] |
|||
public virtual async Task<BackgroundJobInfoDto> CreateAsync(BackgroundJobInfoCreateDto input) |
|||
{ |
|||
if (await BackgroundJobInfoRepository.CheckNameAsync(input.Group, input.Name)) |
|||
{ |
|||
throw new BusinessException(TaskManagementErrorCodes.JobNameAlreadyExists) |
|||
.WithData("Group", input.Group) |
|||
.WithData("Name", input.Name); |
|||
} |
|||
|
|||
var backgroundJobInfo = new BackgroundJobInfo( |
|||
GuidGenerator.Create(), |
|||
input.Name, |
|||
input.Group, |
|||
input.Type, |
|||
input.Args, |
|||
input.BeginTime, |
|||
input.EndTime, |
|||
input.Priority, |
|||
input.MaxCount, |
|||
input.MaxTryCount); |
|||
|
|||
UpdateByInput(backgroundJobInfo, input); |
|||
|
|||
await BackgroundJobInfoRepository.InsertAsync(backgroundJobInfo, autoSave: true); |
|||
|
|||
if (backgroundJobInfo.IsEnabled && backgroundJobInfo.JobType == JobType.Period) |
|||
{ |
|||
await BackgroundJobManager.QueueAsync(backgroundJobInfo); |
|||
} |
|||
|
|||
return ObjectMapper.Map<BackgroundJobInfo, BackgroundJobInfoDto>(backgroundJobInfo); |
|||
} |
|||
|
|||
[Authorize(TaskManagementPermissions.BackgroundJobs.Delete)] |
|||
public virtual async Task DeleteAsync(Guid id) |
|||
{ |
|||
var backgroundJobInfo = await BackgroundJobInfoRepository.GetAsync(id); |
|||
|
|||
await BackgroundJobManager.DeleteAsync(backgroundJobInfo); |
|||
} |
|||
|
|||
public virtual async Task<BackgroundJobInfoDto> GetAsync(Guid id) |
|||
{ |
|||
var backgroundJobInfo = await BackgroundJobInfoRepository.GetAsync(id); |
|||
|
|||
return ObjectMapper.Map<BackgroundJobInfo, BackgroundJobInfoDto>(backgroundJobInfo); |
|||
} |
|||
|
|||
public virtual async Task<PagedResultDto<BackgroundJobInfoDto>> GetListAsync(BackgroundJobInfoGetListInput input) |
|||
{ |
|||
var filter = new BackgroundJobInfoFilter |
|||
{ |
|||
IsAbandoned = input.IsAbandoned, |
|||
JobType = input.JobType, |
|||
BeginCreationTime = input.BeginCreationTime, |
|||
EndCreationTime = input.EndCreationTime, |
|||
BeginLastRunTime = input.BeginLastRunTime, |
|||
EndLastRunTime = input.EndLastRunTime, |
|||
BeginTime = input.BeginTime, |
|||
EndTime = input.EndTime, |
|||
Filter = input.Filter, |
|||
Group = input.Group, |
|||
Name = input.Name, |
|||
Priority = input.Priority, |
|||
Status = input.Status, |
|||
Type = input.Type |
|||
}; |
|||
var totalCount = await BackgroundJobInfoRepository.GetCountAsync(filter); |
|||
var backgroundJobInfos = await BackgroundJobInfoRepository.GetListAsync( |
|||
filter, input.Sorting, input.MaxResultCount, input.SkipCount); |
|||
|
|||
return new PagedResultDto<BackgroundJobInfoDto>(totalCount, |
|||
ObjectMapper.Map<List<BackgroundJobInfo>, List<BackgroundJobInfoDto>>(backgroundJobInfos)); |
|||
} |
|||
|
|||
[Authorize(TaskManagementPermissions.BackgroundJobs.Pause)] |
|||
public virtual async Task PauseAsync(Guid id) |
|||
{ |
|||
var backgroundJobInfo = await BackgroundJobInfoRepository.GetAsync(id); |
|||
|
|||
await BackgroundJobManager.PauseAsync(backgroundJobInfo); |
|||
} |
|||
|
|||
[Authorize(TaskManagementPermissions.BackgroundJobs.Resume)] |
|||
public virtual async Task ResumeAsync(Guid id) |
|||
{ |
|||
var backgroundJobInfo = await BackgroundJobInfoRepository.GetAsync(id); |
|||
|
|||
await BackgroundJobManager.ResumeAsync(backgroundJobInfo); |
|||
} |
|||
|
|||
[Authorize(TaskManagementPermissions.BackgroundJobs.Trigger)] |
|||
public virtual async Task TriggerAsync(Guid id) |
|||
{ |
|||
var backgroundJobInfo = await BackgroundJobInfoRepository.GetAsync(id); |
|||
|
|||
await BackgroundJobManager.TriggerAsync(backgroundJobInfo); |
|||
} |
|||
|
|||
[Authorize(TaskManagementPermissions.BackgroundJobs.Stop)] |
|||
public virtual async Task StopAsync(Guid id) |
|||
{ |
|||
var backgroundJobInfo = await BackgroundJobInfoRepository.GetAsync(id); |
|||
|
|||
await BackgroundJobManager.StopAsync(backgroundJobInfo); |
|||
} |
|||
|
|||
[Authorize(TaskManagementPermissions.BackgroundJobs.Update)] |
|||
public virtual async Task<BackgroundJobInfoDto> UpdateAsync(Guid id, BackgroundJobInfoUpdateDto input) |
|||
{ |
|||
var backgroundJobInfo = await BackgroundJobInfoRepository.GetAsync(id); |
|||
|
|||
var resetJob = backgroundJobInfo.JobType == input.JobType; |
|||
|
|||
UpdateByInput(backgroundJobInfo, input); |
|||
|
|||
backgroundJobInfo.SetConcurrencyStampIfNotNull(input.ConcurrencyStamp); |
|||
|
|||
await BackgroundJobManager.UpdateAsync(backgroundJobInfo, resetJob); |
|||
|
|||
return ObjectMapper.Map<BackgroundJobInfo, BackgroundJobInfoDto>(backgroundJobInfo); |
|||
} |
|||
|
|||
protected virtual void UpdateByInput(BackgroundJobInfo backgroundJobInfo, BackgroundJobInfoCreateOrUpdateDto input) |
|||
{ |
|||
backgroundJobInfo.IsEnabled = input.IsEnabled; |
|||
backgroundJobInfo.LockTimeOut = input.LockTimeOut; |
|||
backgroundJobInfo.Description = input.Description; |
|||
backgroundJobInfo.MaxCount = input.MaxCount; |
|||
backgroundJobInfo.MaxTryCount = input.MaxTryCount; |
|||
|
|||
backgroundJobInfo.Args.RemoveAll(x => !input.Args.ContainsKey(x.Key)); |
|||
backgroundJobInfo.Args.AddIfNotContains(input.Args); |
|||
|
|||
backgroundJobInfo.SetPriority(input.Priority); |
|||
switch (input.JobType) |
|||
{ |
|||
case JobType.Once: |
|||
backgroundJobInfo.SetOnceJob(input.Interval); |
|||
break; |
|||
case JobType.Persistent: |
|||
backgroundJobInfo.SetPersistentJob(input.Interval); |
|||
break; |
|||
case JobType.Period: |
|||
backgroundJobInfo.SetPeriodJob(input.Cron); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
using LINGYUN.Abp.TaskManagement.Permissions; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
[Authorize(TaskManagementPermissions.BackgroundJobLogs.Default)] |
|||
public class BackgroundJobLogAppService : TaskManagementApplicationService, IBackgroundJobLogAppService |
|||
{ |
|||
protected IBackgroundJobLogRepository BackgroundJobLogRepository { get; } |
|||
|
|||
public BackgroundJobLogAppService( |
|||
IBackgroundJobLogRepository backgroundJobLogRepository) |
|||
{ |
|||
BackgroundJobLogRepository = backgroundJobLogRepository; |
|||
} |
|||
|
|||
[Authorize(TaskManagementPermissions.BackgroundJobLogs.Delete)] |
|||
public virtual Task DeleteAsync(long id) |
|||
{ |
|||
return BackgroundJobLogRepository.DeleteAsync(id); |
|||
} |
|||
|
|||
public virtual async Task<BackgroundJobLogDto> GetAsync(long id) |
|||
{ |
|||
var backgroundJobLog = await BackgroundJobLogRepository.GetAsync(id); |
|||
|
|||
return ObjectMapper.Map<BackgroundJobLog, BackgroundJobLogDto>(backgroundJobLog); |
|||
} |
|||
|
|||
public virtual async Task<PagedResultDto<BackgroundJobLogDto>> GetListAsync(BackgroundJobLogGetListInput input) |
|||
{ |
|||
var filter = new BackgroundJobLogFilter |
|||
{ |
|||
BeginRunTime = input.BeginRunTime, |
|||
EndRunTime = input.EndRunTime, |
|||
HasExceptions = input.HasExceptions, |
|||
Filter = input.Filter, |
|||
Group = input.Group, |
|||
Name = input.Name, |
|||
Type = input.Type |
|||
}; |
|||
|
|||
var totalCount = await BackgroundJobLogRepository.GetCountAsync(filter, input.JobId); |
|||
var backgroundJobLogs = await BackgroundJobLogRepository.GetListAsync( |
|||
filter, input.JobId, input.Sorting, input.MaxResultCount, input.SkipCount); |
|||
|
|||
return new PagedResultDto<BackgroundJobLogDto>(totalCount, |
|||
ObjectMapper.Map<List<BackgroundJobLog>, List<BackgroundJobLogDto>>(backgroundJobLogs)); |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using AutoMapper; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
public class TaskManagementApplicationMapperProfile : Profile |
|||
{ |
|||
public TaskManagementApplicationMapperProfile() |
|||
{ |
|||
CreateMap<BackgroundJobInfo, BackgroundJobInfoDto>(); |
|||
CreateMap<BackgroundJobLog, BackgroundJobLogDto>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Application; |
|||
using Volo.Abp.AutoMapper; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
[DependsOn(typeof(TaskManagementApplicationContractsModule))] |
|||
[DependsOn(typeof(TaskManagementDomainModule))] |
|||
[DependsOn(typeof(AbpDddApplicationModule))] |
|||
public class TaskManagementApplicationModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddAutoMapperObjectMapper<TaskManagementApplicationModule>(); |
|||
|
|||
Configure<AbpAutoMapperOptions>(options => |
|||
{ |
|||
options.AddProfile<TaskManagementApplicationMapperProfile>(validate: true); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using LINGYUN.Abp.TaskManagement.Localization; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace LINGYUN.Abp.TaskManagement; |
|||
|
|||
public abstract class TaskManagementApplicationService : ApplicationService |
|||
{ |
|||
protected TaskManagementApplicationService() |
|||
{ |
|||
LocalizationResource = typeof(TaskManagementResource); |
|||
ObjectMapperContext = typeof(TaskManagementApplicationModule); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue