midway-fatcms 0.0.1-beta.12 → 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.
Files changed (31) hide show
  1. package/.eslintrc.json +10 -0
  2. package/.prettierrc.js +4 -0
  3. package/dist/configuration.js +2 -1
  4. package/dist/controller/gateway/AsyncTaskController.d.ts +14 -0
  5. package/dist/controller/gateway/AsyncTaskController.js +97 -0
  6. package/dist/libs/utils/errorToString.d.ts +2 -0
  7. package/dist/libs/utils/errorToString.js +57 -0
  8. package/dist/middleware/global.middleware.js +11 -1
  9. package/dist/models/AsyncTaskModel.d.ts +65 -0
  10. package/dist/models/AsyncTaskModel.js +25 -0
  11. package/dist/models/SystemTables.d.ts +1 -0
  12. package/dist/models/SystemTables.js +1 -0
  13. package/dist/schedule/index.d.ts +2 -1
  14. package/dist/schedule/index.js +2 -1
  15. package/dist/service/asyncTask/AsyncTaskRunnerService.d.ts +8 -0
  16. package/dist/service/asyncTask/AsyncTaskRunnerService.js +110 -0
  17. package/dist/service/asyncTask/AsyncTaskService.d.ts +7 -0
  18. package/dist/service/asyncTask/AsyncTaskService.js +32 -0
  19. package/package.json +5 -2
  20. package/src/configuration.ts +2 -1
  21. package/src/controller/gateway/AsyncTaskController.ts +75 -0
  22. package/src/controller/gateway/StaticController.ts +1 -1
  23. package/src/controller/manage/AnyApiMangeApi.ts +6 -6
  24. package/src/libs/utils/errorToString.ts +66 -0
  25. package/src/middleware/global.middleware.ts +14 -1
  26. package/src/models/AsyncTaskModel.ts +74 -0
  27. package/src/models/SystemTables.ts +1 -0
  28. package/src/schedule/index.ts +1 -1
  29. package/src/service/asyncTask/AsyncTaskRunnerService.ts +114 -0
  30. package/src/service/asyncTask/AsyncTaskService.ts +20 -0
  31. package/tsconfig.json +32 -0
package/.eslintrc.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./node_modules/mwts/",
3
+ "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
4
+ "env": {
5
+ "jest": true
6
+ },
7
+ "rules": {
8
+ "max-len":"off"
9
+ }
10
+ }
package/.prettierrc.js ADDED
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ ...require('mwts/.prettierrc.json'),
3
+ printWidth: 500, // 与ESLint的max-len保持一致
4
+ }
@@ -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;
@@ -0,0 +1,2 @@
1
+ declare function errorToString(error: any): string;
2
+ export { errorToString };
@@ -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
- return !(path.startsWith('/ns/static/'));
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 = {}));
@@ -20,5 +20,6 @@ export declare const SystemTables: {
20
20
  sys_anyapi: string;
21
21
  sys_proxyapi: string;
22
22
  sys_visit_stats: string;
23
+ sys_async_tasks: string;
23
24
  };
24
25
  export declare const SystemDevOpsWorkbench = "devops";
@@ -23,5 +23,6 @@ exports.SystemTables = {
23
23
  sys_anyapi: 'sys_anyapi',
24
24
  sys_proxyapi: 'sys_proxyapi',
25
25
  sys_visit_stats: 'sys_visit_stats',
26
+ sys_async_tasks: 'sys_async_tasks',
26
27
  };
27
28
  exports.SystemDevOpsWorkbench = 'devops';
@@ -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 };
@@ -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,7 @@
1
+ import { Context } from '@midwayjs/koa';
2
+ import { BaseService } from "../../service/base/BaseService";
3
+ export declare class AsyncTaskService extends BaseService {
4
+ protected ctx: Context;
5
+ startTask(): Promise<void>;
6
+ cancelTask(id: number): Promise<void>;
7
+ }
@@ -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.12",
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": {
@@ -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 '../../libs/utils/functions';
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 '../../libs/crud-pro/models/keys';
3
+ import { KeysOfSimpleSQL, KeysOfValidators } from '@/libs/crud-pro/models/keys';
4
4
  import { BaseApiController } from '../base/BaseApiController';
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';
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 "../../libs/utils/functions";
10
+ import {parseJsonObject} from "@/libs/utils/functions";
11
11
 
12
12
  function fixBodyData(body: any) {
13
13
  const dataObj: any = (body.data || {}) as ISysAnyApiEntity;
@@ -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
- return !(path.startsWith('/ns/static/'));
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
+ }
@@ -20,6 +20,7 @@ export const SystemTables = {
20
20
  sys_anyapi: 'sys_anyapi',
21
21
  sys_proxyapi: 'sys_proxyapi',
22
22
  sys_visit_stats: 'sys_visit_stats',
23
+ sys_async_tasks: 'sys_async_tasks',
23
24
  };
24
25
 
25
26
 
@@ -70,4 +70,4 @@ async function startSchedule(app: koa.Application, serviceList: string[]) {
70
70
  }, 2 * 60 * 1000);
71
71
  }
72
72
 
73
- export { startSchedule };
73
+ export { startSchedule, runSchedule };
@@ -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
+ }