Browse Source

Fixed the retry mechanism in webhooks and tasks modules

pull/821/head
cKey 3 years ago
parent
commit
dffe7ffe4e
  1. 6
      apps/vue/src/api/webhooks/model/subscriptionsModel.ts
  2. 26
      apps/vue/src/api/webhooks/send-attempts.ts
  3. 12
      apps/vue/src/api/webhooks/subscriptions.ts
  4. 25
      apps/vue/src/components/Table/src/components/AdvancedSearch.vue
  5. 10
      apps/vue/src/components/Table/src/types/advancedSearch.ts
  6. 65
      apps/vue/src/hooks/abp/useDefineSettings.ts
  7. 55
      apps/vue/src/store/modules/settings.ts
  8. 4
      apps/vue/src/utils/http/axios/checkStatus.ts
  9. 7
      apps/vue/src/views/webhooks/send-attempts/components/SendAttemptModal.vue
  10. 102
      apps/vue/src/views/webhooks/send-attempts/components/SendAttemptTable.vue
  11. 28
      apps/vue/src/views/webhooks/send-attempts/datas/ModalData.ts
  12. 12
      apps/vue/src/views/webhooks/subscriptions/components/SubscriptionModal.vue
  13. 54
      apps/vue/src/views/webhooks/subscriptions/components/SubscriptionTable.vue
  14. 22
      apps/vue/src/views/webhooks/subscriptions/datas/ModalData.ts
  15. 7
      apps/vue/src/views/webhooks/subscriptions/datas/TableData.ts
  16. 2
      apps/vue/types/abp.d.ts
  17. 2
      aspnet-core/modules/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs
  18. 2
      aspnet-core/modules/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogManager.cs
  19. 1
      aspnet-core/modules/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditLog.cs
  20. 2
      aspnet-core/modules/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditingStore.cs
  21. 2
      aspnet-core/modules/dynamic-queryable/LINGYUN.Abp.Dynamic.Queryable.Application.Contracts/LINGYUN/Abp/Dynamic/Queryable/Dto/DynamicParamterDto.cs
  22. 6
      aspnet-core/modules/dynamic-queryable/LINGYUN.Abp.Dynamic.Queryable.Application.Contracts/LINGYUN/Abp/Dynamic/Queryable/Dto/ParamterOptionDto.cs
  23. 67
      aspnet-core/modules/dynamic-queryable/LINGYUN.Abp.Dynamic.Queryable.Application/LINGYUN/Abp/Dynamic/Queryable/DynamicQueryableAppService.cs
  24. 2
      aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Application.Contracts/LINGYUN/Abp/OssManagement/Permissions/AbpOssManagementPermissionDefinitionProvider.cs
  25. 4
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.Abstractions/LINGYUN/Abp/BackgroundTasks/JobEventBase.cs
  26. 27
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.Abstractions/LINGYUN/Abp/BackgroundTasks/JobInfo.cs
  27. 2
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.Abstractions/README.md
  28. 55
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.EventBus/LINGYUN/Abp/BackgroundTasks/EventBus/DistributedJobDispatcher.cs
  29. 3
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.Quartz/LINGYUN/Abp/BackgroundTasks/Quartz/QuartzJobCreator.cs
  30. 2
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/BackgroundJobManager.cs
  31. 12
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/BackgroundWorkerManager.cs
  32. 35
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/IJobDispatcher.cs
  33. 7
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/IJobPublisher.cs
  34. 2
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/IJobScheduler.cs
  35. 30
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/Internal/BackgroundPollingJob.cs
  36. 152
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/Internal/JobExecutedEvent.cs
  37. 21
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/Localization/Resources/en.json
  38. 21
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/Localization/Resources/zh-Hans.json
  39. 26
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/NullJobDispatcher.cs
  40. 10
      aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/README.md
  41. 15
      aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Core/LINGYUN/Abp/Webhooks/AbpWebhooksOptions.cs
  42. 18
      aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/DefaultWebhookSender.cs
  43. 2
      aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordGetListInput.cs
  44. 8
      aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/Extensions/WebhookSubscriptionExtensions.cs
  45. 51
      aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordAppService.cs
  46. 93
      aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionAppService.cs
  47. 5
      aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/Resources/en.json
  48. 5
      aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/Resources/zh-Hans.json
  49. 5
      aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/IWebhookSubscriptionRepository.cs
  50. 27
      aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.EntityFrameworkCore/LINGYUN/Abp/WebhooksManagement/EntityFrameworkCore/EfCoreWebhookSubscriptionRepository.cs
  51. 4
      aspnet-core/services/LY.MicroService.WebhooksManagement.HttpApi.Host/WebhooksManagementHttpApiHostModule.Configure.cs

6
apps/vue/src/api/webhooks/model/subscriptionsModel.ts

