mirror of
https://github.com/cool-team-official/cool-admin-midway.git
synced 2026-01-25 08:38:15 +00:00
新增了一个不依赖redis,cluster模式下可用的本地任务
This commit is contained in:
parent
3392f0a307
commit
6adadc5dc3
1
bootstrap.js
vendored
1
bootstrap.js
vendored
@ -1,3 +1,4 @@
|
||||
process.env.NODE_ENV = 'local';
|
||||
const { Bootstrap } = require('@midwayjs/bootstrap');
|
||||
|
||||
// 显式以组件方式引入用户代码
|
||||
|
||||
44
package.json
44
package.json
@ -5,26 +5,30 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@cool-midway/core": "file:/Users/ap/Documents/src/admin/midway-packages/core",
|
||||
"@midwayjs/bootstrap": "^3.19.3",
|
||||
"@midwayjs/cache-manager": "^3.19.3",
|
||||
"@midwayjs/core": "^3.19.0",
|
||||
"@midwayjs/cron": "^3.19.2",
|
||||
"@midwayjs/cross-domain": "^3.19.3",
|
||||
"@midwayjs/info": "^3.19.2",
|
||||
"@midwayjs/koa": "^3.19.2",
|
||||
"@cool-midway/task": "file:/Users/ap/Documents/src/admin/midway-packages/task",
|
||||
"@midwayjs/bootstrap": "^3.20.0",
|
||||
"@midwayjs/cache-manager": "^3.20.0",
|
||||
"@midwayjs/core": "^3.20.0",
|
||||
"@midwayjs/cron": "^3.20.0",
|
||||
"@midwayjs/cross-domain": "^3.20.0",
|
||||
"@midwayjs/info": "^3.20.0",
|
||||
"@midwayjs/koa": "^3.20.0",
|
||||
"@midwayjs/logger": "^3.4.2",
|
||||
"@midwayjs/static-file": "^3.19.3",
|
||||
"@midwayjs/typeorm": "^3.19.2",
|
||||
"@midwayjs/upload": "^3.19.3",
|
||||
"@midwayjs/validate": "^3.19.2",
|
||||
"@midwayjs/view-ejs": "^3.19.2",
|
||||
"@midwayjs/static-file": "^3.20.0",
|
||||
"@midwayjs/typeorm": "^3.20.0",
|
||||
"@midwayjs/upload": "^3.20.0",
|
||||
"@midwayjs/validate": "^3.20.0",
|
||||
"@midwayjs/view-ejs": "^3.20.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.9",
|
||||
"cron": "^3.5.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"md5": "^2.3.0",
|
||||
"moment": "^2.30.1",
|
||||
"mysql2": "^3.12.0",
|
||||
"sharp": "0.32.6",
|
||||
"sharp": "0.33.5",
|
||||
"sqlite3": "^5.1.7",
|
||||
"svg-captcha": "^1.4.0",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^11.0.5",
|
||||
@ -32,7 +36,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@midwayjs/bundle-helper": "^1.3.0",
|
||||
"@midwayjs/mock": "^3.19.2",
|
||||
"@midwayjs/mock": "^3.20.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "22",
|
||||
"cross-env": "^7.0.3",
|
||||
@ -40,7 +44,7 @@
|
||||
"mwts": "^1.3.0",
|
||||
"mwtsc": "^1.15.1",
|
||||
"pkg": "^5.8.1",
|
||||
"rimraf": "^5.0.5",
|
||||
"rimraf": "^6.0.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "~5.7.3"
|
||||
},
|
||||
@ -49,7 +53,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"cov": "jest --coverage",
|
||||
"lint": "mwts check",
|
||||
@ -63,15 +67,17 @@
|
||||
},
|
||||
"bin": "./bootstrap.js",
|
||||
"pkg": {
|
||||
"scripts": "dist/**/*",
|
||||
"scripts": [
|
||||
"dist/**/*",
|
||||
"node_modules/axios/dist/node/*"
|
||||
],
|
||||
"assets": [
|
||||
"public/**/*",
|
||||
"typings/**/*",
|
||||
"cool/**/*"
|
||||
],
|
||||
"targets": [
|
||||
"node18-macos-x64",
|
||||
"node18-win-x64"
|
||||
"node18-macos-x64"
|
||||
],
|
||||
"outputPath": "build"
|
||||
},
|
||||
|
||||
1232
pnpm-lock.yaml
generated
1232
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
58
src/comm/path.ts
Normal file
58
src/comm/path.ts
Normal 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');
|
||||
};
|
||||
@ -2,13 +2,13 @@ import { CoolConfig } from '@cool-midway/core';
|
||||
import { MidwayConfig } from '@midwayjs/core';
|
||||
import { CoolCacheStore } from '@cool-midway/core';
|
||||
import * as path from 'path';
|
||||
import { getUploadDir } from '../modules/plugin/hooks/upload';
|
||||
import { pUploadPath } from '../comm/path';
|
||||
|
||||
// redis缓存
|
||||
// import { redisStore } from 'cache-manager-ioredis-yet';
|
||||
|
||||
export default {
|
||||
// use for cookie sign key, should change to your own and keep security
|
||||
// 确保每个项目唯一,项目首次启动会自动生成
|
||||
keys: '576848ea-bb0c-4c0c-ac95-c8602ef908b5',
|
||||
koa: {
|
||||
port: 8001,
|
||||
@ -18,12 +18,12 @@ export default {
|
||||
buffer: true,
|
||||
dirs: {
|
||||
default: {
|
||||
prefix: '/public',
|
||||
prefix: '/',
|
||||
dir: path.join(__dirname, '..', '..', 'public'),
|
||||
},
|
||||
static: {
|
||||
prefix: '/upload',
|
||||
dir: getUploadDir(),
|
||||
dir: pUploadPath(),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -61,6 +61,13 @@ export default {
|
||||
cool: {
|
||||
// 已经插件化,本地文件上传查看 plugin/config.ts,其他云存储查看对应插件的使用
|
||||
file: {},
|
||||
// redis配置
|
||||
redis: {
|
||||
port: 6379,
|
||||
host: '127.0.0.1',
|
||||
password: '',
|
||||
db: 0,
|
||||
},
|
||||
// crud配置
|
||||
crud: {
|
||||
// 插入模式,save不会校验字段(允许传入不存在的字段),insert会校验字段
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { CoolConfig } from '@cool-midway/core';
|
||||
import { MidwayConfig } from '@midwayjs/core';
|
||||
import { pSqlitePath } from '../comm/path';
|
||||
import { entities } from '../entities';
|
||||
|
||||
/**
|
||||
* 本地开发 npm run dev 读取的配置文件
|
||||
@ -8,26 +10,16 @@ export default {
|
||||
typeorm: {
|
||||
dataSource: {
|
||||
default: {
|
||||
type: 'mysql',
|
||||
host: '192.168.0.119',
|
||||
port: 3306,
|
||||
username: 'root',
|
||||
password: '123456',
|
||||
database: 'cool',
|
||||
type: 'sqlite',
|
||||
// 数据库文件地址
|
||||
database: pSqlitePath(),
|
||||
// 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
|
||||
synchronize: true,
|
||||
// 打印日志
|
||||
logging: false,
|
||||
// 字符集
|
||||
charset: 'utf8mb4',
|
||||
// 是否开启缓存
|
||||
cache: true,
|
||||
logging: true,
|
||||
// 实体路径
|
||||
entities: ['**/modules/*/entity'],
|
||||
entities,
|
||||
// 扩展配置
|
||||
extra: {
|
||||
keepAliveInitialDelay: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { CoolConfig } from '@cool-midway/core';
|
||||
import { MidwayConfig } from '@midwayjs/core';
|
||||
import { entities } from '../entities';
|
||||
import { pSqlitePath } from '../comm/path';
|
||||
/**
|
||||
* 本地开发 npm run prod 读取的配置文件
|
||||
*/
|
||||
@ -8,31 +9,26 @@ export default {
|
||||
typeorm: {
|
||||
dataSource: {
|
||||
default: {
|
||||
type: 'mysql',
|
||||
host: '192.168.0.119',
|
||||
port: 3306,
|
||||
username: 'root',
|
||||
password: '123456',
|
||||
database: 'cool',
|
||||
type: 'sqlite',
|
||||
// 数据库文件地址
|
||||
database: pSqlitePath(),
|
||||
// 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
|
||||
synchronize: false,
|
||||
// 打印日志
|
||||
logging: false,
|
||||
// 字符集
|
||||
charset: 'utf8mb4',
|
||||
// 是否开启缓存
|
||||
cache: true,
|
||||
// 实体路径
|
||||
entities,
|
||||
// 扩展配置
|
||||
extra: {
|
||||
keepAliveInitialDelay: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cool: {
|
||||
// 是否自动导入数据库,生产环境不建议开,用本地的数据库手动初始化
|
||||
initDB: false,
|
||||
// 实体与路径,跟生成代码、前端请求、swagger文档相关 注意:线上不建议开启,以免暴露敏感信息
|
||||
eps: false,
|
||||
// 是否自动导入模块数据库
|
||||
initDB: true,
|
||||
// 判断是否初始化的方式
|
||||
initJudge: 'db',
|
||||
// 是否自动导入模块菜单
|
||||
initMenu: true,
|
||||
} as CoolConfig,
|
||||
} as MidwayConfig;
|
||||
|
||||
@ -18,7 +18,7 @@ import * as LocalConfig from './config/config.local';
|
||||
import * as ProdConfig from './config/config.prod';
|
||||
import * as cool from '@cool-midway/core';
|
||||
import * as upload from '@midwayjs/upload';
|
||||
import * as os from 'os';
|
||||
import * as task from '@cool-midway/task';
|
||||
|
||||
@Configuration({
|
||||
imports: [
|
||||
@ -38,6 +38,8 @@ import * as os from 'os';
|
||||
upload,
|
||||
// cool-admin 官方组件 https://cool-js.com
|
||||
cool,
|
||||
// 任务与队列
|
||||
// task,
|
||||
{
|
||||
component: info,
|
||||
enabledEnvironment: ['local', 'prod'],
|
||||
|
||||
@ -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
107
src/index.ts
Normal 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';
|
||||
@ -86,7 +86,6 @@
|
||||
"password": "e10adc3949ba59abbe56e057f20f883e",
|
||||
"passwordV": 7,
|
||||
"nickName": "管理员",
|
||||
"headImg": "https://cool-js.com/admin/headimg.jpg",
|
||||
"phone": "18000000000",
|
||||
"email": "team@cool-js.com",
|
||||
"status": 1,
|
||||
|
||||
19
src/modules/demo/config.ts
Normal file
19
src/modules/demo/config.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { ModuleConfig } from '@cool-midway/core';
|
||||
|
||||
/**
|
||||
* 模块配置
|
||||
*/
|
||||
export default () => {
|
||||
return {
|
||||
// 模块名称
|
||||
name: 'demo模块',
|
||||
// 模块描述
|
||||
description: '演示用',
|
||||
// 中间件,只对本模块有效
|
||||
middlewares: [],
|
||||
// 中间件,全局有效
|
||||
globalMiddlewares: [],
|
||||
// 模块加载顺序,默认为0,值越大越优先加载
|
||||
order: 0,
|
||||
} as ModuleConfig;
|
||||
};
|
||||
25
src/modules/demo/controller/open/plugin.ts
Normal file
25
src/modules/demo/controller/open/plugin.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -36,6 +36,18 @@ export class PluginInfoEntity extends BaseEntity {
|
||||
@Column({ comment: '状态 0-禁用 1-启用', default: 0 })
|
||||
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 })
|
||||
pluginJson: any;
|
||||
|
||||
|
||||
@ -6,17 +6,7 @@ import * as moment from 'moment';
|
||||
import { v1 as uuid } from 'uuid';
|
||||
import { CoolCommException } from '@cool-midway/core';
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
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;
|
||||
};
|
||||
import { pUploadPath } from '../../../../comm/path';
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
@ -56,7 +46,7 @@ export class CoolPlugin extends BasePluginHook implements BaseUpload {
|
||||
? await download(url)
|
||||
: fs.readFileSync(url);
|
||||
// 创建文件夹
|
||||
const dirPath = path.join(getUploadDir(), `${moment().format('YYYYMMDD')}`);
|
||||
const dirPath = path.join(pUploadPath(), `${moment().format('YYYYMMDD')}`);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
@ -95,7 +85,7 @@ export class CoolPlugin extends BasePluginHook implements BaseUpload {
|
||||
if (_.isEmpty(ctx.files)) {
|
||||
throw new CoolCommException('上传文件为空');
|
||||
}
|
||||
const basePath = getUploadDir();
|
||||
const basePath = pUploadPath();
|
||||
|
||||
const file = ctx.files[0];
|
||||
const extension = file.filename.split('.').pop();
|
||||
|
||||
@ -29,6 +29,7 @@ import { PluginMap, AnyString } from '../../../../typings/plugin';
|
||||
import { PluginTypesService } from './types';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { pPluginPath } from '../../../comm/path';
|
||||
/**
|
||||
* 插件信息
|
||||
*/
|
||||
@ -254,38 +255,42 @@ export class PluginService extends BaseService {
|
||||
tsContent: string;
|
||||
errorData: string;
|
||||
}> {
|
||||
const decompress = require('decompress');
|
||||
const files = await decompress(filePath);
|
||||
const AdmZip = require('adm-zip');
|
||||
const zip = new AdmZip(filePath);
|
||||
const files = zip.getEntries();
|
||||
let errorData;
|
||||
let pluginJson: PluginInfo,
|
||||
readme: string,
|
||||
logo: string,
|
||||
content: string,
|
||||
tsContent: string;
|
||||
|
||||
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';
|
||||
pluginJson = JSON.parse(
|
||||
_.find(files, { path: 'plugin.json', type: 'file' }).data.toString()
|
||||
);
|
||||
pluginJson = JSON.parse(getFileContent('plugin.json'));
|
||||
|
||||
errorData = 'readme';
|
||||
readme = _.find(files, {
|
||||
path: pluginJson.readme,
|
||||
type: 'file',
|
||||
}).data.toString();
|
||||
readme = getFileContent(pluginJson.readme);
|
||||
|
||||
errorData = 'logo';
|
||||
logo = _.find(files, {
|
||||
path: pluginJson.logo,
|
||||
type: 'file',
|
||||
}).data.toString('base64');
|
||||
content = _.find(files, {
|
||||
path: 'src/index.js',
|
||||
type: 'file',
|
||||
}).data.toString();
|
||||
tsContent =
|
||||
_.find(files, {
|
||||
path: 'source/index.ts',
|
||||
type: 'file',
|
||||
})?.data?.toString() || '';
|
||||
logo = getFileContent(pluginJson.logo, 'base64');
|
||||
|
||||
errorData = 'content';
|
||||
content = getFileContent('src/index.js');
|
||||
|
||||
tsContent = getFileContent('source/index.ts');
|
||||
} catch (e) {
|
||||
throw new CoolCommException('插件信息不完整');
|
||||
}
|
||||
@ -328,6 +333,14 @@ export class PluginService extends BaseService {
|
||||
hook: pluginJson.hook,
|
||||
readme,
|
||||
logo,
|
||||
content: {
|
||||
type: 'comm',
|
||||
data: content,
|
||||
},
|
||||
tsContent: {
|
||||
type: 'ts',
|
||||
data: tsContent,
|
||||
},
|
||||
description: pluginJson.description,
|
||||
pluginJson,
|
||||
config: pluginJson.config,
|
||||
@ -411,10 +424,30 @@ export class PluginService extends BaseService {
|
||||
}> {
|
||||
const filePath = this.pluginPath(keyName);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
this.logger.warn(
|
||||
`插件[${keyName}]文件不存在,请卸载后重新安装: ${filePath}`
|
||||
);
|
||||
return null;
|
||||
// 尝试从数据库中获取
|
||||
const info = await this.pluginInfoEntity.findOne({
|
||||
where: { keyName: Equal(keyName) },
|
||||
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'));
|
||||
}
|
||||
@ -436,12 +469,6 @@ export class PluginService extends BaseService {
|
||||
* @returns
|
||||
*/
|
||||
pluginPath(keyName: string) {
|
||||
return path.join(
|
||||
this.app.getBaseDir(),
|
||||
'..',
|
||||
'cool',
|
||||
'plugin',
|
||||
`${keyName}.cool`
|
||||
);
|
||||
return path.join(pPluginPath(), `${keyName}`);
|
||||
}
|
||||
}
|
||||
|
||||
23
src/modules/task/config.ts
Normal file
23
src/modules/task/config.ts
Normal 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;
|
||||
};
|
||||
59
src/modules/task/controller/admin/info.ts
Normal file
59
src/modules/task/controller/admin/info.ts
Normal 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
40
src/modules/task/db.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
62
src/modules/task/entity/info.ts
Normal file
62
src/modules/task/entity/info.ts
Normal 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;
|
||||
}
|
||||
18
src/modules/task/entity/log.ts
Normal file
18
src/modules/task/entity/log.ts
Normal 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;
|
||||
}
|
||||
17
src/modules/task/event/app.ts
Normal file
17
src/modules/task/event/app.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
38
src/modules/task/middleware/task.ts
Normal file
38
src/modules/task/middleware/task.ts
Normal 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();
|
||||
};
|
||||
}
|
||||
}
|
||||
30
src/modules/task/queue/task.ts
Normal file
30
src/modules/task/queue/task.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
339
src/modules/task/service/bull.ts
Normal file
339
src/modules/task/service/bull.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/modules/task/service/demo.ts
Normal file
19
src/modules/task/service/demo.ts
Normal 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 '任务执行成功';
|
||||
}
|
||||
}
|
||||
153
src/modules/task/service/info.ts
Normal file
153
src/modules/task/service/info.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
336
src/modules/task/service/local.ts
Normal file
336
src/modules/task/service/local.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/modules/user/config.ts
Normal file
34
src/modules/user/config.ts
Normal 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;
|
||||
};
|
||||
13
src/modules/user/controller/admin/address.ts
Normal file
13
src/modules/user/controller/admin/address.ts
Normal 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 {}
|
||||
15
src/modules/user/controller/admin/info.ts
Normal file
15
src/modules/user/controller/admin/info.ts
Normal 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 {}
|
||||
39
src/modules/user/controller/app/address.ts
Normal file
39
src/modules/user/controller/app/address.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
25
src/modules/user/controller/app/comm.ts
Normal file
25
src/modules/user/controller/app/comm.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
65
src/modules/user/controller/app/info.ts
Normal file
65
src/modules/user/controller/app/info.ts
Normal 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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
106
src/modules/user/controller/app/login.ts
Normal file
106
src/modules/user/controller/app/login.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
34
src/modules/user/entity/address.ts
Normal file
34
src/modules/user/entity/address.ts
Normal 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;
|
||||
}
|
||||
34
src/modules/user/entity/info.ts
Normal file
34
src/modules/user/entity/info.ts
Normal 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;
|
||||
}
|
||||
40
src/modules/user/entity/wx.ts
Normal file
40
src/modules/user/entity/wx.ts
Normal 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;
|
||||
}
|
||||
56
src/modules/user/event/app.ts
Normal file
56
src/modules/user/event/app.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
96
src/modules/user/middleware/app.ts
Normal file
96
src/modules/user/middleware/app.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
63
src/modules/user/service/address.ts
Normal file
63
src/modules/user/service/address.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
124
src/modules/user/service/info.ts
Normal file
124
src/modules/user/service/info.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
307
src/modules/user/service/login.ts
Normal file
307
src/modules/user/service/login.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
79
src/modules/user/service/sms.ts
Normal file
79
src/modules/user/service/sms.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
281
src/modules/user/service/wx.ts
Normal file
281
src/modules/user/service/wx.ts
Normal 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
96
typings/feishu.d.ts
vendored
Normal 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
2
typings/plugin.d.ts
vendored
@ -1,3 +1,4 @@
|
||||
import * as feishu from './feishu';
|
||||
import { BaseUpload, MODETYPE } from './upload';
|
||||
type AnyString = string & {};
|
||||
/**
|
||||
@ -5,4 +6,5 @@ type AnyString = string & {};
|
||||
*/
|
||||
interface PluginMap {
|
||||
upload: BaseUpload;
|
||||
feishu: feishu.CoolPlugin;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user