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

24
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) => {
return defAbpHttp.request<PagedResultDto<WebhookSendAttempt>>({
service: remoteServiceName,
@ -47,3 +59,15 @@ export const resend = (id: string) => {
},
});
}
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) => {
return defAbpHttp.request<PagedResultDto<WebhookSubscription>>({
service: remoteServiceName,

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

@ -39,7 +39,17 @@
</template>
<template v-else-if="column.dataIndex==='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" />
<DatePicker
v-else-if="record.javaScriptType==='Date'"
@ -256,6 +266,18 @@
.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) => {
return option.description.toLowerCase().indexOf(input.toLowerCase()) >= 0;
@ -315,6 +337,7 @@
record.field = defineParam.name;
record.javaScriptType = defineParam.javaScriptType;
record.value = undefined;
record.options = defineParam.options ?? [];
if (defineParam.javaScriptType === 'boolean') {
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>,
}
/** 选项 */
export interface ParamterOption {
/** 键名 */
key: string;
/** 键值 */
value: any;
}
/** 自定义字段 */
export interface DefineParamter {
/** 字段名称 */
@ -32,6 +40,8 @@ export interface DefineParamter {
javaScriptType: string;
/** 可用运算条件列表, 用于进一步约束字段可用比较符 */
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;
export function checkStatus(
status: number,
response: any,
msg: string,
errorMessageMode: ErrorMessageMode = 'message',
): void {
@ -20,7 +20,7 @@ export function checkStatus(
const userStore = useUserStoreWithOut();
let errMessage = '';
switch (status) {
switch (response.status) {
case 400:
errMessage = `${msg}`;
break;

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

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

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

@ -1,6 +1,24 @@
<template>
<div class="content">
<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 v-if="column.key === 'responseStatusCode'">
<Tag :color="getHttpStatusColor(record.responseStatusCode)">{{
@ -41,7 +59,8 @@
</template>
<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 { useLocalization } from '/@/hooks/abp/useLocalization';
import { useModal } from '/@/components/Modal';
@ -50,13 +69,13 @@
import { getDataColumns } from '../datas/TableData';
import { getSearchFormSchemas } from '../datas/ModalData';
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';
const { createConfirm, createMessage } = useMessage();
const { L } = useLocalization(['WebhooksManagement', 'AbpUi']);
const [registerModal, { openModal }] = useModal();
const [registerTable, { reload, setLoading }] = useTable({
const [registerTable, { reload, setLoading, getSelectRowKeys, clearSelectedRowKeys }] = useTable({
rowKey: 'id',
title: L('SendAttempts'),
columns: getDataColumns(),
@ -77,6 +96,13 @@
title: L('Actions'),
dataIndex: 'action',
},
rowSelection: {
type: 'checkbox',
},
});
const isManyRecordSelected = computed(() => {
const selectedKeys = getSelectRowKeys();
return selectedKeys.length > 0;
});
function handleEdit(record) {
@ -90,22 +116,74 @@
content: L('ItemWillBeDeletedMessage'),
okCancel: true,
onOk: () => {
setLoading(true);
return deleteById(record.id).then(() => {
createMessage.success(L('SuccessfullyDeleted'));
clearSelectedRowKeys();
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) {
setLoading(true);
resend(record.id)
.then(() => {
createMessage.success(L('Successful'));
})
.finally(() => {
setLoading(false);
});
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('ItemWillBeResendMessageWithFormat', { 0: L('SelectedItems')}),
okCancel: true,
onOk: () => {
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>

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

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

@ -27,6 +27,9 @@
<FormItem name="webhookUri" required :label="L('DisplayName:WebhookUri')">
<Input v-model:value="modelRef.webhookUri" autocomplete="off" />
</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')">
<InputPassword v-model:value="modelRef.secret" autocomplete="off" />
</FormItem>
@ -56,7 +59,6 @@
<FormItem name="headers" :label="L('DisplayName:Headers')">
<CodeEditorX style="height: 300px" :mode="MODE.JSON" v-model="modelRef.headers" />
</FormItem>
</Form>
</BasicModal>
</template>
@ -66,11 +68,11 @@
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useValidation } from '/@/hooks/abp/useValidation';
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 { CodeEditorX, MODE } from '/@/components/CodeEditor';
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 { getById, create, update, getAllAvailableWebhooks } from '/@/api/webhooks/subscriptions';
import {
@ -83,11 +85,11 @@
const SelectOption = Select.Option;
const emit = defineEmits(['change', 'register']);
const { L } = useLocalization('WebhooksManagement');
const { L } = useLocalization(['WebhooksManagement', 'AbpUi']);
const { ruleCreator } = useValidation();
const { createMessage } = useMessage();
const formElRef = ref<any>();
const tenantsRef = ref<Tenant[]>([]);
const tenantsRef = ref<TenantDto[]>([]);
const webhooksGroupRef = ref<WebhookAvailableGroup[]>([]);
const modelRef = ref<WebhookSubscription>(getDefaultModel());
const [registerModal, { closeModal, changeOkLoading }] = useModalInner((model) => {

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

@ -2,12 +2,21 @@
<div class="content">
<BasicTable @register="registerTable">
<template #toolbar>
<a-button
<Button
v-auth="['AbpWebhooks.Subscriptions.Create']"
type="primary"
@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 #bodyCell="{ column, record }">
<template v-if="column.key === 'isActive'">
@ -44,7 +53,8 @@
</template>
<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 { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization';
@ -53,13 +63,13 @@
import { formatPagedRequest } from '/@/utils/http/abp/helper';
import { getDataColumns } from '../datas/TableData';
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';
const { createConfirm } = useMessage();
const { createConfirm, createMessage } = useMessage();
const { L } = useLocalization(['WebhooksManagement', 'AbpUi']);
const [registerModal, { openModal }] = useModal();
const [registerTable, { reload }] = useTable({
const [registerTable, { reload, setLoading, clearSelectedRowKeys, getSelectRowKeys }] = useTable({
rowKey: 'id',
title: L('Subscriptions'),
columns: getDataColumns(),
@ -80,6 +90,13 @@
title: L('Actions'),
dataIndex: 'action',
},
rowSelection: {
type: 'checkbox',
},
});
const deleteManyEnabled = computed(() => {
const selectKeys = getSelectRowKeys();
return selectKeys.length > 0;
});
function handleAddNew() {
@ -97,8 +114,33 @@
content: L('ItemWillBeDeletedMessage'),
okCancel: true,
onOk: () => {
setLoading(true);
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();
}).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> {
return {
labelWidth: 100,
fieldMapToTime: [
['creationTime', ['beginCreationTime', 'endCreationTime'], ['YYYY-MM-DDT00:00:00', 'YYYY-MM-DDT23:59:59']]
],
schemas: [
{
field: 'tenantId',
@ -76,21 +79,10 @@ export function getSearchFormSchemas(): Partial<FormProps> {
},
},
{
field: 'beginCreationTime',
component: 'DatePicker',
label: L('DisplayName:BeginCreationTime'),
colProps: { span: 6 },
componentProps: {
style: {
width: '100%',
},
},
},
{
field: 'endCreationTime',
component: 'DatePicker',
label: L('DisplayName:EndCreationTime'),
colProps: { span: 6 },
field: 'creationTime',
component: 'RangePicker',
label: L('DisplayName:CreationTime'),
colProps: { span: 12 },
componentProps: {
style: {
width: '100%',

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

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

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

@ -6,7 +6,7 @@ declare interface LocalizableStringInfo {
declare type ExtraPropertyDictionary = { [key: string]: any };
declare interface IHasConcurrencyStamp {
concurrencyStamp: string;
concurrencyStamp?: string;
}
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);
}
// 避免循环记录
[DisableAuditing]
public async virtual Task<string> SaveAsync(
AuditLogInfo auditInfo,
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(
AuditLogInfo auditInfo,
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
{
[DisableAuditing]
public class AuditLog : IHasExtraProperties
{
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;
}
// 避免循环记录
[DisableAuditing]
public async virtual Task SaveAsync(AuditLogInfo 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 JavaScriptType { get; set; }
public DynamicComparison[] AvailableComparator { get; set; }
public ParamterOptionDto[] Options { get; set; }
public DynamicParamterDto()
{
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
var localizedProp = L[$"DisplayName:{propertyInfo.Name}"];
var propertyTypeMap = GetPropertyTypeMap(propertyInfo.PropertyType);
dynamicParamters.Add(
new DynamicParamterDto
var dynamicParamter = 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,
Type = propertyInfo.PropertyType.FullName,
Description = localizedProp.Value ?? propertyInfo.Name,
JavaScriptType = propertyTypeMap.JavaScriptType,
AvailableComparator = propertyTypeMap.AvailableComparator
});
var enumName = enumNames[index];
var localizerEnumKey = $"{propertyInfo.Name}:{enumName}";
var localizerEnumName = L[localizerEnumKey];
paramterOptions[index] = new ParamterOptionDto
{
Key = localizerEnumName.ResourceNotFound ? enumName : localizerEnumName.Value,
Value = enumValues.GetValue(index),
};
}
dynamicParamter.Options = paramterOptions;
}
dynamicParamters.Add(dynamicParamter);
}
return Task.FromResult(new ListResultDto<DynamicParamterDto>(dynamicParamters));
@ -96,6 +122,31 @@ public abstract class DynamicQueryableAppService<TEntity, TEntityDto> : Applicat
isNullableType = true;
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;
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
.AddPermission(AbpOssManagementPermissions.OssObject.Default, L("Permission:OssObject"))
.RequireFeatures(AbpOssManagementFeatureNames.OssObject.Default);
.RequireFeatures(AbpOssManagementFeatureNames.OssObject.Enable);
ossobject
.AddChild(AbpOssManagementPermissions.OssObject.Create, L("Permission:Create"))
.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>();
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);
Logger.LogInformation("Job {Group}-{Name} after event with {Event} was executed.", context.EventData.Group, context.EventData.Name, typeof(TEvent).Name);
}
}
catch (Exception ex)
@ -38,7 +40,9 @@ public abstract class JobEventBase<TEvent> : IJobEvent
var currentTenant = context.ServiceProvider.GetRequiredService<ICurrentTenant>();
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);
Logger.LogInformation("Job {Group}-{Name} before event with {Event} was executed.", context.EventData.Group, context.EventData.Name, typeof(TEvent).Name);
}
}
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)
{
maxCount = MaxTryCount - TryCount;
// TODO: 不能用Quartz自带的重试机制
//if (Status == JobStatus.FailedRetry && MaxTryCount > 0)
//{
// maxCount = MaxTryCount - TryCount;
if (maxCount < 0)
{
maxCount = 0;
}
// if (maxCount < 0)
// {
// maxCount = 0;
// }
if (maxCount > 0)
{
// 触发重试时,失败间隔时间调整
Interval = 60;
}
}
// if (maxCount > 0)
// {
// // 触发重试时,失败间隔时间调整
// Interval = 60;
// }
//}
return maxCount;
}

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

@ -5,6 +5,8 @@
## 特性参数
* 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
// fix throw Quartz.SchedulerException: Repeat Interval cannot be zero.
var scheduleBuilder = SimpleScheduleBuilder.Create();
scheduleBuilder.WithRepeatCount(maxCount);
// TODO: 不能用Quartz自带的重试机制
// scheduleBuilder.WithRepeatCount(maxCount);
if (job.Interval > 0)
{
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.LockTimeOut = selector.LockTimeOut ?? jobInfo.LockTimeOut;
jobInfo.Priority = selector.Priority ?? jobInfo.Priority;
jobInfo.TryCount = selector.MaxCount ?? jobInfo.MaxCount;
jobInfo.MaxCount = selector.MaxCount ?? jobInfo.MaxCount;
jobInfo.MaxTryCount = selector.MaxTryCount ?? jobInfo.MaxTryCount;
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.DependencyInjection;
using Volo.Abp.DynamicProxy;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Timing;
@ -18,6 +19,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
protected IJobStore JobStore { get; }
protected IJobPublisher JobPublisher { get; }
protected ICurrentTenant CurrentTenant { get; }
protected IGuidGenerator GuidGenerator { get; }
protected AbpBackgroundTasksOptions Options { get; }
protected AbpBackgroundTasksOptions TasksOptions { get; }
@ -26,6 +28,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
IJobStore jobStore,
IJobPublisher jobPublisher,
ICurrentTenant currentTenant,
IGuidGenerator guidGenerator,
IOptions<AbpBackgroundTasksOptions> options,
IOptions<AbpBackgroundTasksOptions> taskOptions)
{
@ -33,6 +36,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
JobStore = jobStore;
JobPublisher = jobPublisher;
CurrentTenant = currentTenant;
GuidGenerator = guidGenerator;
Options = options.Value;
TasksOptions = taskOptions.Value;
}
@ -50,6 +54,12 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
return;
}
// 如果通过远程接口发布作业, 可能会造成Group与Name重复
// 重新设定Name为唯一Id
var jobId = GuidGenerator.Create();
jobInfo.Id = jobId.ToString();
jobInfo.Name = jobId.ToString();
jobInfo.NodeName = Options.NodeName;
jobInfo.BeginTime = Clock.Now;
jobInfo.CreationTime = Clock.Now;
@ -65,7 +75,7 @@ public class BackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDepen
jobInfo.Interval = selector.Interval ?? jobInfo.Interval;
jobInfo.LockTimeOut = selector.LockTimeOut ?? jobInfo.LockTimeOut;
jobInfo.Priority = selector.Priority ?? jobInfo.Priority;
jobInfo.TryCount = selector.MaxCount ?? jobInfo.MaxCount;
jobInfo.MaxCount = selector.MaxCount ?? jobInfo.MaxCount;
jobInfo.MaxTryCount = selector.MaxTryCount ?? jobInfo.MaxTryCount;
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;
namespace LINGYUN.Abp.BackgroundTasks;
/// <summary>
/// 作业发布接口
/// </summary>
/// <remarks>
/// 使用场景: 发布作业到作业调度器(发布作业到当前运行节点)
/// </remarks>
public interface IJobPublisher
{
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;
/// <summary>
/// 作业调度接口
/// 作业调度接口
/// </summary>
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.Options;
using System;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Auditing;
@ -29,12 +30,37 @@ public class BackgroundPollingJob : IJobRunnable
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);
}
// 非当前节点作业调度
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.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
@ -12,98 +13,107 @@ public class JobExecutedEvent : JobEventBase<JobExecutedEvent>, ITransientDepend
{
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;
}
var store = context.ServiceProvider.GetRequiredService<IJobStore>();
var job = await store.FindAsync(context.EventData.Key, context.EventData.CancellationToken);
if (job != null)
if (job == null)
{
job.TriggerCount += 1;
job.TenantId = context.EventData.TenantId;
job.LastRunTime = context.EventData.RunTime;
job.NextRunTime = context.EventData.NextRunTime;
job.Result = context.EventData.Result ?? "OK";
job.Status = JobStatus.Running;
Logger.LogWarning("Cannot process job change event because job with key {Id} could not be found!", context.EventData.Key);
return;
}
// 一次性任务执行一次后标记为已完成
if (job.JobType == JobType.Once)
{
job.Status = JobStatus.Completed;
}
job.TriggerCount += 1;
job.TenantId = context.EventData.TenantId;
job.LastRunTime = context.EventData.RunTime;
job.NextRunTime = context.EventData.NextRunTime;
job.Result = context.EventData.Result ?? "OK";
job.Status = JobStatus.Running;
// 任务异常后可重试
if (context.EventData.Exception != null)
{
job.IsAbandoned = false;
job.Result = GetExceptionMessage(context.EventData.Exception);
// 任务异常后可重试
if (context.EventData.Exception != null)
{
job.IsAbandoned = false;
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.Status = JobStatus.FailedRetry;
// 多次异常后需要重新计算优先级
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.Interval <= 0)
{
job.Interval = 50;
}
var retryInterval = job.Interval * 1.5;
job.Interval = Convert.ToInt32(retryInterval);
job.Priority = JobPriority.BelowNormal;
}
else if (job.TryCount > (job.MaxTryCount / 1.5))
{
job.Priority = JobPriority.Low;
}
// 当未设置最大重试次数时不会标记停止
if (job.MaxTryCount > 0 && job.TryCount >= job.MaxTryCount)
// 等待时间调整
if (job.Interval <= 0)
{
job.Status = JobStatus.Stopped;
job.IsAbandoned = true;
job.NextRunTime = null;
await RemoveJobQueueAsync(context, job, context.EventData.CancellationToken);
job.Interval = 50;
}
var retryInterval = job.Interval * 1.5;
job.Interval = Convert.ToInt32(retryInterval);
}
// 当未设置最大重试次数时不会标记停止
if (job.MaxTryCount > 0 && job.TryCount >= job.MaxTryCount)
{
job.TryCount += 1;
job.Status = JobStatus.Stopped;
job.IsAbandoned = true;
job.NextRunTime = null;
await RemoveJobQueueAsync(context, job, context.EventData.CancellationToken);
}
else
{
// 成功一次重置重试次数
job.TryCount = 0;
var jobCompleted = false;
// 尝试达到上限则标记已完成
if (job.Status == JobStatus.FailedRetry &&
job.TryCount >= job.MaxTryCount)
{
jobCompleted = true;
}
job.TryCount += 1;
// 失败的作业需要由当前节点来调度
await ScheduleJobAsync(context, job, context.EventData.CancellationToken);
}
}
else
{
// 成功一次重置重试次数
job.TryCount = 0;
var jobCompleted = false;
// 所有任务达到上限则标记已完成
if (job.MaxCount > 0 && job.TriggerCount >= job.MaxCount)
{
jobCompleted = true;
}
// 尝试达到上限则标记已完成
if (job.Status == JobStatus.FailedRetry &&
job.TryCount >= job.MaxTryCount)
{
jobCompleted = true;
}
if (jobCompleted)
{
job.Status = JobStatus.Completed;
job.NextRunTime = null;
// 所有任务达到上限则标记已完成
if (job.MaxCount > 0 && job.TriggerCount >= job.MaxCount)
{
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)
@ -112,6 +122,12 @@ public class JobExecutedEvent : JobEventBase<JobExecutedEvent>, ITransientDepend
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)
{
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": {
"JobAction:Failed": "Failed",
"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": {
"JobAction:Failed": "失败",
"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表达式, 提供可管理的后台任务(队列)功能.
实现了**Volo.Abp.BackgroundJobs.IBackgroundJobManager**, 意味着您也能通过框架后台作业接口添加新作业.
实现了**Volo.Abp.BackgroundWorkers.IBackgroundWorkerManager**, 意味着您也能通过框架后台工作者接口添加新作业.
## 任务类别
@ -10,6 +11,15 @@
* JobType.Period: 周期性任务, 此类型任务会根据Cron表达式来决定运行方式, 适用于报表分析等场景
* 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.Collections.Generic;
using System.Reflection;
using Volo.Abp.Collections;
namespace LINGYUN.Abp.Webhooks;
@ -32,7 +33,10 @@ public class AbpWebhooksOptions
/// 默认请求头
/// </summary>
public IDictionary<string, string> DefaultHttpHeaders { get; }
/// <summary>
/// 默认发送方标识
/// </summary>
public string DefaultAgentIdentifier { get; set; }
public AbpWebhooksOptions()
{
TimeoutDuration = TimeSpan.FromSeconds(60);
@ -53,6 +57,15 @@ public class AbpWebhooksOptions
// 标识来源
{ "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)

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)
{
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))
{
@ -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))
{

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 bool? State { get; set; }
public Guid? WebhookEventId { 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(),
Headers = webhookSubscription.GetWebhookHeaders(),
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())
{
@ -31,7 +33,7 @@ namespace LINGYUN.Abp.WebhooksManagement.Extensions
return null;
}
public static string ToWebhookHeadersString(this WebhookSubscriptionUpdateInput webhookSubscription)
public static string ToWebhookHeadersString(this WebhookSubscriptionCreateOrUpdateInput webhookSubscription)
{
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.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.BackgroundJobs;
@ -52,30 +53,6 @@ public class WebhookSendRecordAppService : WebhooksManagementAppServiceBase, IWe
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)
{
var specification = new WebhookSendRecordGetListSpecification(input);
@ -126,4 +103,30 @@ public class WebhookSendRecordAppService : WebhooksManagementAppServiceBase, IWe
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.Extensions;
using Microsoft.AspNetCore.Authorization;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
@ -35,8 +35,8 @@ public class WebhookSubscriptionAppService : WebhooksManagementAppServiceBase, I
var subscription = new WebhookSubscription(
GuidGenerator.Create(),
input.WebhookUri,
JsonConvert.SerializeObject(input.Webhooks),
JsonConvert.SerializeObject(input.Headers),
input.ToWebhookHeadersString(),
input.ToWebhookHeadersString(),
input.Secret,
input.TenantId ?? CurrentTenant.Id)
{
@ -44,7 +44,7 @@ public class WebhookSubscriptionAppService : WebhooksManagementAppServiceBase, I
Description = input.Description,
};
await SubscriptionRepository.InsertAsync(subscription);
subscription = await SubscriptionRepository.InsertAsync(subscription);
await CurrentUnitOfWork.SaveChangesAsync();
@ -75,20 +75,10 @@ public class WebhookSubscriptionAppService : WebhooksManagementAppServiceBase, I
public async virtual Task<PagedResultDto<WebhookSubscriptionDto>> GetListAsync(WebhookSubscriptionGetListInput input)
{
var filter = new WebhookSubscriptionFilter
{
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 specification = new WebhookSubscriptionGetListSpecification(input);
var totalCount = await SubscriptionRepository.GetCountAsync(filter);
var subscriptions = await SubscriptionRepository.GetListAsync(filter,
var totalCount = await SubscriptionRepository.GetCountAsync(specification);
var subscriptions = await SubscriptionRepository.GetListAsync(specification,
input.Sorting, input.MaxResultCount, input.SkipCount);
return new PagedResultDto<WebhookSubscriptionDto>(totalCount,
@ -99,25 +89,23 @@ public class WebhookSubscriptionAppService : WebhooksManagementAppServiceBase, I
public async virtual Task<WebhookSubscriptionDto> UpdateAsync(Guid id, WebhookSubscriptionUpdateInput input)
{
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);
}
subscription.SetSecret(input.Secret);
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))
var inputHeaders = input.ToWebhookHeadersString();
if (!string.Equals(subscription.Headers, inputHeaders, StringComparison.InvariantCultureIgnoreCase))
{
subscription.Description = input.Description;
subscription.SetHeaders(input.ToWebhookHeadersString());
}
subscription.SetConcurrencyStampIfNotNull(input.ConcurrencyStamp);
await SubscriptionRepository.UpdateAsync(subscription);
subscription = await SubscriptionRepository.UpdateAsync(subscription);
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:Headers": "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:Headers": "发送标头",
"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.Tasks;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Specifications;
namespace LINGYUN.Abp.WebhooksManagement;
@ -15,11 +16,11 @@ public interface IWebhookSubscriptionRepository : IRepository<WebhookSubscriptio
CancellationToken cancellationToken = default);
Task<int> GetCountAsync(
WebhookSubscriptionFilter filter,
ISpecification<WebhookSubscription> specification,
CancellationToken cancellationToken = default);
Task<List<WebhookSubscription>> GetListAsync(
WebhookSubscriptionFilter filter,
ISpecification<WebhookSubscription> specification,
string sorting = $"{nameof(WebhookSubscription.CreationTime)} DESC",
int maxResultCount = 10,
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 Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Specifications;
namespace LINGYUN.Abp.WebhooksManagement.EntityFrameworkCore;
@ -34,39 +35,25 @@ public class EfCoreWebhookSubscriptionRepository :
}
public async virtual Task<int> GetCountAsync(
WebhookSubscriptionFilter filter,
ISpecification<WebhookSubscription> specification,
CancellationToken cancellationToken = default)
{
return await ApplyFilter(await GetDbSetAsync(), filter)
return await (await GetDbSetAsync())
.Where(specification.ToExpression())
.CountAsync(GetCancellationToken(cancellationToken));
}
public async virtual Task<List<WebhookSubscription>> GetListAsync(
WebhookSubscriptionFilter filter,
ISpecification<WebhookSubscription> specification,
string sorting = $"{nameof(WebhookSubscription.CreationTime)} DESC",
int maxResultCount = 10,
int skipCount = 0,
CancellationToken cancellationToken = default)
{
return await ApplyFilter(await GetDbSetAsync(), filter)
return await (await GetDbSetAsync())
.Where(specification.ToExpression())
.OrderBy(sorting ?? $"{nameof(WebhookSubscription.CreationTime)} DESC")
.PageBy(skipCount, maxResultCount)
.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.LockTimeOut = webhooksOptions.TimeoutDuration.TotalSeconds.To<int>();
if (webhooksOptions.IsAutomaticSubscriptionDeactivationEnabled)
{
job.MaxCount = webhooksOptions.MaxConsecutiveFailCountBeforeDeactivateSubscription;
}
});
//options.JobDispatcherSelectors.AddNamespace(
// "LINGYUN.Abp.Webhooks.BackgroundJobs",

Loading…
Cancel
Save