@ -1,6 +1,7 @@
export interface WebhookSubscription extends CreationAuditedEntityDto<string> { export interface WebhookSubscription extends CreationAuditedEntityDto<string>, IHasConcurrencyStamp {
tenantId?: string; tenantId?: string;
webhookUri: string; webhookUri: string;
description?: string;
secret: string; secret: string;
isActive: boolean; isActive: boolean;
webhooks: string[]; webhooks: string[];
@ -9,6 +10,7 @@ export interface WebhookSubscription extends CreationAuditedEntityDto<string> {
export interface WebhookSubscriptionCreateOrUpdate { export interface WebhookSubscriptionCreateOrUpdate {
webhookUri: string; webhookUri: string;
description?: string;
secret: string; secret: string;
isActive: boolean; isActive: boolean;
webhooks: string[]; webhooks: string[];
@ -17,7 +19,7 @@ export interface WebhookSubscriptionCreateOrUpdate {
export type CreateWebhookSubscription = WebhookSubscriptionCreateOrUpdate; export type CreateWebhookSubscription = WebhookSubscriptionCreateOrUpdate;
export type UpdateWebhookSubscription = WebhookSubscriptionCreateOrUpdate; export interface UpdateWebhookSubscription extends WebhookSubscriptionCreateOrUpdate , IHasConcurrencyStamp {};
export interface WebhookAvailable { export interface WebhookAvailable {
name: string; name: string;

26
apps/vue/src/api/webhooks/send-attempts.ts

@ -26,6 +26,18 @@ export const deleteById = (id: string) => {
}); });
} }
export const deleteMany = (keys: string[]) => {
return defAbpHttp.request<void>({
service: remoteServiceName,
controller: controllerName,
action: 'DeleteManyAsync',
uniqueName: 'DeleteManyAsyncByInput',
data: {
recordIds: keys,
},
});
}
export const getList = (input: WebhookSendAttemptGetListInput) => { export const getList = (input: WebhookSendAttemptGetListInput) => {
return defAbpHttp.request<PagedResultDto<WebhookSendAttempt>>({ return defAbpHttp.request<PagedResultDto<WebhookSendAttempt>>({
service: remoteServiceName, service: remoteServiceName,
@ -46,4 +58,16 @@ export const resend = (id: string) => {
id: id, id: id,
}, },
}); });
} }
export const resendMany = (keys: string[]) => {
return defAbpHttp.request<void>({
service: remoteServiceName,
controller: controllerName,
action: 'ResendManyAsync',
uniqueName: 'ResendManyAsyncByInput',
data: {
recordIds: keys,
},
});
}

12
apps/vue/src/api/webhooks/subscriptions.ts

@ -53,6 +53,18 @@ export const deleteById = (id: string) => {
}); });
}; };
export const deleteMany = (keys: string[]) => {
return defAbpHttp.request<void>({
service: remoteServiceName,
controller: controllerName,
action: 'DeleteManyAsync',
uniqueName: 'DeleteManyAsyncByInput',
data: {
recordIds: keys,
},
});
};
export const getList = (input: WebhookSubscriptionGetListInput) => { export const getList = (input: WebhookSubscriptionGetListInput) => {
return defAbpHttp.request<PagedResultDto<WebhookSubscription>>({ return defAbpHttp.request<PagedResultDto<WebhookSubscription>>({
service: remoteServiceName, service: remoteServiceName,

25
apps/vue/src/components/Table/src/components/AdvancedSearch.vue

@ -39,7 +39,17 @@
</template> </template>
<template v-else-if="column.dataIndex==='value'"> <template v-else-if="column.dataIndex==='value'">
<Input v-if="record.javaScriptType==='string'" v-model:value="record.value" /> <Input v-if="record.javaScriptType==='string'" v-model:value="record.value" />
<InputNumber v-else-if="record.javaScriptType==='number'" style="width: 100%;" v-model:value="record.value" /> <Select
v-else-if="record.javaScriptType==='number' && record.options && record.options.length > 0"
style="width: 100%;"
v-model:value="record.value"
:options="getAvailableOptions"
/>
<InputNumber
v-else-if="record.javaScriptType==='number'"
style="width: 100%;"
v-model:value="record.value"
/>
<Switch v-else-if="record.javaScriptType==='boolean'" v-model:checked="record.value" /> <Switch v-else-if="record.javaScriptType==='boolean'" v-model:checked="record.value" />
<DatePicker <DatePicker
v-else-if="record.javaScriptType==='Date'" v-else-if="record.javaScriptType==='Date'"
@ -256,6 +266,18 @@
.filter(c => availableComparator.includes(c.value)); .filter(c => availableComparator.includes(c.value));
} }
}); });
const getAvailableOptions = computed(() => {
const availableParams = unref(getAvailableParams);
if (!availableParams.length) return[];
return availableParams
.map((item) => {
return {
label: item.description,
value: item.name,
children: [],
}
});
});
const filterOption = (input: string, option: any) => { const filterOption = (input: string, option: any) => {
return option.description.toLowerCase().indexOf(input.toLowerCase()) >= 0; return option.description.toLowerCase().indexOf(input.toLowerCase()) >= 0;
@ -315,6 +337,7 @@
record.field = defineParam.name; record.field = defineParam.name;
record.javaScriptType = defineParam.javaScriptType; record.javaScriptType = defineParam.javaScriptType;
record.value = undefined; record.value = undefined;
record.options = defineParam.options ?? [];
if (defineParam.javaScriptType === 'boolean') { if (defineParam.javaScriptType === 'boolean') {
record.value = false; record.value = false;
} }

10
apps/vue/src/components/Table/src/types/advancedSearch.ts

@ -20,6 +20,14 @@ export interface AdvanceSearchProps {
fetchApi?: (...arg: any) => Promise<any>, fetchApi?: (...arg: any) => Promise<any>,
} }
/** 选项 */
export interface ParamterOption {
/** 键名 */
key: string;
/** 键值 */
value: any;
}
/** 自定义字段 */ /** 自定义字段 */
export interface DefineParamter { export interface DefineParamter {
/** 字段名称 */ /** 字段名称 */
@ -32,6 +40,8 @@ export interface DefineParamter {
javaScriptType: string; javaScriptType: string;
/** 可用运算条件列表, 用于进一步约束字段可用比较符 */ /** 可用运算条件列表, 用于进一步约束字段可用比较符 */
availableComparator?: DynamicComparison[]; availableComparator?: DynamicComparison[];
/** 选项 */
options: ParamterOption[];
} }
/** 连接条件 */ /** 连接条件 */

65
apps/vue/src/hooks/abp/useDefineSettings.ts

@ -0,0 +1,65 @@
import { computed, onMounted } from 'vue';
import { SettingGroup } from '/@/api/settings/model/settingModel';
import { useSettingManagementStoreWithOut } from '/@/store/modules/settings';
import { useSettings as useAbpSettings, ISettingProvider } from '/@/hooks/abp/useSettings';
type SettingValue = NameValue<string>;
export function useDefineSettings(settingKey: string, api: (...args) => Promise<ListResultDto<SettingGroup>>) {
const settingStore = useSettingManagementStoreWithOut();
const { settingProvider: abpSettingProvider } = useAbpSettings();
const getSettings = computed(() => {
const abpSettings = abpSettingProvider.getAll();
return [...abpSettings, ...settingStore.getSettings];
});
onMounted(() => {
settingStore.initlize(settingKey, api);
});
function get(name: string): SettingValue | undefined {
return getSettings.value.find((setting) => name === setting.name);
}
function getAll(...names: string[]): SettingValue[] {
if (names.length !== 0) {
return getSettings.value.filter((setting) => names.includes(setting.name));
}
return getSettings.value;
}
function getOrDefault<T>(name: string, defaultValue: T): T | string {
var setting = get(name);
if (!setting) {
return defaultValue;
}
return setting.value;
}
function refresh() {
settingStore.refreshSettings(api);
}
const settingProvider: ISettingProvider = {
getOrEmpty(name: string) {
return getOrDefault(name, '');
},
getAll(...names: string[]) {
return getAll(...names);
},
getNumber(name: string, defaultValue: number = 0) {
var value = getOrDefault(name, defaultValue);
const num = Number(value);
return isNaN(num) ? defaultValue : num;
},
isTrue(name: string) {
var value = getOrDefault(name, 'false');
return value.toLowerCase() === 'true';
},
};
return {
...settingProvider,
refresh,
};
}

55
apps/vue/src/store/modules/settings.ts

@ -0,0 +1,55 @@
import { defineStore } from 'pinia';
import { store } from '/@/store';
import { createLocalStorage } from '/@/utils/cache';
import { SettingGroup } from '/@/api/settings/model/settingModel';
const ls = createLocalStorage();
const SETTING_ID = 'setting-management';
type SettingValue = NameValue<string>;
interface IState {
settingKey: string;
settings: SettingValue[];
}
export const useSettingManagementStore = defineStore({
id: SETTING_ID,
state: (): IState => ({
settingKey: 'unknown',
settings: [],
}),
getters: {
getSettings(state) {
return state.settings;
},
},
actions: {
initlize(settingKey: string, api: (...args) => Promise<ListResultDto<SettingGroup>>) {
this.settingKey = settingKey;
if (this.settings.length === 0) {
ls.get(this.settingKey) || this.refreshSettings(api);
}
},
refreshSettings(api: (...args) => Promise<ListResultDto<SettingGroup>>) {
api().then((res) => {
const settings: SettingValue[] = [];
res.items.forEach((group) => {
group.settings.forEach((setting) => {
setting.details.forEach((detail) => {
settings.push({
name: detail.name,
value: detail.value ?? detail.defaultValue,
});
});
});
});
this.settings = settings;
ls.set(this.settingKey, settings);
});
},
},
});
export function useSettingManagementStoreWithOut() {
return useSettingManagementStore(store);
}

4
apps/vue/src/utils/http/axios/checkStatus.ts

@ -12,7 +12,7 @@ const error = createMessage.error!;
const stp = projectSetting.sessionTimeoutProcessing; const stp = projectSetting.sessionTimeoutProcessing;
export function checkStatus( export function checkStatus(
status: number, response: any,
msg: string, msg: string,
errorMessageMode: ErrorMessageMode = 'message', errorMessageMode: ErrorMessageMode = 'message',
): void { ): void {
@ -20,7 +20,7 @@ export function checkStatus(
const userStore = useUserStoreWithOut(); const userStore = useUserStoreWithOut();
let errMessage = ''; let errMessage = '';
switch (status) { switch (response.status) {
case 400: case 400:
errMessage = `${msg}`; errMessage = `${msg}`;
break; break;

7
apps/vue/src/views/webhooks/send-attempts/components/SendAttemptModal.vue

@ -80,6 +80,9 @@
<FormItem :label="L('DisplayName:WebhookUri')"> <FormItem :label="L('DisplayName:WebhookUri')">
<Input readonly :value="subscriptionRef.webhookUri" /> <Input readonly :value="subscriptionRef.webhookUri" />
</FormItem> </FormItem>
<FormItem :label="L('DisplayName:Description')">
<Textarea readonly :value="subscriptionRef.description" :show-count="true" :auto-size="{ minRows: 3 }" />
</FormItem>
<FormItem :label="L('DisplayName:Secret')"> <FormItem :label="L('DisplayName:Secret')">
<InputPassword readonly :value="subscriptionRef.secret" /> <InputPassword readonly :value="subscriptionRef.secret" />
</FormItem> </FormItem>
@ -109,7 +112,7 @@
import { ref, unref, computed, watch } from 'vue'; import { ref, unref, computed, watch } from 'vue';
import { useTabsStyle } from '/@/hooks/component/useStyles'; import { useTabsStyle } from '/@/hooks/component/useStyles';
import { useLocalization } from '/@/hooks/abp/useLocalization'; import { useLocalization } from '/@/hooks/abp/useLocalization';
import { Checkbox, Form, Tabs, Tag, Input, InputPassword } from 'ant-design-vue'; import { Checkbox, Form, Tabs, Tag, Input, InputPassword, Textarea } from 'ant-design-vue';
import { CodeEditorX, MODE } from '/@/components/CodeEditor'; import { CodeEditorX, MODE } from '/@/components/CodeEditor';
import { BasicModal, useModalInner } from '/@/components/Modal'; import { BasicModal, useModalInner } from '/@/components/Modal';
import { findTenantById } from '/@/api/multi-tenancy/tenants'; import { findTenantById } from '/@/api/multi-tenancy/tenants';
@ -123,7 +126,7 @@
const FormItem = Form.Item; const FormItem = Form.Item;
const TabPane = Tabs.TabPane; const TabPane = Tabs.TabPane;
const { L } = useLocalization('WebhooksManagement'); const { L } = useLocalization(['WebhooksManagement', 'AbpUi']);
const formElRef = ref<any>(); const formElRef = ref<any>();
const activeKey = ref('basic'); const activeKey = ref('basic');
const tenantName = ref(''); const tenantName = ref('');

102
apps/vue/src/views/webhooks/send-attempts/components/SendAttemptTable.vue

@ -1,6 +1,24 @@
<template> <template>
<div class="content"> <div class="content">
<BasicTable @register="registerTable"> <BasicTable @register="registerTable">
<template #toolbar>
<Button
v-if="isManyRecordSelected"
v-auth="['AbpWebhooks.SendAttempts.Resend']"
type="primary"
@click="handleResendMany"
>
{{ L('Resend') }}
</Button>
<Button
v-if="isManyRecordSelected"
v-auth="['AbpWebhooks.SendAttempts.Delete']"
danger
@click="handleDeleteMany"
>
{{ L('Delete') }}
</Button>
</template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'responseStatusCode'"> <template v-if="column.key === 'responseStatusCode'">
<Tag :color="getHttpStatusColor(record.responseStatusCode)">{{ <Tag :color="getHttpStatusColor(record.responseStatusCode)">{{
@ -41,7 +59,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Tag } from 'ant-design-vue'; import { computed } from 'vue';
import { Button, Tag } from 'ant-design-vue';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization'; import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useModal } from '/@/components/Modal'; import { useModal } from '/@/components/Modal';
@ -50,13 +69,13 @@
import { getDataColumns } from '../datas/TableData'; import { getDataColumns } from '../datas/TableData';
import { getSearchFormSchemas } from '../datas/ModalData'; import { getSearchFormSchemas } from '../datas/ModalData';
import { httpStatusCodeMap, getHttpStatusColor } from '../../typing'; import { httpStatusCodeMap, getHttpStatusColor } from '../../typing';
import { getList, deleteById, resend } from '/@/api/webhooks/send-attempts'; import { getList, deleteById, deleteMany, resend, resendMany } from '/@/api/webhooks/send-attempts';
import SendAttemptModal from './SendAttemptModal.vue'; import SendAttemptModal from './SendAttemptModal.vue';
const { createConfirm, createMessage } = useMessage(); const { createConfirm, createMessage } = useMessage();
const { L } = useLocalization(['WebhooksManagement', 'AbpUi']); const { L } = useLocalization(['WebhooksManagement', 'AbpUi']);
const [registerModal, { openModal }] = useModal(); const [registerModal, { openModal }] = useModal();
const [registerTable, { reload, setLoading }] = useTable({ const [registerTable, { reload, setLoading, getSelectRowKeys, clearSelectedRowKeys }] = useTable({
rowKey: 'id', rowKey: 'id',
title: L('SendAttempts'), title: L('SendAttempts'),
columns: getDataColumns(), columns: getDataColumns(),
@ -77,6 +96,13 @@
title: L('Actions'), title: L('Actions'),
dataIndex: 'action', dataIndex: 'action',
}, },
rowSelection: {
type: 'checkbox',
},
});
const isManyRecordSelected = computed(() => {
const selectedKeys = getSelectRowKeys();
return selectedKeys.length > 0;
}); });
function handleEdit(record) { function handleEdit(record) {
@ -90,22 +116,74 @@
content: L('ItemWillBeDeletedMessage'), content: L('ItemWillBeDeletedMessage'),
okCancel: true, okCancel: true,
onOk: () => { onOk: () => {
setLoading(true);
return deleteById(record.id).then(() => { return deleteById(record.id).then(() => {
createMessage.success(L('SuccessfullyDeleted'));
clearSelectedRowKeys();
reload(); reload();
createMessage.success(L('Successful')); }).finally(() => {
setLoading(false);
});
},
});
}
function handleDeleteMany() {
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('ItemWillBeDeletedMessageWithFormat', { 0: L('SelectedItems') }),
okCancel: true,
onOk: () => {
const selectKeys = getSelectRowKeys();
setLoading(true);
return deleteMany(selectKeys).then(() => {
createMessage.success(L('SuccessfullyDeleted'));
clearSelectedRowKeys();
reload();
}).finally(() => {
setLoading(false);
}); });
}, },
}); });
} }
function handleResend(record) { function handleResend(record) {
setLoading(true); createConfirm({
resend(record.id) iconType: 'warning',
.then(() => { title: L('AreYouSure'),
createMessage.success(L('Successful')); content: L('ItemWillBeResendMessageWithFormat', { 0: L('SelectedItems')}),
}) okCancel: true,
.finally(() => { onOk: () => {
setLoading(false); setLoading(true);
}); return resend(record.id).then(() => {
createMessage.success(L('Successful'));
clearSelectedRowKeys();
reload();
}).finally(() => {
setLoading(false);
});
},
});
}
function handleResendMany() {
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('ItemWillBeResendMessageWithFormat', { 0: L('SelectedItems')}),
okCancel: true,
onOk: () => {
const selectKeys = getSelectRowKeys();
setLoading(true);
return resendMany(selectKeys).then(() => {
createMessage.success(L('Successful'));
clearSelectedRowKeys();
reload();
}).finally(() => {
setLoading(false);
});
},
});
} }
</script> </script>

