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