新增了一个不依赖redis,cluster模式下可用的本地任务

This commit is contained in:
COOL 2025-01-16 21:29:59 +08:00
parent 3392f0a307
commit 6adadc5dc3
47 changed files with 4065 additions and 300 deletions

1
bootstrap.js vendored
View File

@ -1,3 +1,4 @@
process.env.NODE_ENV = 'local';
const { Bootstrap } = require('@midwayjs/bootstrap'); const { Bootstrap } = require('@midwayjs/bootstrap');
// 显式以组件方式引入用户代码 // 显式以组件方式引入用户代码

View File

@ -5,26 +5,30 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@cool-midway/core": "file:/Users/ap/Documents/src/admin/midway-packages/core", "@cool-midway/core": "file:/Users/ap/Documents/src/admin/midway-packages/core",
"@midwayjs/bootstrap": "^3.19.3", "@cool-midway/task": "file:/Users/ap/Documents/src/admin/midway-packages/task",
"@midwayjs/cache-manager": "^3.19.3", "@midwayjs/bootstrap": "^3.20.0",
"@midwayjs/core": "^3.19.0", "@midwayjs/cache-manager": "^3.20.0",
"@midwayjs/cron": "^3.19.2", "@midwayjs/core": "^3.20.0",
"@midwayjs/cross-domain": "^3.19.3", "@midwayjs/cron": "^3.20.0",
"@midwayjs/info": "^3.19.2", "@midwayjs/cross-domain": "^3.20.0",
"@midwayjs/koa": "^3.19.2", "@midwayjs/info": "^3.20.0",
"@midwayjs/koa": "^3.20.0",
"@midwayjs/logger": "^3.4.2", "@midwayjs/logger": "^3.4.2",
"@midwayjs/static-file": "^3.19.3", "@midwayjs/static-file": "^3.20.0",
"@midwayjs/typeorm": "^3.19.2", "@midwayjs/typeorm": "^3.20.0",
"@midwayjs/upload": "^3.19.3", "@midwayjs/upload": "^3.20.0",
"@midwayjs/validate": "^3.19.2", "@midwayjs/validate": "^3.20.0",
"@midwayjs/view-ejs": "^3.19.2", "@midwayjs/view-ejs": "^3.20.0",
"adm-zip": "^0.5.16",
"axios": "^1.7.9", "axios": "^1.7.9",
"cron": "^3.5.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"md5": "^2.3.0", "md5": "^2.3.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"mysql2": "^3.12.0", "mysql2": "^3.12.0",
"sharp": "0.32.6", "sharp": "0.33.5",
"sqlite3": "^5.1.7",
"svg-captcha": "^1.4.0", "svg-captcha": "^1.4.0",
"typeorm": "^0.3.20", "typeorm": "^0.3.20",
"uuid": "^11.0.5", "uuid": "^11.0.5",
@ -32,7 +36,7 @@
}, },
"devDependencies": { "devDependencies": {
"@midwayjs/bundle-helper": "^1.3.0", "@midwayjs/bundle-helper": "^1.3.0",
"@midwayjs/mock": "^3.19.2", "@midwayjs/mock": "^3.20.0",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "22", "@types/node": "22",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@ -40,7 +44,7 @@
"mwts": "^1.3.0", "mwts": "^1.3.0",
"mwtsc": "^1.15.1", "mwtsc": "^1.15.1",
"pkg": "^5.8.1", "pkg": "^5.8.1",
"rimraf": "^5.0.5", "rimraf": "^6.0.1",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"typescript": "~5.7.3" "typescript": "~5.7.3"
}, },
@ -49,7 +53,7 @@
}, },
"scripts": { "scripts": {
"start": "NODE_ENV=production node ./bootstrap.js", "start": "NODE_ENV=production node ./bootstrap.js",
"dev": "rimraf src/index.ts && cool check entity --clear && cross-env NODE_ENV=local mwtsc --cleanOutDir --watch --run @midwayjs/mock/app.js", "dev": "rimraf src/index.ts && cool check entity && cross-env NODE_ENV=local mwtsc --cleanOutDir --watch --run @midwayjs/mock/app.js",
"test": "cross-env NODE_ENV=unittest jest", "test": "cross-env NODE_ENV=unittest jest",
"cov": "jest --coverage", "cov": "jest --coverage",
"lint": "mwts check", "lint": "mwts check",
@ -63,15 +67,17 @@
}, },
"bin": "./bootstrap.js", "bin": "./bootstrap.js",
"pkg": { "pkg": {
"scripts": "dist/**/*", "scripts": [
"dist/**/*",
"node_modules/axios/dist/node/*"
],
"assets": [ "assets": [
"public/**/*", "public/**/*",
"typings/**/*", "typings/**/*",
"cool/**/*" "cool/**/*"
], ],
"targets": [ "targets": [
"node18-macos-x64", "node18-macos-x64"
"node18-win-x64"
], ],
"outputPath": "build" "outputPath": "build"
}, },

1232
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

58
src/comm/path.ts Normal file
View File

@ -0,0 +1,58 @@
import * as path from 'path';
import * as os from 'os';
import * as md5 from 'md5';
import * as fs from 'fs';
/**
* keys
* @returns
*/
const getKeys = () => {
const configFile = path.join(__dirname, '../config/config.default.js');
const configContent = fs.readFileSync(configFile, 'utf8');
const keys = configContent.match(/keys: '([^']+)'/)?.[1];
return keys;
};
/**
*
* @returns
*/
export const pDataPath = () => {
const dirPath = path.join(os.homedir(), '.cool-admin', md5(getKeys()));
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
return dirPath;
};
/**
*
* @returns
*/
export const pUploadPath = () => {
const uploadPath = path.join(pDataPath(), 'upload');
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath, { recursive: true });
}
return uploadPath;
};
/**
*
* @returns
*/
export const pPluginPath = () => {
const pluginPath = path.join(pDataPath(), 'plugin');
if (!fs.existsSync(pluginPath)) {
fs.mkdirSync(pluginPath, { recursive: true });
}
return pluginPath;
};
/**
* sqlite
*/
export const pSqlitePath = () => {
return path.join(pDataPath(), 'cool.sqlite');
};

View File