28
apps/vue/src/views/webhooks/send-attempts/datas/ModalData.ts

@ -9,6 +9,9 @@ const { L } = useLocalization(['WebhooksManagement', 'AbpUi']);
export function getSearchFormSchemas(): Partial<FormProps> { export function getSearchFormSchemas(): Partial<FormProps> {
return { return {
labelWidth: 100, labelWidth: 100,
fieldMapToTime: [
['creationTime', ['beginCreationTime', 'endCreationTime'], ['YYYY-MM-DDT00:00:00', 'YYYY-MM-DDT23:59:59']]
],
schemas: [ schemas: [
{ {
field: 'tenantId', field: 'tenantId',
@ -43,29 +46,30 @@ export function getSearchFormSchemas(): Partial<FormProps> {
} }
}, },
{ {
field: 'responseStatusCode', field: 'state',
component: 'Select', component: 'Select',
label: L('DisplayName:ResponseStatusCode'), label: L('DisplayName:State'),
colProps: { span: 6 }, colProps: { span: 6 },
componentProps: { componentProps: {
options: httpStatusOptions, options: [
{ label: L('ResponseState:Successed'), value: true, },
{ label: L('ResponseState:Failed'), value: false, },
],
}, },
}, },
{ {
field: 'beginCreationTime', field: 'responseStatusCode',
component: 'DatePicker', component: 'Select',
label: L('DisplayName:BeginCreationTime'), label: L('DisplayName:ResponseStatusCode'),
colProps: { span: 6 }, colProps: { span: 6 },
componentProps: { componentProps: {
style: { options: httpStatusOptions,
width: '100%',
},
}, },
}, },
{ {
field: 'endCreationTime', field: 'creationTime',
component: 'DatePicker', component: 'RangePicker',
label: L('DisplayName:EndCreationTime'), label: L('DisplayName:BeginCreationTime'),
colProps: { span: 6 }, colProps: { span: 6 },
componentProps: { componentProps: {
style: { style: {

12
apps/vue/src/views/webhooks/subscriptions/components/SubscriptionModal.vue

@ -27,6 +27,9 @@
<FormItem name="webhookUri" required :label="L('DisplayName:WebhookUri')"> <FormItem name="webhookUri" required :label="L('DisplayName:WebhookUri')">
<Input v-model:value="modelRef.webhookUri" autocomplete="off" /> <Input v-model:value="modelRef.webhookUri" autocomplete="off" />
</FormItem> </FormItem>
<FormItem name="description" :label="L('DisplayName:Description')">
<Textarea v-model:value="modelRef.description" :show-count="true" :auto-size="{ minRows: 3 }" />
</FormItem>
<FormItem name="secret" :label="L('DisplayName:Secret')"> <FormItem name="secret" :label="L('DisplayName:Secret')">
<InputPassword v-model:value="modelRef.secret" autocomplete="off" /> <InputPassword v-model:value="modelRef.secret" autocomplete="off" />
</FormItem> </FormItem>
@ -56,7 +59,6 @@
<FormItem name="headers" :label="L('DisplayName:Headers')"> <FormItem name="headers" :label="L('DisplayName:Headers')">
<CodeEditorX style="height: 300px" :mode="MODE.JSON" v-model="modelRef.headers" /> <CodeEditorX style="height: 300px" :mode="MODE.JSON" v-model="modelRef.headers" />
</FormItem> </FormItem>
</Form> </Form>
</BasicModal> </BasicModal>
</template> </template>
@ -66,11 +68,11 @@
import { useLocalization } from '/@/hooks/abp/useLocalization'; import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useValidation } from '/@/hooks/abp/useValidation'; import { useValidation } from '/@/hooks/abp/useValidation';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { Checkbox, Form, Select, Tooltip, Input, InputPassword } from 'ant-design-vue'; import { Checkbox, Form, Select, Tooltip, Input, InputPassword, Textarea } from 'ant-design-vue';
import { isString } from '/@/utils/is'; import { isString } from '/@/utils/is';
import { CodeEditorX, MODE } from '/@/components/CodeEditor'; import { CodeEditorX, MODE } from '/@/components/CodeEditor';
import { BasicModal, useModalInner } from '/@/components/Modal'; import { BasicModal, useModalInner } from '/@/components/Modal';
import { Tenant } from '/@/api/saas/model/tenantModel'; import { TenantDto } from '/@/api/saas/tenant/model';
import { GetListAsyncByInput as getTenants } from '/@/api/saas/tenant'; import { GetListAsyncByInput as getTenants } from '/@/api/saas/tenant';
import { getById, create, update, getAllAvailableWebhooks } from '/@/api/webhooks/subscriptions'; import { getById, create, update, getAllAvailableWebhooks } from '/@/api/webhooks/subscriptions';
import { import {
@ -83,11 +85,11 @@
const SelectOption = Select.Option; const SelectOption = Select.Option;
const emit = defineEmits(['change', 'register']); const emit = defineEmits(['change', 'register']);
const { L } = useLocalization('WebhooksManagement'); const { L } = useLocalization(['WebhooksManagement', 'AbpUi']);
const { ruleCreator } = useValidation(); const { ruleCreator } = useValidation();
const { createMessage } = useMessage(); const { createMessage } = useMessage();
const formElRef = ref<any>(); const formElRef = ref<any>();
const tenantsRef = ref<Tenant[]>([]); const tenantsRef = ref<TenantDto[]>([]);
const webhooksGroupRef = ref<WebhookAvailableGroup[]>([]); const webhooksGroupRef = ref<WebhookAvailableGroup[]>([]);
const modelRef = ref<WebhookSubscription>(getDefaultModel()); const modelRef = ref<WebhookSubscription>(getDefaultModel());
const [registerModal, { closeModal, changeOkLoading }] = useModalInner((model) => { const [registerModal, { closeModal, changeOkLoading }] = useModalInner((model) => {

54
apps/vue/src/views/webhooks/subscriptions/components/SubscriptionTable.vue

@ -2,12 +2,21 @@
<div class="content"> <div class="content">
<BasicTable @register="registerTable"> <BasicTable @register="registerTable">
<template #toolbar> <template #toolbar>
<a-button <Button
v-auth="['AbpWebhooks.Subscriptions.Create']" v-auth="['AbpWebhooks.Subscriptions.Create']"
type="primary" type="primary"
@click="handleAddNew" @click="handleAddNew"
>{{ L('Subscriptions:AddNew') }}</a-button
> >
{{ L('Subscriptions:AddNew') }}
</Button>
<Button
v-if="deleteManyEnabled"
v-auth="['AbpWebhooks.Subscriptions.Delete']"
danger
@click="handleDeleteMany"
>
{{ L('Delete') }}
</Button>
</template> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'isActive'"> <template v-if="column.key === 'isActive'">
@ -44,7 +53,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Tag } from 'ant-design-vue'; import { computed } from 'vue';
import { Button, Tag } from 'ant-design-vue';
import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue'; import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization'; import { useLocalization } from '/@/hooks/abp/useLocalization';
@ -53,13 +63,13 @@
import { formatPagedRequest } from '/@/utils/http/abp/helper'; import { formatPagedRequest } from '/@/utils/http/abp/helper';
import { getDataColumns } from '../datas/TableData'; import { getDataColumns } from '../datas/TableData';
import { getSearchFormSchemas } from '../datas/ModalData'; import { getSearchFormSchemas } from '../datas/ModalData';
import { deleteById, getList } from '/@/api/webhooks/subscriptions'; import { deleteById, deleteMany, getList } from '/@/api/webhooks/subscriptions';
import SubscriptionModal from './SubscriptionModal.vue'; import SubscriptionModal from './SubscriptionModal.vue';
const { createConfirm } = useMessage(); const { createConfirm, createMessage } = useMessage();
const { L } = useLocalization(['WebhooksManagement', 'AbpUi']); const { L } = useLocalization(['WebhooksManagement', 'AbpUi']);
const [registerModal, { openModal }] = useModal(); const [registerModal, { openModal }] = useModal();
const [registerTable, { reload }] = useTable({ const [registerTable, { reload, setLoading, clearSelectedRowKeys, getSelectRowKeys }] = useTable({
rowKey: 'id', rowKey: 'id',
title: L('Subscriptions'), title: L('Subscriptions'),
columns: getDataColumns(), columns: getDataColumns(),
@ -80,6 +90,13 @@
title: L('Actions'), title: L('Actions'),
dataIndex: 'action', dataIndex: 'action',
}, },
rowSelection: {
type: 'checkbox',
},
});
const deleteManyEnabled = computed(() => {
const selectKeys = getSelectRowKeys();
return selectKeys.length > 0;
}); });
function handleAddNew() { function handleAddNew() {
@ -97,8 +114,33 @@
content: L('ItemWillBeDeletedMessage'), content: L('ItemWillBeDeletedMessage'),
okCancel: true, okCancel: true,
onOk: () => { onOk: () => {
setLoading(true);
return deleteById(record.id).then(() => { return deleteById(record.id).then(() => {
createMessage.success(L('SuccessfullyDeleted'));
clearSelectedRowKeys();
reload();
}).finally(() => {
setLoading(false);
});
},
});
}
function handleDeleteMany() {
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('ItemWillBeDeletedMessageWithFormat', { 0: L('SelectedItems') }),
okCancel: true,
onOk: () => {
const selectKeys = getSelectRowKeys();
setLoading(true);
return deleteMany(selectKeys).then(() => {
createMessage.success(L('SuccessfullyDeleted'));
clearSelectedRowKeys();
reload(); reload();
}).finally(() => {
setLoading(false);
}); });
}, },
}); });

22
apps/vue/src/views/webhooks/subscriptions/datas/ModalData.ts

@ -25,6 +25,9 @@ function getAllAvailables(): Promise<any> {
export function getSearchFormSchemas(): Partial<FormProps> { export function getSearchFormSchemas(): Partial<FormProps> {
return { return {
labelWidth: 100, labelWidth: 100,
fieldMapToTime: [
['creationTime', ['beginCreationTime', 'endCreationTime'], ['YYYY-MM-DDT00:00:00', 'YYYY-MM-DDT23:59:59']]
],
schemas: [ schemas: [
{ {
field: 'tenantId', field: 'tenantId',
@ -76,21 +79,10 @@ export function getSearchFormSchemas(): Partial<FormProps> {
}, },
}, },
{ {
field: 'beginCreationTime', field: 'creationTime',
component: 'DatePicker', component: 'RangePicker',
label: L('DisplayName:BeginCreationTime'), label: L('DisplayName:CreationTime'),
colProps: { span: 6 }, colProps: { span: 12 },
componentProps: {
style: {
width: '100%',
},
},
},
{
field: 'endCreationTime',
component: 'DatePicker',
label: L('DisplayName:EndCreationTime'),
colProps: { span: 6 },
componentProps: { componentProps: {
style: { style: {
width: '100%', width: '100%',

7
apps/vue/src/views/webhooks/subscriptions/datas/TableData.ts

@ -35,6 +35,13 @@ export function getDataColumns(): BasicColumn[] {
width: 180, width: 180,
sorter: true, sorter: true,
}, },
{
title: L('DisplayName:Description'),
dataIndex: 'description',
align: 'left',
width: 200,
sorter: true,
},
{ {
title: L('DisplayName:CreationTime'), title: L('DisplayName:CreationTime'),
dataIndex: 'creationTime', dataIndex: 'creationTime',

2
apps/vue/types/abp.d.ts

@ -6,7 +6,7 @@ declare interface LocalizableStringInfo {
declare type ExtraPropertyDictionary = { [key: string]: any }; declare type ExtraPropertyDictionary = { [key: string]: any };
declare interface IHasConcurrencyStamp { declare interface IHasConcurrencyStamp {
concurrencyStamp: string; concurrencyStamp?: string;
} }
declare interface IHasExtraProperties { declare interface IHasExtraProperties {

2
aspnet-core/modules/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs

@ -184,8 +184,6 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch
cancellationToken); cancellationToken);
} }
// 避免循环记录
[DisableAuditing]
public async virtual Task<string> SaveAsync( public async virtual Task<string> SaveAsync(
AuditLogInfo auditInfo, AuditLogInfo auditInfo,
CancellationToken cancellationToken = default(CancellationToken)) CancellationToken cancellationToken = default(CancellationToken))

2
aspnet-core/modules/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogManager.cs

@ -139,8 +139,6 @@ namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore
} }
} }
// 避免循环记录
[DisableAuditing]
public async virtual Task<string> SaveAsync( public async virtual Task<string> SaveAsync(
AuditLogInfo auditInfo, AuditLogInfo auditInfo,
CancellationToken cancellationToken = default(CancellationToken)) CancellationToken cancellationToken = default(CancellationToken))

1
aspnet-core/modules/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditLog.cs

@ -5,7 +5,6 @@ using Volo.Abp.Data;
namespace LINGYUN.Abp.AuditLogging namespace LINGYUN.Abp.AuditLogging
{ {
[DisableAuditing]
public class AuditLog : IHasExtraProperties public class AuditLog : IHasExtraProperties
{ {
public Guid Id { get; set; } public Guid Id { get; set; }

2
aspnet-core/modules/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/AuditingStore.cs

@ -15,8 +15,6 @@ namespace LINGYUN.Abp.AuditLogging
_manager = manager; _manager = manager;
} }
// 避免循环记录
[DisableAuditing]
public async virtual Task SaveAsync(AuditLogInfo auditInfo) public async virtual Task SaveAsync(AuditLogInfo auditInfo)
{ {
await _manager.SaveAsync(auditInfo); await _manager.SaveAsync(auditInfo);

2
aspnet-core/modules/dynamic-queryable/LINGYUN.Abp.Dynamic.Queryable.Application.Contracts/LINGYUN/Abp/Dynamic/Queryable/Dto/DynamicParamterDto.cs

@ -9,8 +9,10 @@ public class DynamicParamterDto
public string Type { get; set; } public string Type { get; set; }
public string JavaScriptType { get; set; } public string JavaScriptType { get; set; }
public DynamicComparison[] AvailableComparator { get; set; } public DynamicComparison[] AvailableComparator { get; set; }
public ParamterOptionDto[] Options { get; set; }
public DynamicParamterDto() public DynamicParamterDto()
{ {
AvailableComparator = new DynamicComparison[0]; AvailableComparator = new DynamicComparison[0];
Options = new ParamterOptionDto[0];
} }
} }

6
aspnet-core/modules/dynamic-queryable/LINGYUN.Abp.Dynamic.Queryable.Application.Contracts/LINGYUN/Abp/Dynamic/Queryable/Dto/ParamterOptionDto.cs

@ -0,0 +1,6 @@
namespace LINGYUN.Abp.Dynamic.Queryable;
public class ParamterOptionDto
{
public string Key { get; set; }
public object Value { get; set; }
}

67
aspnet-core/modules/dynamic-queryable/LINGYUN.Abp.Dynamic.Queryable.Application/LINGYUN/Abp/Dynamic/Queryable/DynamicQueryableAppService.cs

@ -39,15 +39,41 @@ public abstract class DynamicQueryableAppService<TEntity, TEntityDto> : Applicat
// 在本地化文件中定义 DisplayName:PropertyName // 在本地化文件中定义 DisplayName:PropertyName
var localizedProp = L[$"DisplayName:{propertyInfo.Name}"]; var localizedProp = L[$"DisplayName:{propertyInfo.Name}"];
var propertyTypeMap = GetPropertyTypeMap(propertyInfo.PropertyType); var propertyTypeMap = GetPropertyTypeMap(propertyInfo.PropertyType);
dynamicParamters.Add( var dynamicParamter = new DynamicParamterDto
new DynamicParamterDto {
Name = propertyInfo.Name,
Type = propertyInfo.PropertyType.FullName,
Description = localizedProp.Value ?? propertyInfo.Name,
JavaScriptType = propertyTypeMap.JavaScriptType,
AvailableComparator = propertyTypeMap.AvailableComparator
};
var propertyType = propertyInfo.PropertyType;
if (propertyType.IsNullableType())
{
propertyType = propertyType.GetGenericArguments().FirstOrDefault();
}
if (typeof(Enum).IsAssignableFrom(propertyType))
{
var enumNames = Enum.GetNames(propertyType);
var enumValues = Enum.GetValues(propertyType);
var paramterOptions = new ParamterOptionDto[enumNames.Length];
for (var index = 0; index < enumNames.Length; index++)
{ {
Name = propertyInfo.Name, var enumName = enumNames[index];
Type = propertyInfo.PropertyType.FullName, var localizerEnumKey = $"{propertyInfo.Name}:{enumName}";
Description = localizedProp.Value ?? propertyInfo.Name, var localizerEnumName = L[localizerEnumKey];
JavaScriptType = propertyTypeMap.JavaScriptType, paramterOptions[index] = new ParamterOptionDto
AvailableComparator = propertyTypeMap.AvailableComparator {
}); Key = localizerEnumName.ResourceNotFound ? enumName : localizerEnumName.Value,
Value = enumValues.GetValue(index),
};
}
dynamicParamter.Options = paramterOptions;
}
dynamicParamters.Add(dynamicParamter);
} }
return Task.FromResult(new ListResultDto<DynamicParamterDto>(dynamicParamters)); return Task.FromResult(new ListResultDto<DynamicParamterDto>(dynamicParamters));
@ -96,6 +122,31 @@ public abstract class DynamicQueryableAppService<TEntity, TEntityDto> : Applicat
isNullableType = true; isNullableType = true;
propertyType = propertyType.GetGenericArguments().FirstOrDefault(); propertyType = propertyType.GetGenericArguments().FirstOrDefault();
} }
if (typeof(Enum).IsAssignableFrom(propertyType))
{
// 枚举类型只支持如下操作符
// 小于、小于等于、大于、大于等于、等于、不等于、空、非空
availableComparator.AddRange(new[]
{
DynamicComparison.GreaterThan,
DynamicComparison.GreaterThanOrEqual,
DynamicComparison.LessThan,
DynamicComparison.LessThanOrEqual,
DynamicComparison.Equal,
DynamicComparison.NotEqual,
});
if (isNullableType)
{
availableComparator.AddRange(new[]
{
DynamicComparison.Null,
DynamicComparison.NotNull
});
}
return ("number", availableComparator.ToArray());
}
var typeFullName = propertyType.FullName; var typeFullName = propertyType.FullName;
switch (typeFullName) switch (typeFullName)

2
aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Application.Contracts/LINGYUN/Abp/OssManagement/Permissions/AbpOssManagementPermissionDefinitionProvider.cs

@ -18,7 +18,7 @@ namespace LINGYUN.Abp.OssManagement.Permissions
var ossobject = ossManagement var ossobject = ossManagement
.AddPermission(AbpOssManagementPermissions.OssObject.Default, L("Permission:OssObject")) .AddPermission(AbpOssManagementPermissions.OssObject.Default, L("Permission:OssObject"))
.RequireFeatures(AbpOssManagementFeatureNames.OssObject.Default); .RequireFeatures(AbpOssManagementFeatureNames.OssObject.Enable);
ossobject ossobject
.AddChild(AbpOssManagementPermissions.OssObject.Create, L("Permission:Create")) .AddChild(AbpOssManagementPermissions.OssObject.Create, L("Permission:Create"))
.RequireFeatures(AbpOssManagementFeatureNames.OssObject.UploadFile); .RequireFeatures(AbpOssManagementFeatureNames.OssObject.UploadFile);

4
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.Abstractions/LINGYUN/Abp/BackgroundTasks/JobEventBase.cs

@ -22,7 +22,9 @@ public abstract class JobEventBase<TEvent> : IJobEvent
var currentTenant = context.ServiceProvider.GetRequiredService<ICurrentTenant>(); var currentTenant = context.ServiceProvider.GetRequiredService<ICurrentTenant>();
using (currentTenant.Change(context.EventData.TenantId)) using (currentTenant.Change(context.EventData.TenantId))
{ {
Logger.LogInformation("Job {Group}-{Name} after event with {Event} has executing.", context.EventData.Group, context.EventData.Name, typeof(TEvent).Name);
await OnJobAfterExecutedAsync(context); await OnJobAfterExecutedAsync(context);
Logger.LogInformation("Job {Group}-{Name} after event with {Event} was executed.", context.EventData.Group, context.EventData.Name, typeof(TEvent).Name);
} }
} }
catch (Exception ex) catch (Exception ex)
@ -38,7 +40,9 @@ public abstract class JobEventBase<TEvent> : IJobEvent
var currentTenant = context.ServiceProvider.GetRequiredService<ICurrentTenant>(); var currentTenant = context.ServiceProvider.GetRequiredService<ICurrentTenant>();
using (currentTenant.Change(context.EventData.TenantId)) using (currentTenant.Change(context.EventData.TenantId))
{ {
Logger.LogInformation("Job {Group}-{Name} before event with {Event} executing.", context.EventData.Group, context.EventData.Name, typeof(TEvent).Name);
await OnJobBeforeExecutedAsync(context); await OnJobBeforeExecutedAsync(context);
Logger.LogInformation("Job {Group}-{Name} before event with {Event} was executed.", context.EventData.Group, context.EventData.Name, typeof(TEvent).Name);
} }
} }
catch (Exception ex) catch (Exception ex)

27
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.Abstractions/LINGYUN/Abp/BackgroundTasks/JobInfo.cs

@ -139,21 +139,22 @@ public class JobInfo
} }
// 重试 // 重试
if (Status == JobStatus.FailedRetry && MaxTryCount > 0) // TODO: 不能用Quartz自带的重试机制
{ //if (Status == JobStatus.FailedRetry && MaxTryCount > 0)
maxCount = MaxTryCount - TryCount; //{
// maxCount = MaxTryCount - TryCount;
if (maxCount < 0) // if (maxCount < 0)
{ // {
maxCount = 0; // maxCount = 0;
} // }
if (maxCount > 0) // if (maxCount > 0)
{ // {
// 触发重试时,失败间隔时间调整 // // 触发重试时,失败间隔时间调整
Interval = 60; // Interval = 60;
} // }
} //}
return maxCount; return maxCount;
} }

2
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.Abstractions/README.md

@ -5,6 +5,8 @@
## 特性参数 ## 特性参数
* DisableJobActionAttribute 标记此特性不处理作业触发后行为 * DisableJobActionAttribute 标记此特性不处理作业触发后行为
* DisableJobStatusAttribute 标记此特性不处理作业状态
* DisableAuditingAttribute 标记此特性不记录作业日志
## 配置使用 ## 配置使用

55
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.EventBus/LINGYUN/Abp/BackgroundTasks/EventBus/DistributedJobDispatcher.cs

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
namespace LINGYUN.Abp.BackgroundTasks.EventBus;
[Dependency(ReplaceServices = true)]
[ExposeServices(
typeof(IJobDispatcher),
typeof(DistributedJobDispatcher))]
public class DistributedJobDispatcher : IJobDispatcher, ITransientDependency
{
protected IDistributedEventBus EventBus { get; }
public DistributedJobDispatcher(IDistributedEventBus eventBus)
{
EventBus = eventBus;
}
public async virtual Task<bool> DispatchAsync(JobInfo job, CancellationToken cancellationToken = default)
{
var eventData = new JobStartEventData
{
TenantId = job.TenantId,
NodeName = job.NodeName,
IdList = new List<string> { job.Id },
};
await EventBus.PublishAsync(eventData);
return true;
}
public async virtual Task<bool> DispatchAsync(
IEnumerable<JobInfo> jobs,
string nodeName = null,
Guid? tenantId = null,
CancellationToken cancellationToken = default)
{
var eventData = new JobStartEventData
{
TenantId = tenantId,
NodeName = nodeName,
IdList = jobs.Select(job => job.Id).ToList(),
};
await EventBus.PublishAsync(eventData);
return true;
}
}

3
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.Quartz/LINGYUN/Abp/BackgroundTasks/Quartz/QuartzJobCreator.cs

@ -136,7 +136,8 @@ public class QuartzJobCreator : IQuartzJobCreator, ISingletonDependency
// Quartz约定. 重复间隔不能为0 // Quartz约定. 重复间隔不能为0
// fix throw Quartz.SchedulerException: Repeat Interval cannot be zero. // fix throw Quartz.SchedulerException: Repeat Interval cannot be zero.
var scheduleBuilder = SimpleScheduleBuilder.Create(); var scheduleBuilder = SimpleScheduleBuilder.Create();
scheduleBuilder.WithRepeatCount(maxCount); // TODO: 不能用Quartz自带的重试机制
// scheduleBuilder.WithRepeatCount(maxCount);
if (job.Interval > 0) if (job.Interval > 0)
{ {
scheduleBuilder.WithIntervalInSeconds(job.Interval); scheduleBuilder.WithIntervalInSeconds(job.Interval);

2
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/BackgroundJobManager.cs

@ -91,7 +91,7 @@ public class BackgroundJobManager : IBackgroundJobManager, ITransientDependency
jobInfo.Interval = selector.Interval ?? jobInfo.Interval; jobInfo.Interval = selector.Interval ?? jobInfo.Interval;
jobInfo.LockTimeOut = selector.LockTimeOut ?? jobInfo.LockTimeOut; jobInfo.LockTimeOut = selector.LockTimeOut ?? jobInfo.LockTimeOut;
jobInfo.Priority = selector.Priority ?? jobInfo.Priority; jobInfo.Priority = selector.Priority ?? jobInfo.Priority;
jobInfo.TryCount = selector.MaxCount ?? jobInfo.MaxCount; jobInfo.MaxCount = selector.MaxCount ?? jobInfo.MaxCount;
jobInfo.MaxTryCount = selector.MaxTryCount ?? jobInfo.MaxTryCount; jobInfo.MaxTryCount = selector.MaxTryCount ?? jobInfo.MaxTryCount;
if (!selector.NodeName.IsNullOrWhiteSpace()) if (!selector.NodeName.IsNullOrWhiteSpace())

12
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/BackgroundWorkerManager.cs

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Volo.Abp.BackgroundWorkers; using Volo.Abp.BackgroundWorkers;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy; using Volo.Abp.DynamicProxy;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.Timing; using Volo.Abp.Timing;
@ -18,6 +19,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
protected IJobStore JobStore { get; } protected IJobStore JobStore { get; }
protected IJobPublisher JobPublisher { get; } protected IJobPublisher JobPublisher { get; }
protected ICurrentTenant CurrentTenant { get; } protected ICurrentTenant CurrentTenant { get; }
protected IGuidGenerator GuidGenerator { get; }
protected AbpBackgroundTasksOptions Options { get; } protected AbpBackgroundTasksOptions Options { get; }
protected AbpBackgroundTasksOptions TasksOptions { get; } protected AbpBackgroundTasksOptions TasksOptions { get; }
@ -26,6 +28,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
IJobStore jobStore, IJobStore jobStore,
IJobPublisher jobPublisher, IJobPublisher jobPublisher,
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IGuidGenerator guidGenerator,
IOptions<AbpBackgroundTasksOptions> options, IOptions<AbpBackgroundTasksOptions> options,
IOptions<AbpBackgroundTasksOptions> taskOptions) IOptions<AbpBackgroundTasksOptions> taskOptions)
{ {
@ -33,6 +36,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
JobStore = jobStore; JobStore = jobStore;
JobPublisher = jobPublisher; JobPublisher = jobPublisher;
CurrentTenant = currentTenant; CurrentTenant = currentTenant;
GuidGenerator = guidGenerator;
Options = options.Value; Options = options.Value;
TasksOptions = taskOptions.Value; TasksOptions = taskOptions.Value;
} }
@ -50,6 +54,12 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
return; return;
} }
// 如果通过远程接口发布作业, 可能会造成Group与Name重复
// 重新设定Name为唯一Id
var jobId = GuidGenerator.Create();
jobInfo.Id = jobId.ToString();
jobInfo.Name = jobId.ToString();
jobInfo.NodeName = Options.NodeName; jobInfo.NodeName = Options.NodeName;
jobInfo.BeginTime = Clock.Now; jobInfo.BeginTime = Clock.Now;
jobInfo.CreationTime = Clock.Now; jobInfo.CreationTime = Clock.Now;
@ -65,7 +75,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
jobInfo.Interval = selector.Interval ?? jobInfo.Interval; jobInfo.Interval = selector.Interval ?? jobInfo.Interval;
jobInfo.LockTimeOut = selector.LockTimeOut ?? jobInfo.LockTimeOut; jobInfo.LockTimeOut = selector.LockTimeOut ?? jobInfo.LockTimeOut;
jobInfo.Priority = selector.Priority ?? jobInfo.Priority; jobInfo.Priority = selector.Priority ?? jobInfo.Priority;
jobInfo.TryCount = selector.MaxCount ?? jobInfo.MaxCount; jobInfo.MaxCount = selector.MaxCount ?? jobInfo.MaxCount;
jobInfo.MaxTryCount = selector.MaxTryCount ?? jobInfo.MaxTryCount; jobInfo.MaxTryCount = selector.MaxTryCount ?? jobInfo.MaxTryCount;
if (!selector.NodeName.IsNullOrWhiteSpace()) if (!selector.NodeName.IsNullOrWhiteSpace())

35
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/IJobDispatcher.cs

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace LINGYUN.Abp.BackgroundTasks;
/// <summary>
/// 作业调度接口
/// </summary>
/// <remarks>
/// 使用场景: 调度作业到作业调度器(调度到指定运行节点作业)
/// </remarks>
public interface IJobDispatcher
{
/// <summary>
/// 调度单个作业
/// </summary>
/// <param name="job">作业明细</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<bool> DispatchAsync(JobInfo job, CancellationToken cancellationToken = default);
/// <summary>
/// 调度多个作业
/// </summary>
/// <param name="jobs">作业列表</param>
/// <param name="nodeName">运行节点</param>
/// <param name="tenantId">租户标识</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<bool> DispatchAsync(
IEnumerable<JobInfo> jobs,
string nodeName = null,
Guid? tenantId = null,
CancellationToken cancellationToken = default);
}

7
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/IJobPublisher.cs

@ -2,7 +2,12 @@
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LINGYUN.Abp.BackgroundTasks; namespace LINGYUN.Abp.BackgroundTasks;
/// <summary>
/// 作业发布接口
/// </summary>
/// <remarks>
/// 使用场景: 发布作业到作业调度器(发布作业到当前运行节点)
/// </remarks>
public interface IJobPublisher public interface IJobPublisher
{ {
Task<bool> PublishAsync(JobInfo job, CancellationToken cancellationToken = default); Task<bool> PublishAsync(JobInfo job, CancellationToken cancellationToken = default);

2
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/IJobScheduler.cs

@ -4,7 +4,7 @@ using System.Threading;
namespace LINGYUN.Abp.BackgroundTasks; namespace LINGYUN.Abp.BackgroundTasks;
/// <summary> /// <summary>
/// 作业调度接口 /// 作业调度接口
/// </summary> /// </summary>
public interface IJobScheduler public interface IJobScheduler
{ {

30
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/Internal/BackgroundPollingJob.cs

@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Auditing; using Volo.Abp.Auditing;
@ -29,12 +30,37 @@ public class BackgroundPollingJob : IJobRunnable
return; return;
} }
var jobPublisher = context.ServiceProvider.GetRequiredService<IJobPublisher>(); /* changes: 2023-04-06
*
* IJobPublisher
* ,
*
* IJobDispatcher,
* ,
*
*/
foreach (var job in waitingJobs) // 当前节点作业发布
var scheduleJobs = waitingJobs.Where(job => string.Equals(job.NodeName, options.NodeName) || job.NodeName.IsNullOrWhiteSpace());
var jobPublisher = context.ServiceProvider.GetRequiredService<IJobPublisher>();
foreach (var job in scheduleJobs)
{ {
await jobPublisher.PublishAsync(job, context.CancellationToken); await jobPublisher.PublishAsync(job, context.CancellationToken);
} }
// 非当前节点作业调度
var dispatchJobs = waitingJobs.Where(job => !job.NodeName.IsNullOrWhiteSpace() && !string.Equals(job.NodeName, options.NodeName));
if (dispatchJobs.Any())
{
var jobDispatcher = context.ServiceProvider.GetRequiredService<IJobDispatcher>();
foreach (var jobByGroup in dispatchJobs.GroupBy(job => job.NodeName))
{
await jobDispatcher.DispatchAsync(
jobByGroup,
jobByGroup.Key,
tenantId);
}
}
} }
} }
} }

