mirror of
https://github.com/cool-team-official/cool-admin-midway-packages.git
synced 2025-12-10 21:32:48 +00:00
完善rpc
This commit is contained in:
parent
33e7e508f2
commit
89e7c3bc3d
@ -32,11 +32,10 @@
|
||||
"url": "https://cool-js.com"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@midwayjs/logger": "^3.4.2",
|
||||
"mwtsc": "^1.15.1",
|
||||
"@midwayjs/cli": "2.1.1",
|
||||
"@midwayjs/core": "^3.20.0",
|
||||
"@midwayjs/koa": "^3.20.0",
|
||||
"@midwayjs/logger": "^3.4.2",
|
||||
"@midwayjs/mock": "^3.20.0",
|
||||
"@midwayjs/typeorm": "^3.20.0",
|
||||
"@types/download": "^8.0.5",
|
||||
@ -46,6 +45,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"mwts": "^1.3.0",
|
||||
"mwtsc": "^1.15.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "~5.7.3"
|
||||
|
||||
10
core/pnpm-lock.yaml
generated
10
core/pnpm-lock.yaml
generated
@ -44,6 +44,9 @@ importers:
|
||||
moment:
|
||||
specifier: ^2.30.1
|
||||
version: 2.30.1
|
||||
moment-timezone:
|
||||
specifier: ^0.5.46
|
||||
version: 0.5.46
|
||||
pm2:
|
||||
specifier: ^5.4.3
|
||||
version: 5.4.3
|
||||
@ -2816,6 +2819,9 @@ packages:
|
||||
module-details-from-path@1.0.3:
|
||||
resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==}
|
||||
|
||||
moment-timezone@0.5.46:
|
||||
resolution: {integrity: sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==}
|
||||
|
||||
moment@2.30.1:
|
||||
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
||||
|
||||
@ -7452,6 +7458,10 @@ snapshots:
|
||||
|
||||
module-details-from-path@1.0.3: {}
|
||||
|
||||
moment-timezone@0.5.46:
|
||||
dependencies:
|
||||
moment: 2.30.1
|
||||
|
||||
moment@2.30.1: {}
|
||||
|
||||
mqemitter@6.0.2:
|
||||
|
||||
@ -27,6 +27,7 @@ export abstract class BaseService {
|
||||
@Inject()
|
||||
basePgService: BasePgService;
|
||||
|
||||
// sqlite的基类
|
||||
@Inject()
|
||||
baseSqliteService: BaseSqliteService;
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Init, Provide, Inject, App, Config } from '@midwayjs/core';
|
||||
import { Init, Provide, Inject, App, Config, ALL } from '@midwayjs/core';
|
||||
import { Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { CoolValidateException } from '../exception/validate';
|
||||
import { ERRINFO, EVENT } from '../constant/global';
|
||||
@ -10,7 +10,7 @@ import { Brackets, In, Repository, SelectQueryBuilder } from 'typeorm';
|
||||
import { QueryOp } from '../decorator/controller';
|
||||
import * as _ from 'lodash';
|
||||
import { CoolEventManager } from '../event';
|
||||
|
||||
import * as moment from 'moment';
|
||||
/**
|
||||
* 服务基类
|
||||
*/
|
||||
@ -32,6 +32,9 @@ export abstract class BaseSqliteService {
|
||||
@Inject()
|
||||
coolEventManager: CoolEventManager;
|
||||
|
||||
@Config(ALL)
|
||||
allConfig: any;
|
||||
|
||||
// 设置模型
|
||||
setEntity(entity: any) {
|
||||
this.entity = entity;
|
||||
@ -324,6 +327,27 @@ export abstract class BaseSqliteService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据时区偏移获取日期
|
||||
* @param timezone 时区偏移,格式如 '+08:00', '-07:00'
|
||||
* @returns Date
|
||||
*/
|
||||
getDateWithTimezone(): Date {
|
||||
const timezone =
|
||||
this.allConfig.typeorm?.dataSource?.default?.timezone || '+08:00';
|
||||
const match = timezone.match(/^([+-])(\d{2}):(\d{2})$/);
|
||||
if (!match) {
|
||||
throw new CoolValidateException('时区格式错误,应为 "+08:00" 这样的格式');
|
||||
}
|
||||
|
||||
const [_, sign, hours, minutes] = match;
|
||||
const offsetInMinutes =
|
||||
(parseInt(hours) * 60 + parseInt(minutes)) * (sign === '+' ? 1 : -1);
|
||||
const date = new Date();
|
||||
const utc = date.getTime() + date.getTimezoneOffset() * 60000;
|
||||
return new Date(utc + offsetInMinutes * 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增|修改
|
||||
* @param param 数据
|
||||
@ -334,6 +358,7 @@ export abstract class BaseSqliteService {
|
||||
// 判断是否是批量操作
|
||||
if (param instanceof Array) {
|
||||
param.forEach(item => {
|
||||
// 设置时区+08:00
|
||||
item.updateTime = new Date();
|
||||
item.createTime = new Date();
|
||||
});
|
||||
@ -354,8 +379,8 @@ export abstract class BaseSqliteService {
|
||||
: await this.entity.save(param);
|
||||
}
|
||||
if (type == 'add') {
|
||||
param.createTime = new Date();
|
||||
param.updateTime = new Date();
|
||||
param.createTime = moment(new Date()).format('YYYY-MM-DD HH:mm:ss');
|
||||
param.updateTime = moment(new Date()).format('YYYY-MM-DD HH:mm:ss');
|
||||
upsert == 'normal'
|
||||
? await this.entity.insert(param)
|
||||
: await this.entity.save(param);
|
||||
|
||||
11
rpc/.editorconfig
Normal file
11
rpc/.editorconfig
Normal file
@ -0,0 +1,11 @@
|
||||
# 🎨 editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
22
rpc/.eslintrc.json
Normal file
22
rpc/.eslintrc.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "./node_modules/mwts/",
|
||||
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"node/no-extraneous-import": "off",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"no-empty": "off",
|
||||
"node/no-extraneous-require": "off",
|
||||
"node/no-unpublished-import": "off",
|
||||
"eqeqeq": "off",
|
||||
"node/no-unsupported-features/node-builtins": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"no-control-regex": "off",
|
||||
"prefer-const": "off"
|
||||
}
|
||||
}
|
||||
4
rpc/.gitattributes
vendored
Normal file
4
rpc/.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.code-snippets text eol=lf
|
||||
13
rpc/.gitignore
vendored
Normal file
13
rpc/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
logs/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
node_modules/
|
||||
coverage/
|
||||
dist/
|
||||
.idea/
|
||||
run/
|
||||
.DS_Store
|
||||
*.sw*
|
||||
*.un~
|
||||
.tsbuildinfo
|
||||
.tsbuildinfo.*
|
||||
3
rpc/.prettierrc.js
Normal file
3
rpc/.prettierrc.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
...require('mwts/.prettierrc.json')
|
||||
}
|
||||
10
rpc/index.d.ts
vendored
Normal file
10
rpc/index.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
export * from './dist/index';
|
||||
|
||||
declare module '@midwayjs/core/dist/interface' {
|
||||
interface MidwayConfig {
|
||||
book?: PowerPartial<{
|
||||
a: number;
|
||||
b: string;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
7
rpc/jest.config.js
Normal file
7
rpc/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
|
||||
coveragePathIgnorePatterns: ['<rootDir>/test/'],
|
||||
setupFilesAfterEnv: ['./jest.setup.js']
|
||||
};
|
||||
1
rpc/jest.setup.js
Normal file
1
rpc/jest.setup.js
Normal file
@ -0,0 +1 @@
|
||||
jest.setTimeout(30000);
|
||||
58
rpc/package.json
Normal file
58
rpc/package.json
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@cool-midway/rpc",
|
||||
"version": "8.0.0",
|
||||
"description": "cool-admin midway rpc",
|
||||
"main": "dist/index.js",
|
||||
"typings": "index.d.ts",
|
||||
"scripts": {
|
||||
"build": "mwtsc --cleanOutDir",
|
||||
"test": "cross-env NODE_ENV=unittest jest",
|
||||
"cov": "jest --coverage",
|
||||
"lint": "mwts check",
|
||||
"lint:fix": "mwts fix"
|
||||
},
|
||||
"keywords": [
|
||||
"cool",
|
||||
"cool-admin",
|
||||
"cooljs"
|
||||
],
|
||||
"author": "COOL",
|
||||
"files": [
|
||||
"dist/**/*.js",
|
||||
"dist/**/*.d.ts",
|
||||
"index.d.ts"
|
||||
],
|
||||
"readme": "README.md",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://cool-js.com"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cool-midway/core": "file:/Users/ap/Documents/src/admin/midway-packages/core",
|
||||
"@midwayjs/cli": "^2.1.1",
|
||||
"@midwayjs/core": "^3.20.0",
|
||||
"@midwayjs/koa": "^3.20.0",
|
||||
"@midwayjs/logger": "^3.4.2",
|
||||
"@midwayjs/mock": "^3.20.0",
|
||||
"@midwayjs/redis": "^3.20.0",
|
||||
"@midwayjs/typeorm": "^3.20.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.7",
|
||||
"cross-env": "^7.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"mwts": "^1.3.0",
|
||||
"mwtsc": "^1.15.1",
|
||||
"sqlstring": "^2.3.3",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.7.3",
|
||||
"uuid": "^11.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"ioredis": "^5.4.2",
|
||||
"moleculer": "^0.14.35"
|
||||
}
|
||||
}
|
||||
8714
rpc/pnpm-lock.yaml
generated
Normal file
8714
rpc/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
rpc/src/README.md
Normal file
27
rpc/src/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
### COOL-ADMIN
|
||||
|
||||
cool-admin一个很酷的后台权限管理系统,开源免费,模块化、插件化、极速开发CRUD,方便快速构建迭代后台管理系统
|
||||
|
||||
大数据、微服务、AI编码快速开发!!!
|
||||
|
||||
|
||||
### 技术栈
|
||||
|
||||
- 后端:node.js midway.js koa.js mysql typescript
|
||||
- 前端:vue.js element-plus jsx pinia vue-router
|
||||
|
||||
### 官网
|
||||
|
||||
[https://cool-js.com](https://cool-js.com)
|
||||
|
||||
|
||||
### 演示地址
|
||||
|
||||
[https://show.cool-admin.com](https://show.cool-admin.com)
|
||||
|
||||
- 账户:admin
|
||||
- 密码:123456
|
||||
|
||||
### 项目地址
|
||||
|
||||
[https://github.com/cool-team-official/cool-admin-midway](https://github.com/cool-team-official/cool-admin-midway)
|
||||
6
rpc/src/config/config.default.ts
Normal file
6
rpc/src/config/config.default.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* cool的配置
|
||||
*/
|
||||
export default {
|
||||
cool: {},
|
||||
};
|
||||
26
rpc/src/configuration.ts
Normal file
26
rpc/src/configuration.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Configuration } from '@midwayjs/core';
|
||||
import * as DefaultConfig from './config/config.default';
|
||||
import { IMidwayContainer } from '@midwayjs/core';
|
||||
import { CoolRpc } from './rpc';
|
||||
import { CoolRpcDecorator } from './decorator';
|
||||
|
||||
@Configuration({
|
||||
namespace: 'cool:rpc',
|
||||
importConfigs: [
|
||||
{
|
||||
default: DefaultConfig,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class CoolRpcConfiguration {
|
||||
async onReady(container: IMidwayContainer) {
|
||||
global['moleculer.transactions'] = {};
|
||||
(await container.getAsync(CoolRpc)).init();
|
||||
// 装饰器
|
||||
await container.getAsync(CoolRpcDecorator);
|
||||
}
|
||||
|
||||
async onStop(container: IMidwayContainer): Promise<void> {
|
||||
(await container.getAsync(CoolRpc)).stop();
|
||||
}
|
||||
}
|
||||
19
rpc/src/decorator/event/event.ts
Normal file
19
rpc/src/decorator/event/event.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {
|
||||
Scope,
|
||||
ScopeEnum,
|
||||
saveClassMetadata,
|
||||
saveModule,
|
||||
} from '@midwayjs/core';
|
||||
|
||||
export const COOL_RPC_EVENT_KEY = 'decorator:cool:rpc:event';
|
||||
|
||||
export function CoolRpcEvent(): ClassDecorator {
|
||||
return (target: any) => {
|
||||
// 将装饰的类,绑定到该装饰器,用于后续能获取到 class
|
||||
saveModule(COOL_RPC_EVENT_KEY, target);
|
||||
// 保存一些元数据信息,任意你希望存的东西
|
||||
saveClassMetadata(COOL_RPC_EVENT_KEY, {}, target);
|
||||
// 指定 IoC 容器创建实例的作用域
|
||||
Scope(ScopeEnum.Singleton)(target);
|
||||
};
|
||||
}
|
||||
23
rpc/src/decorator/event/handler.ts
Normal file
23
rpc/src/decorator/event/handler.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { attachClassMetadata } from '@midwayjs/core';
|
||||
|
||||
export const COOL_RPC_EVENT_HANDLER_KEY = 'decorator:cool:rpc:event:handler';
|
||||
|
||||
/**
|
||||
* 事件
|
||||
* @param eventName 事件名称
|
||||
* @returns
|
||||
*/
|
||||
export function CoolRpcEventHandler(eventName?: string): MethodDecorator {
|
||||
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
// 将装饰的类,绑定到该装饰器,用于后续能获取到 class
|
||||
attachClassMetadata(
|
||||
COOL_RPC_EVENT_HANDLER_KEY,
|
||||
{
|
||||
propertyKey,
|
||||
descriptor,
|
||||
eventName,
|
||||
},
|
||||
target
|
||||
);
|
||||
};
|
||||
}
|
||||
101
rpc/src/decorator/index.ts
Normal file
101
rpc/src/decorator/index.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { CoolCommException } from '@cool-midway/core';
|
||||
import {
|
||||
Provide,
|
||||
Scope,
|
||||
ScopeEnum,
|
||||
JoinPoint,
|
||||
Init,
|
||||
MidwayDecoratorService,
|
||||
Inject,
|
||||
} from '@midwayjs/core';
|
||||
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||||
import { COOL_RPC_TRANSACTION, TransactionOptions } from './transaction';
|
||||
import { v1 as uuid } from 'uuid';
|
||||
|
||||
/**
|
||||
* 装饰器
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class CoolRpcDecorator {
|
||||
@Inject()
|
||||
decoratorService: MidwayDecoratorService;
|
||||
|
||||
@Inject()
|
||||
typeORMDataSourceManager: TypeORMDataSourceManager;
|
||||
|
||||
@Init()
|
||||
async init() {
|
||||
// 事务
|
||||
await this.transaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务
|
||||
*/
|
||||
async transaction() {
|
||||
this.decoratorService.registerMethodHandler(
|
||||
COOL_RPC_TRANSACTION,
|
||||
options => {
|
||||
return {
|
||||
around: async (joinPoint: JoinPoint) => {
|
||||
const option: TransactionOptions = options.metadata;
|
||||
let isCaller = false;
|
||||
let rpcTransactionId;
|
||||
if (joinPoint.args[0]) {
|
||||
isCaller = false;
|
||||
rpcTransactionId = joinPoint.args[0].rpcTransactionId;
|
||||
}
|
||||
// 如果没有事务ID,手动创建
|
||||
if (!rpcTransactionId) {
|
||||
isCaller = true;
|
||||
rpcTransactionId = uuid();
|
||||
}
|
||||
|
||||
let data;
|
||||
const dataSource = this.typeORMDataSourceManager.getDataSource(
|
||||
option?.connectionName || 'default'
|
||||
);
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
// 使用我们的新queryRunner建立真正的数据库连
|
||||
await queryRunner.connect();
|
||||
if (option && option.isolation) {
|
||||
await queryRunner.startTransaction(option.isolation);
|
||||
} else {
|
||||
await queryRunner.startTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
global['moleculer.transactions'][rpcTransactionId] = queryRunner;
|
||||
// 半小时后清除
|
||||
setTimeout(() => {
|
||||
global['moleculer.transactions'][rpcTransactionId].release();
|
||||
delete global['moleculer.transactions'][rpcTransactionId];
|
||||
}, 1800 * 1000);
|
||||
joinPoint.args.push(rpcTransactionId);
|
||||
joinPoint.args.push(queryRunner);
|
||||
data = await joinPoint.proceed(...joinPoint.args);
|
||||
if (isCaller) {
|
||||
global['moleculer:broker'].broadcast('moleculer.transaction', {
|
||||
rpcTransactionId,
|
||||
commit: true,
|
||||
});
|
||||
}
|
||||
//await queryRunner.commitTransaction();
|
||||
} catch (error) {
|
||||
//await queryRunner.rollbackTransaction();
|
||||
if (isCaller) {
|
||||
global['moleculer:broker'].broadcast('moleculer.transaction', {
|
||||
rpcTransactionId,
|
||||
commit: false,
|
||||
});
|
||||
}
|
||||
throw new CoolCommException(error.message);
|
||||
}
|
||||
return data;
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
84
rpc/src/decorator/rpc.ts
Normal file
84
rpc/src/decorator/rpc.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import {
|
||||
Scope,
|
||||
ScopeEnum,
|
||||
saveClassMetadata,
|
||||
saveModule,
|
||||
} from '@midwayjs/core';
|
||||
|
||||
export const MOLECYLER_KEY = 'decorator:cool:rpc';
|
||||
|
||||
export type MethodTypes =
|
||||
| 'add'
|
||||
| 'delete'
|
||||
| 'update'
|
||||
| 'page'
|
||||
| 'info'
|
||||
| 'list';
|
||||
|
||||
// 字段匹配
|
||||
export interface FieldEq {
|
||||
// 字段
|
||||
column: string;
|
||||
// 请求参数
|
||||
requestParam: string;
|
||||
}
|
||||
|
||||
// 关联查询
|
||||
export interface LeftJoinOp {
|
||||
// 实体
|
||||
entity: any;
|
||||
// 别名
|
||||
alias: string;
|
||||
// 关联条件
|
||||
condition: string;
|
||||
}
|
||||
|
||||
// Crud配置
|
||||
export interface CurdOption {
|
||||
// 路由前缀,不配置默认是按Controller下的文件夹路径
|
||||
prefix?: string;
|
||||
// curd api接口
|
||||
method: MethodTypes[];
|
||||
// 分页查询配置
|
||||
pageQueryOp?: QueryOp;
|
||||
// 非分页查询配置
|
||||
listQueryOp?: QueryOp;
|
||||
// 插入参数
|
||||
insertParam?: Function;
|
||||
// info 忽略返回属性
|
||||
infoIgnoreProperty?: string[];
|
||||
// 实体
|
||||
entity: { entityKey?: any; connectionName?: string } | any;
|
||||
}
|
||||
|
||||
// 查询配置
|
||||
export interface QueryOp {
|
||||
// 需要模糊查询的字段
|
||||
keyWordLikeFields?: string[];
|
||||
// 查询条件
|
||||
where?: Function;
|
||||
// 查询字段
|
||||
select?: string[];
|
||||
// 字段相等
|
||||
fieldEq?: string[] | FieldEq[];
|
||||
// 添加排序条件
|
||||
addOrderBy?: {};
|
||||
// 关联配置
|
||||
leftJoin?: LeftJoinOp[];
|
||||
}
|
||||
|
||||
/**
|
||||
* moleculer 微服务配置
|
||||
* @param option
|
||||
* @returns
|
||||
*/
|
||||
export function CoolRpcService(option?: CurdOption): ClassDecorator {
|
||||
return (target: any) => {
|
||||
// 将装饰的类,绑定到该装饰器,用于后续能获取到 class
|
||||
saveModule(MOLECYLER_KEY, target);
|
||||
// 保存一些元数据信息,任意你希望存的东西
|
||||
saveClassMetadata(MOLECYLER_KEY, option, target);
|
||||
// 指定 IoC 容器创建实例的作用域,这里注册为请求作用域,这样能取到 ctx
|
||||
Scope(ScopeEnum.Request)(target);
|
||||
};
|
||||
}
|
||||
22
rpc/src/decorator/transaction.ts
Normal file
22
rpc/src/decorator/transaction.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import * as _ from 'lodash';
|
||||
import { createCustomMethodDecorator } from '@midwayjs/core';
|
||||
|
||||
type IsolationLevel =
|
||||
| 'READ UNCOMMITTED'
|
||||
| 'READ COMMITTED'
|
||||
| 'REPEATABLE READ'
|
||||
| 'SERIALIZABLE';
|
||||
|
||||
export interface TransactionOptions {
|
||||
connectionName?: string;
|
||||
isolation?: IsolationLevel;
|
||||
}
|
||||
|
||||
// 装饰器内部的唯一 id
|
||||
export const COOL_RPC_TRANSACTION = 'decorator:cool_rpc_transaction';
|
||||
|
||||
export function CoolRpcTransaction(
|
||||
option?: TransactionOptions
|
||||
): MethodDecorator {
|
||||
return createCustomMethodDecorator(COOL_RPC_TRANSACTION, option);
|
||||
}
|
||||
28
rpc/src/index.ts
Normal file
28
rpc/src/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
export { CoolRpcConfiguration as Configuration } from './configuration';
|
||||
|
||||
export * from './test';
|
||||
export * from './rpc';
|
||||
export * from './decorator/rpc';
|
||||
export * from './decorator/event/event';
|
||||
export * from './decorator/event/handler';
|
||||
export * from './service/base';
|
||||
export * from './service/mysql';
|
||||
export * from './service/postgres';
|
||||
export * from './service/sqlite';
|
||||
export * from './decorator/transaction';
|
||||
export * from './transaction/event';
|
||||
export * from './decorator/index';
|
||||
|
||||
export interface CoolRpcConfig {
|
||||
// 服务名称
|
||||
name: string;
|
||||
// redis
|
||||
redis: RedisConfig & RedisConfig[] & unknown;
|
||||
}
|
||||
|
||||
export interface RedisConfig {
|
||||
host: string;
|
||||
password: string;
|
||||
port: number;
|
||||
db: number;
|
||||
}
|
||||
275
rpc/src/rpc.ts
Normal file
275
rpc/src/rpc.ts
Normal file
@ -0,0 +1,275 @@
|
||||
import { ILogger, IMidwayApplication, Inject } from '@midwayjs/core';
|
||||
import {
|
||||
App,
|
||||
Config,
|
||||
getClassMetadata,
|
||||
listModule,
|
||||
Logger,
|
||||
Provide,
|
||||
Scope,
|
||||
ScopeEnum,
|
||||
} from '@midwayjs/core';
|
||||
import { ServiceBroker } from 'moleculer';
|
||||
import { CoolRpcConfig } from '.';
|
||||
import { CoolCoreException, CoolValidateException } from '@cool-midway/core';
|
||||
import { v1 as uuid } from 'uuid';
|
||||
import { BaseRpcService } from './service/base';
|
||||
import { CurdOption, MOLECYLER_KEY } from './decorator/rpc';
|
||||
import { COOL_RPC_EVENT_KEY } from './decorator/event/event';
|
||||
import { COOL_RPC_EVENT_HANDLER_KEY } from './decorator/event/handler';
|
||||
import * as _ from 'lodash';
|
||||
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||||
import { camelCase } from '@midwayjs/core/dist/util/camelCase';
|
||||
// import { AgentService } from '@moleculer/lab';
|
||||
|
||||
/**
|
||||
* 微服务
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class CoolRpc {
|
||||
broker: ServiceBroker;
|
||||
|
||||
@Inject()
|
||||
typeORMDataSourceManager: TypeORMDataSourceManager;
|
||||
|
||||
@Logger()
|
||||
coreLogger: ILogger;
|
||||
|
||||
@Config('cool.rpc')
|
||||
rpcConfig: CoolRpcConfig;
|
||||
|
||||
@Config('cool')
|
||||
coolConfig;
|
||||
|
||||
@App()
|
||||
app: IMidwayApplication;
|
||||
|
||||
cruds;
|
||||
|
||||
async init() {
|
||||
if (!this.rpcConfig?.name) {
|
||||
throw new CoolCoreException(
|
||||
'cool.rpc.name config is require and every service name must be unique'
|
||||
);
|
||||
}
|
||||
|
||||
let redisConfig;
|
||||
|
||||
if (!this.rpcConfig?.redis && !this.coolConfig?.redis) {
|
||||
throw new CoolCoreException('cool.rpc.redis or cool.redis is require');
|
||||
}
|
||||
|
||||
redisConfig = this.rpcConfig?.redis
|
||||
? this.rpcConfig?.redis
|
||||
: this.coolConfig?.redis;
|
||||
|
||||
const transporter = {
|
||||
type: 'Redis',
|
||||
options: {},
|
||||
};
|
||||
if (redisConfig instanceof Array) {
|
||||
transporter.options = {
|
||||
cluster: {
|
||||
nodes: redisConfig,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
transporter.options = redisConfig;
|
||||
}
|
||||
|
||||
this.broker = new ServiceBroker({
|
||||
nodeID: `${this.rpcConfig.name}-${uuid()}`,
|
||||
transporter,
|
||||
// metrics: {
|
||||
// enabled: true,
|
||||
// reporter: 'Laboratory',
|
||||
// },
|
||||
// tracing: {
|
||||
// enabled: true,
|
||||
// exporter: 'Laboratory',
|
||||
// },
|
||||
...this.rpcConfig,
|
||||
});
|
||||
|
||||
// this.broker.createService({
|
||||
// name: this.rpcConfig.name,
|
||||
// mixins: [],
|
||||
// // settings: {
|
||||
// // name: 'test',
|
||||
// // port: 3210,
|
||||
// // token: '123123',
|
||||
// // apiKey: '92C18ZR-ERM45EG-HT8GQGQ-4MHCXAT',
|
||||
// // },
|
||||
// });
|
||||
|
||||
global['moleculer:broker'] = this.broker;
|
||||
|
||||
await this.initService();
|
||||
await this.createService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得事件
|
||||
* @returns
|
||||
*/
|
||||
async getEvents() {
|
||||
const allEvents = {};
|
||||
const modules = listModule(COOL_RPC_EVENT_KEY);
|
||||
for (const module of modules) {
|
||||
const moduleInstance = await this.app
|
||||
.getApplicationContext()
|
||||
.getAsync(module);
|
||||
moduleInstance['broker'] = this.broker;
|
||||
const events = getClassMetadata(COOL_RPC_EVENT_HANDLER_KEY, module);
|
||||
for (const event of events) {
|
||||
allEvents[event.eventName ? event.eventName : event.propertyKey] = {
|
||||
handler(ctx) {
|
||||
moduleInstance[event.propertyKey](ctx.params);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return allEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建服务
|
||||
*/
|
||||
async createService() {
|
||||
const _this = this;
|
||||
this.broker.createService({
|
||||
name: this.rpcConfig.name,
|
||||
events: await this.getEvents(),
|
||||
actions: {
|
||||
async call(ctx) {
|
||||
const { service, method, params } = ctx.params;
|
||||
const targetName = _.upperFirst(service);
|
||||
const target = _.find(_this.cruds, { name: targetName });
|
||||
if (!target) {
|
||||
throw new CoolValidateException('找不到服务');
|
||||
}
|
||||
const curdOption: CurdOption = getClassMetadata(
|
||||
MOLECYLER_KEY,
|
||||
target
|
||||
);
|
||||
|
||||
const cls = await _this.app
|
||||
.getApplicationContext()
|
||||
.getAsync(camelCase(service));
|
||||
const serviceInstance: BaseRpcService = new target();
|
||||
Object.assign(serviceInstance, cls);
|
||||
serviceInstance.setModel(_this.getModel(curdOption));
|
||||
serviceInstance.setApp(_this.app);
|
||||
serviceInstance.init();
|
||||
|
||||
// 如果是通用crud方法 注入参数
|
||||
if (
|
||||
['add', 'delete', 'update', 'page', 'info', 'list'].includes(method)
|
||||
) {
|
||||
if (!curdOption.method.includes(method)) {
|
||||
throw new CoolValidateException('方法不存在');
|
||||
}
|
||||
}
|
||||
return serviceInstance[method](params);
|
||||
},
|
||||
},
|
||||
});
|
||||
this.broker.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化service,设置entity
|
||||
*/
|
||||
async initService() {
|
||||
// 获得所有的service
|
||||
this.cruds = listModule(MOLECYLER_KEY);
|
||||
for (const crud of this.cruds) {
|
||||
const curdOption: CurdOption = getClassMetadata(MOLECYLER_KEY, crud);
|
||||
const serviceInstance: BaseRpcService = await this.app
|
||||
.getApplicationContext()
|
||||
.getAsync(crud);
|
||||
serviceInstance.setModel(this.getModel(curdOption));
|
||||
serviceInstance.setCurdOption(curdOption);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得Model
|
||||
* @param curdOption
|
||||
*/
|
||||
getModel(curdOption) {
|
||||
// 获得到model
|
||||
let entityModel;
|
||||
const { entity } = curdOption || {};
|
||||
if (entity) {
|
||||
const dataSourceName =
|
||||
this.typeORMDataSourceManager.getDataSourceNameByModel(entity);
|
||||
entityModel = this.typeORMDataSourceManager
|
||||
.getDataSource(dataSourceName)
|
||||
.getRepository(entity);
|
||||
}
|
||||
return entityModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用服务
|
||||
* @param name 服务名称
|
||||
* @param controller 接口服务
|
||||
* @param method 方法
|
||||
* @param params 参数
|
||||
* @returns
|
||||
*/
|
||||
async call(name: string, service: string, method: string, params?: {}) {
|
||||
return this.broker.call(`${name}.call`, { service, method, params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送事件
|
||||
* @param name 事件名称
|
||||
* @param params 事件参数
|
||||
* @param node 节点名称
|
||||
*/
|
||||
async event(name: string, params: any, node?: string | string[]) {
|
||||
this.broker.emit(name, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送广播事件
|
||||
* @param name
|
||||
* @param params
|
||||
* @param node 节点名称
|
||||
*/
|
||||
async broadcastEvent(name: string, params: any, node?: string | string[]) {
|
||||
this.broker.broadcast(name, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送本地广播事件
|
||||
* @param name
|
||||
* @param params
|
||||
* @param node 节点名称
|
||||
*/
|
||||
async broadcastLocalEvent(
|
||||
name: string,
|
||||
params: any,
|
||||
node?: string | string[]
|
||||
) {
|
||||
this.broker.broadcastLocal(name, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得原始的broker对象
|
||||
* @returns
|
||||
*/
|
||||
getBroker() {
|
||||
return this.broker;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止
|
||||
*/
|
||||
stop() {
|
||||
this.broker.stop();
|
||||
}
|
||||
}
|
||||
300
rpc/src/service/base.ts
Normal file
300
rpc/src/service/base.ts
Normal file
@ -0,0 +1,300 @@
|
||||
import { Config, Init, Inject, Provide } from '@midwayjs/core';
|
||||
import { Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { BaseMysqlService } from './mysql';
|
||||
import { BasePgService } from './postgres';
|
||||
import {
|
||||
CoolValidateException,
|
||||
ERRINFO,
|
||||
QueryOp,
|
||||
CoolEventManager,
|
||||
CoolCoreException,
|
||||
} from '@cool-midway/core';
|
||||
import { Application, Context } from '@midwayjs/koa';
|
||||
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||||
import { Repository, SelectQueryBuilder } from 'typeorm';
|
||||
import * as _ from 'lodash';
|
||||
import { BaseSqliteService } from './sqlite';
|
||||
|
||||
/**
|
||||
* 服务基类
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export abstract class BaseRpcService {
|
||||
// mysql的基类
|
||||
@Inject()
|
||||
baseMysqlService: BaseMysqlService;
|
||||
|
||||
// postgres的基类
|
||||
@Inject()
|
||||
basePgService: BasePgService;
|
||||
|
||||
// sqlite的基类
|
||||
@Inject()
|
||||
baseSqliteService: BaseSqliteService;
|
||||
|
||||
// 数据库类型
|
||||
@Config('typeorm.dataSource.default.type')
|
||||
ormType;
|
||||
|
||||
// 当前服务名称
|
||||
service: BaseMysqlService | BasePgService | BaseSqliteService;
|
||||
|
||||
// 模型
|
||||
protected entity: Repository<any>;
|
||||
|
||||
protected sqlParams;
|
||||
|
||||
protected curdOption;
|
||||
|
||||
@Inject()
|
||||
typeORMDataSourceManager: TypeORMDataSourceManager;
|
||||
|
||||
@Inject()
|
||||
coolEventManager: CoolEventManager;
|
||||
|
||||
@Inject('ctx')
|
||||
baseCtx: Context;
|
||||
|
||||
baseApp: Application;
|
||||
|
||||
// 设置模型
|
||||
setModel(entity: any) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
setCurdOption(curdOption) {
|
||||
this.curdOption = curdOption;
|
||||
}
|
||||
|
||||
@Init()
|
||||
async init() {
|
||||
const services = {
|
||||
mysql: this.baseMysqlService,
|
||||
mariadb: this.baseMysqlService,
|
||||
postgres: this.basePgService,
|
||||
sqlite: this.baseSqliteService,
|
||||
};
|
||||
this.service = services[this.ormType];
|
||||
if (!this.service) throw new CoolCoreException('暂不支持当前数据库类型');
|
||||
this.sqlParams = this.service.sqlParams;
|
||||
await this.service.init();
|
||||
}
|
||||
|
||||
// 设置模型
|
||||
setEntity(entity: any) {
|
||||
this.entity = entity;
|
||||
this.service.setEntity(entity);
|
||||
}
|
||||
|
||||
// 设置请求上下文
|
||||
setCtx(ctx: Context) {
|
||||
this.baseCtx = ctx;
|
||||
this.service.setCtx(ctx);
|
||||
}
|
||||
|
||||
// 设置应用对象
|
||||
setApp(app) {
|
||||
this.baseApp = app;
|
||||
this.service.setApp(app);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置sql
|
||||
* @param condition 条件是否成立
|
||||
* @param sql sql语句
|
||||
* @param params 参数
|
||||
*/
|
||||
setSql(condition, sql, params) {
|
||||
return this.service.setSql(condition, sql, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得查询个数的SQL
|
||||
* @param sql
|
||||
*/
|
||||
getCountSql(sql) {
|
||||
return this.service.getCountSql(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数安全性检查
|
||||
* @param params
|
||||
*/
|
||||
async paramSafetyCheck(params) {
|
||||
return await this.service.paramSafetyCheck(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原生查询
|
||||
* @param sql
|
||||
* @param params
|
||||
* @param connectionName
|
||||
*/
|
||||
async nativeQuery(sql, params?, connectionName?) {
|
||||
return await this.service.nativeQuery(sql, params, connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得ORM管理
|
||||
* @param connectionName 连接名称
|
||||
*/
|
||||
getOrmManager(connectionName = 'default') {
|
||||
return this.service.getOrmManager(connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作entity获得分页数据,不用写sql
|
||||
* @param find QueryBuilder
|
||||
* @param query
|
||||
* @param autoSort
|
||||
* @param connectionName
|
||||
*/
|
||||
async entityRenderPage(
|
||||
find: SelectQueryBuilder<any>,
|
||||
query,
|
||||
autoSort = true
|
||||
) {
|
||||
return await this.service.entityRenderPage(find, query, autoSort);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行SQL并获得分页数据
|
||||
* @param sql 执行的sql语句
|
||||
* @param query 分页查询条件
|
||||
* @param autoSort 是否自动排序
|
||||
* @param connectionName 连接名称
|
||||
*/
|
||||
async sqlRenderPage(sql, query, autoSort = true, connectionName?) {
|
||||
return await this.service.sqlRenderPage(
|
||||
sql,
|
||||
query,
|
||||
autoSort,
|
||||
connectionName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得单个ID
|
||||
* @param id ID
|
||||
* @param infoIgnoreProperty 忽略返回属性
|
||||
*/
|
||||
async info(id: any, infoIgnoreProperty?: string[]) {
|
||||
this.service.setEntity(this.entity);
|
||||
return await this.service.info(id, infoIgnoreProperty);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3
|
||||
*/
|
||||
async delete(ids: any) {
|
||||
this.service.setEntity(this.entity);
|
||||
await this.modifyBefore(ids, 'delete');
|
||||
await this.service.delete(ids);
|
||||
await this.modifyAfter(ids, 'delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除
|
||||
* @param ids 删除的ID数组
|
||||
* @param entity 实体
|
||||
*/
|
||||
async softDelete(ids: number[], entity?: Repository<any>) {
|
||||
this.service.setEntity(this.entity);
|
||||
await this.service.softDelete(ids, entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改
|
||||
* @param param 数据
|
||||
*/
|
||||
async update(param: any) {
|
||||
this.service.setEntity(this.entity);
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
if (!param.id && !(param instanceof Array))
|
||||
throw new CoolValidateException(ERRINFO.NOID);
|
||||
await this.addOrUpdate(param, 'update');
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增
|
||||
* @param param 数据
|
||||
*/
|
||||
async add(param: any | any[]): Promise<Object> {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
delete param.id;
|
||||
await this.addOrUpdate(param, 'add');
|
||||
return {
|
||||
id:
|
||||
param instanceof Array
|
||||
? param.map(e => {
|
||||
return e.id ? e.id : e._id;
|
||||
})
|
||||
: param.id
|
||||
? param.id
|
||||
: param._id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增|修改
|
||||
* @param param 数据
|
||||
*/
|
||||
async addOrUpdate(param: any | any[], type: 'add' | 'update' = 'add') {
|
||||
this.service.setEntity(this.entity);
|
||||
await this.modifyBefore(param, type);
|
||||
await this.service.addOrUpdate(param, type);
|
||||
await this.modifyAfter(param, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 非分页查询
|
||||
* @param query 查询条件
|
||||
* @param option 查询配置
|
||||
* @param connectionName 连接名
|
||||
*/
|
||||
async list(query, option, connectionName?): Promise<any> {
|
||||
this.service.setEntity(this.entity);
|
||||
return await this.service.list(query, option, connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
* @param query 查询条件
|
||||
* @param option 查询配置
|
||||
* @param connectionName 连接名
|
||||
*/
|
||||
async page(query, option, connectionName?) {
|
||||
this.service.setEntity(this.entity);
|
||||
return await this.service.page(query, option, connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询配置
|
||||
* @param query 前端查询
|
||||
* @param option
|
||||
*/
|
||||
async getOptionFind(query, option: QueryOp) {
|
||||
this.service.setEntity(this.entity);
|
||||
return await this.service.getOptionFind(query, option);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增|修改|删除 之后的操作
|
||||
* @param data 对应数据
|
||||
*/
|
||||
async modifyAfter(
|
||||
data: any,
|
||||
type: 'delete' | 'update' | 'add'
|
||||
): Promise<void> {}
|
||||
|
||||
/**
|
||||
* 新增|修改|删除 之前的操作
|
||||
* @param data 对应数据
|
||||
*/
|
||||
async modifyBefore(
|
||||
data: any,
|
||||
type: 'delete' | 'update' | 'add'
|
||||
): Promise<void> {}
|
||||
}
|
||||
500
rpc/src/service/mysql.ts
Normal file
500
rpc/src/service/mysql.ts
Normal file
@ -0,0 +1,500 @@
|
||||
import { Init, Provide, Inject, Config } from '@midwayjs/core';
|
||||
import { Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import {
|
||||
CoolValidateException,
|
||||
ERRINFO,
|
||||
EVENT,
|
||||
QueryOp,
|
||||
CoolEventManager,
|
||||
} from '@cool-midway/core';
|
||||
import { Application, Context } from '@midwayjs/koa';
|
||||
import * as SqlString from 'sqlstring';
|
||||
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||||
import { Brackets, Equal, In, Repository, SelectQueryBuilder } from 'typeorm';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
/**
|
||||
* 服务基类
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export abstract class BaseMysqlService {
|
||||
// 分页配置
|
||||
@Config('cool')
|
||||
private _coolConfig;
|
||||
|
||||
// 模型
|
||||
entity: Repository<any>;
|
||||
|
||||
sqlParams;
|
||||
|
||||
@Inject()
|
||||
typeORMDataSourceManager: TypeORMDataSourceManager;
|
||||
|
||||
@Inject()
|
||||
coolEventManager: CoolEventManager;
|
||||
|
||||
// 设置模型
|
||||
setEntity(entity: any) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
// 设置请求上下文
|
||||
setCtx(ctx: Context) {
|
||||
this.baseCtx = ctx;
|
||||
}
|
||||
|
||||
baseApp: Application;
|
||||
|
||||
// 设置应用对象
|
||||
setApp(app: Application) {
|
||||
this.baseApp = app;
|
||||
}
|
||||
|
||||
@Inject('ctx')
|
||||
baseCtx: Context;
|
||||
|
||||
// 初始化
|
||||
@Init()
|
||||
init() {
|
||||
this.sqlParams = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置sql
|
||||
* @param condition 条件是否成立
|
||||
* @param sql sql语句
|
||||
* @param params 参数
|
||||
*/
|
||||
setSql(condition, sql, params) {
|
||||
let rSql = false;
|
||||
if (condition || condition === 0) {
|
||||
rSql = true;
|
||||
this.sqlParams = this.sqlParams.concat(params);
|
||||
}
|
||||
return rSql ? sql : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得查询个数的SQL
|
||||
* @param sql
|
||||
*/
|
||||
getCountSql(sql) {
|
||||
sql = sql
|
||||
.replace(new RegExp('LIMIT', 'gm'), 'limit ')
|
||||
.replace(new RegExp('\n', 'gm'), ' ');
|
||||
if (sql.includes('limit')) {
|
||||
const sqlArr = sql.split('limit ');
|
||||
sqlArr.pop();
|
||||
sql = sqlArr.join('limit ');
|
||||
}
|
||||
return `select count(*) as count from (${sql}) a`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数安全性检查
|
||||
* @param params
|
||||
*/
|
||||
async paramSafetyCheck(params) {
|
||||
const lp = params.toLowerCase();
|
||||
return !(
|
||||
lp.indexOf('update ') > -1 ||
|
||||
lp.indexOf('select ') > -1 ||
|
||||
lp.indexOf('delete ') > -1 ||
|
||||
lp.indexOf('insert ') > -1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原生查询
|
||||
* @param sql
|
||||
* @param params
|
||||
* @param connectionName
|
||||
*/
|
||||
async nativeQuery(sql, params?, connectionName?) {
|
||||
if (_.isEmpty(params)) {
|
||||
params = this.sqlParams;
|
||||
}
|
||||
let newParams = [];
|
||||
newParams = newParams.concat(params);
|
||||
this.sqlParams = [];
|
||||
for (const param of newParams) {
|
||||
SqlString.escape(param);
|
||||
}
|
||||
return await this.getOrmManager(connectionName).query(sql, newParams || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得ORM管理
|
||||
* @param connectionName 连接名称
|
||||
*/
|
||||
getOrmManager(connectionName = 'default') {
|
||||
return this.typeORMDataSourceManager.getDataSource(connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作entity获得分页数据,不用写sql
|
||||
* @param find QueryBuilder
|
||||
* @param query
|
||||
* @param autoSort
|
||||
* @param connectionName
|
||||
*/
|
||||
async entityRenderPage(
|
||||
find: SelectQueryBuilder<any>,
|
||||
query,
|
||||
autoSort = true
|
||||
) {
|
||||
const {
|
||||
size = this._coolConfig.crud.pageSize,
|
||||
page = 1,
|
||||
order = 'createTime',
|
||||
sort = 'desc',
|
||||
isExport = false,
|
||||
maxExportLimit,
|
||||
} = query;
|
||||
const count = await find.getCount();
|
||||
let dataFind: SelectQueryBuilder<any>;
|
||||
if (isExport && maxExportLimit > 0) {
|
||||
dataFind = find.limit(maxExportLimit);
|
||||
} else {
|
||||
dataFind = find.offset((page - 1) * size).limit(size);
|
||||
}
|
||||
if (autoSort) {
|
||||
find.addOrderBy(order, sort.toUpperCase());
|
||||
}
|
||||
return {
|
||||
list: await dataFind.getRawMany(),
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
size: parseInt(size),
|
||||
total: count,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行SQL并获得分页数据
|
||||
* @param sql 执行的sql语句
|
||||
* @param query 分页查询条件
|
||||
* @param autoSort 是否自动排序
|
||||
* @param connectionName 连接名称
|
||||
*/
|
||||
async sqlRenderPage(sql, query, autoSort = true, connectionName?) {
|
||||
const {
|
||||
size = this._coolConfig.crud.pageSize,
|
||||
page = 1,
|
||||
order = 'createTime',
|
||||
sort = 'desc',
|
||||
isExport = false,
|
||||
maxExportLimit,
|
||||
} = query;
|
||||
if (order && sort && autoSort) {
|
||||
if (!(await this.paramSafetyCheck(order + sort))) {
|
||||
throw new CoolValidateException('非法传参~');
|
||||
}
|
||||
sql += ` ORDER BY ${SqlString.escapeId(order)} ${this.checkSort(sort)}`;
|
||||
}
|
||||
if (isExport && maxExportLimit > 0) {
|
||||
this.sqlParams.push(parseInt(maxExportLimit));
|
||||
sql += ' LIMIT ? ';
|
||||
}
|
||||
if (!isExport) {
|
||||
this.sqlParams.push((page - 1) * size);
|
||||
this.sqlParams.push(parseInt(size));
|
||||
sql += ' LIMIT ?,? ';
|
||||
}
|
||||
|
||||
let params = [];
|
||||
params = params.concat(this.sqlParams);
|
||||
const result = await this.nativeQuery(sql, params, connectionName);
|
||||
const countResult = await this.nativeQuery(
|
||||
this.getCountSql(sql),
|
||||
params,
|
||||
connectionName
|
||||
);
|
||||
return {
|
||||
list: result,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
size: parseInt(size),
|
||||
total: parseInt(countResult[0] ? countResult[0].count : 0),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查排序
|
||||
* @param sort 排序
|
||||
* @returns
|
||||
*/
|
||||
checkSort(sort) {
|
||||
if (!['desc', 'asc'].includes(sort.toLowerCase())) {
|
||||
throw new CoolValidateException('sort 非法传参~');
|
||||
}
|
||||
return sort;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得单个ID
|
||||
* @param id ID
|
||||
* @param infoIgnoreProperty 忽略返回属性
|
||||
*/
|
||||
async info(id: any, infoIgnoreProperty?: string[]) {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
if (!id) {
|
||||
throw new CoolValidateException(ERRINFO.NOID);
|
||||
}
|
||||
const info = await this.entity.findOneBy({ id: Equal(id) });
|
||||
if (info && infoIgnoreProperty) {
|
||||
for (const property of infoIgnoreProperty) {
|
||||
delete info[property];
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3
|
||||
*/
|
||||
async delete(ids: any) {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
if (ids instanceof String) {
|
||||
ids = ids.split(',');
|
||||
}
|
||||
// 启动软删除发送事件
|
||||
if (this._coolConfig.crud?.softDelete) {
|
||||
this.softDelete(ids);
|
||||
}
|
||||
await this.entity.delete(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除
|
||||
* @param ids 删除的ID数组
|
||||
* @param entity 实体
|
||||
*/
|
||||
async softDelete(ids: number[], entity?: Repository<any>) {
|
||||
const data = await this.entity.find({
|
||||
where: {
|
||||
id: In(ids),
|
||||
},
|
||||
});
|
||||
if (_.isEmpty(data)) return;
|
||||
const _entity = entity ? entity : this.entity;
|
||||
const params = {
|
||||
data,
|
||||
ctx: this.baseCtx,
|
||||
entity: _entity,
|
||||
};
|
||||
if (data.length > 0) {
|
||||
this.coolEventManager.emit(EVENT.SOFT_DELETE, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增|修改
|
||||
* @param param 数据
|
||||
*/
|
||||
async addOrUpdate(param: any | any[], type: 'add' | 'update' = 'add') {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
delete param.createTime;
|
||||
// 判断是否是批量操作
|
||||
if (param instanceof Array) {
|
||||
param.forEach(item => {
|
||||
item.updateTime = new Date();
|
||||
item.createTime = new Date();
|
||||
});
|
||||
await this.entity.save(param);
|
||||
} else {
|
||||
const upsert = this._coolConfig.crud?.upsert || 'normal';
|
||||
if (type == 'update') {
|
||||
if (upsert == 'save') {
|
||||
const info = await this.entity.findOneBy({ id: param.id });
|
||||
param = {
|
||||
...info,
|
||||
...param,
|
||||
};
|
||||
}
|
||||
param.updateTime = new Date();
|
||||
upsert == 'normal'
|
||||
? await this.entity.update(param.id, param)
|
||||
: await this.entity.save(param);
|
||||
}
|
||||
if (type == 'add') {
|
||||
param.createTime = new Date();
|
||||
param.updateTime = new Date();
|
||||
upsert == 'normal'
|
||||
? await this.entity.insert(param)
|
||||
: await this.entity.save(param);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 非分页查询
|
||||
* @param query 查询条件
|
||||
* @param option 查询配置
|
||||
* @param connectionName 连接名
|
||||
*/
|
||||
async list(query, option, connectionName?): Promise<any> {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
const sql = await this.getOptionFind(query, option);
|
||||
return this.nativeQuery(sql, [], connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
* @param query 查询条件
|
||||
* @param option 查询配置
|
||||
* @param connectionName 连接名
|
||||
*/
|
||||
async page(query, option, connectionName?) {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
const sql = await this.getOptionFind(query, option);
|
||||
return this.sqlRenderPage(sql, query, false, connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询配置
|
||||
* @param query 前端查询
|
||||
* @param option
|
||||
*/
|
||||
async getOptionFind(query, option: QueryOp) {
|
||||
let { order = 'createTime', sort = 'desc', keyWord = '' } = query;
|
||||
const sqlArr = ['SELECT'];
|
||||
const selects = ['a.*'];
|
||||
const find = this.entity.createQueryBuilder('a');
|
||||
if (option) {
|
||||
if (typeof option === 'function') {
|
||||
// @ts-ignore
|
||||
option = await option(this.baseCtx, this.baseApp);
|
||||
}
|
||||
// 判断是否有关联查询,有的话取个别名
|
||||
if (!_.isEmpty(option.join)) {
|
||||
for (const item of option.join) {
|
||||
selects.push(`${item.alias}.*`);
|
||||
find[item.type || 'leftJoin'](
|
||||
item.entity,
|
||||
item.alias,
|
||||
item.condition
|
||||
);
|
||||
}
|
||||
}
|
||||
// 默认条件
|
||||
if (option.where) {
|
||||
const wheres =
|
||||
typeof option.where === 'function'
|
||||
? await option.where(this.baseCtx, this.baseApp)
|
||||
: option.where;
|
||||
if (!_.isEmpty(wheres)) {
|
||||
for (const item of wheres) {
|
||||
if (
|
||||
item.length == 2 ||
|
||||
(item.length == 3 && (item[2] || item[2] === 0))
|
||||
) {
|
||||
for (const key in item[1]) {
|
||||
this.sqlParams.push(item[1][key]);
|
||||
}
|
||||
find.andWhere(item[0], item[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 附加排序
|
||||
if (!_.isEmpty(option.addOrderBy)) {
|
||||
for (const key in option.addOrderBy) {
|
||||
if (order && order == key) {
|
||||
sort = option.addOrderBy[key].toUpperCase();
|
||||
}
|
||||
find.addOrderBy(
|
||||
SqlString.escapeId(key),
|
||||
this.checkSort(option.addOrderBy[key].toUpperCase())
|
||||
);
|
||||
}
|
||||
}
|
||||
// 关键字模糊搜索
|
||||
if (keyWord || keyWord === 0) {
|
||||
keyWord = `%${keyWord}%`;
|
||||
find.andWhere(
|
||||
new Brackets(qb => {
|
||||
const keyWordLikeFields = option.keyWordLikeFields || [];
|
||||
for (let i = 0; i < option.keyWordLikeFields?.length || 0; i++) {
|
||||
qb.orWhere(`${keyWordLikeFields[i]} like :keyWord`, {
|
||||
keyWord,
|
||||
});
|
||||
this.sqlParams.push(keyWord);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
// 筛选字段
|
||||
if (!_.isEmpty(option.select)) {
|
||||
sqlArr.push(option.select.join(','));
|
||||
find.select(option.select);
|
||||
} else {
|
||||
sqlArr.push(selects.join(','));
|
||||
}
|
||||
// 字段全匹配
|
||||
if (!_.isEmpty(option.fieldEq)) {
|
||||
for (let key of option.fieldEq) {
|
||||
const c = {};
|
||||
// 如果key有包含.的情况下操作
|
||||
if (typeof key === 'string' && key.includes('.')) {
|
||||
const keys = key.split('.');
|
||||
const lastKey = keys.pop();
|
||||
key = { requestParam: lastKey, column: key };
|
||||
}
|
||||
// 单表字段无别名的情况下操作
|
||||
if (typeof key === 'string') {
|
||||
if (query[key] || query[key] === 0) {
|
||||
c[key] = query[key];
|
||||
const eq = query[key] instanceof Array ? 'in' : '=';
|
||||
if (eq === 'in') {
|
||||
find.andWhere(`${key} ${eq} (:${key})`, c);
|
||||
} else {
|
||||
find.andWhere(`${key} ${eq} :${key}`, c);
|
||||
}
|
||||
this.sqlParams.push(query[key]);
|
||||
}
|
||||
} else {
|
||||
if (query[key.requestParam] || query[key.requestParam] === 0) {
|
||||
c[key.column] = query[key.requestParam];
|
||||
const eq = query[key.requestParam] instanceof Array ? 'in' : '=';
|
||||
if (eq === 'in') {
|
||||
find.andWhere(`${key.column} ${eq} (:${key.column})`, c);
|
||||
} else {
|
||||
find.andWhere(`${key.column} ${eq} :${key.column}`, c);
|
||||
}
|
||||
this.sqlParams.push(query[key.requestParam]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sqlArr.push(selects.join(','));
|
||||
}
|
||||
// 接口请求的排序
|
||||
if (sort && order) {
|
||||
const sorts = sort.toUpperCase().split(',');
|
||||
const orders = order.split(',');
|
||||
if (sorts.length != orders.length) {
|
||||
throw new CoolValidateException(ERRINFO.SORTFIELD);
|
||||
}
|
||||
for (const i in sorts) {
|
||||
find.addOrderBy(
|
||||
SqlString.escapeId(orders[i]),
|
||||
this.checkSort(sorts[i])
|
||||
);
|
||||
}
|
||||
}
|
||||
if (option?.extend) {
|
||||
await option?.extend(find, this.baseCtx, this.baseApp);
|
||||
}
|
||||
const sqls = find.getSql().split('FROM');
|
||||
sqlArr.push('FROM');
|
||||
// 取sqls的最后一个
|
||||
sqlArr.push(sqls[sqls.length - 1]);
|
||||
return sqlArr.join(' ');
|
||||
}
|
||||
}
|
||||
640
rpc/src/service/postgres.ts
Normal file
640
rpc/src/service/postgres.ts
Normal file
@ -0,0 +1,640 @@
|
||||
import { Init, Provide, Inject, Config } from '@midwayjs/core';
|
||||
import {
|
||||
CoolValidateException,
|
||||
ERRINFO,
|
||||
EVENT,
|
||||
QueryOp,
|
||||
CoolEventManager,
|
||||
} from '@cool-midway/core';
|
||||
import { Application, Context } from '@midwayjs/koa';
|
||||
import { Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||||
import { Brackets, In, Repository, SelectQueryBuilder } from 'typeorm';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
/**
|
||||
* 服务基类
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export abstract class BasePgService {
|
||||
// 分页配置
|
||||
@Config('cool')
|
||||
private _coolConfig;
|
||||
|
||||
// 模型
|
||||
entity: Repository<any>;
|
||||
|
||||
sqlParams;
|
||||
|
||||
@Inject()
|
||||
typeORMDataSourceManager: TypeORMDataSourceManager;
|
||||
|
||||
@Inject()
|
||||
coolEventManager: CoolEventManager;
|
||||
|
||||
// 设置模型
|
||||
setEntity(entity: any) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
// 设置请求上下文
|
||||
setCtx(ctx: Context) {
|
||||
this.baseCtx = ctx;
|
||||
}
|
||||
|
||||
baseApp: Application;
|
||||
|
||||
// 设置应用对象
|
||||
setApp(app: Application) {
|
||||
this.baseApp = app;
|
||||
}
|
||||
|
||||
@Inject('ctx')
|
||||
baseCtx: Context;
|
||||
|
||||
// 初始化
|
||||
@Init()
|
||||
init() {
|
||||
this.sqlParams = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置sql
|
||||
* @param condition 条件是否成立
|
||||
* @param sql sql语句
|
||||
* @param params 参数
|
||||
*/
|
||||
setSql(condition, sql, params) {
|
||||
let rSql = false;
|
||||
if (condition || condition === 0) {
|
||||
rSql = true;
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const param = params[i];
|
||||
if (param instanceof Array) {
|
||||
// 将这个? 替换成 $1,$2,$3
|
||||
const replaceStr = [];
|
||||
for (let j = 0; j < param.length; j++) {
|
||||
replaceStr.push('$' + (this.sqlParams.length + j + 1));
|
||||
}
|
||||
this.sqlParams = this.sqlParams.concat(...params);
|
||||
sql = sql.replace('?', replaceStr.join(','));
|
||||
} else {
|
||||
sql = sql.replace('?', '$' + (this.sqlParams.length + 1));
|
||||
this.sqlParams.push(param);
|
||||
}
|
||||
}
|
||||
}
|
||||
return rSql ? sql : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得查询个数的SQL
|
||||
* @param sql
|
||||
*/
|
||||
getCountSql(sql) {
|
||||
sql = sql
|
||||
.replace(new RegExp('LIMIT', 'gm'), 'limit ')
|
||||
.replace(new RegExp('\n', 'gm'), ' ');
|
||||
if (sql.includes('limit')) {
|
||||
const sqlArr = sql.split('limit ');
|
||||
sqlArr.pop();
|
||||
sql = sqlArr.join('limit ');
|
||||
}
|
||||
return `select count(*) as count from (${sql}) a`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数安全性检查
|
||||
* @param params
|
||||
*/
|
||||
async paramSafetyCheck(params) {
|
||||
const lp = params.toLowerCase();
|
||||
return !(
|
||||
lp.indexOf('update ') > -1 ||
|
||||
lp.indexOf('select ') > -1 ||
|
||||
lp.indexOf('delete ') > -1 ||
|
||||
lp.indexOf('insert ') > -1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原生查询
|
||||
* @param sql
|
||||
* @param params
|
||||
* @param connectionName
|
||||
*/
|
||||
async nativeQuery(sql, params?, connectionName?) {
|
||||
sql = this.convertToPostgres(sql);
|
||||
if (_.isEmpty(params)) {
|
||||
params = this.sqlParams;
|
||||
}
|
||||
let newParams = [];
|
||||
// sql没处理过?的情况下
|
||||
if (sql.includes('?')) {
|
||||
for (const item of params) {
|
||||
// 如果是数组,将这个? 替换成 $1,$2,$3
|
||||
if (item instanceof Array) {
|
||||
const replaceStr = [];
|
||||
for (let i = 0; i < item.length; i++) {
|
||||
replaceStr.push('$' + (newParams.length + i + 1));
|
||||
}
|
||||
newParams.push(...item);
|
||||
sql = sql.replace('?', replaceStr.join(','));
|
||||
} else {
|
||||
sql = sql.replace('?', '$' + (newParams.length + 1));
|
||||
newParams.push(item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newParams = params;
|
||||
}
|
||||
this.sqlParams = [];
|
||||
return await this.getOrmManager(connectionName).query(sql, newParams || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得ORM管理
|
||||
* @param connectionName 连接名称
|
||||
*/
|
||||
getOrmManager(connectionName = 'default') {
|
||||
return this.typeORMDataSourceManager.getDataSource(connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作entity获得分页数据,不用写sql
|
||||
* @param find QueryBuilder
|
||||
* @param query
|
||||
* @param autoSort
|
||||
* @param connectionName
|
||||
*/
|
||||
async entityRenderPage(
|
||||
find: SelectQueryBuilder<any>,
|
||||
query,
|
||||
autoSort = true
|
||||
) {
|
||||
const {
|
||||
size = this._coolConfig.crud.pageSize,
|
||||
page = 1,
|
||||
order = 'createTime',
|
||||
sort = 'desc',
|
||||
isExport = false,
|
||||
maxExportLimit,
|
||||
} = query;
|
||||
const count = await find.getCount();
|
||||
let dataFind: SelectQueryBuilder<any>;
|
||||
if (isExport && maxExportLimit > 0) {
|
||||
dataFind = find.limit(maxExportLimit);
|
||||
} else {
|
||||
dataFind = find.offset((page - 1) * size).limit(size);
|
||||
}
|
||||
if (autoSort) {
|
||||
find.addOrderBy(order, sort.toUpperCase());
|
||||
}
|
||||
return {
|
||||
list: await dataFind.getRawMany(),
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
size: parseInt(size),
|
||||
total: count,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将mysql语句转换为postgres语句
|
||||
* @param sql
|
||||
* @returns
|
||||
*/
|
||||
protected convertToPostgres(sql) {
|
||||
// 首先确保表名被正确引用
|
||||
sql = sql.replace(/(?<!")(\b\w+\b)\.(?!\w+")/g, '"$1".');
|
||||
// 然后确保字段名被正确引用
|
||||
return sql.replace(/\.(\w+)(?!\w)/g, '."$1"');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询sql中的参数个数
|
||||
* @param sql
|
||||
* @returns
|
||||
*/
|
||||
protected countDollarSigns(sql) {
|
||||
const matches = sql.match(/\$\d+/g);
|
||||
return matches ? matches.length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行SQL并获得分页数据
|
||||
* @param sql 执行的sql语句
|
||||
* @param query 分页查询条件
|
||||
* @param autoSort 是否自动排序
|
||||
* @param connectionName 连接名称
|
||||
*/
|
||||
async sqlRenderPage(sql, query, autoSort = true, connectionName?) {
|
||||
const {
|
||||
size = this._coolConfig.crud.pageSize,
|
||||
page = 1,
|
||||
order = 'createTime',
|
||||
sort = 'desc',
|
||||
isExport = false,
|
||||
maxExportLimit,
|
||||
} = query;
|
||||
sql = `SELECT * FROM (${sql}) a `;
|
||||
if (order && sort && autoSort) {
|
||||
if (!(await this.paramSafetyCheck(order + sort))) {
|
||||
throw new CoolValidateException('非法传参~');
|
||||
}
|
||||
sql += `ORDER BY a."${order}" ${this.checkSort(sort)}`;
|
||||
}
|
||||
let cutParams = 0;
|
||||
const paramCount = this.countDollarSigns(sql);
|
||||
if (isExport && maxExportLimit > 0) {
|
||||
this.sqlParams.push(parseInt(maxExportLimit));
|
||||
cutParams = 1;
|
||||
sql += ` LIMIT $${paramCount + 1}`;
|
||||
}
|
||||
if (!isExport) {
|
||||
this.sqlParams.push(parseInt(size));
|
||||
this.sqlParams.push((page - 1) * size);
|
||||
cutParams = 2;
|
||||
sql += ` LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}`;
|
||||
}
|
||||
let params = [];
|
||||
params = params.concat(this.sqlParams);
|
||||
const result = await this.nativeQuery(sql, params, connectionName);
|
||||
params = params.slice(0, -cutParams);
|
||||
const countResult = await this.nativeQuery(
|
||||
this.getCountSql(sql),
|
||||
params,
|
||||
connectionName
|
||||
);
|
||||
return {
|
||||
list: result,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
size: parseInt(size),
|
||||
total: parseInt(countResult[0] ? countResult[0].count : 0),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查排序
|
||||
* @param sort 排序
|
||||
* @returns
|
||||
*/
|
||||
checkSort(sort) {
|
||||
if (!['desc', 'asc'].includes(sort.toLowerCase())) {
|
||||
throw new CoolValidateException('sort 非法传参~');
|
||||
}
|
||||
return sort;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得单个ID
|
||||
* @param id ID
|
||||
* @param infoIgnoreProperty 忽略返回属性
|
||||
*/
|
||||
async info(id: any, infoIgnoreProperty?: string[]) {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
if (!id) {
|
||||
throw new CoolValidateException(ERRINFO.NOID);
|
||||
}
|
||||
const info = await this.entity.findOneBy({ id });
|
||||
if (info && infoIgnoreProperty) {
|
||||
for (const property of infoIgnoreProperty) {
|
||||
delete info[property];
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3
|
||||
*/
|
||||
async delete(ids: any) {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
if (ids instanceof String) {
|
||||
ids = ids.split(',');
|
||||
}
|
||||
// 启动软删除发送事件
|
||||
if (this._coolConfig.crud?.softDelete) {
|
||||
this.softDelete(ids);
|
||||
}
|
||||
await this.entity.delete(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除
|
||||
* @param ids 删除的ID数组
|
||||
* @param entity 实体
|
||||
*/
|
||||
async softDelete(ids: number[], entity?: Repository<any>) {
|
||||
const data = await this.entity.find({
|
||||
where: {
|
||||
id: In(ids),
|
||||
},
|
||||
});
|
||||
if (_.isEmpty(data)) return;
|
||||
const _entity = entity ? entity : this.entity;
|
||||
const params = {
|
||||
data,
|
||||
ctx: this.baseCtx,
|
||||
entity: _entity,
|
||||
};
|
||||
if (data.length > 0) {
|
||||
this.coolEventManager.emit(EVENT.SOFT_DELETE, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增|修改
|
||||
* @param param 数据
|
||||
*/
|
||||
async addOrUpdate(param: any | any[], type: 'add' | 'update' = 'add') {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
delete param.createTime;
|
||||
// 判断是否是批量操作
|
||||
if (param instanceof Array) {
|
||||
param.forEach(item => {
|
||||
item.updateTime = new Date();
|
||||
item.createTime = new Date();
|
||||
});
|
||||
await this.entity.save(param);
|
||||
} else {
|
||||
const upsert = this._coolConfig.crud?.upsert || 'normal';
|
||||
if (type == 'update') {
|
||||
if (upsert == 'save') {
|
||||
const info = await this.entity.findOneBy({ id: param.id });
|
||||
param = {
|
||||
...info,
|
||||
...param,
|
||||
};
|
||||
}
|
||||
param.updateTime = new Date();
|
||||
upsert == 'normal'
|
||||
? await this.entity.update(param.id, param)
|
||||
: await this.entity.save(param);
|
||||
}
|
||||
if (type == 'add') {
|
||||
param.createTime = new Date();
|
||||
param.updateTime = new Date();
|
||||
upsert == 'normal'
|
||||
? await this.entity.insert(param)
|
||||
: await this.entity.save(param);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 非分页查询
|
||||
* @param query 查询条件
|
||||
* @param option 查询配置
|
||||
* @param connectionName 连接名
|
||||
*/
|
||||
async list(query, option, connectionName?): Promise<any> {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
const sql = await this.getOptionFind(query, option);
|
||||
return this.nativeQuery(sql, [], connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
* @param query 查询条件
|
||||
* @param option 查询配置
|
||||
* @param connectionName 连接名
|
||||
*/
|
||||
async page(query, option, connectionName?) {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
const sql = await this.getOptionFind(query, option);
|
||||
return this.sqlRenderPage(sql, query, false, connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询配置
|
||||
* @param query 前端查询
|
||||
* @param option
|
||||
*/
|
||||
async getOptionFind(query, option: QueryOp) {
|
||||
let { order = 'createTime', sort = 'desc', keyWord = '' } = query;
|
||||
const sqlArr = ['SELECT'];
|
||||
const selects = ['a.*'];
|
||||
const find = this.entity.createQueryBuilder('a');
|
||||
if (option) {
|
||||
if (typeof option === 'function') {
|
||||
// @ts-ignore
|
||||
option = await option(this.baseCtx, this.baseApp);
|
||||
}
|
||||
// 判断是否有关联查询,有的话取个别名
|
||||
if (!_.isEmpty(option.join)) {
|
||||
for (const item of option.join) {
|
||||
selects.push(`${item.alias}.*`);
|
||||
find[item.type || 'leftJoin'](
|
||||
item.entity,
|
||||
item.alias,
|
||||
item.condition
|
||||
);
|
||||
}
|
||||
}
|
||||
// 默认条件
|
||||
if (option.where) {
|
||||
const wheres =
|
||||
typeof option.where === 'function'
|
||||
? await option.where(this.baseCtx, this.baseApp)
|
||||
: option.where;
|
||||
if (!_.isEmpty(wheres)) {
|
||||
for (const item of wheres) {
|
||||
if (
|
||||
item.length == 2 ||
|
||||
(item.length == 3 && (item[2] || item[2] === 0))
|
||||
) {
|
||||
for (const key in item[1]) {
|
||||
this.sqlParams.push(item[1][key]);
|
||||
}
|
||||
find.andWhere(item[0], item[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 附加排序
|
||||
if (!_.isEmpty(option.addOrderBy)) {
|
||||
for (const key in option.addOrderBy) {
|
||||
if (order && order == key) {
|
||||
sort = option.addOrderBy[key].toUpperCase();
|
||||
}
|
||||
find.addOrderBy(
|
||||
`${this.matchColumn(option?.select, key)}.${key}`,
|
||||
this.checkSort(option.addOrderBy[key].toUpperCase())
|
||||
);
|
||||
}
|
||||
}
|
||||
// 关键字模糊搜索
|
||||
if (keyWord || keyWord == 0) {
|
||||
keyWord = `%${keyWord}%`;
|
||||
find.andWhere(
|
||||
new Brackets(qb => {
|
||||
const keyWordLikeFields = option.keyWordLikeFields || [];
|
||||
for (let i = 0; i < option.keyWordLikeFields?.length || 0; i++) {
|
||||
let column = keyWordLikeFields[i];
|
||||
column = column.includes('.') ? column : `a.${column}`;
|
||||
const values = {};
|
||||
values[`keyWord${i}`] = keyWord;
|
||||
qb.orWhere(`${column} like :keyWord${i}`, values);
|
||||
this.sqlParams.push(keyWord);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
// 筛选字段
|
||||
if (!_.isEmpty(option.select)) {
|
||||
sqlArr.push(option.select.join(','));
|
||||
find.select(option.select);
|
||||
} else {
|
||||
sqlArr.push(selects.join(','));
|
||||
}
|
||||
// 字段全匹配
|
||||
if (!_.isEmpty(option.fieldEq)) {
|
||||
for (let key of option.fieldEq) {
|
||||
const c = {};
|
||||
let column;
|
||||
// 如果key有包含.的情况下操作
|
||||
if (typeof key === 'string' && key.includes('.')) {
|
||||
const keys = key.split('.');
|
||||
const lastKey = keys.pop();
|
||||
key = { requestParam: lastKey, column: key };
|
||||
column = key;
|
||||
} else {
|
||||
column = `a.${key}`;
|
||||
}
|
||||
// 单表字段无别名的情况下操作
|
||||
if (typeof key === 'string') {
|
||||
if (query[key] || query[key] == 0) {
|
||||
c[key] = query[key];
|
||||
const eq = query[key] instanceof Array ? 'in' : '=';
|
||||
if (eq === 'in') {
|
||||
find.andWhere(`${column} ${eq} (:...${key})`, c);
|
||||
} else {
|
||||
find.andWhere(`${column} ${eq} :${key}`, c);
|
||||
}
|
||||
this.sqlParams.push(query[key]);
|
||||
}
|
||||
} else {
|
||||
if (query[key.requestParam] || query[key.requestParam] == 0) {
|
||||
c[key.column] = query[key.requestParam];
|
||||
const eq = query[key.requestParam] instanceof Array ? 'in' : '=';
|
||||
if (eq === 'in') {
|
||||
find.andWhere(`${key.column} ${eq} (:${key.column})`, c);
|
||||
} else {
|
||||
find.andWhere(`${key.column} ${eq} :${key.column}`, c);
|
||||
}
|
||||
this.sqlParams.push(query[key.requestParam]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sqlArr.push(selects.join(','));
|
||||
}
|
||||
// 接口请求的排序
|
||||
if (sort && order) {
|
||||
const sorts = sort.toUpperCase().split(',');
|
||||
const orders = order.split(',');
|
||||
if (sorts.length != orders.length) {
|
||||
throw new CoolValidateException(ERRINFO.SORTFIELD);
|
||||
}
|
||||
for (const i in sorts) {
|
||||
find.addOrderBy(
|
||||
`${this.matchColumn(option?.select, orders[i])}.${orders[i]}`,
|
||||
this.checkSort(sorts[i])
|
||||
);
|
||||
}
|
||||
}
|
||||
if (option?.extend) {
|
||||
await option?.extend(find, this.baseCtx, this.baseApp);
|
||||
}
|
||||
const sqls = find.getSql().split('FROM');
|
||||
sqlArr.push('FROM');
|
||||
// 取sqls的最后一个
|
||||
sqlArr.push(sqls[sqls.length - 1]);
|
||||
sqlArr.forEach((item, index) => {
|
||||
if (item.includes('ORDER BY')) {
|
||||
sqlArr[index] = this.replaceOrderByPrefix(item);
|
||||
}
|
||||
});
|
||||
return sqlArr.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换sql中的表别名
|
||||
* @param sql
|
||||
* @returns
|
||||
*/
|
||||
replaceOrderByPrefix(sql) {
|
||||
// 使用正则表达式匹配 ORDER BY 后面的部分
|
||||
// 这里假设 ORDER BY 后面跟着的是由空格分隔的字段名,且字段名由双引号包围
|
||||
const orderByRegex =
|
||||
/ORDER BY\s+("[^"]+_[^"]+")(\s*(ASC|DESC)?\s*(,\s*"[^"]+_[^"]+")*)/gi;
|
||||
|
||||
// 定义替换函数
|
||||
// @ts-ignore
|
||||
function replaceMatch(match, p1, p2) {
|
||||
// 将 p1 中的 "a_" 替换为 "a."
|
||||
const replacedField = p1.replace(/a_([^"]+)/g, 'a.$1');
|
||||
// 如果有其他字段,递归调用替换函数
|
||||
const replacedRest = p2.replace(/("[^"]+_)/g, (m, p) =>
|
||||
p.replace('a_', 'a.')
|
||||
);
|
||||
// 组合替换后的字段和其他部分
|
||||
return `ORDER BY ${replacedField.replace(/"/g, '')}${replacedRest.replace(
|
||||
/"/g,
|
||||
''
|
||||
)}`;
|
||||
}
|
||||
|
||||
// 使用替换函数替换匹配到的内容
|
||||
const replacedOrderBySql = sql.replace(orderByRegex, replaceMatch);
|
||||
|
||||
// 移除所有双引号
|
||||
const sqlWithoutQuotes = replacedOrderBySql.replace(/"/g, '');
|
||||
|
||||
return sqlWithoutQuotes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选的字段匹配
|
||||
* @param select 筛选的字段
|
||||
* @param field 字段
|
||||
* @returns 字段在哪个表中
|
||||
*/
|
||||
protected matchColumn(select: string[] = [], field: string) {
|
||||
for (const column of select) {
|
||||
// 检查字段是否有别名,考虑 'AS' 关键字的不同大小写形式
|
||||
const aliasPattern = new RegExp(`\\b\\w+\\s+as\\s+${field}\\b`, 'i');
|
||||
const aliasMatch = column.match(aliasPattern);
|
||||
if (aliasMatch) {
|
||||
// 提取别名前的字段和表名
|
||||
const fieldPattern = new RegExp(
|
||||
`(\\w+)\\.(\\w+)\\s+as\\s+${field}`,
|
||||
'i'
|
||||
);
|
||||
const fieldMatch = column.match(fieldPattern);
|
||||
if (fieldMatch) {
|
||||
// 返回匹配到的表名
|
||||
return fieldMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
// 检查字段是否直接在选择列表中
|
||||
const fieldPattern = new RegExp(`\\b(\\w+)\\.${field}\\b`, 'i');
|
||||
const fieldMatch = column.match(fieldPattern);
|
||||
if (fieldMatch) {
|
||||
// 如果直接匹配到字段,返回字段所属的表名
|
||||
return fieldMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配到任何特定的表或别名,返回默认的 'a' 表
|
||||
return 'a';
|
||||
}
|
||||
}
|
||||
558
rpc/src/service/sqlite.ts
Normal file
558
rpc/src/service/sqlite.ts
Normal file
@ -0,0 +1,558 @@
|
||||
import { Init, Provide, Inject, Config, ALL } from '@midwayjs/core';
|
||||
import { Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import {
|
||||
CoolValidateException,
|
||||
ERRINFO,
|
||||
EVENT,
|
||||
QueryOp,
|
||||
CoolEventManager,
|
||||
} from '@cool-midway/core';
|
||||
import { Application, Context } from '@midwayjs/koa';
|
||||
import * as SqlString from 'sqlstring';
|
||||
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||||
import { Brackets, In, Repository, SelectQueryBuilder } from 'typeorm';
|
||||
import * as _ from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
/**
|
||||
* 服务基类
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export abstract class BaseSqliteService {
|
||||
// 分页配置
|
||||
@Config('cool')
|
||||
private _coolConfig;
|
||||
|
||||
// 模型
|
||||
entity: Repository<any>;
|
||||
|
||||
sqlParams;
|
||||
|
||||
@Inject()
|
||||
typeORMDataSourceManager: TypeORMDataSourceManager;
|
||||
|
||||
@Inject()
|
||||
coolEventManager: CoolEventManager;
|
||||
|
||||
@Config(ALL)
|
||||
allConfig: any;
|
||||
|
||||
// 设置模型
|
||||
setEntity(entity: any) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
// 设置请求上下文
|
||||
setCtx(ctx: Context) {
|
||||
this.baseCtx = ctx;
|
||||
}
|
||||
|
||||
baseApp: Application;
|
||||
|
||||
// 设置应用对象
|
||||
setApp(app: Application) {
|
||||
this.baseApp = app;
|
||||
}
|
||||
|
||||
@Inject('ctx')
|
||||
baseCtx: Context;
|
||||
|
||||
// 初始化
|
||||
@Init()
|
||||
init() {
|
||||
this.sqlParams = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置sql
|
||||
* @param condition 条件是否成立
|
||||
* @param sql sql语句
|
||||
* @param params 参数
|
||||
*/
|
||||
setSql(condition, sql, params) {
|
||||
let rSql = false;
|
||||
if (condition || condition === 0) {
|
||||
rSql = true;
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const param = params[i];
|
||||
if (param instanceof Array) {
|
||||
// 将这个? 替换成 $1,$2,$3
|
||||
const replaceStr = [];
|
||||
for (let j = 0; j < param.length; j++) {
|
||||
replaceStr.push('$' + (this.sqlParams.length + j + 1));
|
||||
}
|
||||
this.sqlParams = this.sqlParams.concat(...params);
|
||||
sql = sql.replace('?', replaceStr.join(','));
|
||||
} else {
|
||||
sql = sql.replace('?', '$' + (this.sqlParams.length + 1));
|
||||
this.sqlParams.push(param);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (rSql ? sql : '').replace(/\$\d+/g, '?');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得查询个数的SQL
|
||||
* @param sql
|
||||
*/
|
||||
getCountSql(sql) {
|
||||
sql = sql
|
||||
.replace(new RegExp('LIMIT', 'gm'), 'limit ')
|
||||
.replace(new RegExp('\n', 'gm'), ' ');
|
||||
if (sql.includes('limit')) {
|
||||
const sqlArr = sql.split('limit ');
|
||||
sqlArr.pop();
|
||||
sql = sqlArr.join('limit ');
|
||||
}
|
||||
return `select count(*) as count from (${sql}) a`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数安全性检查
|
||||
* @param params
|
||||
*/
|
||||
async paramSafetyCheck(params) {
|
||||
const lp = params.toLowerCase();
|
||||
return !(
|
||||
lp.indexOf('update ') > -1 ||
|
||||
lp.indexOf('select ') > -1 ||
|
||||
lp.indexOf('delete ') > -1 ||
|
||||
lp.indexOf('insert ') > -1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原生查询
|
||||
* @param sql
|
||||
* @param params
|
||||
* @param connectionName
|
||||
*/
|
||||
async nativeQuery(sql, params?, connectionName?) {
|
||||
if (_.isEmpty(params)) {
|
||||
params = this.sqlParams;
|
||||
}
|
||||
const newParams = [];
|
||||
// sql没处理过?的情况下
|
||||
for (const item of params) {
|
||||
// 如果是数组,将这个? 替换成 $1,$2,$3
|
||||
if (item instanceof Array) {
|
||||
const replaceStr = [];
|
||||
for (let i = 0; i < item.length; i++) {
|
||||
replaceStr.push('$' + (newParams.length + i + 1));
|
||||
}
|
||||
newParams.push(...item);
|
||||
sql = sql.replace('?', replaceStr.join(','));
|
||||
} else {
|
||||
sql = sql.replace('?', '$' + (newParams.length + 1));
|
||||
newParams.push(item);
|
||||
}
|
||||
}
|
||||
this.sqlParams = [];
|
||||
return await this.getOrmManager(connectionName).query(
|
||||
sql.replace(/\$\d+/g, '?'),
|
||||
newParams || []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得ORM管理
|
||||
* @param connectionName 连接名称
|
||||
*/
|
||||
getOrmManager(connectionName = 'default') {
|
||||
return this.typeORMDataSourceManager.getDataSource(connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作entity获得分页数据,不用写sql
|
||||
* @param find QueryBuilder
|
||||
* @param query
|
||||
* @param autoSort
|
||||
* @param connectionName
|
||||
*/
|
||||
async entityRenderPage(
|
||||
find: SelectQueryBuilder<any>,
|
||||
query,
|
||||
autoSort = true
|
||||
) {
|
||||
const {
|
||||
size = this._coolConfig.crud.pageSize,
|
||||
page = 1,
|
||||
order = 'createTime',
|
||||
sort = 'desc',
|
||||
isExport = false,
|
||||
maxExportLimit,
|
||||
} = query;
|
||||
const count = await find.getCount();
|
||||
let dataFind: SelectQueryBuilder<any>;
|
||||
if (isExport && maxExportLimit > 0) {
|
||||
dataFind = find.limit(maxExportLimit);
|
||||
} else {
|
||||
dataFind = find.offset((page - 1) * size).limit(size);
|
||||
}
|
||||
if (autoSort) {
|
||||
find.addOrderBy(order, sort.toUpperCase());
|
||||
}
|
||||
return {
|
||||
list: await dataFind.getRawMany(),
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
size: parseInt(size),
|
||||
total: count,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行SQL并获得分页数据
|
||||
* @param sql 执行的sql语句
|
||||
* @param query 分页查询条件
|
||||
* @param autoSort 是否自动排序
|
||||
* @param connectionName 连接名称
|
||||
*/
|
||||
async sqlRenderPage(sql, query, autoSort = true, connectionName?) {
|
||||
const {
|
||||
size = this._coolConfig.crud.pageSize,
|
||||
page = 1,
|
||||
order = 'createTime',
|
||||
sort = 'desc',
|
||||
isExport = false,
|
||||
maxExportLimit,
|
||||
} = query;
|
||||
sql = `SELECT * FROM (${sql}) a`;
|
||||
if (order && sort && autoSort) {
|
||||
if (!(await this.paramSafetyCheck(order + sort))) {
|
||||
throw new CoolValidateException('非法传参~');
|
||||
}
|
||||
sql += ` ORDER BY a.${SqlString.escapeId(order)} ${this.checkSort(sort)}`;
|
||||
}
|
||||
let cutParams = 0;
|
||||
if (isExport && maxExportLimit > 0) {
|
||||
this.sqlParams.push(parseInt(maxExportLimit));
|
||||
cutParams = 1;
|
||||
sql += ' LIMIT ? ';
|
||||
}
|
||||
if (!isExport) {
|
||||
this.sqlParams.push((page - 1) * size);
|
||||
this.sqlParams.push(parseInt(size));
|
||||
cutParams = 2;
|
||||
sql += ' LIMIT ?,? ';
|
||||
}
|
||||
|
||||
let params = [];
|
||||
params = params.concat(this.sqlParams);
|
||||
const result = await this.nativeQuery(sql, params, connectionName);
|
||||
params = params.slice(0, -cutParams);
|
||||
const countResult = await this.nativeQuery(
|
||||
this.getCountSql(sql),
|
||||
params,
|
||||
connectionName
|
||||
);
|
||||
return {
|
||||
list: result,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
size: parseInt(size),
|
||||
total: parseInt(countResult[0] ? countResult[0].count : 0),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查排序
|
||||
* @param sort 排序
|
||||
* @returns
|
||||
*/
|
||||
checkSort(sort) {
|
||||
if (!['desc', 'asc'].includes(sort.toLowerCase())) {
|
||||
throw new CoolValidateException('sort 非法传参~');
|
||||
}
|
||||
return sort;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得单个ID
|
||||
* @param id ID
|
||||
* @param infoIgnoreProperty 忽略返回属性
|
||||
*/
|
||||
async info(id: any, infoIgnoreProperty?: string[]) {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
if (!id) {
|
||||
throw new CoolValidateException(ERRINFO.NOID);
|
||||
}
|
||||
const info = await this.entity.findOneBy({ id });
|
||||
if (info && infoIgnoreProperty) {
|
||||
for (const property of infoIgnoreProperty) {
|
||||
delete info[property];
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3
|
||||
*/
|
||||
async delete(ids: any) {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
if (ids instanceof String) {
|
||||
ids = ids.split(',');
|
||||
}
|
||||
// 启动软删除发送事件
|
||||
if (this._coolConfig.crud?.softDelete) {
|
||||
this.softDelete(ids);
|
||||
}
|
||||
await this.entity.delete(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除
|
||||
* @param ids 删除的ID数组
|
||||
* @param entity 实体
|
||||
*/
|
||||
async softDelete(ids: number[], entity?: Repository<any>) {
|
||||
const data = await this.entity.find({
|
||||
where: {
|
||||
id: In(ids),
|
||||
},
|
||||
});
|
||||
if (_.isEmpty(data)) return;
|
||||
const _entity = entity ? entity : this.entity;
|
||||
const params = {
|
||||
data,
|
||||
ctx: this.baseCtx,
|
||||
entity: _entity,
|
||||
};
|
||||
if (data.length > 0) {
|
||||
this.coolEventManager.emit(EVENT.SOFT_DELETE, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据时区偏移获取日期
|
||||
* @param timezone 时区偏移,格式如 '+08:00', '-07:00'
|
||||
* @returns Date
|
||||
*/
|
||||
getDateWithTimezone(): Date {
|
||||
const timezone =
|
||||
this.allConfig.typeorm?.dataSource?.default?.timezone || '+08:00';
|
||||
const match = timezone.match(/^([+-])(\d{2}):(\d{2})$/);
|
||||
if (!match) {
|
||||
throw new CoolValidateException('时区格式错误,应为 "+08:00" 这样的格式');
|
||||
}
|
||||
|
||||
const [_, sign, hours, minutes] = match;
|
||||
const offsetInMinutes =
|
||||
(parseInt(hours) * 60 + parseInt(minutes)) * (sign === '+' ? 1 : -1);
|
||||
const date = new Date();
|
||||
const utc = date.getTime() + date.getTimezoneOffset() * 60000;
|
||||
return new Date(utc + offsetInMinutes * 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增|修改
|
||||
* @param param 数据
|
||||
*/
|
||||
async addOrUpdate(param: any | any[], type: 'add' | 'update' = 'add') {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
delete param.createTime;
|
||||
// 判断是否是批量操作
|
||||
if (param instanceof Array) {
|
||||
param.forEach(item => {
|
||||
// 设置时区+08:00
|
||||
item.updateTime = new Date();
|
||||
item.createTime = new Date();
|
||||
});
|
||||
await this.entity.save(param);
|
||||
} else {
|
||||
const upsert = this._coolConfig.crud?.upsert || 'normal';
|
||||
if (type == 'update') {
|
||||
if (upsert == 'save') {
|
||||
const info = await this.entity.findOneBy({ id: param.id });
|
||||
param = {
|
||||
...info,
|
||||
...param,
|
||||
};
|
||||
}
|
||||
param.updateTime = new Date();
|
||||
upsert == 'normal'
|
||||
? await this.entity.update(param.id, param)
|
||||
: await this.entity.save(param);
|
||||
}
|
||||
if (type == 'add') {
|
||||
param.createTime = moment(new Date()).format('YYYY-MM-DD HH:mm:ss');
|
||||
param.updateTime = moment(new Date()).format('YYYY-MM-DD HH:mm:ss');
|
||||
upsert == 'normal'
|
||||
? await this.entity.insert(param)
|
||||
: await this.entity.save(param);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 非分页查询
|
||||
* @param query 查询条件
|
||||
* @param option 查询配置
|
||||
* @param connectionName 连接名
|
||||
*/
|
||||
async list(query, option, connectionName?): Promise<any> {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
const sql = await this.getOptionFind(query, option);
|
||||
return this.nativeQuery(sql, [], connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
* @param query 查询条件
|
||||
* @param option 查询配置
|
||||
* @param connectionName 连接名
|
||||
*/
|
||||
async page(query, option, connectionName?) {
|
||||
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
|
||||
const sql = await this.getOptionFind(query, option);
|
||||
return this.sqlRenderPage(sql, query, false, connectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询配置
|
||||
* @param query 前端查询
|
||||
* @param option
|
||||
*/
|
||||
async getOptionFind(query, option: QueryOp) {
|
||||
let { order = 'createTime', sort = 'desc', keyWord = '' } = query;
|
||||
const sqlArr = ['SELECT'];
|
||||
const selects = ['a.*'];
|
||||
const find = this.entity.createQueryBuilder('a');
|
||||
if (option) {
|
||||
if (typeof option === 'function') {
|
||||
// @ts-ignore
|
||||
option = await option(this.baseCtx, this.baseApp);
|
||||
}
|
||||
// 判断是否有关联查询,有的话取个别名
|
||||
if (!_.isEmpty(option.join)) {
|
||||
for (const item of option.join) {
|
||||
selects.push(`${item.alias}.*`);
|
||||
find[item.type || 'leftJoin'](
|
||||
item.entity,
|
||||
item.alias,
|
||||
item.condition
|
||||
);
|
||||
}
|
||||
}
|
||||
// 默认条件
|
||||
if (option.where) {
|
||||
const wheres =
|
||||
typeof option.where === 'function'
|
||||
? await option.where(this.baseCtx, this.baseApp)
|
||||
: option.where;
|
||||
if (!_.isEmpty(wheres)) {
|
||||
for (const item of wheres) {
|
||||
if (
|
||||
item.length == 2 ||
|
||||
(item.length == 3 && (item[2] || item[2] === 0))
|
||||
) {
|
||||
for (const key in item[1]) {
|
||||
this.sqlParams.push(item[1][key]);
|
||||
}
|
||||
find.andWhere(item[0], item[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 附加排序
|
||||
if (!_.isEmpty(option.addOrderBy)) {
|
||||
for (const key in option.addOrderBy) {
|
||||
if (order && order == key) {
|
||||
sort = option.addOrderBy[key].toUpperCase();
|
||||
}
|
||||
find.addOrderBy(
|
||||
SqlString.escapeId(key),
|
||||
this.checkSort(option.addOrderBy[key].toUpperCase())
|
||||
);
|
||||
}
|
||||
}
|
||||
// 关键字模糊搜索
|
||||
if (keyWord || keyWord === 0) {
|
||||
keyWord = `%${keyWord}%`;
|
||||
find.andWhere(
|
||||
new Brackets(qb => {
|
||||
const keyWordLikeFields = option.keyWordLikeFields || [];
|
||||
for (let i = 0; i < option.keyWordLikeFields?.length || 0; i++) {
|
||||
qb.orWhere(`${keyWordLikeFields[i]} like :keyWord`, {
|
||||
keyWord,
|
||||
});
|
||||
this.sqlParams.push(keyWord);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
// 筛选字段
|
||||
if (!_.isEmpty(option.select)) {
|
||||
sqlArr.push(option.select.join(','));
|
||||
find.select(option.select);
|
||||
} else {
|
||||
sqlArr.push(selects.join(','));
|
||||
}
|
||||
// 字段全匹配
|
||||
if (!_.isEmpty(option.fieldEq)) {
|
||||
for (let key of option.fieldEq) {
|
||||
const c = {};
|
||||
// 如果key有包含.的情况下操作
|
||||
if (typeof key === 'string' && key.includes('.')) {
|
||||
const keys = key.split('.');
|
||||
const lastKey = keys.pop();
|
||||
key = { requestParam: lastKey, column: key };
|
||||
}
|
||||
// 单表字段无别名的情况下操作
|
||||
if (typeof key === 'string') {
|
||||
if (query[key] || query[key] == 0) {
|
||||
c[key] = query[key];
|
||||
const eq = query[key] instanceof Array ? 'in' : '=';
|
||||
if (eq === 'in') {
|
||||
find.andWhere(`${key} ${eq} (:${key})`, c);
|
||||
} else {
|
||||
find.andWhere(`${key} ${eq} :${key}`, c);
|
||||
}
|
||||
// this.sqlParams.push(query[key]);
|
||||
}
|
||||
} else {
|
||||
if (query[key.requestParam] || query[key.requestParam] == 0) {
|
||||
c[key.column] = query[key.requestParam];
|
||||
const eq = query[key.requestParam] instanceof Array ? 'in' : '=';
|
||||
if (eq === 'in') {
|
||||
find.andWhere(`${key.column} ${eq} (:${key.column})`, c);
|
||||
} else {
|
||||
find.andWhere(`${key.column} ${eq} :${key.column}`, c);
|
||||
}
|
||||
// this.sqlParams.push(query[key.requestParam]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sqlArr.push(selects.join(','));
|
||||
}
|
||||
// 接口请求的排序
|
||||
if (sort && order) {
|
||||
const sorts = sort.toUpperCase().split(',');
|
||||
const orders = order.split(',');
|
||||
if (sorts.length != orders.length) {
|
||||
throw new CoolValidateException(ERRINFO.SORTFIELD);
|
||||
}
|
||||
for (const i in sorts) {
|
||||
find.addOrderBy(
|
||||
SqlString.escapeId(orders[i]),
|
||||
this.checkSort(sorts[i])
|
||||
);
|
||||
}
|
||||
}
|
||||
if (option?.extend) {
|
||||
await option?.extend(find, this.baseCtx, this.baseApp);
|
||||
}
|
||||
const sqls = find.getSql().split('FROM');
|
||||
sqlArr.push('FROM');
|
||||
// 取sqls的最后一个
|
||||
sqlArr.push(sqls[sqls.length - 1]);
|
||||
return sqlArr.join(' ');
|
||||
}
|
||||
}
|
||||
25
rpc/src/test.ts
Normal file
25
rpc/src/test.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Controller, Inject, Post, Provide } from '@midwayjs/core';
|
||||
import { BaseController } from '@cool-midway/core';
|
||||
import { CoolRpc } from './rpc';
|
||||
|
||||
/**
|
||||
* 本地开发调试
|
||||
*/
|
||||
@Provide()
|
||||
@Controller('/rpc')
|
||||
export class RpcTestController extends BaseController {
|
||||
@Inject()
|
||||
rpc: CoolRpc;
|
||||
|
||||
@Inject()
|
||||
ctx;
|
||||
|
||||
/**
|
||||
* 测试
|
||||
*/
|
||||
@Post('/test')
|
||||
async test() {
|
||||
const { name, service, method, params } = this.ctx.request.body;
|
||||
return this.rpc.call(name, service, method, params);
|
||||
}
|
||||
}
|
||||
40
rpc/src/transaction/event.ts
Normal file
40
rpc/src/transaction/event.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Logger, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { CoolRpcEvent, CoolRpcEventHandler } from '..';
|
||||
import { ILogger } from '@midwayjs/logger';
|
||||
|
||||
/**
|
||||
* moleculer 事件处理
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
@CoolRpcEvent()
|
||||
export class MoleculerTransactionHandler {
|
||||
@Logger()
|
||||
coreLogger: ILogger;
|
||||
|
||||
/**
|
||||
* 注册事件
|
||||
* @param params
|
||||
*/
|
||||
@CoolRpcEventHandler('moleculer.transaction') // 唯一参数,eventName,事件名,可不填,默认为方法名
|
||||
async handler(params) {
|
||||
const { rpcTransactionId, commit } = params;
|
||||
this.coreLogger.info(
|
||||
`\x1B[36m [cool:core] MoleculerTransaction event params: ${JSON.stringify(
|
||||
params
|
||||
)} \x1B[0m`
|
||||
);
|
||||
if (global['moleculer.transactions'][rpcTransactionId]) {
|
||||
this.coreLogger.info(
|
||||
`\x1B[36m [cool:core] MoleculerTransaction event ${
|
||||
commit ? 'commitTransaction' : 'rollbackTransaction'
|
||||
} ID: ${rpcTransactionId} \x1B[0m`
|
||||
);
|
||||
await global['moleculer.transactions'][rpcTransactionId][
|
||||
commit ? 'commitTransaction' : 'rollbackTransaction'
|
||||
]();
|
||||
await global['moleculer.transactions'][rpcTransactionId].release();
|
||||
delete global['moleculer.transactions'][rpcTransactionId];
|
||||
}
|
||||
}
|
||||
}
|
||||
28
rpc/tsconfig.json
Normal file
28
rpc/tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"inlineSourceMap":false,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"stripInternal": true,
|
||||
"skipLibCheck": true,
|
||||
"noImplicitReturns": false,
|
||||
"pretty": true,
|
||||
"declaration": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"exclude": [
|
||||
"*.js",
|
||||
"*.ts",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"test"
|
||||
]
|
||||
}
|
||||
@ -4,9 +4,6 @@
|
||||
"description": "cool-admin midway task",
|
||||
"main": "dist/index.js",
|
||||
"typings": "index.d.ts",
|
||||
"bin": {
|
||||
"cool": "dist/bin/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "mwtsc --cleanOutDir",
|
||||
"test": "cross-env NODE_ENV=unittest jest",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user