@ -2,13 +2,13 @@ import { CoolConfig } from '@cool-midway/core';
import { MidwayConfig } from '@midwayjs/core'; import { MidwayConfig } from '@midwayjs/core';
import { CoolCacheStore } from '@cool-midway/core'; import { CoolCacheStore } from '@cool-midway/core';
import * as path from 'path'; import * as path from 'path';
import { getUploadDir } from '../modules/plugin/hooks/upload'; import { pUploadPath } from '../comm/path';
// redis缓存 // redis缓存
// import { redisStore } from 'cache-manager-ioredis-yet'; // import { redisStore } from 'cache-manager-ioredis-yet';
export default { export default {
// use for cookie sign key, should change to your own and keep security // 确保每个项目唯一,项目首次启动会自动生成
keys: '576848ea-bb0c-4c0c-ac95-c8602ef908b5', keys: '576848ea-bb0c-4c0c-ac95-c8602ef908b5',
koa: { koa: {
port: 8001, port: 8001,
@ -18,12 +18,12 @@ export default {
buffer: true, buffer: true,
dirs: { dirs: {
default: { default: {
prefix: '/public', prefix: '/',
dir: path.join(__dirname, '..', '..', 'public'), dir: path.join(__dirname, '..', '..', 'public'),
}, },
static: { static: {
prefix: '/upload', prefix: '/upload',
dir: getUploadDir(), dir: pUploadPath(),
}, },
}, },
}, },
@ -61,6 +61,13 @@ export default {
cool: { cool: {
// 已经插件化,本地文件上传查看 plugin/config.ts其他云存储查看对应插件的使用 // 已经插件化,本地文件上传查看 plugin/config.ts其他云存储查看对应插件的使用
file: {}, file: {},
// redis配置
redis: {
port: 6379,
host: '127.0.0.1',
password: '',
db: 0,
},
// crud配置 // crud配置
crud: { crud: {
// 插入模式save不会校验字段(允许传入不存在的字段)insert会校验字段 // 插入模式save不会校验字段(允许传入不存在的字段)insert会校验字段

View File

@ -1,5 +1,7 @@
import { CoolConfig } from '@cool-midway/core'; import { CoolConfig } from '@cool-midway/core';
import { MidwayConfig } from '@midwayjs/core'; import { MidwayConfig } from '@midwayjs/core';
import { pSqlitePath } from '../comm/path';
import { entities } from '../entities';
/** /**
* npm run dev * npm run dev
@ -8,26 +10,16 @@ export default {
typeorm: { typeorm: {
dataSource: { dataSource: {
default: { default: {
type: 'mysql', type: 'sqlite',
host: '192.168.0.119', // 数据库文件地址
port: 3306, database: pSqlitePath(),
username: 'root',
password: '123456',
database: 'cool',
// 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失 // 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
synchronize: true, synchronize: true,
// 打印日志 // 打印日志
logging: false, logging: true,
// 字符集
charset: 'utf8mb4',
// 是否开启缓存
cache: true,
// 实体路径 // 实体路径
entities: ['**/modules/*/entity'], entities,
// 扩展配置 // 扩展配置
extra: {
keepAliveInitialDelay: 10000,
},
}, },
}, },
}, },

View File

@ -1,6 +1,7 @@
import { CoolConfig } from '@cool-midway/core'; import { CoolConfig } from '@cool-midway/core';
import { MidwayConfig } from '@midwayjs/core'; import { MidwayConfig } from '@midwayjs/core';
import { entities } from '../entities'; import { entities } from '../entities';
import { pSqlitePath } from '../comm/path';
/** /**
* npm run prod * npm run prod
*/ */
@ -8,31 +9,26 @@ export default {
typeorm: { typeorm: {
dataSource: { dataSource: {
default: { default: {
type: 'mysql', type: 'sqlite',
host: '192.168.0.119', // 数据库文件地址
port: 3306, database: pSqlitePath(),
username: 'root',
password: '123456',
database: 'cool',
// 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失 // 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
synchronize: false, synchronize: false,
// 打印日志 // 打印日志
logging: false, logging: false,
// 字符集
charset: 'utf8mb4',
// 是否开启缓存
cache: true,
// 实体路径 // 实体路径
entities, entities,
// 扩展配置
extra: {
keepAliveInitialDelay: 10000,
},
}, },
}, },
}, },
cool: { cool: {
// 是否自动导入数据库,生产环境不建议开,用本地的数据库手动初始化 // 实体与路径跟生成代码、前端请求、swagger文档相关 注意:线上不建议开启,以免暴露敏感信息
initDB: false, eps: false,
// 是否自动导入模块数据库
initDB: true,
// 判断是否初始化的方式
initJudge: 'db',
// 是否自动导入模块菜单
initMenu: true,
} as CoolConfig, } as CoolConfig,
} as MidwayConfig; } as MidwayConfig;

View File

@ -18,7 +18,7 @@ import * as LocalConfig from './config/config.local';
import * as ProdConfig from './config/config.prod'; import * as ProdConfig from './config/config.prod';
import * as cool from '@cool-midway/core'; import * as cool from '@cool-midway/core';
import * as upload from '@midwayjs/upload'; import * as upload from '@midwayjs/upload';
import * as os from 'os'; import * as task from '@cool-midway/task';
@Configuration({ @Configuration({
imports: [ imports: [
@ -38,6 +38,8 @@ import * as os from 'os';
upload, upload,
// cool-admin 官方组件 https://cool-js.com // cool-admin 官方组件 https://cool-js.com
cool, cool,
// 任务与队列
// task,
{ {
component: info, component: info,
enabledEnvironment: ['local', 'prod'], enabledEnvironment: ['local', 'prod'],

View File

@ -1,2 +1,45 @@
// 自动生成的文件,请勿手动修改 // 自动生成的文件,请勿手动修改
export const entities = []; import * as entity0 from './modules/user/entity/wx';
import * as entity1 from './modules/user/entity/info';
import * as entity2 from './modules/user/entity/address';
import * as entity3 from './modules/task/entity/log';
import * as entity4 from './modules/task/entity/info';
import * as entity5 from './modules/space/entity/type';
import * as entity6 from './modules/space/entity/info';
import * as entity7 from './modules/recycle/entity/data';
import * as entity8 from './modules/plugin/entity/info';
import * as entity9 from './modules/dict/entity/type';
import * as entity10 from './modules/dict/entity/info';
import * as entity11 from './modules/base/entity/sys/user_role';
import * as entity12 from './modules/base/entity/sys/user';
import * as entity13 from './modules/base/entity/sys/role_menu';
import * as entity14 from './modules/base/entity/sys/role_department';
import * as entity15 from './modules/base/entity/sys/role';
import * as entity16 from './modules/base/entity/sys/param';
import * as entity17 from './modules/base/entity/sys/menu';
import * as entity18 from './modules/base/entity/sys/log';
import * as entity19 from './modules/base/entity/sys/department';
import * as entity20 from './modules/base/entity/sys/conf';
export const entities = [
...Object.values(entity0),
...Object.values(entity1),
...Object.values(entity2),
...Object.values(entity3),
...Object.values(entity4),
...Object.values(entity5),
...Object.values(entity6),
...Object.values(entity7),
...Object.values(entity8),
...Object.values(entity9),
...Object.values(entity10),
...Object.values(entity11),
...Object.values(entity12),
...Object.values(entity13),
...Object.values(entity14),
...Object.values(entity15),
...Object.values(entity16),
...Object.values(entity17),
...Object.values(entity18),
...Object.values(entity19),
...Object.values(entity20),
];

107
src/index.ts Normal file
View File

@ -0,0 +1,107 @@
/** This file generated by @midwayjs/bundle-helper */
export { MainConfiguration as Configuration } from './configuration';
export * from './comm/path';
export * from './comm/utils';
export * from './config/config.default';
export * from './modules/user/entity/wx';
export * from './modules/user/entity/info';
export * from './modules/user/entity/address';
export * from './modules/task/entity/log';
export * from './modules/task/entity/info';
export * from './modules/space/entity/type';
export * from './modules/space/entity/info';
export * from './modules/recycle/entity/data';
export * from './modules/plugin/entity/info';
export * from './modules/dict/entity/type';
export * from './modules/dict/entity/info';
export * from './modules/base/entity/sys/user_role';
export * from './modules/base/entity/sys/user';
export * from './modules/base/entity/sys/role_menu';
export * from './modules/base/entity/sys/role_department';
export * from './modules/base/entity/sys/role';
export * from './modules/base/entity/sys/param';
export * from './modules/base/entity/sys/menu';
export * from './modules/base/entity/sys/log';
export * from './modules/base/entity/sys/department';
export * from './modules/base/entity/sys/conf';
export * from './entities';
export * from './config/config.local';
export * from './config/config.prod';
export * from './interface';
export * from './modules/base/service/sys/conf';
export * from './modules/base/service/sys/log';
export * from './modules/base/middleware/log';
export * from './modules/base/middleware/authority';
export * from './modules/base/config';
export * from './modules/plugin/interface';
export * from './modules/plugin/service/center';
export * from './modules/plugin/event/init';
export * from './modules/plugin/service/types';
export * from './modules/plugin/service/info';
export * from './modules/base/dto/login';
export * from './modules/base/service/sys/data';
export * from './modules/base/service/sys/menu';
export * from './modules/base/service/sys/department';
export * from './modules/base/service/sys/perms';
export * from './modules/base/service/sys/role';
export * from './modules/base/service/sys/login';
export * from './modules/base/service/sys/user';
export * from './modules/base/controller/admin/comm';
export * from './modules/base/service/sys/param';
export * from './modules/base/controller/admin/open';
export * from './modules/base/controller/admin/sys/department';
export * from './modules/base/controller/admin/sys/log';
export * from './modules/base/controller/admin/sys/menu';
export * from './modules/base/controller/admin/sys/param';
export * from './modules/base/controller/admin/sys/role';
export * from './modules/base/controller/admin/sys/user';
export * from './modules/base/controller/app/comm';
export * from './modules/base/event/menu';
export * from './modules/base/job/log';
export * from './modules/demo/config';
export * from './modules/demo/controller/open/plugin';
export * from './modules/dict/config';
export * from './modules/dict/service/info';
export * from './modules/dict/controller/admin/info';
export * from './modules/dict/service/type';
export * from './modules/dict/controller/admin/type';
export * from './modules/dict/controller/app/info';
export * from './modules/plugin/config';
export * from './modules/plugin/controller/admin/info';
export * from './modules/plugin/event/app';
export * from './modules/plugin/hooks/base';
export * from './modules/plugin/hooks/upload/interface';
export * from './modules/plugin/hooks/upload/index';
export * from './modules/recycle/config';
export * from './modules/recycle/service/data';
export * from './modules/recycle/controller/admin/data';
export * from './modules/recycle/event/data';
export * from './modules/recycle/schedule/data';
export * from './modules/space/config';
export * from './modules/space/service/info';
export * from './modules/space/controller/admin/info';
export * from './modules/space/service/type';
export * from './modules/space/controller/admin/type';
export * from './modules/task/service/bull';
export * from './modules/task/queue/task';
export * from './modules/task/service/local';
export * from './modules/task/service/info';
export * from './modules/task/middleware/task';
export * from './modules/task/config';
export * from './modules/task/controller/admin/info';
export * from './modules/task/event/app';
export * from './modules/task/service/demo';
export * from './modules/user/middleware/app';
export * from './modules/user/config';
export * from './modules/user/service/address';
export * from './modules/user/controller/admin/address';
export * from './modules/user/controller/admin/info';
export * from './modules/user/controller/app/address';
export * from './modules/user/service/wx';
export * from './modules/user/controller/app/comm';
export * from './modules/user/service/sms';
export * from './modules/user/service/info';
export * from './modules/user/controller/app/info';
export * from './modules/user/service/login';
export * from './modules/user/controller/app/login';
export * from './modules/user/event/app';

View File

@ -86,7 +86,6 @@
"password": "e10adc3949ba59abbe56e057f20f883e", "password": "e10adc3949ba59abbe56e057f20f883e",
"passwordV": 7, "passwordV": 7,
"nickName": "管理员", "nickName": "管理员",
"headImg": "https://cool-js.com/admin/headimg.jpg",
"phone": "18000000000", "phone": "18000000000",
"email": "team@cool-js.com", "email": "team@cool-js.com",
"status": 1, "status": 1,

View File

@ -0,0 +1,19 @@
import { ModuleConfig } from '@cool-midway/core';
/**
*
*/
export default () => {
return {
// 模块名称
name: 'demo模块',
// 模块描述
description: '演示用',
// 中间件,只对本模块有效
middlewares: [],
// 中间件,全局有效
globalMiddlewares: [],
// 模块加载顺序默认为0值越大越优先加载
order: 0,
} as ModuleConfig;
};

View File

@ -0,0 +1,25 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { PluginService } from '../../../plugin/service/info';
import { Get, Inject } from '@midwayjs/core';
/**
*
*/
@CoolController()
export class OpenDemoPluginController extends BaseController {
@Inject()
pluginService: PluginService;
@Get('/invoke', { summary: '调用插件' })
async invoke() {
// 获取插件实例
const instance = await this.pluginService.getInstance('feishu');
instance.sendByHook({
msg_type: 'text',
content: {
text: '测试',
},
});
return this.ok();
}
}

View File

@ -36,6 +36,18 @@ export class PluginInfoEntity extends BaseEntity {
@Column({ comment: '状态 0-禁用 1-启用', default: 0 }) @Column({ comment: '状态 0-禁用 1-启用', default: 0 })
status: number; status: number;
@Column({ comment: '内容', type: 'json' })
content: {
type: 'comm' | 'module';
data: string;
};
@Column({ comment: 'ts内容', type: 'json' })
tsContent: {
type: 'ts';
data: string;
};
@Column({ comment: '插件的plugin.json', type: 'json', nullable: true }) @Column({ comment: '插件的plugin.json', type: 'json', nullable: true })
pluginJson: any; pluginJson: any;

View File

@ -6,17 +6,7 @@ import * as moment from 'moment';
import { v1 as uuid } from 'uuid'; import { v1 as uuid } from 'uuid';
import { CoolCommException } from '@cool-midway/core'; import { CoolCommException } from '@cool-midway/core';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as os from 'os'; import { pUploadPath } from '../../../../comm/path';
import * as pkg from '../../../../../package.json';
// 获得上传目录
export const getUploadDir = () => {
const uploadDir = path.join(os.homedir(), `.${pkg.name}`, 'upload');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
return uploadDir;
};
/** /**
* *
@ -56,7 +46,7 @@ export class CoolPlugin extends BasePluginHook implements BaseUpload {
? await download(url) ? await download(url)
: fs.readFileSync(url); : fs.readFileSync(url);
// 创建文件夹 // 创建文件夹
const dirPath = path.join(getUploadDir(), `${moment().format('YYYYMMDD')}`); const dirPath = path.join(pUploadPath(), `${moment().format('YYYYMMDD')}`);
if (!fs.existsSync(dirPath)) { if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true }); fs.mkdirSync(dirPath, { recursive: true });
} }
@ -95,7 +85,7 @@ export class CoolPlugin extends BasePluginHook implements BaseUpload {
if (_.isEmpty(ctx.files)) { if (_.isEmpty(ctx.files)) {
throw new CoolCommException('上传文件为空'); throw new CoolCommException('上传文件为空');
} }
const basePath = getUploadDir(); const basePath = pUploadPath();
const file = ctx.files[0]; const file = ctx.files[0];
const extension = file.filename.split('.').pop(); const extension = file.filename.split('.').pop();

View File

@ -29,6 +29,7 @@ import { PluginMap, AnyString } from '../../../../typings/plugin';
import { PluginTypesService } from './types'; import { PluginTypesService } from './types';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import { pPluginPath } from '../../../comm/path';
/** /**
* *
*/ */
@ -254,38 +255,42 @@ export class PluginService extends BaseService {
tsContent: string; tsContent: string;
errorData: string; errorData: string;
}> { }> {
const decompress = require('decompress'); const AdmZip = require('adm-zip');
const files = await decompress(filePath); const zip = new AdmZip(filePath);
const files = zip.getEntries();
let errorData; let errorData;
let pluginJson: PluginInfo, let pluginJson: PluginInfo,
readme: string, readme: string,
logo: string, logo: string,
content: string, content: string,
tsContent: string; tsContent: string;
try { try {
// 通用方法获取文件内容
const getFileContent = (
entryName: string,
encoding: 'utf-8' | 'base64' = 'utf-8'
) => {
const file = _.find(files, { entryName });
if (!file) {
throw new Error(`File ${entryName} not found`);
}
return file?.getData()?.toString(encoding);
};
errorData = 'plugin.json'; errorData = 'plugin.json';
pluginJson = JSON.parse( pluginJson = JSON.parse(getFileContent('plugin.json'));
_.find(files, { path: 'plugin.json', type: 'file' }).data.toString()
);
errorData = 'readme'; errorData = 'readme';
readme = _.find(files, { readme = getFileContent(pluginJson.readme);
path: pluginJson.readme,
type: 'file',
}).data.toString();
errorData = 'logo'; errorData = 'logo';
logo = _.find(files, { logo = getFileContent(pluginJson.logo, 'base64');
path: pluginJson.logo,
type: 'file', errorData = 'content';
}).data.toString('base64'); content = getFileContent('src/index.js');
content = _.find(files, {
path: 'src/index.js', tsContent = getFileContent('source/index.ts');
type: 'file',
}).data.toString();
tsContent =
_.find(files, {
path: 'source/index.ts',
type: 'file',
})?.data?.toString() || '';
} catch (e) { } catch (e) {
throw new CoolCommException('插件信息不完整'); throw new CoolCommException('插件信息不完整');
} }
@ -328,6 +333,14 @@ export class PluginService extends BaseService {
hook: pluginJson.hook, hook: pluginJson.hook,
readme, readme,
logo, logo,
content: {
type: 'comm',
data: content,
},
tsContent: {
type: 'ts',
data: tsContent,
},
description: pluginJson.description, description: pluginJson.description,
pluginJson, pluginJson,
config: pluginJson.config, config: pluginJson.config,
@ -411,10 +424,30 @@ export class PluginService extends BaseService {
}> { }> {
const filePath = this.pluginPath(keyName); const filePath = this.pluginPath(keyName);
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
this.logger.warn( // 尝试从数据库中获取
`插件[${keyName}]文件不存在,请卸载后重新安装: ${filePath}` const info = await this.pluginInfoEntity.findOne({
); where: { keyName: Equal(keyName) },
return null; select: ['content', 'tsContent'],
});
if (info) {
// 保存插件到文件
this.saveData(
{
content: info.content,
tsContent: info.tsContent,
},
keyName
);
return {
content: info.content,
tsContent: info.tsContent,
};
} else {
this.logger.warn(
`插件[${keyName}]文件不存在,请卸载后重新安装: ${filePath}`
);
return;
}
} }
return JSON.parse(await fs.promises.readFile(filePath, 'utf-8')); return JSON.parse(await fs.promises.readFile(filePath, 'utf-8'));
} }
@ -436,12 +469,6 @@ export class PluginService extends BaseService {
* @returns * @returns
*/ */
pluginPath(keyName: string) { pluginPath(keyName: string) {
return path.join( return path.join(pPluginPath(), `${keyName}`);
this.app.getBaseDir(),
'..',
'cool',
'plugin',
`${keyName}.cool`
);
} }
} }

View File

@ -0,0 +1,23 @@
import { ModuleConfig } from '@cool-midway/core';
import { TaskMiddleware } from './middleware/task';
/**
*
*/
export default () => {
return {
// 模块名称
name: '任务调度',
// 模块描述
description: '任务调度模块支持分布式任务由redis整个集群的任务',
// 中间件
middlewares: [TaskMiddleware],
// 模块加载顺序默认为0值越大越优先加载
order: 0,
// 日志
log: {
// 日志保留时间,单位天
keepDays: 20,
},
} as ModuleConfig;
};

View File

@ -0,0 +1,59 @@
import { Body, Get, Inject, Post, Provide, Query } from '@midwayjs/core';
import { CoolController, BaseController } from '@cool-midway/core';
import { TaskInfoEntity } from '../../entity/info';
import { TaskInfoService } from '../../service/info';
/**
*
*/
@Provide()
@CoolController({
api: ['add', 'delete', 'update', 'info', 'page'],
entity: TaskInfoEntity,
service: TaskInfoService,
before: ctx => {
ctx.request.body.limit = ctx.request.body.repeatCount;
},
pageQueryOp: {
fieldEq: ['status', 'type'],
},
})
export class TaskInfoController extends BaseController {
@Inject()
taskInfoService: TaskInfoService;
/**
*
*/
@Post('/once', { summary: '执行一次' })
async once(@Body('id') id: number) {
await this.taskInfoService.once(id);
this.ok();
}
/**
*
*/
@Post('/stop', { summary: '停止' })
async stop(@Body('id') id: number) {
await this.taskInfoService.stop(id);
this.ok();
}
/**
*
*/
@Post('/start', { summary: '开始' })
async start(@Body('id') id: number, @Body('type') type: number) {
await this.taskInfoService.start(id, type);
this.ok();
}
/**
*
*/
@Get('/log', { summary: '日志' })
async log(@Query() params: any) {
return this.ok(await this.taskInfoService.log(params));
}
}

40
src/modules/task/db.json Normal file
View File

@ -0,0 +1,40 @@
{
"task_info": [
{
"id": 1,
"jobId": "089f554c-fdd4-4093-9f84-4cfb6af2f514",
"repeatConf": "{\"count\":1,\"type\":1,\"limit\":5,\"name\":\"每秒执行,总共5次\",\"taskType\":1,\"every\":1000,\"service\":\"taskDemoService.test()\",\"status\":1,\"id\":1,\"createTime\":\"2021-03-10 14:25:13\",\"updateTime\":\"2021-03-10 14:25:13\",\"jobId\":1}",
"name": "每秒执行一次",
"cron": null,
"limit": null,
"every": 1000,
"remark": null,
"status": 0,
"startDate": null,
"endDate": null,
"data": null,
"service": "taskDemoService.test(1,2)",
"type": 1,
"nextRunTime": "2021-3-10 14:25:18",
"taskType": 1
},
{
"id": 2,
"jobId": "9e1f42c8-b127-449b-b0a4-d53c60b79e75",
"repeatConf": "{\"count\":1,\"id\":2,\"createTime\":\"2021-03-10 14:25:53\",\"updateTime\":\"2021-03-10 14:25:55\",\"name\":\"cron任务5秒执行一次\",\"cron\":\"0/5 * * * * ? \",\"status\":1,\"service\":\"taskDemoService.test()\",\"type\":1,\"nextRunTime\":\"2021-03-10 14:26:00\",\"taskType\":0,\"jobId\":2}",
"name": "cron任务5秒执行一次",
"cron": "0/5 * * * * * ",
"limit": null,
"every": null,
"remark": null,
"status": 0,
"startDate": null,
"endDate": null,
"data": null,
"service": "taskDemoService.test()",
"type": 1,
"nextRunTime": null,
"taskType": 0
}
]
}

View File

@ -0,0 +1,62 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity } from 'typeorm';
/**
*
*/
@Entity('task_info')
export class TaskInfoEntity extends BaseEntity {
@Column({ comment: '任务ID', nullable: true })
jobId: string;
@Column({ comment: '任务配置', nullable: true, length: 1000 })
repeatConf: string;
@Column({ comment: '名称' })
name: string;
@Column({ comment: 'cron', nullable: true })
cron: string;
@Column({ comment: '最大执行次数 不传为无限次', nullable: true })
limit: number;
@Column({
comment: '每间隔多少毫秒执行一次 如果cron设置了 这项设置就无效',
nullable: true,
})
every: number;
@Column({ comment: '备注', nullable: true })
remark: string;
@Column({ comment: '状态 0-停止 1-运行', default: 1 })
status: number;
@Column({ comment: '开始时间', nullable: true })
startDate: Date;
@Column({ comment: '结束时间', nullable: true })
endDate: Date;
@Column({ comment: '数据', nullable: true })
data: string;
@Column({ comment: '执行的service实例ID', nullable: true })
service: string;
@Column({ comment: '状态 0-系统 1-用户', default: 0 })
type: number;
@Column({ comment: '下一次执行时间', nullable: true })
nextRunTime: Date;
@Column({ comment: '状态 0-cron 1-时间间隔', default: 0 })
taskType: number;
@Column({ type: 'datetime', nullable: true })
lastExecuteTime: Date;
@Column({ type: 'datetime', nullable: true })
lockExpireTime: Date;
}

View File

@ -0,0 +1,18 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Index, Entity } from 'typeorm';
/**
*
*/
@Entity('task_log')
export class TaskLogEntity extends BaseEntity {
@Index()
@Column({ comment: '任务ID', nullable: true })
taskId: number;
@Column({ comment: '状态 0-失败 1-成功', default: 0 })
status: number;
@Column({ comment: '详情描述', nullable: true, type: 'text' })
detail: string;
}

View File

@ -0,0 +1,17 @@
import { Inject } from '@midwayjs/core';
import { CoolEvent, Event } from '@cool-midway/core';
import { TaskInfoService } from '../service/info';
/**
*
*/
@CoolEvent()
export class TaskAppEvent {
@Inject()
taskInfoService: TaskInfoService;
@Event('onServerReady')
async onServerReady() {
this.taskInfoService.initTask();
}
}

View File

@ -0,0 +1,38 @@
import { CoolCommException } from '@cool-midway/core';
import { Inject, Middleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
import { IMiddleware } from '@midwayjs/core';
import { TaskInfoQueue } from '../queue/task';
import { TaskInfoService } from '../service/info';
/**
*
*/
@Middleware()
export class TaskMiddleware implements IMiddleware<Context, NextFunction> {
@Inject()
taskInfoQueue: TaskInfoQueue;
@Inject()
taskInfoService: TaskInfoService;
resolve() {
return async (ctx: Context, next: NextFunction) => {
const urls = ctx.url.split('/');
const type = await this.taskInfoService.initType();
if (
['add', 'update', 'once', 'stop', 'start'].includes(
urls[urls.length - 1]
) &&
type == 'bull'
) {
if (!this.taskInfoQueue.metaQueue) {
throw new CoolCommException(
'task插件未启用或redis配置错误或redis版本过低(>=6.x)'
);
}
}
await next();
};
}
}

View File

@ -0,0 +1,30 @@
import { App, Inject } from '@midwayjs/core';
import { BaseCoolQueue, CoolQueue } from '@cool-midway/task';
import { TaskBullService } from '../service/bull';
import { IMidwayApplication } from '@midwayjs/core';
/**
*
*/
@CoolQueue()
export abstract class TaskInfoQueue extends BaseCoolQueue {
@App()
app: IMidwayApplication;
@Inject()
taskBullService: TaskBullService;
async data(job, done: any): Promise<void> {
try {
const result = await this.taskBullService.invokeService(job.data.service);
this.taskBullService.record(job.data, 1, JSON.stringify(result));
} catch (error) {
this.taskBullService.record(job.data, 0, error.message);
}
if (!job.data.isOnce) {
this.taskBullService.updateNextRunTime(job.data.jobId);
this.taskBullService.updateStatus(job.data.id);
}
done();
}
}

View File

@ -0,0 +1,339 @@
import {
App,
Config,
Inject,
Logger,
Provide,
Scope,
ScopeEnum,
} from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Equal, LessThan, Repository } from 'typeorm';
import { TaskInfoEntity } from '../entity/info';
import { TaskLogEntity } from '../entity/log';
import { ILogger } from '@midwayjs/logger';
import * as _ from 'lodash';
import { Utils } from '../../../comm/utils';
import { TaskInfoQueue } from '../queue/task';
import { IMidwayApplication } from '@midwayjs/core';
import { v4 as uuidv4 } from 'uuid';
import * as moment from 'moment';
/**
*
*/
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class TaskBullService extends BaseService {
@InjectEntityModel(TaskInfoEntity)
taskInfoEntity: Repository<TaskInfoEntity>;
@Logger()
logger: ILogger;
@InjectEntityModel(TaskLogEntity)
taskLogEntity: Repository<TaskLogEntity>;
@Inject()
taskInfoQueue: TaskInfoQueue;
@App()
app: IMidwayApplication;
@Inject()
utils: Utils;
@Config('task.log.keepDays')
keepDays: number;
/**
*
* @param id
*/
async stop(id) {
const task = await this.taskInfoEntity.findOneBy({ id: Equal(id) });
if (task) {
const result = await this.taskInfoQueue.getJobSchedulers();
const job = _.find(result, e => {
return e.template?.data?.jobId === task.jobId;
});
if (job) {
await this.taskInfoQueue.removeJobScheduler(job.key);
}
task.status = 0;
await this.taskInfoEntity.update(task.id, task);
await this.updateNextRunTime(task.jobId);
}
}
/**
*
* @param taskId
*/
async remove(taskId) {
const info = await this.taskInfoEntity.findOneBy({ id: Equal(taskId) });
const result = await this.taskInfoQueue.getJobSchedulers();
const job = _.find(result, { id: info?.jobId });
if (job) {
await this.taskInfoQueue.removeJobScheduler(job.key);
}
}
/**
*
* @param id
* @param type
*/
async start(id, type?) {
const task = await this.taskInfoEntity.findOneBy({ id: Equal(id) });
task.status = 1;
if (type || type == 0) {
task.type = type;
}
await this.addOrUpdate(task);
}
/**
*
* @param id
*/
async once(id) {
const task = await this.taskInfoEntity.findOneBy({ id: Equal(id) });
if (task) {
await this.taskInfoQueue.add(
{
...task,
isOnce: true,
},
{
jobId: task.jobId,
removeOnComplete: true,
removeOnFail: true,
}
);
}
}
/**
*
* @param jobId
*/
async exist(jobId) {
const info = await this.taskInfoEntity.findOneBy({ jobId: Equal(jobId) });
const result = await this.taskInfoQueue.getJobSchedulers();
const ids = result.map(e => {
return e.id;
});
return ids.includes(info?.jobId);
}
/**
*
* @param params
*/
async addOrUpdate(params) {
delete params.repeatCount;
let repeatConf;
if (!params.jobId) {
params.jobId = uuidv4();
}
await this.getOrmManager().transaction(async transactionalEntityManager => {
if (params.taskType === 0) {
params.limit = null;
params.every = null;
} else {
params.cron = null;
}
await transactionalEntityManager.save(TaskInfoEntity, params);
if (params.status === 1) {
const exist = await this.exist(params.id);
if (exist) {
await this.remove(params.id);
}
const { every, limit, startDate, endDate, cron } = params;
const repeat = {
every,
limit,
jobId: params.jobId,
startDate,
endDate,
cron,
};
await this.utils.removeEmptyP(repeat);
const result = await this.taskInfoQueue.add(params, {
jobId: params.jobId,
removeOnComplete: true,
removeOnFail: true,
repeat,
});
if (!result) {
throw new Error('任务添加失败,请检查任务配置');
}
// await transactionalEntityManager.update(TaskInfoEntity, params.id, {
// jobId: params.id,
// type: params.type,
// });
repeatConf = result.opts;
}
});
if (params.status === 1) {
this.utils.sleep(1000);
await this.updateNextRunTime(params.jobId);
await this.taskInfoEntity.update(params.id, {
repeatConf: JSON.stringify(repeatConf.repeat),
});
}
}
/**
*
* @param ids
*/
async delete(ids) {
let idArr;
if (ids instanceof Array) {
idArr = ids;
} else {
idArr = ids.split(',');
}
for (const id of idArr) {
const task = await this.taskInfoEntity.findOneBy({ id });
const exist = await this.exist(task.id);
if (exist) {
this.stop(task.id);
}
await this.taskInfoEntity.delete({ id });
await this.taskLogEntity.delete({ taskId: id });
}
}
/**
* 20
* @param task
* @param status
* @param detail
*/
async record(task, status, detail?) {
const info = await this.taskInfoEntity.findOneBy({
jobId: Equal(task.jobId),
});
await this.taskLogEntity.save({
taskId: info.id,
status,
detail: detail || '',
});
// 删除时间超过20天的日志
await this.taskLogEntity.delete({
taskId: info.id,
createTime: LessThan(moment().subtract(this.keepDays, 'days').toDate()),
});
}
/**
*
*/
async initTask() {
try {
await this.utils.sleep(3000);
this.logger.info('init task....');
const runningTasks = await this.taskInfoEntity.findBy({ status: 1 });
if (!_.isEmpty(runningTasks)) {
for (const task of runningTasks) {
const job = await this.exist(task.id); // 任务已存在就不添加
if (!job) {
this.logger.info(`init task ${task.name}`);
await this.addOrUpdate(task);
}
}
}
} catch (e) {}
}
/**
* ID
* @param jobId
*/
async getNextRunTime(jobId) {
let nextRunTime;
const result = await this.taskInfoQueue.getJobSchedulers();
const task = _.find(result, e => {
return e.template?.data?.jobId === jobId;
});
if (task) {
nextRunTime = new Date(task.next);
}
return nextRunTime;
}
/**
*
* @param jobId
*/
async updateNextRunTime(jobId) {
await this.taskInfoEntity.update(
{ jobId },
{
nextRunTime: await this.getNextRunTime(jobId),
}
);
}
/**
*
* @param id
* @returns
*/
async info(id: any): Promise<any> {
const info = await this.taskInfoEntity.findOneBy({ id });
return {
...info,
repeatCount: info.limit,
};
}
/**
*
*/
async updateStatus(jobId) {
const result = await this.taskInfoQueue.getJobSchedulers();
const job = _.find(result, { id: jobId + '' });
if (!job) {
return;
}
const task = await this.taskInfoEntity.findOneBy({ id: job.id });
const nextTime = await this.getNextRunTime(task.jobId);
if (task) {
// if (task.nextRunTime.getTime() == nextTime.getTime()) {
// task.status = 0;
// task.nextRunTime = nextTime;
// this.taskInfoQueue.removeRepeatableByKey(job.key);
// } else {
task.nextRunTime = nextTime;
// }
await this.taskInfoEntity.update(task.id, task);
}
}
/**
* service
* @param serviceStr
*/
async invokeService(serviceStr) {
if (serviceStr) {
const arr = serviceStr.split('.');
const service = await this.app
.getApplicationContext()
.getAsync(_.lowerFirst(arr[0]));
for (let i = 1; i < arr.length; i++) {
const child = arr[i];
if (child.includes('(')) {
const [methodName, paramsStr] = child.split('(');
const params = paramsStr
.replace(')', '')
.split(',')
.map(param => param.trim());
if (params.length === 1 && params[0] === '') {
return service[methodName]();
} else {
const parsedParams = params.map(param => {
try {
return JSON.parse(param);
} catch (e) {
return param; // 如果不是有效的JSON,则返回原始字符串
}
});
return service[methodName](...parsedParams);
}
}
}
}
}
}

View File

@ -0,0 +1,19 @@
import { Logger, Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import { ILogger } from '@midwayjs/logger';
/**
*
*/
@Provide()
export class TaskDemoService extends BaseService {
@Logger()
logger: ILogger;
/**
*
*/
async test(a, b) {
this.logger.info('我被调用了', a, b);
return '任务执行成功';
}
}

View File

@ -0,0 +1,153 @@
import { App, Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { TaskInfoEntity } from '../entity/info';
import * as _ from 'lodash';
import { IMidwayApplication } from '@midwayjs/core';
import { CoolQueueHandle } from '@cool-midway/task';
import { TaskBullService } from './bull';
import { TaskLocalService } from './local';
import { TaskLogEntity } from '../entity/log';
/**
*
*/
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class TaskInfoService extends BaseService {
@InjectEntityModel(TaskInfoEntity)
taskInfoEntity: Repository<TaskInfoEntity>;
@InjectEntityModel(TaskLogEntity)
taskLogEntity: Repository<TaskLogEntity>;
type: 'local' | 'bull' = 'local';
@App()
app: IMidwayApplication;
@Inject()
taskBullService: TaskBullService;
@Inject()
taskLocalService: TaskLocalService;
@Init()
async init() {
await super.init();
await this.initType();
this.setEntity(this.taskInfoEntity);
}
/**
*
*/
async initType() {
try {
const check = await this.app
.getApplicationContext()
.getAsync(CoolQueueHandle);
if (check) {
this.type = 'bull';
} else {
this.type = 'local';
}
} catch (e) {
this.type = 'local';
}
return this.type;
}
/**
*
* @param id
*/
async stop(id) {
this.type === 'bull'
? this.taskBullService.stop(id)
: this.taskLocalService.stop(id);
}
/**
*
* @param id
* @param type
*/
async start(id, type?) {
this.type === 'bull'
? this.taskBullService.start(id)
: this.taskLocalService.start(id, type);
}
/**
*
* @param id
*/
async once(id) {
this.type === 'bull'
? this.taskBullService.once(id)
: this.taskLocalService.once(id);
}
/**
*
* @param jobId
*/
async exist(jobId) {
this.type === 'bull'
? this.taskBullService.exist(jobId)
: this.taskLocalService.exist(jobId);
}
/**
*
* @param params
*/
async addOrUpdate(params) {
this.type === 'bull'
? this.taskBullService.addOrUpdate(params)
: this.taskLocalService.addOrUpdate(params);
}
/**
*
* @param ids
*/
async delete(ids) {
this.type === 'bull'
? this.taskBullService.delete(ids)
: this.taskLocalService.delete(ids);
}
/**
*
* @param query
*/
async log(query) {
const { id, status } = query;
const find = await this.taskLogEntity
.createQueryBuilder('a')
.select(['a.*', 'b.name as taskName'])
.leftJoin(TaskInfoEntity, 'b', 'a.taskId = b.id')
.where('a.taskId = :id', { id });
if (status || status == 0) {
find.andWhere('a.status = :status', { status });
}
return await this.entityRenderPage(find, query);
}
/**
*
*/
async initTask() {
this.type === 'bull'
? this.taskBullService.initTask()
: this.taskLocalService.initTask();
}
/**
*
* @param id
* @returns
*/
async info(id: any): Promise<any> {
this.type === 'bull'
? this.taskBullService.info(id)
: this.taskLocalService.info(id);
}
}

View File

@ -0,0 +1,336 @@
import {
App,
Config,
Inject,
Logger,
Provide,
Scope,
ScopeEnum,
} from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Equal, LessThan, Repository } from 'typeorm';
import { TaskInfoEntity } from '../entity/info';
import { TaskLogEntity } from '../entity/log';
import { ILogger } from '@midwayjs/logger';
import * as _ from 'lodash';
import { Utils } from '../../../comm/utils';
import { IMidwayApplication } from '@midwayjs/core';
import { v4 as uuidv4 } from 'uuid';
import * as moment from 'moment';
import * as CronJob from 'cron';
/**
*
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class TaskLocalService extends BaseService {
@InjectEntityModel(TaskInfoEntity)
taskInfoEntity: Repository<TaskInfoEntity>;
@Logger()
logger: ILogger;
@InjectEntityModel(TaskLogEntity)
taskLogEntity: Repository<TaskLogEntity>;
@App()
app: IMidwayApplication;
@Inject()
utils: Utils;
@Config('task.log.keepDays')
keepDays: number;
// 存储所有运行的任务
private cronJobs: Map<string, CronJob.CronJob> = new Map();
/**
*
*/
async stop(id) {
const task = await this.taskInfoEntity.findOneBy({ id: Equal(id) });
if (task) {
const job = this.cronJobs.get(task.jobId);
if (job) {
job.stop();
this.cronJobs.delete(task.jobId);
}
task.status = 0;
await this.taskInfoEntity.update(task.id, task);
await this.updateNextRunTime(task.jobId);
}
}
/**
*
*/
async start(id, type?) {
const task = await this.taskInfoEntity.findOneBy({ id: Equal(id) });
task.status = 1;
if (type || type == 0) {
task.type = type;
}
await this.addOrUpdate(task);
}
/**
*
*/
async once(id) {
const task = await this.taskInfoEntity.findOneBy({ id: Equal(id) });
if (task) {
await this.executeJob(task);
}
}
/**
*
*/
async exist(jobId) {
return this.cronJobs.has(jobId);
}
/**
*
*/
private createCronJob(task) {
let cronTime;
if (task.taskType === 0) {
// cron 类型
cronTime = task.cron;
} else {
// 间隔类型
cronTime = `*/${task.every / 1000} * * * * *`;
}
const job = new CronJob.CronJob(
cronTime,
async () => {
await this.executeJob(task);
},
null,
false,
'Asia/Shanghai'
);
this.cronJobs.set(task.jobId, job);
job.start();
return job;
}
/**
*
*/
private async executeJob(task) {
await this.executor(task);
}
/**
*
*/
async addOrUpdate(params) {
if (!params.jobId) {
params.jobId = uuidv4();
}
await this.getOrmManager().transaction(async transactionalEntityManager => {
if (params.taskType === 0) {
params.limit = null;
params.every = null;
} else {
params.cron = null;
}
await transactionalEntityManager.save(TaskInfoEntity, params);
if (params.status === 1) {
const exist = await this.exist(params.jobId);
if (exist) {
const job = this.cronJobs.get(params.jobId);
job.stop();
this.cronJobs.delete(params.jobId);
}
this.createCronJob(params);
}
});
if (params.status === 1) {
await this.utils.sleep(1000);
await this.updateNextRunTime(params.jobId);
}
}
/**
*
*/
async delete(ids) {
let idArr;
if (ids instanceof Array) {
idArr = ids;
} else {
idArr = ids.split(',');
}
for (const id of idArr) {
const task = await this.taskInfoEntity.findOneBy({ id });
if (task) {
const job = this.cronJobs.get(task.jobId);
if (job) {
job.stop();
this.cronJobs.delete(task.jobId);
}
await this.taskInfoEntity.delete({ id });
await this.taskLogEntity.delete({ taskId: id });
}
}
}
/**
*
*/
async record(task, status, detail?) {
const info = await this.taskInfoEntity.findOneBy({
jobId: Equal(task.jobId),
});
await this.taskLogEntity.save({
taskId: info.id,
status,
detail: detail || '',
});
await this.taskLogEntity.delete({
taskId: info.id,
createTime: LessThan(moment().subtract(this.keepDays, 'days').toDate()),
});
}
/**
*
*/
async getNextRunTime(jobId) {
const job = this.cronJobs.get(jobId);
return job ? job.nextDate().toJSDate() : null;
}
/**
*
*/
async updateNextRunTime(jobId) {
const nextRunTime = await this.getNextRunTime(jobId);
if (nextRunTime) {
await this.taskInfoEntity.update({ jobId }, { nextRunTime });
}
}
/**
*
*/
async initTask() {
try {
await this.utils.sleep(3000);
this.logger.info('init local task....');
const runningTasks = await this.taskInfoEntity.findBy({ status: 1 });
if (!_.isEmpty(runningTasks)) {
for (const task of runningTasks) {
const job = await this.exist(task.jobId);
if (!job) {
this.logger.info(`init local task ${task.name}`);
await this.addOrUpdate(task);
}
}
}
} catch (e) {
this.logger.error('Init local task error:', e);
}
}
/**
* service
*/
async invokeService(serviceStr) {
if (serviceStr) {
const arr = serviceStr.split('.');
const service = await this.app
.getApplicationContext()
.getAsync(_.lowerFirst(arr[0]));
for (let i = 1; i < arr.length; i++) {
const child = arr[i];
if (child.includes('(')) {
const [methodName, paramsStr] = child.split('(');
const params = paramsStr
.replace(')', '')
.split(',')
.map(param => param.trim());
if (params.length === 1 && params[0] === '') {
return service[methodName]();
} else {
const parsedParams = params.map(param => {
try {
return JSON.parse(param);
} catch (e) {
return param;
}
});
return service[methodName](...parsedParams);
}
}
}
}
}
/**
*
*/
async info(id: any): Promise<any> {
const info = await this.taskInfoEntity.findOneBy({ id });
return {
...info,
repeatCount: info.limit,
};
}
/**
*
*/
async executor(task: any): Promise<void> {
try {
const currentTime = moment();
const lockExpireTime = moment().add(5, 'minutes');
const result = await this.taskInfoEntity
.createQueryBuilder()
.update()
.set({
lastExecuteTime: currentTime,
lockExpireTime: lockExpireTime,
})
.where('id = :id', { id: task.id })
.andWhere('lockExpireTime IS NULL OR lockExpireTime < :currentTime', {
currentTime,
})
.execute();
// 如果更新失败affected === 0说明其他实例正在执行
if (result.affected === 0) {
return;
}
const serviceResult = await this.invokeService(task.service);
await this.record(task, 1, JSON.stringify(serviceResult));
} catch (error) {
await this.record(task, 0, error.message);
} finally {
// 释放锁
await this.taskInfoEntity.update(
{ id: task.id },
{ lockExpireTime: null }
);
}
if (!task.isOnce) {
await this.updateNextRunTime(task.jobId);
await this.taskInfoEntity.update({ id: task.id }, { status: 1 });
}
}
}

View File

@ -0,0 +1,34 @@
import { ModuleConfig } from '@cool-midway/core';
import { UserMiddleware } from './middleware/app';
/**
*
*/
export default () => {
return {
// 模块名称
name: '用户模块',
// 模块描述
description: 'APP、小程序、公众号等用户',
// 中间件,只对本模块有效
middlewares: [],
// 中间件,全局有效
globalMiddlewares: [UserMiddleware],
// 模块加载顺序默认为0值越大越优先加载
order: 0,
// 短信
sms: {
// 验证码有效期,单位秒
timeout: 60 * 3,
},
// jwt
jwt: {
// token 过期时间,单位秒
expire: 60 * 60 * 24,
// 刷新token 过期时间,单位秒
refreshExpire: 60 * 60 * 24 * 30,
// jwt 秘钥
secret: '52dee820-a5d9-46ed-858b-ea193c3f84e2x',
},
} as ModuleConfig;
};

View File

@ -0,0 +1,13 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { UserAddressEntity } from '../../entity/address';
import { UserAddressService } from '../../service/address';
/**
* -
*/
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: UserAddressEntity,
service: UserAddressService,
})
export class AdminUserAddressesController extends BaseController {}

View File

@ -0,0 +1,15 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { UserInfoEntity } from '../../entity/info';
/**
*
*/
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: UserInfoEntity,
pageQueryOp: {
fieldEq: ['status', 'gender', 'loginType'],
keyWordLikeFields: ['nickName', 'phone'],
},
})
export class AdminUserInfoController extends BaseController {}

View File

@ -0,0 +1,39 @@
import { Get, Inject, Provide } from '@midwayjs/core';
import { CoolController, BaseController } from '@cool-midway/core';
import { UserAddressEntity } from '../../entity/address';
import { UserAddressService } from '../../service/address';
/**
*
*/
@Provide()
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: UserAddressEntity,
service: UserAddressService,
insertParam: ctx => {
return {
userId: ctx.user.id,
};
},
pageQueryOp: {
where: async ctx => {
return [['userId =:userId', { userId: ctx.user.id }]];
},
addOrderBy: {
isDefault: 'DESC',
},
},
})
export class AppUserAddressController extends BaseController {
@Inject()
userAddressService: UserAddressService;
@Inject()
ctx;
@Get('/default', { summary: '默认地址' })
async default() {
return this.ok(await this.userAddressService.default(this.ctx.user.id));
}
}

View File

@ -0,0 +1,25 @@
import {
CoolController,
BaseController,
CoolUrlTag,
TagTypes,
CoolTag,
} from '@cool-midway/core';
import { Body, Inject, Post } from '@midwayjs/core';
import { UserWxService } from '../../service/wx';
/**
*
*/
@CoolUrlTag()
@CoolController()
export class UserCommController extends BaseController {
@Inject()
userWxService: UserWxService;
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/wxMpConfig', { summary: '获取微信公众号配置' })
public async getWxMpConfig(@Body('url') url: string) {
return this.ok(await this.userWxService.getWxMpConfig(url));
}
}

View File

@ -0,0 +1,65 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { Body, Get, Inject, Post } from '@midwayjs/core';
import { UserInfoService } from '../../service/info';
import { UserInfoEntity } from '../../entity/info';
/**
*
*/
@CoolController({
api: [],
entity: UserInfoEntity,
})
export class AppUserInfoController extends BaseController {
@Inject()
ctx;
@Inject()
userInfoService: UserInfoService;
@Get('/person', { summary: '获取用户信息' })
async person() {
return this.ok(await this.userInfoService.person(this.ctx.user.id));
}
@Post('/updatePerson', { summary: '更新用户信息' })
async updatePerson(@Body() body) {
return this.ok(
await this.userInfoService.updatePerson(this.ctx.user.id, body)
);
}
@Post('/updatePassword', { summary: '更新用户密码' })
async updatePassword(
@Body('password') password: string,
@Body('code') code: string
) {
await this.userInfoService.updatePassword(this.ctx.user.id, password, code);
return this.ok();
}
@Post('/logoff', { summary: '注销' })
async logoff() {
await this.userInfoService.logoff(this.ctx.user.id);
return this.ok();
}
@Post('/bindPhone', { summary: '绑定手机号' })
async bindPhone(@Body('phone') phone: string, @Body('code') code: string) {
await this.userInfoService.bindPhone(this.ctx.user.id, phone, code);
return this.ok();
}
@Post('/miniPhone', { summary: '绑定小程序手机号' })
async miniPhone(@Body() body) {
const { code, encryptedData, iv } = body;
return this.ok(
await this.userInfoService.miniPhone(
this.ctx.user.id,
code,
encryptedData,
iv
)
);
}
}

View File

@ -0,0 +1,106 @@
import {
CoolController,
BaseController,
CoolUrlTag,
TagTypes,
CoolTag,
} from '@cool-midway/core';
import { Body, Get, Inject, Post, Query } from '@midwayjs/core';
import { UserLoginService } from '../../service/login';
import { BaseSysLoginService } from '../../../base/service/sys/login';
/**
*
*/
@CoolUrlTag()
@CoolController()
export class AppUserLoginController extends BaseController {
@Inject()
userLoginService: UserLoginService;
@Inject()
baseSysLoginService: BaseSysLoginService;
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/mini', { summary: '小程序登录' })
async mini(@Body() body) {
const { code, encryptedData, iv } = body;
return this.ok(await this.userLoginService.mini(code, encryptedData, iv));
}
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/mp', { summary: '公众号登录' })
async mp(@Body('code') code: string) {
return this.ok(await this.userLoginService.mp(code));
}
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/wxApp', { summary: '微信APP授权登录' })
async app(@Body('code') code: string) {
return this.ok(await this.userLoginService.wxApp(code));
}
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/phone', { summary: '手机号登录' })
async phone(@Body('phone') phone: string, @Body('smsCode') smsCode: string) {
return this.ok(await this.userLoginService.phoneVerifyCode(phone, smsCode));
}
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/uniPhone', { summary: '一键手机号登录' })
async uniPhone(
@Body('access_token') access_token: string,
@Body('openid') openid: string,
@Body('appId') appId: string
) {
return this.ok(
await this.userLoginService.uniPhone(access_token, openid, appId)
);
}
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/miniPhone', { summary: '绑定小程序手机号' })
async miniPhone(@Body() body) {
const { code, encryptedData, iv } = body;
return this.ok(
await this.userLoginService.miniPhone(code, encryptedData, iv)
);
}
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/captcha', { summary: '图片验证码' })
async captcha(
@Query('width') width: number,
@Query('height') height: number,
@Query('color') color: string
) {
return this.ok(
await this.baseSysLoginService.captcha(width, height, color)
);
}
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/smsCode', { summary: '验证码' })
async smsCode(
@Body('phone') phone: string,
@Body('captchaId') captchaId: string,
@Body('code') code: string
) {
return this.ok(await this.userLoginService.smsCode(phone, captchaId, code));
}
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/refreshToken', { summary: '刷新token' })
public async refreshToken(@Body('refreshToken') refreshToken) {
return this.ok(await this.userLoginService.refreshToken(refreshToken));
}
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/password', { summary: '密码登录' })
async password(
@Body('phone') phone: string,
@Body('password') password: string
) {
return this.ok(await this.userLoginService.password(phone, password));
}
}

View File

@ -0,0 +1,34 @@
import { BaseEntity } from '@cool-midway/core';
import { Entity, Column, Index } from 'typeorm';
/**
* -
*/
@Entity('user_address')
export class UserAddressEntity extends BaseEntity {
@Index()
@Column({ comment: '用户ID' })
userId: number;
@Column({ comment: '联系人' })
contact: string;
@Index()
@Column({ comment: '手机号', length: 11 })
phone: string;
@Column({ comment: '省' })
province: string;
@Column({ comment: '市' })
city: string;
@Column({ comment: '区' })
district: string;
@Column({ comment: '地址' })
address: string;
@Column({ comment: '是否默认', default: false })
isDefault: boolean;
}

View File

@ -0,0 +1,34 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity, Index } from 'typeorm';
/**
*
*/
@Entity('user_info')
export class UserInfoEntity extends BaseEntity {
@Index({ unique: true })
@Column({ comment: '登录唯一ID', nullable: true })
unionid: string;
@Column({ comment: '头像', nullable: true })
avatarUrl: string;
@Column({ comment: '昵称', nullable: true })
nickName: string;
@Index({ unique: true })
@Column({ comment: '手机号', nullable: true })
phone: string;
@Column({ comment: '性别 0-未知 1-男 2-女', default: 0 })
gender: number;
@Column({ comment: '状态 0-禁用 1-正常 2-已注销', default: 1 })
status: number;
@Column({ comment: '登录方式 0-小程序 1-公众号 2-H5', default: 0 })
loginType: number;
@Column({ comment: '密码', nullable: true })
password: string;
}

View File

@ -0,0 +1,40 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity, Index } from 'typeorm';
/**
*
*/
@Entity('user_wx')
export class UserWxEntity extends BaseEntity {
@Index()
@Column({ comment: '微信unionid', nullable: true })
unionid: string;
@Index()
@Column({ comment: '微信openid' })
openid: string;
@Column({ comment: '头像', nullable: true })
avatarUrl: string;
@Column({ comment: '昵称', nullable: true })
nickName: string;
@Column({ comment: '性别 0-未知 1-男 2-女', default: 0 })
gender: number;
@Column({ comment: '语言', nullable: true })
language: string;
@Column({ comment: '城市', nullable: true })
city: string;
@Column({ comment: '省份', nullable: true })
province: string;
@Column({ comment: '国家', nullable: true })
country: string;
@Column({ comment: '类型 0-小程序 1-公众号 2-H5 3-APP', default: 0 })
type: number;
}

View File

@ -0,0 +1,56 @@
import { CoolEvent, Event } from '@cool-midway/core';
import { App, Config, ILogger, Logger } from '@midwayjs/core';
import { IMidwayKoaApplication } from '@midwayjs/koa';
import * as fs from 'fs';
import * as path from 'path';
import { v1 as uuid } from 'uuid';
/**
* jwt.secret
*/
@CoolEvent()
export class UserAppEvent {
@Logger()
coreLogger: ILogger;
@Config('module')
config;
@App()
app: IMidwayKoaApplication;
@Event('onMenuInit')
async onMenuInit() {
if (this.app.getEnv() != 'local') return;
this.checkConfig();
}
/**
*
*/
async checkConfig() {
if (this.config.user.jwt.secret == 'cool-app-xxxxxx') {
this.coreLogger.warn(
'\x1B[36m 检测到模块[user] jwt.secret 配置是默认值,请不要关闭!即将自动修改... \x1B[0m'
);
setTimeout(() => {
const filePath = path.join(
this.app.getBaseDir(),
'..',
'src',
'modules',
'user',
'config.ts'
);
// 替换文件内容
let fileData = fs.readFileSync(filePath, 'utf8');
const secret = uuid().replace(/-/g, '');
this.config.user.jwt.secret = secret;
fs.writeFileSync(filePath, fileData.replace('cool-app-xxxxxx', secret));
this.coreLogger.info(
'\x1B[36m [cool:module:user] midwayjs cool module user auto modify jwt.secret\x1B[0m'
);
}, 6000);
}
}
}

View File

@ -0,0 +1,96 @@
import { ALL, Config, Middleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
import { IMiddleware, Init, Inject } from '@midwayjs/core';
import * as jwt from 'jsonwebtoken';
import * as _ from 'lodash';
import { CoolUrlTagData, RESCODE, TagTypes } from '@cool-midway/core';
/**
*
*/
@Middleware()
export class UserMiddleware implements IMiddleware<Context, NextFunction> {
@Config(ALL)
coolConfig;
@Inject()
coolUrlTagData: CoolUrlTagData;
@Config('module.user.jwt')
jwtConfig;
ignoreUrls: string[] = [];
@Config('koa.globalPrefix')
prefix;
@Init()
async init() {
this.ignoreUrls = this.coolUrlTagData.byKey(TagTypes.IGNORE_TOKEN, 'app');
}
resolve() {
return async (ctx: Context, next: NextFunction) => {
let { url } = ctx;
url = url.replace(this.prefix, '').split('?')[0];
if (_.startsWith(url, '/app/')) {
const token = ctx.get('Authorization');
try {
ctx.user = jwt.verify(token, this.jwtConfig.secret);
if (ctx.user.isRefresh) {
ctx.status = 401;
ctx.body = {
code: RESCODE.COMMFAIL,
message: '登录失效~',
};
return;
}
} catch (error) {}
// 使用matchUrl方法来检查URL是否应该被忽略
const isIgnored = this.ignoreUrls.some(pattern =>
this.matchUrl(pattern, url)
);
if (isIgnored) {
await next();
return;
} else {
if (!ctx.user) {
ctx.status = 401;
ctx.body = {
code: RESCODE.COMMFAIL,
message: '登录失效~',
};
return;
}
}
}
await next();
};
}
// 匹配URL的方法
matchUrl(pattern, url) {
const patternSegments = pattern.split('/').filter(Boolean);
const urlSegments = url.split('/').filter(Boolean);
// 如果段的数量不同,则无法匹配
if (patternSegments.length !== urlSegments.length) {
return false;
}
// 逐段进行匹配
for (let i = 0; i < patternSegments.length; i++) {
if (patternSegments[i].startsWith(':')) {
// 如果模式段以':'开始,我们认为它是一个参数,可以匹配任何内容
continue;
}
// 如果两个段不相同,则不匹配
if (patternSegments[i] !== urlSegments[i]) {
return false;
}
}
// 所有段都匹配
return true;
}
}

View File

@ -0,0 +1,63 @@
import { Init, Inject, Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import { Equal, Repository } from 'typeorm';
import { UserAddressEntity } from '../entity/address';
import { InjectEntityModel } from '@midwayjs/typeorm';
/**
*
*/
@Provide()
export class UserAddressService extends BaseService {
@InjectEntityModel(UserAddressEntity)
userAddressEntity: Repository<UserAddressEntity>;
@Inject()
ctx;
@Init()
async init() {
await super.init();
this.setEntity(this.userAddressEntity);
}
/**
*
*/
async list() {
return this.userAddressEntity
.createQueryBuilder()
.where('userId = :userId ', { userId: this.ctx.user.id })
.addOrderBy('isDefault', 'DESC')
.getMany();
}
/**
*
* @param data
* @param type
*/
async modifyAfter(data: any, type: 'add' | 'update' | 'delete') {
if (type == 'add' || type == 'update') {
if (data.isDefault) {
await this.userAddressEntity
.createQueryBuilder()
.update()
.set({ isDefault: false })
.where('userId = :userId ', { userId: this.ctx.user.id })
.andWhere('id != :id', { id: data.id })
.execute();
}
}
}
/**
*
*/
async default(userId) {
return await this.userAddressEntity.findOneBy({
userId: Equal(userId),
isDefault: true,
});
}
}

View File

@ -0,0 +1,124 @@
import { BaseService, CoolCommException } from '@cool-midway/core';
import { Inject, Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import * as md5 from 'md5';
import { Equal, Repository } from 'typeorm';
import { v1 as uuid } from 'uuid';
import { PluginService } from '../../plugin/service/info';
import { UserInfoEntity } from '../entity/info';
import { UserSmsService } from './sms';
import { UserWxService } from './wx';
/**
*
*/
@Provide()
export class UserInfoService extends BaseService {
@InjectEntityModel(UserInfoEntity)
userInfoEntity: Repository<UserInfoEntity>;
@Inject()
pluginService: PluginService;
@Inject()
userSmsService: UserSmsService;
@Inject()
userWxService: UserWxService;
/**
*
* @param userId
* @param code
* @param encryptedData
* @param iv
*/
async miniPhone(userId: number, code: any, encryptedData: any, iv: any) {
const phone = await this.userWxService.miniPhone(code, encryptedData, iv);
await this.userInfoEntity.update({ id: Equal(userId) }, { phone });
return phone;
}
/**
*
* @param id
* @returns
*/
async person(id) {
const info = await this.userInfoEntity.findOneBy({ id: Equal(id) });
delete info.password;
return info;
}
/**
*
* @param userId
*/
async logoff(userId: number) {
await this.userInfoEntity.update(
{ id: userId },
{
status: 2,
phone: null,
unionid: null,
nickName: `已注销-00${userId}`,
avatarUrl: null,
}
);
}
/**
*
* @param id
* @param param
* @returns
*/
async updatePerson(id, param) {
const info = await this.person(id);
if (!info) throw new CoolCommException('用户不存在');
try {
// 修改了头像要重新处理
if (param.avatarUrl && info.avatarUrl != param.avatarUrl) {
const file = await this.pluginService.getInstance('upload');
param.avatarUrl = await file.downAndUpload(
param.avatarUrl,
uuid() + '.png'
);
}
} catch (err) {}
try {
return await this.userInfoEntity.update({ id }, param);
} catch (err) {
throw new CoolCommException('更新失败,参数错误或者手机号已存在');
}
}
/**
*
* @param userId
* @param password
* @param
*/
async updatePassword(userId, password, code) {
const user = await this.userInfoEntity.findOneBy({ id: userId });
const check = await this.userSmsService.checkCode(user.phone, code);
if (!check) {
throw new CoolCommException('验证码错误');
}
await this.userInfoEntity.update(user.id, { password: md5(password) });
}
/**
*
* @param userId
* @param phone
* @param code
*/
async bindPhone(userId, phone, code) {
const check = await this.userSmsService.checkCode(phone, code);
if (!check) {
throw new CoolCommException('验证码错误');
}
await this.userInfoEntity.update({ id: userId }, { phone });
}
}

View File

@ -0,0 +1,307 @@
import { Config, Inject, Provide } from '@midwayjs/core';
import { BaseService, CoolCommException } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Equal, Repository } from 'typeorm';
import { UserInfoEntity } from '../entity/info';
import { UserWxService } from './wx';
import * as jwt from 'jsonwebtoken';
import { UserWxEntity } from '../entity/wx';
import { BaseSysLoginService } from '../../base/service/sys/login';
import { UserSmsService } from './sms';
import { v1 as uuid } from 'uuid';
import * as md5 from 'md5';
import { PluginService } from '../../plugin/service/info';
/**
*
*/
@Provide()
export class UserLoginService extends BaseService {
@InjectEntityModel(UserInfoEntity)
userInfoEntity: Repository<UserInfoEntity>;
@InjectEntityModel(UserWxEntity)
userWxEntity: Repository<UserWxEntity>;
@Inject()
userWxService: UserWxService;
@Config('module.user.jwt')
jwtConfig;
@Inject()
baseSysLoginService: BaseSysLoginService;
@Inject()
pluginService: PluginService;
@Inject()
userSmsService: UserSmsService;
/**
*
* @param phone
* @param captchaId
* @param code
*/
async smsCode(phone, captchaId, code) {
// 1、检查图片验证码 2、发送短信验证码
const check = await this.baseSysLoginService.captchaCheck(captchaId, code);
if (!check) {
throw new CoolCommException('图片验证码错误');
}
await this.userSmsService.sendSms(phone);
}
/**
*
* @param phone
* @param smsCode
*/
async phoneVerifyCode(phone, smsCode) {
// 1、检查短信验证码 2、登录
const check = await this.userSmsService.checkCode(phone, smsCode);
if (check) {
return await this.phone(phone);
} else {
throw new CoolCommException('验证码错误');
}
}
/**
*
* @param code
* @param encryptedData
* @param iv
*/
async miniPhone(code, encryptedData, iv) {
const phone = await this.userWxService.miniPhone(code, encryptedData, iv);
if (phone) {
return await this.phone(phone);
} else {
throw new CoolCommException('获得手机号失败,请检查配置');
}
}
/**
*
* @param access_token
* @param openid
*/
async uniPhone(access_token, openid, appId) {
const instance: any = await this.pluginService.getInstance('uniphone');
const phone = await instance.getPhone(access_token, openid, appId);
if (phone) {
return await this.phone(phone);
} else {
throw new CoolCommException('获得手机号失败,请检查配置');
}
}
/**
*
* @param phone
* @returns
*/
async phone(phone: string) {
let user: any = await this.userInfoEntity.findOneBy({
phone: Equal(phone),
});
if (!user) {
user = {
phone,
unionid: phone,
loginType: 2,
nickName: phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2'),
};
await this.userInfoEntity.insert(user);
}
return this.token({ id: user.id });
}
/**
*
* @param code
*/
async mp(code: string) {
let wxUserInfo = await this.userWxService.mpUserInfo(code);
if (wxUserInfo) {
delete wxUserInfo.privilege;
wxUserInfo = await this.saveWxInfo(
{
openid: wxUserInfo.openid,
unionid: wxUserInfo.unionid,
avatarUrl: wxUserInfo.headimgurl,
nickName: wxUserInfo.nickname,
gender: wxUserInfo.sex,
city: wxUserInfo.city,
province: wxUserInfo.province,
country: wxUserInfo.country,
},
1
);
return this.wxLoginToken(wxUserInfo);
} else {
throw new Error('微信登录失败');
}
}
/**
* APP授权登录
* @param code
*/
async wxApp(code: string) {
let wxUserInfo = await this.userWxService.appUserInfo(code);
if (wxUserInfo) {
delete wxUserInfo.privilege;
wxUserInfo = await this.saveWxInfo(
{
openid: wxUserInfo.openid,
unionid: wxUserInfo.unionid,
avatarUrl: wxUserInfo.headimgurl,
nickName: wxUserInfo.nickname,
gender: wxUserInfo.sex,
city: wxUserInfo.city,
province: wxUserInfo.province,
country: wxUserInfo.country,
},
1
);
return this.wxLoginToken(wxUserInfo);
} else {
throw new Error('微信登录失败');
}
}
/**
*
* @param wxUserInfo
* @param type
* @returns
*/
async saveWxInfo(wxUserInfo, type) {
const find: any = { openid: wxUserInfo.openid };
let wxInfo: any = await this.userWxEntity.findOneBy(find);
if (wxInfo) {
wxUserInfo.id = wxInfo.id;
}
await this.userWxEntity.save({
...wxUserInfo,
type,
});
return wxUserInfo;
}
/**
*
* @param code
* @param encryptedData
* @param iv
*/
async mini(code, encryptedData, iv) {
let wxUserInfo = await this.userWxService.miniUserInfo(
code,
encryptedData,
iv
);
if (wxUserInfo) {
// 保存
wxUserInfo = await this.saveWxInfo(wxUserInfo, 0);
return await this.wxLoginToken(wxUserInfo);
}
}
/**
* token
* @param wxUserInfo
* @returns
*/
async wxLoginToken(wxUserInfo) {
const unionid = wxUserInfo.unionid ? wxUserInfo.unionid : wxUserInfo.openid;
let userInfo: any = await this.userInfoEntity.findOneBy({ unionid });
if (!userInfo) {
const file = await this.pluginService.getInstance('upload');
const avatarUrl = await file.downAndUpload(
wxUserInfo.avatarUrl,
uuid() + '.png'
);
userInfo = {
unionid,
nickName: wxUserInfo.nickName,
avatarUrl,
gender: wxUserInfo.gender,
};
await this.userInfoEntity.insert(userInfo);
}
return this.token({ id: userInfo.id });
}
/**
* token
* @param refreshToken
*/
async refreshToken(refreshToken) {
try {
const info = jwt.verify(refreshToken, this.jwtConfig.secret);
if (!info['isRefresh']) {
throw new CoolCommException('token类型非refreshToken');
}
const userInfo = await this.userInfoEntity.findOneBy({
id: info['id'],
});
return this.token({ id: userInfo.id });
} catch (e) {
throw new CoolCommException(
'刷新token失败请检查refreshToken是否正确或过期'
);
}
}
/**
*
* @param phone
* @param password
*/
async password(phone, password) {
const user = await this.userInfoEntity.findOneBy({ phone });
if (user && user.password == md5(password)) {
return this.token({
id: user.id,
});
} else {
throw new CoolCommException('账号或密码错误');
}
}
/**
* token
* @param info
* @returns
*/
async token(info) {
const { expire, refreshExpire } = this.jwtConfig;
return {
expire,
token: await this.generateToken(info),
refreshExpire,
refreshToken: await this.generateToken(info, true),
};
}
/**
* token
* @param tokenInfo
* @param roleIds
*/
async generateToken(info, isRefresh = false) {
const { expire, refreshExpire, secret } = this.jwtConfig;
const tokenInfo = {
isRefresh,
...info,
};
return jwt.sign(tokenInfo, secret, {
expiresIn: isRefresh ? refreshExpire : expire,
});
}
}

View File

@ -0,0 +1,79 @@
import { Provide, Config, Inject, Init, InjectClient } from '@midwayjs/core';
import { BaseService, CoolCommException } from '@cool-midway/core';
import * as _ from 'lodash';
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
import { PluginService } from '../../plugin/service/info';
/**
*
*/
@Provide()
export class UserSmsService extends BaseService {
// 获得模块的配置信息
@Config('module.user.sms')
config;
@InjectClient(CachingFactory, 'default')
midwayCache: MidwayCache;
@Inject()
pluginService: PluginService;
plugin;
@Init()
async init() {
for (const key of ['sms-tx', 'sms-ali']) {
try {
this.plugin = await this.pluginService.getInstance(key);
if (this.plugin) {
this.config.pluginKey = key;
break;
}
} catch (e) {
continue;
}
}
}
/**
*
* @param phone
*/
async sendSms(phone) {
// 随机四位验证码
const code = _.random(1000, 9999);
const pluginKey = this.config.pluginKey;
if (!this.plugin)
throw new CoolCommException(
'未配置短信插件请到插件市场下载安装配置https://cool-js.com/plugin?keyWord=短信'
);
try {
if (pluginKey == 'sms-tx') {
await this.plugin.send([phone], [code]);
}
if (pluginKey == 'sms-ali') {
await this.plugin.send([phone], {
code,
});
}
this.midwayCache.set(`sms:${phone}`, code, this.config.timeout * 1000);
} catch (error) {
throw new CoolCommException('发送过于频繁,请稍后再试');
}
}
/**
*
* @param phone
* @param code
* @returns
*/
async checkCode(phone, code) {
const cacheCode = await this.midwayCache.get(`sms:${phone}`);
if (code && cacheCode == code) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,281 @@
import { BaseService, CoolCommException } from '@cool-midway/core';
import { Config, Inject, Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import axios from 'axios';
import * as crypto from 'crypto';
import * as moment from 'moment';
import { Equal, Repository } from 'typeorm';
import { v1 as uuid } from 'uuid';
import { PluginService } from '../../plugin/service/info';
import { UserInfoEntity } from '../entity/info';
import { UserWxEntity } from '../entity/wx';
/**
*
*/
@Provide()
export class UserWxService extends BaseService {
@Config('module.user')
config;
@InjectEntityModel(UserInfoEntity)
userInfoEntity: Repository<UserInfoEntity>;
@InjectEntityModel(UserWxEntity)
userWxEntity: Repository<UserWxEntity>;
@Inject()
pluginService: PluginService;
/**
*
* @returns
*/
async getPlugin() {
try {
const wxPlugin: any = await this.pluginService.getInstance('wx');
return wxPlugin;
} catch (error) {
throw new CoolCommException(
'未配置微信插件请到插件市场下载安装配置https://cool-js.com/plugin/70'
);
}
}
/**
*
* @returns
*/
async getMiniApp() {
const wxPlugin: any = await this.getPlugin();
return wxPlugin.MiniApp();
}
/**
*
* @returns
*/
async getOfficialAccount() {
const wxPlugin: any = await this.getPlugin();
return wxPlugin.OfficialAccount();
}
/**
* App实例
* @returns
*/
async getOpenPlatform() {
const wxPlugin: any = await this.getPlugin();
return wxPlugin.OpenPlatform();
}
/**
* openId
* @param userId
* @param type 0- 1- 2-App
*/
async getOpenid(userId: number, type = 0) {
const user = await this.userInfoEntity.findOneBy({
id: Equal(userId),
status: 1,
});
if (!user) {
throw new CoolCommException('用户不存在或已被禁用');
}
const wx = await this.userWxEntity
.createQueryBuilder('a')
.where('a.type = :type', { type })
.andWhere('(a.unionid = :unionid or a.openid =:openid )', {
unionid: user.unionid,
openid: user.unionid,
})
.getOne();
return wx ? wx.openid : null;
}
/**
*
* @param appId
* @param appSecret
* @param url URL#(JS接口页面的完整URL)
*/
public async getWxMpConfig(url: string) {
const token = await this.getWxToken();
const ticket = await axios.get(
'https://api.weixin.qq.com/cgi-bin/ticket/getticket',
{
params: {
access_token: token,
type: 'jsapi',
},
}
);
const account = (await this.getOfficialAccount()).getAccount();
const appid = account.getAppId();
// 返回结果集
const result = {
timestamp: parseInt(moment().valueOf() / 1000 + ''),
nonceStr: uuid(),
appId: appid, //appid
signature: '',
};
const signArr = [];
signArr.push('jsapi_ticket=' + ticket.data.ticket);
signArr.push('noncestr=' + result.nonceStr);
signArr.push('timestamp=' + result.timestamp);
signArr.push('url=' + decodeURI(url));
// 敏感信息加密处理
result.signature = crypto
.createHash('sha1')
.update(signArr.join('&'))
.digest('hex')
.toUpperCase();
return result;
}
/**
*
* @param code
*/
async mpUserInfo(code) {
const token = await this.openOrMpToken(code, 'mp');
return await this.openOrMpUserInfo(token);
}
/**
* app用户信息
* @param code
*/
async appUserInfo(code) {
const token = await this.openOrMpToken(code, 'open');
return await this.openOrMpUserInfo(token);
}
/**
* token code
* @param appid
* @param secret
*/
public async getWxToken(type = 'mp') {
let app;
if (type == 'mp') {
app = await this.getOfficialAccount();
} else {
app = await this.getOpenPlatform();
}
return await app.getAccessToken().getToken();
}
/**
*
* @param token
*/
async openOrMpUserInfo(token) {
return await axios
.get('https://api.weixin.qq.com/sns/userinfo', {
params: {
access_token: token.access_token,
openid: token.openid,
lang: 'zh_CN',
},
})
.then(res => {
return res.data;
});
}
/**
* token嗯
* @param code
* @param type
*/
async openOrMpToken(code, type = 'mp') {
const account =
type == 'mp'
? (await this.getOfficialAccount()).getAccount()
: (await this.getMiniApp()).getAccount();
const result = await axios.get(
'https://api.weixin.qq.com/sns/oauth2/access_token',
{
params: {
appid: account.getAppId(),
secret: account.getSecret(),
code,
grant_type: 'authorization_code',
},
}
);
return result.data;
}
/**
* session
* @param code code
* @param conf
*/
async miniSession(code) {
const app = await this.getMiniApp();
const utils = app.getUtils();
const result = await utils.codeToSession(code);
return result;
}
/**
*
* @param code
* @param encryptedData
* @param iv
*/
async miniUserInfo(code, encryptedData, iv) {
const session = await this.miniSession(code);
if (session.errcode) {
throw new CoolCommException('登录失败,请重试');
}
const info: any = await this.miniDecryptData(
encryptedData,
iv,
session.session_key
);
if (info) {
delete info['watermark'];
return {
...info,
openid: session['openid'],
unionid: session['unionid'],
};
}
return null;
}
/**
*
* @param code
* @param encryptedData
* @param iv
*/
async miniPhone(code, encryptedData, iv) {
const session = await this.miniSession(code);
if (session.errcode) {
throw new CoolCommException('获取手机号失败,请刷新重试');
}
const result = await this.miniDecryptData(
encryptedData,
iv,
session.session_key
);
return result.phoneNumber;
}
/**
*
* @param encryptedData
* @param iv
* @param sessionKey
*/
async miniDecryptData(encryptedData, iv, sessionKey) {
const app = await this.getMiniApp();
const utils = app.getUtils();
return await utils.decryptSession(sessionKey, iv, encryptedData);
}
}

96
typings/feishu.d.ts vendored Normal file
View File

@ -0,0 +1,96 @@
import { BasePlugin } from '@cool-midway/plugin-cli';
import axios from 'axios';
interface Message {
/** 类型 */
msg_type:
| 'text'
| 'post'
| 'image'
| 'file'
| 'audio'
| 'media'
| 'sticker'
| 'interactive'
| 'share_chat'
| 'share_user';
/** 内容 */
content: any;
}
/**
*
*/
export declare class CoolPlugin extends BasePlugin {
/**
* webHook消息
* @param message
* @returns
*/
sendByHook(message: Message): Promise<axios.AxiosResponse<any, any>>;
/**
*
* @param message
* @param options receive_id receive_id_type message_id ID uuid ID
* @returns
*/
sendByApp(
message: Message,
options: {
receive_id: string;
receive_id_type: 'open_id' | 'user_id' | 'chat_id' | 'union_id' | 'email';
message_id?: string;
uuid?: string;
},
): Promise<axios.AxiosResponse<any, any>>;
/**
*
* @param filePath
* @param image_type message avatar
* @returns
*/
uploadImage(
filePath: string,
image_type?: 'message' | 'avatar',
): Promise<any>;
/**
*
* @param filePath
* @param file_type
* @returns
*/
uploadFile(
filePath: any,
file_type?: 'opus' | 'mp4' | 'pdf' | 'doc' | 'xls' | 'ppt' | 'stream',
): Promise<any>;
/**
*
* @returns
*/
chatList(): Promise<any>;
/**
*
* @param options emails mobiles include_resigned
* @returns
*/
getUserInfos(options: {
emails?: string[];
mobiles?: string[];
include_resigned?: boolean;
}): Promise<any>;
/**
* 使 ID
* @param options emails mobiles include_resigned
* @returns
*/
getUserIds(options: {
emails?: string[];
mobiles?: string[];
include_resigned?: boolean;
}): Promise<any>;
/**
* token
* @returns
*/
getToken(): Promise<any>;
}
export declare const Plugin: typeof CoolPlugin;
export {};

2
typings/plugin.d.ts vendored
View File

@ -1,3 +1,4 @@
import * as feishu from './feishu';
import { BaseUpload, MODETYPE } from './upload'; import { BaseUpload, MODETYPE } from './upload';
type AnyString = string & {}; type AnyString = string & {};
/** /**
@ -5,4 +6,5 @@ type AnyString = string & {};
*/ */
interface PluginMap { interface PluginMap {
upload: BaseUpload; upload: BaseUpload;
feishu: feishu.CoolPlugin;
} }