152
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/Internal/JobExecutedEvent.cs

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -12,98 +13,107 @@ public class JobExecutedEvent : JobEventBase<JobExecutedEvent>, ITransientDepend
{ {
if (context.EventData.Type.IsDefined(typeof(DisableJobStatusAttribute), true)) if (context.EventData.Type.IsDefined(typeof(DisableJobStatusAttribute), true))
{ {
Logger.LogWarning("The job change event could not be processed because the job marked the DisableJobStatus attribute!");
return; return;
} }
var store = context.ServiceProvider.GetRequiredService<IJobStore>(); var store = context.ServiceProvider.GetRequiredService<IJobStore>();
var job = await store.FindAsync(context.EventData.Key, context.EventData.CancellationToken); var job = await store.FindAsync(context.EventData.Key, context.EventData.CancellationToken);
if (job != null) if (job == null)
{ {
job.TriggerCount += 1; Logger.LogWarning("Cannot process job change event because job with key {Id} could not be found!", context.EventData.Key);
job.TenantId = context.EventData.TenantId; return;
job.LastRunTime = context.EventData.RunTime; }
job.NextRunTime = context.EventData.NextRunTime;
job.Result = context.EventData.Result ?? "OK";
job.Status = JobStatus.Running;
// 一次性任务执行一次后标记为已完成 job.TriggerCount += 1;
if (job.JobType == JobType.Once) job.TenantId = context.EventData.TenantId;
{ job.LastRunTime = context.EventData.RunTime;
job.Status = JobStatus.Completed; job.NextRunTime = context.EventData.NextRunTime;
} job.Result = context.EventData.Result ?? "OK";
job.Status = JobStatus.Running;
// 任务异常后可重试 // 任务异常后可重试
if (context.EventData.Exception != null) if (context.EventData.Exception != null)
{ {
job.IsAbandoned = false; job.IsAbandoned = false;
job.Result = GetExceptionMessage(context.EventData.Exception); job.Result = GetExceptionMessage(context.EventData.Exception);
// 周期性任务不用改变重试策略 // 周期性任务不用改变重试策略
if (job.JobType != JobType.Period) if (job.JobType != JobType.Period)
{
// 将任务标记为运行中, 会被轮询重新进入队列
job.Status = JobStatus.FailedRetry;
// 多次异常后需要重新计算优先级
if (job.TryCount <= (job.MaxTryCount / 2) &&
job.TryCount > (job.MaxTryCount / 3))
{ {
// 将任务标记为运行中, 会被轮询重新进入队列 job.Priority = JobPriority.BelowNormal;
job.Status = JobStatus.FailedRetry; }
// 多次异常后需要重新计算优先级 else if (job.TryCount > (job.MaxTryCount / 1.5))
if (job.TryCount <= (job.MaxTryCount / 2) && {
job.TryCount > (job.MaxTryCount / 3)) job.Priority = JobPriority.Low;
{
job.Priority = JobPriority.BelowNormal;
}
else if (job.TryCount > (job.MaxTryCount / 1.5))
{
job.Priority = JobPriority.Low;
}
// 等待时间调整
if (job.Interval <= 0)
{
job.Interval = 50;
}
var retryInterval = job.Interval * 1.5;
job.Interval = Convert.ToInt32(retryInterval);
} }
// 当未设置最大重试次数时不会标记停止 // 等待时间调整
if (job.MaxTryCount > 0 && job.TryCount >= job.MaxTryCount) if (job.Interval <= 0)
{ {
job.Status = JobStatus.Stopped; job.Interval = 50;
job.IsAbandoned = true;
job.NextRunTime = null;
await RemoveJobQueueAsync(context, job, context.EventData.CancellationToken);
} }
var retryInterval = job.Interval * 1.5;
job.Interval = Convert.ToInt32(retryInterval);
}
// 当未设置最大重试次数时不会标记停止
if (job.MaxTryCount > 0 && job.TryCount >= job.MaxTryCount)
{
job.TryCount += 1; job.TryCount += 1;
job.Status = JobStatus.Stopped;
job.IsAbandoned = true;
job.NextRunTime = null;
await RemoveJobQueueAsync(context, job, context.EventData.CancellationToken);
} }
else else
{ {
// 成功一次重置重试次数 job.TryCount += 1;
job.TryCount = 0; // 失败的作业需要由当前节点来调度
var jobCompleted = false; await ScheduleJobAsync(context, job, context.EventData.CancellationToken);
}
// 尝试达到上限则标记已完成 }
if (job.Status == JobStatus.FailedRetry && else
job.TryCount >= job.MaxTryCount) {
{ // 成功一次重置重试次数
jobCompleted = true; job.TryCount = 0;
} var jobCompleted = false;
// 所有任务达到上限则标记已完成 // 尝试达到上限则标记已完成
if (job.MaxCount > 0 && job.TriggerCount >= job.MaxCount) if (job.Status == JobStatus.FailedRetry &&
{ job.TryCount >= job.MaxTryCount)
jobCompleted = true; {
} jobCompleted = true;
}
if (jobCompleted) // 所有任务达到上限则标记已完成
{ if (job.MaxCount > 0 && job.TriggerCount >= job.MaxCount)
job.Status = JobStatus.Completed; {
job.NextRunTime = null; jobCompleted = true;
}
await RemoveJobQueueAsync(context, job, context.EventData.CancellationToken); // 一次性任务执行一次后标记为已完成
} if (job.JobType == JobType.Once)
{
jobCompleted = true;
} }
await store.StoreAsync(job, context.EventData.CancellationToken); if (jobCompleted)
{
job.Status = JobStatus.Completed;
job.NextRunTime = null;
await RemoveJobQueueAsync(context, job, context.EventData.CancellationToken);
}
} }
await store.StoreAsync(job, context.EventData.CancellationToken);
} }
private async Task RemoveJobQueueAsync(JobEventContext context, JobInfo jobInfo, CancellationToken cancellationToken = default) private async Task RemoveJobQueueAsync(JobEventContext context, JobInfo jobInfo, CancellationToken cancellationToken = default)
@ -112,6 +122,12 @@ public class JobExecutedEvent : JobEventBase<JobExecutedEvent>, ITransientDepend
await jobScheduler.RemoveAsync(jobInfo, cancellationToken); await jobScheduler.RemoveAsync(jobInfo, cancellationToken);
} }
private async Task ScheduleJobAsync(JobEventContext context, JobInfo jobInfo, CancellationToken cancellationToken = default)
{
var jobScheduler = context.ServiceProvider.GetRequiredService<IJobScheduler>();
await jobScheduler.QueueAsync(jobInfo, cancellationToken);
}
private string GetExceptionMessage(Exception exception) private string GetExceptionMessage(Exception exception)
{ {
if (exception.InnerException != null) if (exception.InnerException != null)

21
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/Localization/Resources/en.json

@ -3,6 +3,25 @@
"texts": { "texts": {
"JobAction:Failed": "Failed", "JobAction:Failed": "Failed",
"JobAction:Successed": "Successed", "JobAction:Successed": "Successed",
"JobAction:Completed": "Completed" "JobAction:Completed": "Completed",
"Status:None": "None",
"Status:Completed": "Completed",
"Status:Running": "Running",
"Status:Queuing": "Queuing",
"Status:Paused": "Paused",
"Status:FailedRetry": "Failed Retry",
"Status:Stopped": "Stopped",
"JobType:Once": "Once",
"JobType:Period": "Period",
"JobType:Persistent": "Persistent",
"Priority:Low": "Low",
"Priority:BelowNormal": "Below Normal",
"Priority:Normal": "Normal",
"Priority:AboveNormal": "Above Normal",
"Priority:High": "High",
"Source:None": "None",
"Source:Source": "Source",
"Source:User": "User",
"Source:System": "System"
} }
} }

