midway-fatcms 0.0.1-beta.11 → 0.0.1-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +10 -0
- package/.prettierrc.js +4 -0
- package/dist/configuration.js +2 -1
- package/dist/controller/gateway/AsyncTaskController.d.ts +14 -0
- package/dist/controller/gateway/AsyncTaskController.js +97 -0
- package/dist/libs/crud-pro/services/CrudProGenSqlCondition.js +4 -4
- package/dist/libs/utils/errorToString.d.ts +2 -0
- package/dist/libs/utils/errorToString.js +57 -0
- package/dist/middleware/global.middleware.js +11 -1
- package/dist/models/AsyncTaskModel.d.ts +65 -0
- package/dist/models/AsyncTaskModel.js +25 -0
- package/dist/models/SystemTables.d.ts +1 -0
- package/dist/models/SystemTables.js +1 -0
- package/dist/schedule/index.d.ts +2 -1
- package/dist/schedule/index.js +2 -1
- package/dist/service/asyncTask/AsyncTaskRunnerService.d.ts +8 -0
- package/dist/service/asyncTask/AsyncTaskRunnerService.js +110 -0
- package/dist/service/asyncTask/AsyncTaskService.d.ts +7 -0
- package/dist/service/asyncTask/AsyncTaskService.js +32 -0
- package/package.json +5 -2
- package/src/configuration.ts +2 -1
- package/src/controller/gateway/AsyncTaskController.ts +75 -0
- package/src/controller/gateway/StaticController.ts +1 -1
- package/src/controller/manage/AnyApiMangeApi.ts +6 -6
- package/src/libs/crud-pro/services/CrudProGenSqlCondition.ts +4 -4
- package/src/libs/utils/errorToString.ts +66 -0
- package/src/middleware/global.middleware.ts +14 -1
- package/src/models/AsyncTaskModel.ts +74 -0
- package/src/models/SystemTables.ts +1 -0
- package/src/schedule/index.ts +1 -1
- package/src/service/asyncTask/AsyncTaskRunnerService.ts +114 -0
- package/src/service/asyncTask/AsyncTaskService.ts +20 -0
- package/tsconfig.json +32 -0
package/.eslintrc.json
ADDED
package/.prettierrc.js
ADDED
package/dist/configuration.js
CHANGED
|
@@ -54,7 +54,7 @@ let ContainerLifeCycle = class ContainerLifeCycle {
|
|
|
54
54
|
const config = this.app.getConfig();
|
|
55
55
|
if (config.fatcmsScheduleService) {
|
|
56
56
|
// 启动定时任务
|
|
57
|
-
const scheduleServiceList = ['proxyApiLoadService', 'workbenchService', 'visitStatService'];
|
|
57
|
+
const scheduleServiceList = ['proxyApiLoadService', 'workbenchService', 'visitStatService', 'asyncTaskRunnerService'];
|
|
58
58
|
await (0, schedule_1.startSchedule)(this.app, scheduleServiceList);
|
|
59
59
|
logger.info('ContainerLifeCycle ==> onReady 启动定时任务 ');
|
|
60
60
|
}
|
|
@@ -71,6 +71,7 @@ __decorate([
|
|
|
71
71
|
], ContainerLifeCycle.prototype, "app", void 0);
|
|
72
72
|
ContainerLifeCycle = __decorate([
|
|
73
73
|
(0, core_1.Configuration)({
|
|
74
|
+
// namespace: 'fatcms',
|
|
74
75
|
imports: [
|
|
75
76
|
koa,
|
|
76
77
|
validate,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Context } from '@midwayjs/koa';
|
|
2
|
+
import { BaseApiController } from '../base/BaseApiController';
|
|
3
|
+
/**
|
|
4
|
+
* 异步任务框架
|
|
5
|
+
*/
|
|
6
|
+
export declare class AsyncTaskController extends BaseApiController {
|
|
7
|
+
protected ctx: Context;
|
|
8
|
+
private asyncTaskService;
|
|
9
|
+
getMyTasks(): Promise<import("../../libs/crud-pro/models/ExecuteContext").ExecuteContext>;
|
|
10
|
+
createTask(): Promise<import("../../libs/crud-pro/models/ExecuteContext").ExecuteContext>;
|
|
11
|
+
cancelTask(id: number): Promise<{
|
|
12
|
+
success: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.AsyncTaskController = void 0;
|
|
16
|
+
const core_1 = require("@midwayjs/core");
|
|
17
|
+
const BaseApiController_1 = require("../base/BaseApiController");
|
|
18
|
+
const AsyncTaskService_1 = require("../../service/asyncTask/AsyncTaskService");
|
|
19
|
+
const AsyncTaskModel_1 = require("../../models/AsyncTaskModel");
|
|
20
|
+
const keys_1 = require("../../libs/crud-pro/models/keys");
|
|
21
|
+
const SystemTables_1 = require("../../models/SystemTables");
|
|
22
|
+
const exceptions_1 = require("../../libs/crud-pro/exceptions");
|
|
23
|
+
function fixCancelBodyData(body, id) {
|
|
24
|
+
if (!body.data || !body.condition) {
|
|
25
|
+
throw new exceptions_1.CommonException("参数不正确");
|
|
26
|
+
}
|
|
27
|
+
// fix condition
|
|
28
|
+
const conditionObj = (body.condition || {});
|
|
29
|
+
conditionObj.id = id;
|
|
30
|
+
body.condition = conditionObj;
|
|
31
|
+
// fix data
|
|
32
|
+
const dataObj = (body.data || {});
|
|
33
|
+
dataObj.task_status = AsyncTaskModel_1.TaskStatus.CANCELLED;
|
|
34
|
+
body.data = dataObj;
|
|
35
|
+
}
|
|
36
|
+
function fixCreateBodyData(body) {
|
|
37
|
+
if (!body.data) {
|
|
38
|
+
throw new exceptions_1.CommonException("参数不正确");
|
|
39
|
+
}
|
|
40
|
+
// fix data
|
|
41
|
+
const dataObj = (body.data || {});
|
|
42
|
+
dataObj.task_status = AsyncTaskModel_1.TaskStatus.PENDING;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 异步任务框架
|
|
46
|
+
*/
|
|
47
|
+
let AsyncTaskController = class AsyncTaskController extends BaseApiController_1.BaseApiController {
|
|
48
|
+
// 获取任务列表
|
|
49
|
+
async getMyTasks() {
|
|
50
|
+
return this.executeSysSimpleSQL(SystemTables_1.SystemTables.sys_async_tasks, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_PAGE);
|
|
51
|
+
}
|
|
52
|
+
// 创建任务
|
|
53
|
+
async createTask() {
|
|
54
|
+
fixCreateBodyData(this.ctx.request.body);
|
|
55
|
+
const res = await this.executeSysSimpleSQL(SystemTables_1.SystemTables.sys_async_tasks, keys_1.KeysOfSimpleSQL.SIMPLE_INSERT);
|
|
56
|
+
await this.asyncTaskService.startTask();
|
|
57
|
+
return res;
|
|
58
|
+
}
|
|
59
|
+
// 取消任务
|
|
60
|
+
async cancelTask(id) {
|
|
61
|
+
fixCancelBodyData(this.ctx.request.body, id);
|
|
62
|
+
await this.asyncTaskService.cancelTask(id);
|
|
63
|
+
await this.executeSysSimpleSQL(SystemTables_1.SystemTables.sys_async_tasks, keys_1.KeysOfSimpleSQL.SIMPLE_UPDATE);
|
|
64
|
+
return { success: true };
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
__decorate([
|
|
68
|
+
(0, core_1.Inject)(),
|
|
69
|
+
__metadata("design:type", Object)
|
|
70
|
+
], AsyncTaskController.prototype, "ctx", void 0);
|
|
71
|
+
__decorate([
|
|
72
|
+
(0, core_1.Inject)(),
|
|
73
|
+
__metadata("design:type", AsyncTaskService_1.AsyncTaskService)
|
|
74
|
+
], AsyncTaskController.prototype, "asyncTaskService", void 0);
|
|
75
|
+
__decorate([
|
|
76
|
+
(0, core_1.Get)('/getMyTasks'),
|
|
77
|
+
__metadata("design:type", Function),
|
|
78
|
+
__metadata("design:paramtypes", []),
|
|
79
|
+
__metadata("design:returntype", Promise)
|
|
80
|
+
], AsyncTaskController.prototype, "getMyTasks", null);
|
|
81
|
+
__decorate([
|
|
82
|
+
(0, core_1.Post)('/createTask'),
|
|
83
|
+
__metadata("design:type", Function),
|
|
84
|
+
__metadata("design:paramtypes", []),
|
|
85
|
+
__metadata("design:returntype", Promise)
|
|
86
|
+
], AsyncTaskController.prototype, "createTask", null);
|
|
87
|
+
__decorate([
|
|
88
|
+
(0, core_1.Post)('/cancelTask'),
|
|
89
|
+
__param(0, (0, core_1.Query)('id')),
|
|
90
|
+
__metadata("design:type", Function),
|
|
91
|
+
__metadata("design:paramtypes", [Number]),
|
|
92
|
+
__metadata("design:returntype", Promise)
|
|
93
|
+
], AsyncTaskController.prototype, "cancelTask", null);
|
|
94
|
+
AsyncTaskController = __decorate([
|
|
95
|
+
(0, core_1.Controller)('/ns/gw/AsyncTask')
|
|
96
|
+
], AsyncTaskController);
|
|
97
|
+
exports.AsyncTaskController = AsyncTaskController;
|
|
@@ -193,28 +193,28 @@ class CrudProGenSqlCondition {
|
|
|
193
193
|
tmpArgList.push(value0);
|
|
194
194
|
tmpSql = `${toSqlColumnName(key)} COLLATE UTF8MB4_GENERAL_CI like concat(?, '%')`; // like前缀匹配
|
|
195
195
|
if (this.sqlCfg.sqlDbType === keys_1.SqlDbType.postgres) {
|
|
196
|
-
tmpSql = `${toSqlColumnName(key)} like concat(
|
|
196
|
+
tmpSql = `${toSqlColumnName(key)} like concat(?::text, '%')`; // like前缀匹配
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
else if (equalsIgnoreCase(keys_1.KeysOfConditions.$NOT_LIKE, compare)) {
|
|
200
200
|
tmpArgList.push(value0);
|
|
201
201
|
tmpSql = `${toSqlColumnName(key)} COLLATE UTF8MB4_GENERAL_CI not like concat(?, '%')`;
|
|
202
202
|
if (this.sqlCfg.sqlDbType === keys_1.SqlDbType.postgres) {
|
|
203
|
-
tmpSql = `${toSqlColumnName(key)} not like concat(
|
|
203
|
+
tmpSql = `${toSqlColumnName(key)} not like concat(?::text, '%')`;
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
else if (equalsIgnoreCase(keys_1.KeysOfConditions.$LIKE_INCLUDE, compare)) {
|
|
207
207
|
tmpArgList.push(value0);
|
|
208
208
|
tmpSql = `${toSqlColumnName(key)} COLLATE UTF8MB4_GENERAL_CI like concat('%', ?, '%')`; // like包含
|
|
209
209
|
if (this.sqlCfg.sqlDbType === keys_1.SqlDbType.postgres) {
|
|
210
|
-
tmpSql = `${toSqlColumnName(key)} like concat('%',
|
|
210
|
+
tmpSql = `${toSqlColumnName(key)} like concat('%', ?::text, '%')`; // like包含
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
else if (equalsIgnoreCase(keys_1.KeysOfConditions.$NOT_LIKE_INCLUDE, compare)) {
|
|
214
214
|
tmpArgList.push(value0);
|
|
215
215
|
tmpSql = `${toSqlColumnName(key)} COLLATE UTF8MB4_GENERAL_CI not like concat('%',?, '%')`; // like不包含
|
|
216
216
|
if (this.sqlCfg.sqlDbType === keys_1.SqlDbType.postgres) {
|
|
217
|
-
tmpSql = `${toSqlColumnName(key)} not like concat('%'
|
|
217
|
+
tmpSql = `${toSqlColumnName(key)} not like concat('%',?::text, '%')`; // like不包含
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
else if (equalsIgnoreCase(keys_1.KeysOfConditions.$MATCH, compare)) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.errorToString = void 0;
|
|
4
|
+
function errorToString(error) {
|
|
5
|
+
var _a;
|
|
6
|
+
// 处理非错误对象的情况
|
|
7
|
+
if (typeof error === 'undefined') {
|
|
8
|
+
return 'Error: undefined was thrown';
|
|
9
|
+
}
|
|
10
|
+
if (error === null) {
|
|
11
|
+
return 'Error: null was thrown';
|
|
12
|
+
}
|
|
13
|
+
// 处理原始值类型
|
|
14
|
+
const primitiveTypes = ['string', 'number', 'boolean', 'symbol'];
|
|
15
|
+
if (primitiveTypes.includes(typeof error)) {
|
|
16
|
+
return `Error: A primitive value was thrown\nValue: ${String(error)}`;
|
|
17
|
+
}
|
|
18
|
+
// 处理 Error 对象
|
|
19
|
+
if (error instanceof Error) {
|
|
20
|
+
const errorDetails = [];
|
|
21
|
+
errorDetails.push(`Error: ${error.name || 'Error'}`);
|
|
22
|
+
errorDetails.push(`Message: ${error.message || 'No error message'}`);
|
|
23
|
+
if (error.stack) {
|
|
24
|
+
errorDetails.push('Stack Trace:');
|
|
25
|
+
errorDetails.push(error.stack);
|
|
26
|
+
}
|
|
27
|
+
const extraProperties = ['code', 'fileName', 'lineNumber', 'columnNumber'];
|
|
28
|
+
extraProperties.forEach(prop => {
|
|
29
|
+
if (error[prop] !== undefined) {
|
|
30
|
+
errorDetails.push(`${prop}: ${error[prop]}`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const customProps = Object.keys(error).filter(key => !['name', 'message', 'stack', ...extraProperties].includes(key));
|
|
34
|
+
if (customProps.length > 0) {
|
|
35
|
+
errorDetails.push('Custom Properties:');
|
|
36
|
+
customProps.forEach(prop => {
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.stringify(error[prop], null, 2) || String(error[prop]);
|
|
39
|
+
errorDetails.push(` ${prop}: ${value}`);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
errorDetails.push(` ${prop}: [Object cannot be serialized]`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return errorDetails.join('\n');
|
|
47
|
+
}
|
|
48
|
+
// 处理普通对象
|
|
49
|
+
try {
|
|
50
|
+
const objectDetails = JSON.stringify(error, null, 2);
|
|
51
|
+
return `Error: A plain object was thrown\nObject: ${objectDetails}`;
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
return `Error: An object was thrown but could not be serialized\nType: ${((_a = error.constructor) === null || _a === void 0 ? void 0 : _a.name) || 'Unknown'}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.errorToString = errorToString;
|
|
@@ -186,13 +186,23 @@ async function trackRequest(ctx) {
|
|
|
186
186
|
console.error('trackRequestError', e);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
+
const excludePathPrefix = [
|
|
190
|
+
'/ns/static/',
|
|
191
|
+
'/ns/api/helpers'
|
|
192
|
+
];
|
|
189
193
|
/**
|
|
190
194
|
* 全局中间件
|
|
191
195
|
*/
|
|
192
196
|
let GlobalMiddleware = class GlobalMiddleware {
|
|
193
197
|
match(ctx) {
|
|
194
198
|
const path = ctx.path;
|
|
195
|
-
|
|
199
|
+
for (let i = 0; i < excludePathPrefix.length; i++) {
|
|
200
|
+
const excludePathPrefix1 = excludePathPrefix[i];
|
|
201
|
+
if (path.startsWith(excludePathPrefix1)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
196
206
|
}
|
|
197
207
|
resolve() {
|
|
198
208
|
return async (ctx, next) => {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface SysAsyncTaskEntity {
|
|
2
|
+
id: number;
|
|
3
|
+
task_uuid: string;
|
|
4
|
+
task_name: string;
|
|
5
|
+
task_description: string | null;
|
|
6
|
+
task_type: string;
|
|
7
|
+
task_status: TaskStatus;
|
|
8
|
+
input_params: any;
|
|
9
|
+
output_result: any | null;
|
|
10
|
+
input_file_path: string | null;
|
|
11
|
+
input_file_format: FileFormat | null;
|
|
12
|
+
input_file_size: number | null;
|
|
13
|
+
output_file_path: string | null;
|
|
14
|
+
output_file_format: FileFormat | null;
|
|
15
|
+
output_file_size: number | null;
|
|
16
|
+
input_total_records: number | null;
|
|
17
|
+
output_total_records: number | null;
|
|
18
|
+
processed_records: number;
|
|
19
|
+
progress: number;
|
|
20
|
+
error_message: string | null;
|
|
21
|
+
error_records: any | null;
|
|
22
|
+
created_by: string | null;
|
|
23
|
+
priority: number;
|
|
24
|
+
retry_count: number;
|
|
25
|
+
max_retries: number;
|
|
26
|
+
parent_task_id: number | null;
|
|
27
|
+
created_at: Date;
|
|
28
|
+
started_at: Date | null;
|
|
29
|
+
completed_at: Date | null;
|
|
30
|
+
updated_at: Date;
|
|
31
|
+
expired_at: Date | null;
|
|
32
|
+
}
|
|
33
|
+
export declare enum TaskStatus {
|
|
34
|
+
PENDING = "PENDING",
|
|
35
|
+
RUNNING = "RUNNING",
|
|
36
|
+
SUCCEEDED = "SUCCEEDED",
|
|
37
|
+
FAILED = "FAILED",
|
|
38
|
+
CANCELLED = "CANCELLED",
|
|
39
|
+
PAUSED = "PAUSED"
|
|
40
|
+
}
|
|
41
|
+
export declare enum FileFormat {
|
|
42
|
+
XLSX = "xlsx",
|
|
43
|
+
CSV = "csv",
|
|
44
|
+
JSON = "json",
|
|
45
|
+
PDF = "pdf",
|
|
46
|
+
TXT = "txt",
|
|
47
|
+
XML = "xml",
|
|
48
|
+
ZIP = "zip",
|
|
49
|
+
HTML = "html",
|
|
50
|
+
PNG = "png",
|
|
51
|
+
JPG = "jpg"
|
|
52
|
+
}
|
|
53
|
+
export interface ITaskHandler {
|
|
54
|
+
execute(ctx: TaskContext): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
export interface TaskContext {
|
|
57
|
+
task: SysAsyncTaskEntity;
|
|
58
|
+
}
|
|
59
|
+
export interface TaskHandlerConfig {
|
|
60
|
+
taskType: string;
|
|
61
|
+
handler: ITaskHandler;
|
|
62
|
+
maxRetries?: number;
|
|
63
|
+
priority?: number;
|
|
64
|
+
expireSeconds?: number;
|
|
65
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileFormat = exports.TaskStatus = void 0;
|
|
4
|
+
var TaskStatus;
|
|
5
|
+
(function (TaskStatus) {
|
|
6
|
+
TaskStatus["PENDING"] = "PENDING";
|
|
7
|
+
TaskStatus["RUNNING"] = "RUNNING";
|
|
8
|
+
TaskStatus["SUCCEEDED"] = "SUCCEEDED";
|
|
9
|
+
TaskStatus["FAILED"] = "FAILED";
|
|
10
|
+
TaskStatus["CANCELLED"] = "CANCELLED";
|
|
11
|
+
TaskStatus["PAUSED"] = "PAUSED";
|
|
12
|
+
})(TaskStatus = exports.TaskStatus || (exports.TaskStatus = {}));
|
|
13
|
+
var FileFormat;
|
|
14
|
+
(function (FileFormat) {
|
|
15
|
+
FileFormat["XLSX"] = "xlsx";
|
|
16
|
+
FileFormat["CSV"] = "csv";
|
|
17
|
+
FileFormat["JSON"] = "json";
|
|
18
|
+
FileFormat["PDF"] = "pdf";
|
|
19
|
+
FileFormat["TXT"] = "txt";
|
|
20
|
+
FileFormat["XML"] = "xml";
|
|
21
|
+
FileFormat["ZIP"] = "zip";
|
|
22
|
+
FileFormat["HTML"] = "html";
|
|
23
|
+
FileFormat["PNG"] = "png";
|
|
24
|
+
FileFormat["JPG"] = "jpg";
|
|
25
|
+
})(FileFormat = exports.FileFormat || (exports.FileFormat = {}));
|
package/dist/schedule/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import * as koa from '@midwayjs/koa';
|
|
2
|
+
declare function runSchedule(app: koa.Application, serviceList: string[]): Promise<void>;
|
|
2
3
|
declare function startSchedule(app: koa.Application, serviceList: string[]): Promise<void>;
|
|
3
|
-
export { startSchedule };
|
|
4
|
+
export { startSchedule, runSchedule };
|
package/dist/schedule/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.startSchedule = void 0;
|
|
3
|
+
exports.runSchedule = exports.startSchedule = void 0;
|
|
4
4
|
const contextLogger_1 = require("../models/contextLogger");
|
|
5
5
|
const Transaction_1 = require("../libs/crud-pro/models/Transaction");
|
|
6
6
|
const userSession_1 = require("../models/userSession");
|
|
@@ -60,6 +60,7 @@ async function runSchedule(app, serviceList) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
exports.runSchedule = runSchedule;
|
|
63
64
|
async function startSchedule(app, serviceList) {
|
|
64
65
|
await runSchedule(app, serviceList);
|
|
65
66
|
setInterval(() => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Context } from '@midwayjs/koa';
|
|
2
|
+
import { BaseService } from "../../service/base/BaseService";
|
|
3
|
+
import { IScheduleService } from "../../interface";
|
|
4
|
+
export declare class AsyncTaskRunnerService extends BaseService implements IScheduleService {
|
|
5
|
+
protected ctx: Context;
|
|
6
|
+
private curdProService;
|
|
7
|
+
runBySchedule(): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.AsyncTaskRunnerService = void 0;
|
|
13
|
+
const core_1 = require("@midwayjs/core");
|
|
14
|
+
const BaseService_1 = require("../../service/base/BaseService");
|
|
15
|
+
const CurdProService_1 = require("../../service/curd/CurdProService");
|
|
16
|
+
const AsyncTaskModel_1 = require("../../models/AsyncTaskModel");
|
|
17
|
+
const global_config_1 = require("../../libs/global-config/global-config");
|
|
18
|
+
const SystemTables_1 = require("../../models/SystemTables");
|
|
19
|
+
const keys_1 = require("../../libs/crud-pro/models/keys");
|
|
20
|
+
const errorToString_1 = require("../../libs/utils/errorToString");
|
|
21
|
+
class AsyncTaskRunner {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.isBusy = false;
|
|
24
|
+
this.taskHandlerMap = new Map();
|
|
25
|
+
}
|
|
26
|
+
async executeTaskList(taskList) {
|
|
27
|
+
if (!taskList || taskList.length === 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.isBusy = true;
|
|
31
|
+
for (let i = 0; i < taskList.length; i++) {
|
|
32
|
+
const taskElement = taskList[i];
|
|
33
|
+
try {
|
|
34
|
+
await this.executeTask(taskElement);
|
|
35
|
+
taskElement.task_status = AsyncTaskModel_1.TaskStatus.SUCCEEDED;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
taskElement.task_status = AsyncTaskModel_1.TaskStatus.FAILED;
|
|
39
|
+
taskElement.error_message = (0, errorToString_1.errorToString)(error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async executeTask(taskElement) {
|
|
44
|
+
const taskType = taskElement.task_type;
|
|
45
|
+
const taskHandler = this.taskHandlerMap.get(taskType);
|
|
46
|
+
if (!taskHandler) {
|
|
47
|
+
throw new Error('TaskHandler not found , taskType = ' + taskType);
|
|
48
|
+
}
|
|
49
|
+
await taskHandler.execute({ task: taskElement });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const ASYNC_TASK_RUNNER = new AsyncTaskRunner();
|
|
53
|
+
const LOCK_KEY_ASYNC_TASK_RUNNER = "LOCK_KEY_ASYNC_TASK_RUNNER";
|
|
54
|
+
let AsyncTaskRunnerService = class AsyncTaskRunnerService extends BaseService_1.BaseService {
|
|
55
|
+
async runBySchedule() {
|
|
56
|
+
if (ASYNC_TASK_RUNNER.isBusy) {
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
}
|
|
59
|
+
const lock = await this.redisService.set(LOCK_KEY_ASYNC_TASK_RUNNER, 1, "EX", 3600, "NX");
|
|
60
|
+
if (lock !== 'OK') {
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
}
|
|
63
|
+
const { SystemDbName, SystemDbType } = global_config_1.GLOBAL_STATIC_CONFIG.getConfig();
|
|
64
|
+
const queryRes = await this.curdProService.executeCrudByCfg({
|
|
65
|
+
condition: {
|
|
66
|
+
task_status: AsyncTaskModel_1.TaskStatus.PENDING
|
|
67
|
+
},
|
|
68
|
+
limit: 10
|
|
69
|
+
}, {
|
|
70
|
+
sqlTable: SystemTables_1.SystemTables.sys_async_tasks,
|
|
71
|
+
sqlSimpleName: keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_PAGE,
|
|
72
|
+
sqlDatabase: SystemDbName,
|
|
73
|
+
sqlDbType: SystemDbType,
|
|
74
|
+
});
|
|
75
|
+
const taskList = queryRes.getResRows();
|
|
76
|
+
if (taskList.length === 0) {
|
|
77
|
+
return Promise.resolve();
|
|
78
|
+
}
|
|
79
|
+
const taskIds = taskList.map(elem => elem.id).filter(Boolean);
|
|
80
|
+
await this.curdProService.executeCrudByCfg({
|
|
81
|
+
condition: {
|
|
82
|
+
id: {
|
|
83
|
+
"$in": taskIds
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
data: {
|
|
87
|
+
task_status: AsyncTaskModel_1.TaskStatus.RUNNING
|
|
88
|
+
}
|
|
89
|
+
}, {
|
|
90
|
+
sqlTable: SystemTables_1.SystemTables.sys_async_tasks,
|
|
91
|
+
sqlSimpleName: keys_1.KeysOfSimpleSQL.SIMPLE_UPDATE,
|
|
92
|
+
sqlDatabase: SystemDbName,
|
|
93
|
+
sqlDbType: SystemDbType,
|
|
94
|
+
});
|
|
95
|
+
ASYNC_TASK_RUNNER.executeTaskList(taskList);
|
|
96
|
+
return Promise.resolve();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
__decorate([
|
|
100
|
+
(0, core_1.Inject)(),
|
|
101
|
+
__metadata("design:type", Object)
|
|
102
|
+
], AsyncTaskRunnerService.prototype, "ctx", void 0);
|
|
103
|
+
__decorate([
|
|
104
|
+
(0, core_1.Inject)(),
|
|
105
|
+
__metadata("design:type", CurdProService_1.CurdProService)
|
|
106
|
+
], AsyncTaskRunnerService.prototype, "curdProService", void 0);
|
|
107
|
+
AsyncTaskRunnerService = __decorate([
|
|
108
|
+
(0, core_1.Provide)()
|
|
109
|
+
], AsyncTaskRunnerService);
|
|
110
|
+
exports.AsyncTaskRunnerService = AsyncTaskRunnerService;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.AsyncTaskService = void 0;
|
|
13
|
+
const core_1 = require("@midwayjs/core");
|
|
14
|
+
const BaseService_1 = require("../../service/base/BaseService");
|
|
15
|
+
const schedule_1 = require("../../schedule");
|
|
16
|
+
let AsyncTaskService = class AsyncTaskService extends BaseService_1.BaseService {
|
|
17
|
+
async startTask() {
|
|
18
|
+
(0, schedule_1.runSchedule)(this.app, ['asyncTaskRunnerService']).then(schedule => {
|
|
19
|
+
console.log(schedule);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async cancelTask(id) {
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
__decorate([
|
|
26
|
+
(0, core_1.Inject)(),
|
|
27
|
+
__metadata("design:type", Object)
|
|
28
|
+
], AsyncTaskService.prototype, "ctx", void 0);
|
|
29
|
+
AsyncTaskService = __decorate([
|
|
30
|
+
(0, core_1.Provide)()
|
|
31
|
+
], AsyncTaskService);
|
|
32
|
+
exports.AsyncTaskService = AsyncTaskService;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "midway-fatcms",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.13",
|
|
4
4
|
"description": "This is a midway component sample",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
"src/**/*.txt",
|
|
24
24
|
"src/**/*.html",
|
|
25
25
|
"src/**/*.ico",
|
|
26
|
-
"index.d.ts"
|
|
26
|
+
"index.d.ts",
|
|
27
|
+
"tsconfig.json",
|
|
28
|
+
".prettierrc.js",
|
|
29
|
+
".eslintrc.json"
|
|
27
30
|
],
|
|
28
31
|
"license": "MIT",
|
|
29
32
|
"devDependencies": {
|
package/src/configuration.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { startSchedule } from './schedule';
|
|
|
17
17
|
import { privateAES } from './libs/utils/crypto-utils';
|
|
18
18
|
|
|
19
19
|
@Configuration({
|
|
20
|
+
// namespace: 'fatcms',
|
|
20
21
|
imports: [
|
|
21
22
|
koa,
|
|
22
23
|
validate,
|
|
@@ -67,7 +68,7 @@ export class ContainerLifeCycle {
|
|
|
67
68
|
|
|
68
69
|
if (config.fatcmsScheduleService) {
|
|
69
70
|
// 启动定时任务
|
|
70
|
-
const scheduleServiceList = ['proxyApiLoadService', 'workbenchService', 'visitStatService'];
|
|
71
|
+
const scheduleServiceList = ['proxyApiLoadService', 'workbenchService', 'visitStatService', 'asyncTaskRunnerService'];
|
|
71
72
|
await startSchedule(this.app, scheduleServiceList);
|
|
72
73
|
logger.info('ContainerLifeCycle ==> onReady 启动定时任务 ' );
|
|
73
74
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Controller, Inject, Get, Post, Query } from '@midwayjs/core';
|
|
2
|
+
|
|
3
|
+
import { Context } from '@midwayjs/koa';
|
|
4
|
+
import { BaseApiController } from '../base/BaseApiController';
|
|
5
|
+
import {AsyncTaskService} from "@/service/asyncTask/AsyncTaskService";
|
|
6
|
+
import {SysAsyncTaskEntity, TaskStatus} from "@/models/AsyncTaskModel";
|
|
7
|
+
import {KeysOfSimpleSQL} from "@/libs/crud-pro/models/keys";
|
|
8
|
+
import {SystemTables} from "@/models/SystemTables";
|
|
9
|
+
import {CommonException} from "@/libs/crud-pro/exceptions";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
function fixCancelBodyData(body:any, id: number) {
|
|
13
|
+
if(!body.data || !body.condition) {
|
|
14
|
+
throw new CommonException("参数不正确");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// fix condition
|
|
18
|
+
const conditionObj: SysAsyncTaskEntity = (body.condition || {}) as any;
|
|
19
|
+
conditionObj.id = id;
|
|
20
|
+
body.condition = conditionObj;
|
|
21
|
+
|
|
22
|
+
// fix data
|
|
23
|
+
const dataObj: SysAsyncTaskEntity = (body.data || {}) as any;
|
|
24
|
+
dataObj.task_status = TaskStatus.CANCELLED;
|
|
25
|
+
body.data = dataObj;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
function fixCreateBodyData(body:any) {
|
|
30
|
+
if(!body.data) {
|
|
31
|
+
throw new CommonException("参数不正确");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// fix data
|
|
35
|
+
const dataObj: SysAsyncTaskEntity = (body.data || {}) as any;
|
|
36
|
+
dataObj.task_status = TaskStatus.PENDING;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 异步任务框架
|
|
42
|
+
*/
|
|
43
|
+
@Controller('/ns/gw/AsyncTask')
|
|
44
|
+
export class AsyncTaskController extends BaseApiController {
|
|
45
|
+
@Inject()
|
|
46
|
+
protected ctx: Context;
|
|
47
|
+
|
|
48
|
+
@Inject()
|
|
49
|
+
private asyncTaskService: AsyncTaskService;
|
|
50
|
+
|
|
51
|
+
// 获取任务列表
|
|
52
|
+
@Get('/getMyTasks')
|
|
53
|
+
async getMyTasks() {
|
|
54
|
+
return this.executeSysSimpleSQL(SystemTables.sys_async_tasks, KeysOfSimpleSQL.SIMPLE_QUERY_PAGE);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 创建任务
|
|
58
|
+
@Post('/createTask')
|
|
59
|
+
async createTask() {
|
|
60
|
+
fixCreateBodyData(this.ctx.request.body);
|
|
61
|
+
const res = await this.executeSysSimpleSQL(SystemTables.sys_async_tasks, KeysOfSimpleSQL.SIMPLE_INSERT);
|
|
62
|
+
await this.asyncTaskService.startTask();
|
|
63
|
+
return res;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 取消任务
|
|
67
|
+
@Post('/cancelTask')
|
|
68
|
+
async cancelTask(@Query('id') id: number) {
|
|
69
|
+
fixCancelBodyData(this.ctx.request.body, id);
|
|
70
|
+
await this.asyncTaskService.cancelTask(id);
|
|
71
|
+
await this.executeSysSimpleSQL(SystemTables.sys_async_tasks, KeysOfSimpleSQL.SIMPLE_UPDATE);
|
|
72
|
+
return { success: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
}
|
|
@@ -6,7 +6,7 @@ import * as fs from 'fs/promises';
|
|
|
6
6
|
import * as fs2 from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import * as https from 'https';
|
|
9
|
-
import { parseJsonObject } from '
|
|
9
|
+
import { parseJsonObject } from '@/libs/utils/functions';
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
interface HttpGetRes {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Controller, Inject, Post } from '@midwayjs/core';
|
|
2
2
|
import { Context } from '@midwayjs/koa';
|
|
3
|
-
import { KeysOfSimpleSQL, KeysOfValidators } from '
|
|
3
|
+
import { KeysOfSimpleSQL, KeysOfValidators } from '@/libs/crud-pro/models/keys';
|
|
4
4
|
import { BaseApiController } from '../base/BaseApiController';
|
|
5
|
-
import { checkPermission } from '
|
|
6
|
-
import { SystemFuncCode } from '
|
|
7
|
-
import { SystemTables } from '
|
|
8
|
-
import { ISysAnyApiEntity } from '
|
|
5
|
+
import { checkPermission } from '@/middleware/permission.middleware';
|
|
6
|
+
import { SystemFuncCode } from '@/models/SystemPerm';
|
|
7
|
+
import { SystemTables } from '@/models/SystemTables';
|
|
8
|
+
import { ISysAnyApiEntity } from '@/models/SystemEntities';
|
|
9
9
|
import * as md5 from 'md5';
|
|
10
|
-
import {parseJsonObject} from "
|
|
10
|
+
import {parseJsonObject} from "@/libs/utils/functions";
|
|
11
11
|
|
|
12
12
|
function fixBodyData(body: any) {
|
|
13
13
|
const dataObj: any = (body.data || {}) as ISysAnyApiEntity;
|
|
@@ -213,7 +213,7 @@ class CrudProGenSqlCondition {
|
|
|
213
213
|
tmpSql = `${toSqlColumnName(key)} COLLATE UTF8MB4_GENERAL_CI like concat(?, '%')`; // like前缀匹配
|
|
214
214
|
|
|
215
215
|
if (this.sqlCfg.sqlDbType === SqlDbType.postgres) {
|
|
216
|
-
tmpSql = `${toSqlColumnName(key)} like concat(
|
|
216
|
+
tmpSql = `${toSqlColumnName(key)} like concat(?::text, '%')`; // like前缀匹配
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
} else if (equalsIgnoreCase(KeysOfConditions.$NOT_LIKE, compare)) {
|
|
@@ -222,7 +222,7 @@ class CrudProGenSqlCondition {
|
|
|
222
222
|
|
|
223
223
|
|
|
224
224
|
if (this.sqlCfg.sqlDbType === SqlDbType.postgres) {
|
|
225
|
-
tmpSql = `${toSqlColumnName(key)} not like concat(
|
|
225
|
+
tmpSql = `${toSqlColumnName(key)} not like concat(?::text, '%')`;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
}
|
|
@@ -233,7 +233,7 @@ class CrudProGenSqlCondition {
|
|
|
233
233
|
tmpSql = `${toSqlColumnName(key)} COLLATE UTF8MB4_GENERAL_CI like concat('%', ?, '%')`; // like包含
|
|
234
234
|
|
|
235
235
|
if (this.sqlCfg.sqlDbType === SqlDbType.postgres) {
|
|
236
|
-
tmpSql = `${toSqlColumnName(key)} like concat('%',
|
|
236
|
+
tmpSql = `${toSqlColumnName(key)} like concat('%', ?::text, '%')`; // like包含
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
} else if (equalsIgnoreCase(KeysOfConditions.$NOT_LIKE_INCLUDE, compare)) {
|
|
@@ -241,7 +241,7 @@ class CrudProGenSqlCondition {
|
|
|
241
241
|
tmpSql = `${toSqlColumnName(key)} COLLATE UTF8MB4_GENERAL_CI not like concat('%',?, '%')`; // like不包含
|
|
242
242
|
|
|
243
243
|
if (this.sqlCfg.sqlDbType === SqlDbType.postgres) {
|
|
244
|
-
tmpSql = `${toSqlColumnName(key)} not like concat('%'
|
|
244
|
+
tmpSql = `${toSqlColumnName(key)} not like concat('%',?::text, '%')`; // like不包含
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function errorToString(error:any): string {
|
|
2
|
+
// 处理非错误对象的情况
|
|
3
|
+
if (typeof error === 'undefined') {
|
|
4
|
+
return 'Error: undefined was thrown';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (error === null) {
|
|
8
|
+
return 'Error: null was thrown';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 处理原始值类型
|
|
12
|
+
const primitiveTypes = ['string', 'number', 'boolean', 'symbol'];
|
|
13
|
+
if (primitiveTypes.includes(typeof error)) {
|
|
14
|
+
return `Error: A primitive value was thrown\nValue: ${String(error)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 处理 Error 对象
|
|
18
|
+
if (error instanceof Error) {
|
|
19
|
+
const errorDetails = [];
|
|
20
|
+
errorDetails.push(`Error: ${error.name || 'Error'}`);
|
|
21
|
+
errorDetails.push(`Message: ${error.message || 'No error message'}`);
|
|
22
|
+
|
|
23
|
+
if (error.stack) {
|
|
24
|
+
errorDetails.push('Stack Trace:');
|
|
25
|
+
errorDetails.push(error.stack);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const extraProperties = ['code', 'fileName', 'lineNumber', 'columnNumber'];
|
|
29
|
+
extraProperties.forEach(prop => {
|
|
30
|
+
if (error[prop] !== undefined) {
|
|
31
|
+
errorDetails.push(`${prop}: ${error[prop]}`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const customProps = Object.keys(error).filter(
|
|
36
|
+
key => !['name', 'message', 'stack', ...extraProperties].includes(key)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (customProps.length > 0) {
|
|
40
|
+
errorDetails.push('Custom Properties:');
|
|
41
|
+
customProps.forEach(prop => {
|
|
42
|
+
try {
|
|
43
|
+
const value = JSON.stringify(error[prop], null, 2) || String(error[prop]);
|
|
44
|
+
errorDetails.push(` ${prop}: ${value}`);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
errorDetails.push(` ${prop}: [Object cannot be serialized]`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return errorDetails.join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 处理普通对象
|
|
55
|
+
try {
|
|
56
|
+
const objectDetails = JSON.stringify(error, null, 2);
|
|
57
|
+
return `Error: A plain object was thrown\nObject: ${objectDetails}`;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return `Error: An object was thrown but could not be serialized\nType: ${error.constructor?.name || 'Unknown'}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
errorToString
|
|
66
|
+
}
|
|
@@ -209,6 +209,13 @@ async function trackRequest(ctx: Context) {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
|
|
213
|
+
const excludePathPrefix = [
|
|
214
|
+
'/ns/static/',
|
|
215
|
+
'/ns/api/helpers'
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
|
|
212
219
|
/**
|
|
213
220
|
* 全局中间件
|
|
214
221
|
*/
|
|
@@ -217,7 +224,13 @@ export class GlobalMiddleware implements IMiddleware<Context, NextFunction> {
|
|
|
217
224
|
|
|
218
225
|
match(ctx: Context): boolean {
|
|
219
226
|
const path: string = ctx.path;
|
|
220
|
-
|
|
227
|
+
for (let i = 0; i < excludePathPrefix.length; i++) {
|
|
228
|
+
const excludePathPrefix1 = excludePathPrefix[i];
|
|
229
|
+
if (path.startsWith(excludePathPrefix1)) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
221
234
|
}
|
|
222
235
|
|
|
223
236
|
resolve() {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface SysAsyncTaskEntity {
|
|
2
|
+
id: number;
|
|
3
|
+
task_uuid: string;
|
|
4
|
+
task_name: string;
|
|
5
|
+
task_description: string | null;
|
|
6
|
+
task_type: string;
|
|
7
|
+
task_status: TaskStatus;
|
|
8
|
+
input_params: any;
|
|
9
|
+
output_result: any | null;
|
|
10
|
+
|
|
11
|
+
input_file_path: string | null;
|
|
12
|
+
input_file_format: FileFormat | null;
|
|
13
|
+
input_file_size: number | null;
|
|
14
|
+
|
|
15
|
+
output_file_path: string | null;
|
|
16
|
+
output_file_format: FileFormat | null;
|
|
17
|
+
output_file_size: number | null;
|
|
18
|
+
|
|
19
|
+
input_total_records: number | null;
|
|
20
|
+
output_total_records: number | null;
|
|
21
|
+
processed_records: number;
|
|
22
|
+
progress: number;
|
|
23
|
+
error_message: string | null;
|
|
24
|
+
error_records: any | null;
|
|
25
|
+
created_by: string | null;
|
|
26
|
+
priority: number;
|
|
27
|
+
retry_count: number;
|
|
28
|
+
max_retries: number;
|
|
29
|
+
parent_task_id: number | null;
|
|
30
|
+
created_at: Date;
|
|
31
|
+
started_at: Date | null;
|
|
32
|
+
completed_at: Date | null;
|
|
33
|
+
updated_at: Date;
|
|
34
|
+
expired_at: Date | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export enum TaskStatus {
|
|
38
|
+
PENDING = 'PENDING', // 等待中,还未进入排队
|
|
39
|
+
RUNNING = 'RUNNING', // 运行中
|
|
40
|
+
SUCCEEDED = 'SUCCEEDED',
|
|
41
|
+
FAILED = 'FAILED',
|
|
42
|
+
CANCELLED = 'CANCELLED', // 取消
|
|
43
|
+
PAUSED = 'PAUSED', // 暂停
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export enum FileFormat {
|
|
47
|
+
XLSX = 'xlsx',
|
|
48
|
+
CSV = 'csv',
|
|
49
|
+
JSON = 'json',
|
|
50
|
+
PDF = 'pdf',
|
|
51
|
+
TXT = 'txt',
|
|
52
|
+
XML = 'xml',
|
|
53
|
+
ZIP = 'zip',
|
|
54
|
+
HTML = 'html',
|
|
55
|
+
PNG = 'png',
|
|
56
|
+
JPG = 'jpg',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
export interface ITaskHandler {
|
|
61
|
+
execute(ctx: TaskContext): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TaskContext {
|
|
65
|
+
task: SysAsyncTaskEntity;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TaskHandlerConfig {
|
|
69
|
+
taskType: string;
|
|
70
|
+
handler: ITaskHandler;
|
|
71
|
+
maxRetries?: number;
|
|
72
|
+
priority?: number;
|
|
73
|
+
expireSeconds?: number;
|
|
74
|
+
}
|
package/src/schedule/index.ts
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {Inject, Provide} from '@midwayjs/core';
|
|
2
|
+
import {Context} from '@midwayjs/koa';
|
|
3
|
+
import {BaseService} from "@/service/base/BaseService";
|
|
4
|
+
import {IScheduleService} from "@/interface";
|
|
5
|
+
import {CurdProService} from "@/service/curd/CurdProService";
|
|
6
|
+
import {ITaskHandler, SysAsyncTaskEntity, TaskStatus} from "@/models/AsyncTaskModel";
|
|
7
|
+
import {GLOBAL_STATIC_CONFIG} from "@/libs/global-config/global-config";
|
|
8
|
+
import {SystemTables} from "@/models/SystemTables";
|
|
9
|
+
import {KeysOfSimpleSQL} from "@/libs/crud-pro/models/keys";
|
|
10
|
+
import {errorToString} from "@/libs/utils/errorToString";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncTaskRunner {
|
|
14
|
+
isBusy: boolean = false;
|
|
15
|
+
taskHandlerMap: Map<string, ITaskHandler> = new Map();
|
|
16
|
+
|
|
17
|
+
async executeTaskList(taskList: SysAsyncTaskEntity[]) {
|
|
18
|
+
if (!taskList || taskList.length === 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.isBusy = true;
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < taskList.length; i++) {
|
|
24
|
+
const taskElement = taskList[i];
|
|
25
|
+
try {
|
|
26
|
+
await this.executeTask(taskElement);
|
|
27
|
+
taskElement.task_status = TaskStatus.SUCCEEDED;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
taskElement.task_status = TaskStatus.FAILED;
|
|
30
|
+
taskElement.error_message = errorToString(error)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async executeTask(taskElement: SysAsyncTaskEntity) {
|
|
37
|
+
const taskType = taskElement.task_type;
|
|
38
|
+
const taskHandler = this.taskHandlerMap.get(taskType);
|
|
39
|
+
if (!taskHandler) {
|
|
40
|
+
throw new Error('TaskHandler not found , taskType = ' + taskType);
|
|
41
|
+
}
|
|
42
|
+
await taskHandler.execute({task: taskElement});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const ASYNC_TASK_RUNNER = new AsyncTaskRunner();
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
const LOCK_KEY_ASYNC_TASK_RUNNER = "LOCK_KEY_ASYNC_TASK_RUNNER"
|
|
51
|
+
|
|
52
|
+
@Provide()
|
|
53
|
+
export class AsyncTaskRunnerService extends BaseService implements IScheduleService{
|
|
54
|
+
|
|
55
|
+
@Inject()
|
|
56
|
+
protected ctx: Context;
|
|
57
|
+
|
|
58
|
+
@Inject()
|
|
59
|
+
private curdProService: CurdProService;
|
|
60
|
+
|
|
61
|
+
async runBySchedule () {
|
|
62
|
+
if (ASYNC_TASK_RUNNER.isBusy) {
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const lock = await this.redisService.set(LOCK_KEY_ASYNC_TASK_RUNNER, 1,"EX", 3600, "NX");
|
|
67
|
+
if (lock !== 'OK') {
|
|
68
|
+
return Promise.resolve();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
const { SystemDbName, SystemDbType } = GLOBAL_STATIC_CONFIG.getConfig();
|
|
73
|
+
|
|
74
|
+
const queryRes = await this.curdProService.executeCrudByCfg({
|
|
75
|
+
condition: {
|
|
76
|
+
task_status: TaskStatus.PENDING
|
|
77
|
+
},
|
|
78
|
+
limit: 10
|
|
79
|
+
},{
|
|
80
|
+
sqlTable: SystemTables.sys_async_tasks,
|
|
81
|
+
sqlSimpleName: KeysOfSimpleSQL.SIMPLE_QUERY_PAGE,
|
|
82
|
+
sqlDatabase: SystemDbName,
|
|
83
|
+
sqlDbType: SystemDbType,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const taskList = queryRes.getResRows();
|
|
87
|
+
|
|
88
|
+
if (taskList.length === 0) {
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const taskIds = taskList.map(elem => elem.id).filter(Boolean)
|
|
93
|
+
|
|
94
|
+
await this.curdProService.executeCrudByCfg({
|
|
95
|
+
condition: {
|
|
96
|
+
id: {
|
|
97
|
+
"$in": taskIds
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
data: {
|
|
101
|
+
task_status: TaskStatus.RUNNING
|
|
102
|
+
}
|
|
103
|
+
},{
|
|
104
|
+
sqlTable: SystemTables.sys_async_tasks,
|
|
105
|
+
sqlSimpleName: KeysOfSimpleSQL.SIMPLE_UPDATE,
|
|
106
|
+
sqlDatabase: SystemDbName,
|
|
107
|
+
sqlDbType: SystemDbType,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
ASYNC_TASK_RUNNER.executeTaskList(taskList);
|
|
112
|
+
return Promise.resolve();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {Inject, Provide} from '@midwayjs/core';
|
|
2
|
+
import {Context} from '@midwayjs/koa';
|
|
3
|
+
import {BaseService} from "@/service/base/BaseService";
|
|
4
|
+
import {runSchedule} from "@/schedule";
|
|
5
|
+
|
|
6
|
+
@Provide()
|
|
7
|
+
export class AsyncTaskService extends BaseService {
|
|
8
|
+
@Inject()
|
|
9
|
+
protected ctx: Context;
|
|
10
|
+
|
|
11
|
+
async startTask() {
|
|
12
|
+
runSchedule(this.app, ['asyncTaskRunnerService']).then(schedule => {
|
|
13
|
+
console.log(schedule);
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async cancelTask(id: number) {
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compileOnSave": true,
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "es2018",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"experimentalDecorators": true,
|
|
8
|
+
"emitDecoratorMetadata": true,
|
|
9
|
+
"inlineSourceMap":false,
|
|
10
|
+
"noImplicitThis": true,
|
|
11
|
+
"noUnusedLocals": true,
|
|
12
|
+
"stripInternal": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"noImplicitReturns": false,
|
|
15
|
+
"pretty": true,
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"forceConsistentCasingInFileNames": true,
|
|
18
|
+
"outDir": "dist",
|
|
19
|
+
"rootDir": "src",
|
|
20
|
+
"baseUrl": ".",
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["src/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"exclude": [
|
|
26
|
+
"*.js",
|
|
27
|
+
"*.ts",
|
|
28
|
+
"dist",
|
|
29
|
+
"node_modules",
|
|
30
|
+
"test"
|
|
31
|
+
]
|
|
32
|
+
}
|