21
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/Localization/Resources/zh-Hans.json

@ -3,6 +3,25 @@
"texts": { "texts": {
"JobAction:Failed": "失败", "JobAction:Failed": "失败",
"JobAction:Successed": "成功", "JobAction:Successed": "成功",
"JobAction:Completed": "完成" "JobAction:Completed": "完成",
"Status:None": "未定义",
"Status:Completed": "已完成",
"Status:Running": "运行中",
"Status:Queuing": "队列中",
"Status:Paused": "已暂停",
"Status:FailedRetry": "失败重试",
"Status:Stopped": "已停止",
"JobType:Once": "一次性",
"JobType:Period": "周期性",
"JobType:Persistent": "持续性",
"Priority:Low": "低",
"Priority:BelowNormal": "低于正常",
"Priority:Normal": "正常",
"Priority:AboveNormal": "高于正常",
"Priority:High": "高",
"Source:None": "未定义",
"Source:Source": "来源",
"Source:User": "用户",
"Source:System": "系统内置"
} }
} }

26
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/LINGYUN/Abp/BackgroundTasks/NullJobDispatcher.cs

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace LINGYUN.Abp.BackgroundTasks;
[Dependency(TryRegister = true)]
public class NullJobDispatcher : IJobDispatcher, ISingletonDependency
{
public static readonly IJobDispatcher Instance = new NullJobDispatcher();
public Task<bool> DispatchAsync(JobInfo job, CancellationToken cancellationToken = default)
{
return Task.FromResult(false);
}
public Task<bool> DispatchAsync(
IEnumerable<JobInfo> jobs,
string nodeName = null,
Guid? tenantId = null,
CancellationToken cancellationToken = default)
{
return Task.FromResult(false);
}
}

10
aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks/README.md

@ -3,6 +3,7 @@
后台任务(队列)模块,Abp提供的后台作业与后台工作者不支持Cron表达式, 提供可管理的后台任务(队列)功能. 后台任务(队列)模块,Abp提供的后台作业与后台工作者不支持Cron表达式, 提供可管理的后台任务(队列)功能.
实现了**Volo.Abp.BackgroundJobs.IBackgroundJobManager**, 意味着您也能通过框架后台作业接口添加新作业. 实现了**Volo.Abp.BackgroundJobs.IBackgroundJobManager**, 意味着您也能通过框架后台作业接口添加新作业.
实现了**Volo.Abp.BackgroundWorkers.IBackgroundWorkerManager**, 意味着您也能通过框架后台工作者接口添加新作业.
## 任务类别 ## 任务类别
@ -10,6 +11,15 @@
* JobType.Period: 周期性任务, 此类型任务会根据Cron表达式来决定运行方式, 适用于报表分析等场景 * JobType.Period: 周期性任务, 此类型任务会根据Cron表达式来决定运行方式, 适用于报表分析等场景
* JobType.Persistent: 持续性任务, 此类型任务按照给定重复次数、重复间隔运行, 适用于接口压测等场景 * JobType.Persistent: 持续性任务, 此类型任务按照给定重复次数、重复间隔运行, 适用于接口压测等场景
## 接口说明
* [IJobPublisher](/LINGYUN/Abp/BackgroundTasks/IJobPublisher.cs): 作业发布接口, 将指定作业发布到当前节点
* [IJobDispatcher](/LINGYUN/Abp/BackgroundTasks/IJobDispatcher.cs): 作业调度接口, 将指定作业调度到指定节点
* [IJobScheduler](/LINGYUN/Abp/BackgroundTasks/IJobScheduler.cs): 调度器接口, 管理当前运行节点作业调度器
* [IJobLockProvider](/LINGYUN/Abp/BackgroundTasks/IJobLockProvider.cs): 作业锁定接口, 指定作业加锁, 防止重复运行, 锁定时长参见作业 **LockTimeOut**
* [IJobEventTrigger](/LINGYUN/Abp/BackgroundTasks/IJobEventTrigger.cs): 作业事件触发器接口, 作业运行前与运行后监听接口
* [IJobStore](/LINGYUN/Abp/BackgroundTasks/IJobStore.cs): 作业持久化接口
## 配置使用 ## 配置使用
模块按需引用 模块按需引用

15
aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Core/LINGYUN/Abp/Webhooks/AbpWebhooksOptions.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
using Volo.Abp.Collections; using Volo.Abp.Collections;
namespace LINGYUN.Abp.Webhooks; namespace LINGYUN.Abp.Webhooks;
@ -32,7 +33,10 @@ public class AbpWebhooksOptions
/// 默认请求头 /// 默认请求头
/// </summary> /// </summary>
public IDictionary<string, string> DefaultHttpHeaders { get; } public IDictionary<string, string> DefaultHttpHeaders { get; }
/// <summary>
/// 默认发送方标识
/// </summary>
public string DefaultAgentIdentifier { get; set; }
public AbpWebhooksOptions() public AbpWebhooksOptions()
{ {
TimeoutDuration = TimeSpan.FromSeconds(60); TimeoutDuration = TimeSpan.FromSeconds(60);
@ -53,6 +57,15 @@ public class AbpWebhooksOptions
// 标识来源 // 标识来源
{ "X-Requested-From", "abp-webhooks" }, { "X-Requested-From", "abp-webhooks" },
}; };
DefaultAgentIdentifier = "Abp Webhooks";
var assembly = typeof(AbpWebhooksOptions).Assembly;
var versionAttr = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
if (versionAttr != null)
{
DefaultAgentIdentifier += " " + versionAttr.InformationalVersion;
}
} }
public void AddHeader(string key, string value) public void AddHeader(string key, string value)

18
aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/DefaultWebhookSender.cs

@ -135,7 +135,14 @@ namespace LINGYUN.Abp.Webhooks
foreach (var header in webhookSenderArgs.Headers) foreach (var header in webhookSenderArgs.Headers)
{ {
if (!request.Headers.Contains(header.Key)) if (request.Headers.Contains(header.Key) && request.Headers.Remove(header.Key))
{
if (request.Headers.TryAddWithoutValidation(header.Key, header.Value))
{
continue;
}
}
else
{ {
if (request.Headers.TryAddWithoutValidation(header.Key, header.Value)) if (request.Headers.TryAddWithoutValidation(header.Key, header.Value))
{ {
@ -143,7 +150,14 @@ namespace LINGYUN.Abp.Webhooks
} }
} }
if (!request.Content.Headers.Contains(header.Key)) if (request.Content.Headers.Contains(header.Key) && request.Content.Headers.Remove(header.Key))
{
if (request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value))
{
continue;
}
}
else
{ {
if (request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value)) if (request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value))
{ {

2
aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordGetListInput.cs

@ -10,6 +10,8 @@ public class WebhookSendRecordGetListInput : PagedAndSortedResultRequestDto
public Guid? TenantId { get; set; } public Guid? TenantId { get; set; }
public bool? State { get; set; }
public Guid? WebhookEventId { get; set; } public Guid? WebhookEventId { get; set; }
public Guid? SubscriptionId { get; set; } public Guid? SubscriptionId { get; set; }

8
aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/Extensions/WebhookSubscriptionExtensions.cs

@ -17,11 +17,13 @@ namespace LINGYUN.Abp.WebhooksManagement.Extensions
Webhooks = webhookSubscription.GetSubscribedWebhooks(), Webhooks = webhookSubscription.GetSubscribedWebhooks(),
Headers = webhookSubscription.GetWebhookHeaders(), Headers = webhookSubscription.GetWebhookHeaders(),
CreationTime = webhookSubscription.CreationTime, CreationTime = webhookSubscription.CreationTime,
CreatorId = webhookSubscription.CreatorId CreatorId = webhookSubscription.CreatorId,
Description = webhookSubscription.Description,
ConcurrencyStamp = webhookSubscription.ConcurrencyStamp,
}; };
} }
public static string ToSubscribedWebhooksString(this WebhookSubscriptionUpdateInput webhookSubscription) public static string ToSubscribedWebhooksString(this WebhookSubscriptionCreateOrUpdateInput webhookSubscription)
{ {
if (webhookSubscription.Webhooks.Any()) if (webhookSubscription.Webhooks.Any())
{ {
@ -31,7 +33,7 @@ namespace LINGYUN.Abp.WebhooksManagement.Extensions
return null; return null;
} }
public static string ToWebhookHeadersString(this WebhookSubscriptionUpdateInput webhookSubscription) public static string ToWebhookHeadersString(this WebhookSubscriptionCreateOrUpdateInput webhookSubscription)
{ {
if (webhookSubscription.Headers.Any()) if (webhookSubscription.Headers.Any())
{ {

51
aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordAppService.cs

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.BackgroundJobs; using Volo.Abp.BackgroundJobs;
@ -52,30 +53,6 @@ public class WebhookSendRecordAppService : WebhooksManagementAppServiceBase, IWe
await RecordRepository.DeleteManyAsync(sendRecords); await RecordRepository.DeleteManyAsync(sendRecords);
} }
private class WebhookSendRecordGetListSpecification : Volo.Abp.Specifications.Specification<WebhookSendRecord>
{
protected WebhookSendRecordGetListInput Filter { get; }
public WebhookSendRecordGetListSpecification(WebhookSendRecordGetListInput filter)
{
Filter = filter;
}
public override Expression<Func<WebhookSendRecord, bool>> ToExpression()
{
Expression<Func<WebhookSendRecord, bool>> expression = _ => true;
return expression
.AndIf(Filter.TenantId.HasValue, x => x.TenantId == Filter.TenantId)
.AndIf(Filter.WebhookEventId.HasValue, x => x.WebhookEventId == Filter.WebhookEventId)
.AndIf(Filter.SubscriptionId.HasValue, x => x.WebhookSubscriptionId == Filter.SubscriptionId)
.AndIf(Filter.ResponseStatusCode.HasValue, x => x.ResponseStatusCode == Filter.ResponseStatusCode)
.AndIf(Filter.BeginCreationTime.HasValue, x => x.CreationTime.CompareTo(Filter.BeginCreationTime) >= 0)
.AndIf(Filter.EndCreationTime.HasValue, x => x.CreationTime.CompareTo(Filter.EndCreationTime) <= 0)
.AndIf(!Filter.Filter.IsNullOrWhiteSpace(), x => x.Response.Contains(Filter.Filter));
}
}
public async virtual Task<PagedResultDto<WebhookSendRecordDto>> GetListAsync(WebhookSendRecordGetListInput input) public async virtual Task<PagedResultDto<WebhookSendRecordDto>> GetListAsync(WebhookSendRecordGetListInput input)
{ {
var specification = new WebhookSendRecordGetListSpecification(input); var specification = new WebhookSendRecordGetListSpecification(input);
@ -126,4 +103,30 @@ public class WebhookSendRecordAppService : WebhooksManagementAppServiceBase, IWe
await ResendAsync(recordId); await ResendAsync(recordId);
} }
} }
private class WebhookSendRecordGetListSpecification : Volo.Abp.Specifications.Specification<WebhookSendRecord>
{
protected WebhookSendRecordGetListInput Filter { get; }
public WebhookSendRecordGetListSpecification(WebhookSendRecordGetListInput filter)
{
Filter = filter;
}
public override Expression<Func<WebhookSendRecord, bool>> ToExpression()
{
Expression<Func<WebhookSendRecord, bool>> expression = _ => true;
return expression
.AndIf(Filter.TenantId.HasValue, x => x.TenantId == Filter.TenantId)
.AndIf(Filter.State == true, x => x.ResponseStatusCode > HttpStatusCode.Continue && x.ResponseStatusCode < HttpStatusCode.BadRequest)
.AndIf(Filter.State == false, x => x.ResponseStatusCode >= HttpStatusCode.BadRequest && x.ResponseStatusCode <= HttpStatusCode.NetworkAuthenticationRequired)
.AndIf(Filter.WebhookEventId.HasValue, x => x.WebhookEventId == Filter.WebhookEventId)
.AndIf(Filter.SubscriptionId.HasValue, x => x.WebhookSubscriptionId == Filter.SubscriptionId)
.AndIf(Filter.ResponseStatusCode.HasValue, x => x.ResponseStatusCode == Filter.ResponseStatusCode)
.AndIf(Filter.BeginCreationTime.HasValue, x => x.CreationTime >= Filter.BeginCreationTime)
.AndIf(Filter.EndCreationTime.HasValue, x => x.CreationTime <= Filter.EndCreationTime)
.AndIf(!Filter.Filter.IsNullOrWhiteSpace(), x => x.Response.Contains(Filter.Filter));
}
}
} }

93
aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionAppService.cs

@ -2,10 +2,10 @@
using LINGYUN.Abp.WebhooksManagement.Authorization; using LINGYUN.Abp.WebhooksManagement.Authorization;
using LINGYUN.Abp.WebhooksManagement.Extensions; using LINGYUN.Abp.WebhooksManagement.Extensions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
@ -35,8 +35,8 @@ public class WebhookSubscriptionAppService : WebhooksManagementAppServiceBase, I
var subscription = new WebhookSubscription( var subscription = new WebhookSubscription(
GuidGenerator.Create(), GuidGenerator.Create(),
input.WebhookUri, input.WebhookUri,
JsonConvert.SerializeObject(input.Webhooks), input.ToWebhookHeadersString(),
JsonConvert.SerializeObject(input.Headers), input.ToWebhookHeadersString(),
input.Secret, input.Secret,
input.TenantId ?? CurrentTenant.Id) input.TenantId ?? CurrentTenant.Id)
{ {
@ -44,7 +44,7 @@ public class WebhookSubscriptionAppService : WebhooksManagementAppServiceBase, I
Description = input.Description, Description = input.Description,
}; };
await SubscriptionRepository.InsertAsync(subscription); subscription = await SubscriptionRepository.InsertAsync(subscription);
await CurrentUnitOfWork.SaveChangesAsync(); await CurrentUnitOfWork.SaveChangesAsync();
@ -75,20 +75,10 @@ public class WebhookSubscriptionAppService : WebhooksManagementAppServiceBase, I
public async virtual Task<PagedResultDto<WebhookSubscriptionDto>> GetListAsync(WebhookSubscriptionGetListInput input) public async virtual Task<PagedResultDto<WebhookSubscriptionDto>> GetListAsync(WebhookSubscriptionGetListInput input)
{ {
var filter = new WebhookSubscriptionFilter var specification = new WebhookSubscriptionGetListSpecification(input);
{
Filter = input.Filter,
BeginCreationTime = input.BeginCreationTime,
EndCreationTime = input.EndCreationTime,
IsActive = input.IsActive,
Secret = input.Secret,
TenantId = input.TenantId,
Webhooks = input.Webhooks,
WebhookUri = input.WebhookUri
};
var totalCount = await SubscriptionRepository.GetCountAsync(filter); var totalCount = await SubscriptionRepository.GetCountAsync(specification);
var subscriptions = await SubscriptionRepository.GetListAsync(filter, var subscriptions = await SubscriptionRepository.GetListAsync(specification,
input.Sorting, input.MaxResultCount, input.SkipCount); input.Sorting, input.MaxResultCount, input.SkipCount);
return new PagedResultDto<WebhookSubscriptionDto>(totalCount, return new PagedResultDto<WebhookSubscriptionDto>(totalCount,
@ -99,25 +89,23 @@ public class WebhookSubscriptionAppService : WebhooksManagementAppServiceBase, I
public async virtual Task<WebhookSubscriptionDto> UpdateAsync(Guid id, WebhookSubscriptionUpdateInput input) public async virtual Task<WebhookSubscriptionDto> UpdateAsync(Guid id, WebhookSubscriptionUpdateInput input)
{ {
var subscription = await SubscriptionRepository.GetAsync(id); var subscription = await SubscriptionRepository.GetAsync(id);
if (!string.Equals(subscription.WebhookUri, input.WebhookUri))
UpdateByInput(subscription, input);
var inputWebhooks = input.ToSubscribedWebhooksString();
if (!string.Equals(subscription.Webhooks, inputWebhooks, StringComparison.InvariantCultureIgnoreCase))
{ {
await CheckSubscribedAsync(input); subscription.SetWebhooks(inputWebhooks);
} }
var inputHeaders = input.ToWebhookHeadersString();
subscription.SetSecret(input.Secret); if (!string.Equals(subscription.Headers, inputHeaders, StringComparison.InvariantCultureIgnoreCase))
subscription.SetWebhookUri(input.WebhookUri);
subscription.SetWebhooks(input.ToSubscribedWebhooksString());
subscription.SetHeaders(input.ToWebhookHeadersString());
subscription.SetTenantId(input.TenantId);
subscription.IsActive = input.IsActive;
if (!string.Equals(subscription.Description, input.Description, StringComparison.InvariantCultureIgnoreCase))
{ {
subscription.Description = input.Description; subscription.SetHeaders(input.ToWebhookHeadersString());
} }
subscription.SetConcurrencyStampIfNotNull(input.ConcurrencyStamp); subscription.SetConcurrencyStampIfNotNull(input.ConcurrencyStamp);
await SubscriptionRepository.UpdateAsync(subscription); subscription = await SubscriptionRepository.UpdateAsync(subscription);
await CurrentUnitOfWork.SaveChangesAsync(); await CurrentUnitOfWork.SaveChangesAsync();
@ -168,4 +156,51 @@ public class WebhookSubscriptionAppService : WebhooksManagementAppServiceBase, I
} }
} }
} }
protected virtual void UpdateByInput(WebhookSubscription subscription, WebhookSubscriptionCreateOrUpdateInput input)
{
if (!string.Equals(subscription.Secret, input.Secret, StringComparison.InvariantCultureIgnoreCase))
{
subscription.SetSecret(input.Secret);
}
if (!string.Equals(subscription.WebhookUri, input.WebhookUri, StringComparison.InvariantCultureIgnoreCase))
{
subscription.SetWebhookUri(input.WebhookUri);
}
if (!string.Equals(subscription.Description, input.Description, StringComparison.InvariantCultureIgnoreCase))
{
subscription.Description = input.Description;
}
if (!Equals(subscription.TenantId, input.TenantId))
{
subscription.SetTenantId(input.TenantId);
}
subscription.IsActive = input.IsActive;
}
private class WebhookSubscriptionGetListSpecification : Volo.Abp.Specifications.Specification<WebhookSubscription>
{
protected WebhookSubscriptionGetListInput Filter { get; }
public WebhookSubscriptionGetListSpecification(WebhookSubscriptionGetListInput filter)
{
Filter = filter;
}
public override Expression<Func<WebhookSubscription, bool>> ToExpression()
{
Expression<Func<WebhookSubscription, bool>> expression = _ => true;
return expression
.AndIf(Filter.TenantId.HasValue, x => x.TenantId == Filter.TenantId)
.AndIf(Filter.IsActive.HasValue, x => x.IsActive == Filter.IsActive)
.AndIf(Filter.BeginCreationTime.HasValue, x => x.CreationTime >= Filter.BeginCreationTime)
.AndIf(Filter.EndCreationTime.HasValue, x => x.CreationTime <= Filter.EndCreationTime)
.AndIf(!Filter.WebhookUri.IsNullOrWhiteSpace(), x => x.WebhookUri == Filter.WebhookUri)
.AndIf(!Filter.Secret.IsNullOrWhiteSpace(), x => x.Secret == Filter.Secret)
.AndIf(!Filter.Webhooks.IsNullOrWhiteSpace(), x => x.Webhooks.Contains("\"" + Filter.Webhooks + "\""))
.AndIf(!Filter.Filter.IsNullOrWhiteSpace(), x => x.WebhookUri.Contains(Filter.Filter) ||
x.Secret.Contains(Filter.Filter) || x.Webhooks.Contains(Filter.Filter));
}
}
} }

5
aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/Resources/en.json

@ -55,6 +55,9 @@
"DisplayName:Webhooks": "Webhooks", "DisplayName:Webhooks": "Webhooks",
"DisplayName:Headers": "Headers", "DisplayName:Headers": "Headers",
"DisplayName:RequestHeaders": "Request Headers", "DisplayName:RequestHeaders": "Request Headers",
"DisplayName:ResponseHeaders": "Response Headers" "DisplayName:ResponseHeaders": "Response Headers",
"DisplayName:State": "State",
"ResponseState:Successed": "Successed",
"ResponseState:Failed": "Failed"
} }
} }

5
aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/Resources/zh-Hans.json

@ -55,6 +55,9 @@
"DisplayName:Webhooks": "事件列表", "DisplayName:Webhooks": "事件列表",
"DisplayName:Headers": "发送标头", "DisplayName:Headers": "发送标头",
"DisplayName:RequestHeaders": "请求标头", "DisplayName:RequestHeaders": "请求标头",
"DisplayName:ResponseHeaders": "响应标头" "DisplayName:ResponseHeaders": "响应标头",
"DisplayName:State": "状态",
"ResponseState:Successed": "成功",
"ResponseState:Failed": "失败"
} }
} }

5
aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/IWebhookSubscriptionRepository.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Specifications;
namespace LINGYUN.Abp.WebhooksManagement; namespace LINGYUN.Abp.WebhooksManagement;
@ -15,11 +16,11 @@ public interface IWebhookSubscriptionRepository : IRepository<WebhookSubscriptio
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
Task<int> GetCountAsync( Task<int> GetCountAsync(
WebhookSubscriptionFilter filter, ISpecification<WebhookSubscription> specification,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
Task<List<WebhookSubscription>> GetListAsync( Task<List<WebhookSubscription>> GetListAsync(
WebhookSubscriptionFilter filter, ISpecification<WebhookSubscription> specification,
string sorting = $"{nameof(WebhookSubscription.CreationTime)} DESC", string sorting = $"{nameof(WebhookSubscription.CreationTime)} DESC",
int maxResultCount = 10, int maxResultCount = 10,
int skipCount = 0, int skipCount = 0,

27
aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.EntityFrameworkCore/LINGYUN/Abp/WebhooksManagement/EntityFrameworkCore/EfCoreWebhookSubscriptionRepository.cs

@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Specifications;
namespace LINGYUN.Abp.WebhooksManagement.EntityFrameworkCore; namespace LINGYUN.Abp.WebhooksManagement.EntityFrameworkCore;
@ -34,39 +35,25 @@ public class EfCoreWebhookSubscriptionRepository :
} }
public async virtual Task<int> GetCountAsync( public async virtual Task<int> GetCountAsync(
WebhookSubscriptionFilter filter, ISpecification<WebhookSubscription> specification,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
return await ApplyFilter(await GetDbSetAsync(), filter) return await (await GetDbSetAsync())
.Where(specification.ToExpression())
.CountAsync(GetCancellationToken(cancellationToken)); .CountAsync(GetCancellationToken(cancellationToken));
} }
public async virtual Task<List<WebhookSubscription>> GetListAsync( public async virtual Task<List<WebhookSubscription>> GetListAsync(
WebhookSubscriptionFilter filter, ISpecification<WebhookSubscription> specification,
string sorting = $"{nameof(WebhookSubscription.CreationTime)} DESC", string sorting = $"{nameof(WebhookSubscription.CreationTime)} DESC",
int maxResultCount = 10, int maxResultCount = 10,
int skipCount = 0, int skipCount = 0,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
return await ApplyFilter(await GetDbSetAsync(), filter) return await (await GetDbSetAsync())
.Where(specification.ToExpression())
.OrderBy(sorting ?? $"{nameof(WebhookSubscription.CreationTime)} DESC") .OrderBy(sorting ?? $"{nameof(WebhookSubscription.CreationTime)} DESC")
.PageBy(skipCount, maxResultCount) .PageBy(skipCount, maxResultCount)
.ToListAsync(GetCancellationToken(cancellationToken)); .ToListAsync(GetCancellationToken(cancellationToken));
} }
protected virtual IQueryable<WebhookSubscription> ApplyFilter(
IQueryable<WebhookSubscription> queryable,
WebhookSubscriptionFilter filter)
{
return queryable
.WhereIf(filter.TenantId.HasValue, x => x.TenantId == filter.TenantId)
.WhereIf(filter.IsActive.HasValue, x => x.IsActive == filter.IsActive)
.WhereIf(!filter.WebhookUri.IsNullOrWhiteSpace(), x => x.WebhookUri == filter.WebhookUri)
.WhereIf(!filter.Secret.IsNullOrWhiteSpace(), x => x.Secret == filter.Secret)
.WhereIf(!filter.Webhooks.IsNullOrWhiteSpace(), x => x.Webhooks.Contains("\"" + filter.Webhooks + "\""))
.WhereIf(filter.BeginCreationTime.HasValue, x => x.CreationTime.CompareTo(filter.BeginCreationTime) >= 0)
.WhereIf(filter.EndCreationTime.HasValue, x => x.CreationTime.CompareTo(filter.EndCreationTime) <= 0)
.WhereIf(!filter.Filter.IsNullOrWhiteSpace(), x => x.WebhookUri.Contains(filter.Filter) ||
x.Secret.Contains(filter.Filter) || x.Webhooks.Contains(filter.Filter));
}
} }

4
aspnet-core/services/LY.MicroService.WebhooksManagement.HttpApi.Host/WebhooksManagementHttpApiHostModule.Configure.cs

@ -145,6 +145,10 @@ public partial class WebhooksManagementHttpApiHostModule
job.MaxTryCount = webhooksOptions.MaxSendAttemptCount; job.MaxTryCount = webhooksOptions.MaxSendAttemptCount;
// 需要锁定作业 // 需要锁定作业
job.LockTimeOut = webhooksOptions.TimeoutDuration.TotalSeconds.To<int>(); job.LockTimeOut = webhooksOptions.TimeoutDuration.TotalSeconds.To<int>();
if (webhooksOptions.IsAutomaticSubscriptionDeactivationEnabled)
{
job.MaxCount = webhooksOptions.MaxConsecutiveFailCountBeforeDeactivateSubscription;
}
}); });
//options.JobDispatcherSelectors.AddNamespace( //options.JobDispatcherSelectors.AddNamespace(
// "LINGYUN.Abp.Webhooks.BackgroundJobs", // "LINGYUN.Abp.Webhooks.BackgroundJobs",

Loading…
Cancel
Save