mirror of
https://github.com/cool-team-official/cool-admin-midway.git
synced 2025-12-10 08:02:48 +00:00
完善多租户&国际化
This commit is contained in:
parent
b165e97500
commit
71ee2e7393
2
.vscode/middleware.code-snippets
vendored
2
.vscode/middleware.code-snippets
vendored
@ -2,7 +2,7 @@
|
||||
"middleware": {
|
||||
"prefix": "middleware",
|
||||
"body": [
|
||||
"import { Middleware } from '@midwayjs/decorator';",
|
||||
"import { Middleware } from '@midwayjs/core';",
|
||||
"import { NextFunction, Context } from '@midwayjs/koa';",
|
||||
"import { IMiddleware } from '@midwayjs/core';",
|
||||
"",
|
||||
|
||||
3
.vscode/service.code-snippets
vendored
3
.vscode/service.code-snippets
vendored
@ -2,7 +2,8 @@
|
||||
"service": {
|
||||
"prefix": "service",
|
||||
"body": [
|
||||
"import { BaseService, Init, Provide } from '@cool-midway/core';",
|
||||
"import { Init, Provide } from '@midwayjs/core';",
|
||||
"import { BaseService } from '@cool-midway/core';",
|
||||
"import { InjectEntityModel } from '@midwayjs/typeorm';",
|
||||
"import { Repository } from 'typeorm';",
|
||||
"",
|
||||
|
||||
1
bootstrap.js
vendored
1
bootstrap.js
vendored
@ -1,4 +1,3 @@
|
||||
process.env.NODE_ENV = 'local';
|
||||
const { Bootstrap } = require('@midwayjs/bootstrap');
|
||||
|
||||
// 显式以组件方式引入用户代码
|
||||
|
||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@ -1344,8 +1344,8 @@ packages:
|
||||
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
caniuse-lite@1.0.30001692:
|
||||
resolution: {integrity: sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==}
|
||||
caniuse-lite@1.0.30001695:
|
||||
resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==}
|
||||
|
||||
chalk@2.4.2:
|
||||
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
||||
@ -3179,6 +3179,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
mkdirp@2.1.6:
|
||||
resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
mkdirp@3.0.1:
|
||||
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
||||
engines: {node: '>=10'}
|
||||
@ -6106,7 +6111,7 @@ snapshots:
|
||||
|
||||
browserslist@4.24.4:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001692
|
||||
caniuse-lite: 1.0.30001695
|
||||
electron-to-chromium: 1.5.83
|
||||
node-releases: 2.0.19
|
||||
update-browserslist-db: 1.1.2(browserslist@4.24.4)
|
||||
@ -6244,7 +6249,7 @@ snapshots:
|
||||
|
||||
camelcase@6.3.0: {}
|
||||
|
||||
caniuse-lite@1.0.30001692: {}
|
||||
caniuse-lite@1.0.30001695: {}
|
||||
|
||||
chalk@2.4.2:
|
||||
dependencies:
|
||||
@ -8352,6 +8357,8 @@ snapshots:
|
||||
|
||||
mkdirp@2.1.3: {}
|
||||
|
||||
mkdirp@2.1.6: {}
|
||||
|
||||
mkdirp@3.0.1: {}
|
||||
|
||||
module-details-from-path@1.0.3: {}
|
||||
@ -9716,7 +9723,7 @@ snapshots:
|
||||
debug: 4.4.0
|
||||
dotenv: 16.4.7
|
||||
glob: 10.4.5
|
||||
mkdirp: 2.1.3
|
||||
mkdirp: 2.1.6
|
||||
reflect-metadata: 0.2.2
|
||||
sha.js: 2.4.11
|
||||
tslib: 2.8.1
|
||||
|
||||
@ -150,4 +150,45 @@ export class Utils {
|
||||
}
|
||||
return camelCaseObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配URL
|
||||
* @param pattern
|
||||
* @param url
|
||||
* @returns
|
||||
*/
|
||||
matchUrl(pattern, url) {
|
||||
// 将 pattern 和 url 按 `/` 分割
|
||||
const patternParts = pattern.split('/').filter(Boolean);
|
||||
const urlParts = url.split('/').filter(Boolean);
|
||||
// 如果长度不匹配且 pattern 不包含 **,直接返回 false
|
||||
if (patternParts.length !== urlParts.length && !pattern.includes('**')) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < patternParts.length; i++) {
|
||||
const patternPart = patternParts[i];
|
||||
const urlPart = urlParts[i];
|
||||
// 如果 patternPart 是 **,匹配剩余的所有部分
|
||||
if (patternPart === '**') {
|
||||
return true;
|
||||
}
|
||||
// 如果 patternPart 以 : 开头,说明是参数,直接匹配任意非空值
|
||||
if (patternPart.startsWith(':')) {
|
||||
if (!urlPart) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 如果 patternPart 是 *,匹配任意非空部分
|
||||
if (patternPart === '*') {
|
||||
if (!urlPart) {
|
||||
return false;
|
||||
}
|
||||
} else if (patternPart !== urlPart) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 如果 pattern 和 url 的部分数量一致,则匹配成功
|
||||
return patternParts.length === urlParts.length;
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,15 +65,19 @@ export default {
|
||||
cool: {
|
||||
// 已经插件化,本地文件上传查看 plugin/config.ts,其他云存储查看对应插件的使用
|
||||
file: {},
|
||||
rpc: {
|
||||
name: 'main',
|
||||
// 是否开启多租户
|
||||
tenant: {
|
||||
// 是否开启多租户
|
||||
enable: true,
|
||||
// 需要过滤多租户的url
|
||||
urls: [],
|
||||
},
|
||||
// redis配置
|
||||
redis: {
|
||||
port: 6379,
|
||||
host: '127.0.0.1',
|
||||
password: '',
|
||||
db: 0,
|
||||
// 国际化配置
|
||||
i18n: {
|
||||
// 是否开启
|
||||
enable: false,
|
||||
// 语言
|
||||
languages: ['zh-cn', 'zh-tw', 'en'],
|
||||
},
|
||||
// crud配置
|
||||
crud: {
|
||||
|
||||
@ -28,10 +28,10 @@ export default {
|
||||
// 实体与路径,跟生成代码、前端请求、swagger文档相关 注意:线上不建议开启,以免暴露敏感信息
|
||||
eps: true,
|
||||
// 是否自动导入模块数据库
|
||||
initDB: false,
|
||||
initDB: true,
|
||||
// 判断是否初始化的方式
|
||||
initJudge: 'db',
|
||||
// 是否自动导入模块菜单
|
||||
initMenu: false,
|
||||
initMenu: true,
|
||||
} as CoolConfig,
|
||||
} as MidwayConfig;
|
||||
|
||||
@ -18,8 +18,8 @@ import * as LocalConfig from './config/config.local';
|
||||
import * as ProdConfig from './config/config.prod';
|
||||
import * as cool from '@cool-midway/core';
|
||||
import * as upload from '@midwayjs/upload';
|
||||
import * as task from '@cool-midway/task';
|
||||
import * as rpc from '@cool-midway/rpc';
|
||||
// import * as task from '@cool-midway/task';
|
||||
// import * as rpc from '@cool-midway/rpc';
|
||||
|
||||
@Configuration({
|
||||
imports: [
|
||||
@ -40,9 +40,9 @@ import * as rpc from '@cool-midway/rpc';
|
||||
// cool-admin 官方组件 https://cool-js.com
|
||||
cool,
|
||||
// rpc 微服务 远程调用
|
||||
rpc,
|
||||
// rpc,
|
||||
// 任务与队列
|
||||
task,
|
||||
// task,
|
||||
{
|
||||
component: info,
|
||||
enabledEnvironment: ['local', 'prod'],
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { BaseLogMiddleware } from './middleware/log';
|
||||
import { BaseAuthorityMiddleware } from './middleware/authority';
|
||||
import { ModuleConfig } from '@cool-midway/core';
|
||||
import { BaseTranslateMiddleware } from './middleware/translate';
|
||||
|
||||
/**
|
||||
* 模块的配置
|
||||
@ -12,7 +13,11 @@ export default () => {
|
||||
// 模块描述
|
||||
description: '基础的权限管理功能,包括登录,权限校验',
|
||||
// 中间件
|
||||
globalMiddlewares: [BaseAuthorityMiddleware],
|
||||
globalMiddlewares: [
|
||||
BaseTranslateMiddleware,
|
||||
BaseAuthorityMiddleware,
|
||||
BaseLogMiddleware,
|
||||
],
|
||||
// 模块加载顺序,默认为0,值越大越优先加载
|
||||
order: 10,
|
||||
// app参数配置允许读取的key
|
||||
|
||||
@ -12,10 +12,12 @@ import {
|
||||
ASYNC_CONTEXT_KEY,
|
||||
ASYNC_CONTEXT_MANAGER_KEY,
|
||||
AsyncContextManager,
|
||||
Config,
|
||||
IMidwayApplication,
|
||||
IMidwayContext,
|
||||
Inject,
|
||||
} from '@midwayjs/core';
|
||||
import { Utils } from '../../../comm/utils';
|
||||
|
||||
/**
|
||||
* 不操作租户
|
||||
@ -43,19 +45,67 @@ export class TenantSubscriber implements EntitySubscriberInterface<any> {
|
||||
@Inject()
|
||||
ctx: IMidwayContext;
|
||||
|
||||
@Config('cool.tenant')
|
||||
tenant: {
|
||||
// 是否开启多租户
|
||||
enable: boolean;
|
||||
// 需要过滤多租户的url
|
||||
urls: string[];
|
||||
};
|
||||
|
||||
@Inject()
|
||||
utils: Utils;
|
||||
|
||||
/**
|
||||
* 检查是否需要租户
|
||||
*/
|
||||
checkHandler() {
|
||||
const ctx = this.getCtx();
|
||||
if (!ctx) return false;
|
||||
const url = ctx?.url;
|
||||
if (!url) return false;
|
||||
if (this.tenant.enable) {
|
||||
const isNeedTenant = this.tenant.urls.some(pattern =>
|
||||
this.utils.matchUrl(pattern, url)
|
||||
);
|
||||
return isNeedTenant;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ctx
|
||||
*/
|
||||
getCtx(): any {
|
||||
try {
|
||||
const contextManager: AsyncContextManager = this.app
|
||||
.getApplicationContext()
|
||||
.get(ASYNC_CONTEXT_MANAGER_KEY);
|
||||
return contextManager.active().getValue(ASYNC_CONTEXT_KEY);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从登录的用户中获取租户ID
|
||||
* @returns string | undefined
|
||||
*/
|
||||
getTenantId(): number | undefined {
|
||||
const contextManager: AsyncContextManager = this.app
|
||||
.getApplicationContext()
|
||||
.get(ASYNC_CONTEXT_MANAGER_KEY);
|
||||
const ctx: any = contextManager.active().getValue(ASYNC_CONTEXT_KEY);
|
||||
const url = ctx?.url;
|
||||
let ctx, url, tenantId;
|
||||
ctx = this.getCtx();
|
||||
if (!ctx || this.checkHandler()) return undefined;
|
||||
url = ctx?.url;
|
||||
|
||||
if (_.startsWith(url, '/admin/')) {
|
||||
return ctx?.admin?.tenantId;
|
||||
tenantId = ctx?.admin?.tenantId;
|
||||
} else if (_.startsWith(url, '/app/')) {
|
||||
tenantId = ctx?.user?.tenantId;
|
||||
}
|
||||
if (tenantId && url) {
|
||||
return tenantId;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,43 +113,47 @@ export class TenantSubscriber implements EntitySubscriberInterface<any> {
|
||||
* @param queryBuilder
|
||||
*/
|
||||
afterSelectQueryBuilder(queryBuilder: SelectQueryBuilder<any>) {
|
||||
if (!this.tenant.enable) return;
|
||||
const tenantId = this.getTenantId();
|
||||
if (tenantId) {
|
||||
queryBuilder.where('tenantId = :tenantId', { tenantId });
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 插入时添加租户ID
|
||||
// * @param queryBuilder
|
||||
// */
|
||||
// async afterInsertQueryBuilder(queryBuilder: InsertQueryBuilder<any>) {
|
||||
// const tenantId = await this.getTenantId();
|
||||
// if (tenantId) {
|
||||
// queryBuilder.values({ tenantId });
|
||||
// }
|
||||
// }
|
||||
/**
|
||||
* 插入时添加租户ID
|
||||
* @param queryBuilder
|
||||
*/
|
||||
async afterInsertQueryBuilder(queryBuilder: InsertQueryBuilder<any>) {
|
||||
if (!this.tenant.enable) return;
|
||||
const tenantId = await this.getTenantId();
|
||||
if (tenantId) {
|
||||
queryBuilder.values({ tenantId });
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 更新时添加租户ID和条件
|
||||
// * @param queryBuilder
|
||||
// */
|
||||
// async afterUpdateQueryBuilder(queryBuilder: UpdateQueryBuilder<any>) {
|
||||
// const tenantId = await this.getTenantId();
|
||||
// if (tenantId) {
|
||||
// queryBuilder.set({ tenantId });
|
||||
// queryBuilder.where('tenantId = :tenantId', { tenantId });
|
||||
// }
|
||||
// }
|
||||
/**
|
||||
* 更新时添加租户ID和条件
|
||||
* @param queryBuilder
|
||||
*/
|
||||
async afterUpdateQueryBuilder(queryBuilder: UpdateQueryBuilder<any>) {
|
||||
if (!this.tenant.enable) return;
|
||||
const tenantId = await this.getTenantId();
|
||||
if (tenantId) {
|
||||
queryBuilder.set({ tenantId });
|
||||
queryBuilder.where('tenantId = :tenantId', { tenantId });
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 删除时添加租户ID和条件
|
||||
// * @param queryBuilder
|
||||
// */
|
||||
// async afterDeleteQueryBuilder(queryBuilder: DeleteQueryBuilder<any>) {
|
||||
// const tenantId = await this.getTenantId();
|
||||
// if (tenantId) {
|
||||
// queryBuilder.where('tenantId = :tenantId', { tenantId });
|
||||
// }
|
||||
// }
|
||||
/**
|
||||
* 删除时添加租户ID和条件
|
||||
* @param queryBuilder
|
||||
*/
|
||||
async afterDeleteQueryBuilder(queryBuilder: DeleteQueryBuilder<any>) {
|
||||
if (!this.tenant.enable) return;
|
||||
const tenantId = await this.getTenantId();
|
||||
if (tenantId) {
|
||||
queryBuilder.where('tenantId = :tenantId', { tenantId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
import { CoolBaseEntity } from '@cool-midway/core';
|
||||
|
||||
/**
|
||||
* 模型基类
|
||||
* 实体基类
|
||||
*/
|
||||
export abstract class BaseEntity extends CoolBaseEntity {
|
||||
// 默认自增
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
Inject,
|
||||
Logger,
|
||||
} from '@midwayjs/core';
|
||||
import { BaseTranslateService } from '../service/translate';
|
||||
|
||||
/**
|
||||
* 导入菜单
|
||||
@ -25,6 +26,9 @@ export class BaseMenuEvent {
|
||||
@Inject()
|
||||
coolEventManager: CoolEventManager;
|
||||
|
||||
@Inject()
|
||||
baseTranslateService: BaseTranslateService;
|
||||
|
||||
@Event('onMenuImport')
|
||||
async onMenuImport(datas) {
|
||||
for (const module in datas) {
|
||||
@ -36,5 +40,11 @@ export class BaseMenuEvent {
|
||||
);
|
||||
}
|
||||
this.coolEventManager.emit('onMenuInit', {});
|
||||
this.baseTranslateService.check();
|
||||
}
|
||||
|
||||
@Event('onServerReady')
|
||||
async onServerReady() {
|
||||
this.baseTranslateService.loadTranslations();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { App, Config, Inject, Middleware } from '@midwayjs/core';
|
||||
import * as _ from 'lodash';
|
||||
import { CoolUrlTagData, RESCODE, TagTypes } from '@cool-midway/core';
|
||||
import { CoolCommException, CoolUrlTagData, TagTypes } from '@cool-midway/core';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { NextFunction, Context } from '@midwayjs/koa';
|
||||
import {
|
||||
@ -10,6 +10,7 @@ import {
|
||||
InjectClient,
|
||||
} from '@midwayjs/core';
|
||||
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
|
||||
import { Utils } from '../../../comm/utils';
|
||||
|
||||
/**
|
||||
* 权限校验
|
||||
@ -33,6 +34,9 @@ export class BaseAuthorityMiddleware
|
||||
@App()
|
||||
app: IMidwayApplication;
|
||||
|
||||
@Inject()
|
||||
utils: Utils;
|
||||
|
||||
ignoreUrls: string[] = [];
|
||||
|
||||
@Init()
|
||||
@ -51,19 +55,14 @@ export class BaseAuthorityMiddleware
|
||||
if (_.startsWith(url, adminUrl)) {
|
||||
try {
|
||||
ctx.admin = jwt.verify(token, this.jwtConfig.jwt.secret);
|
||||
ctx.admin.tenantId = 1;
|
||||
if (ctx.admin.isRefresh) {
|
||||
ctx.status = 401;
|
||||
ctx.body = {
|
||||
code: RESCODE.COMMFAIL,
|
||||
message: '登录失效~',
|
||||
};
|
||||
return;
|
||||
throw new CoolCommException('登录失效~');
|
||||
}
|
||||
} catch (error) {}
|
||||
// 使用matchUrl方法来检查URL是否应该被忽略
|
||||
const isIgnored = this.ignoreUrls.some(pattern =>
|
||||
this.matchUrl(pattern, url)
|
||||
this.utils.matchUrl(pattern, url)
|
||||
);
|
||||
if (isIgnored) {
|
||||
await next();
|
||||
@ -79,21 +78,13 @@ export class BaseAuthorityMiddleware
|
||||
);
|
||||
if (passwordV != ctx.admin.passwordVersion) {
|
||||
ctx.status = 401;
|
||||
ctx.body = {
|
||||
code: RESCODE.COMMFAIL,
|
||||
message: '登录失效~',
|
||||
};
|
||||
return;
|
||||
throw new CoolCommException('登录失效~');
|
||||
}
|
||||
// 超管拥有所有权限
|
||||
if (ctx.admin.username == 'admin' && !ctx.admin.isRefresh) {
|
||||
if (rToken !== token && this.jwtConfig.jwt.sso) {
|
||||
ctx.status = 401;
|
||||
ctx.body = {
|
||||
code: RESCODE.COMMFAIL,
|
||||
message: '登录失效~',
|
||||
};
|
||||
return;
|
||||
throw new CoolCommException('登录失效~');
|
||||
} else {
|
||||
await next();
|
||||
return;
|
||||
@ -111,19 +102,11 @@ export class BaseAuthorityMiddleware
|
||||
// 如果传的token是refreshToken则校验失败
|
||||
if (ctx.admin.isRefresh) {
|
||||
ctx.status = 401;
|
||||
ctx.body = {
|
||||
code: RESCODE.COMMFAIL,
|
||||
message: '登录失效~',
|
||||
};
|
||||
return;
|
||||
throw new CoolCommException('登录失效~');
|
||||
}
|
||||
if (!rToken) {
|
||||
ctx.status = 401;
|
||||
ctx.body = {
|
||||
code: RESCODE.COMMFAIL,
|
||||
message: '登录失效或无权限访问~',
|
||||
};
|
||||
return;
|
||||
throw new CoolCommException('登录失效或无权限访问~');
|
||||
}
|
||||
if (rToken !== token && this.jwtConfig.jwt.sso) {
|
||||
statusCode = 401;
|
||||
@ -147,40 +130,10 @@ export class BaseAuthorityMiddleware
|
||||
}
|
||||
if (statusCode > 200) {
|
||||
ctx.status = statusCode;
|
||||
ctx.body = {
|
||||
code: RESCODE.COMMFAIL,
|
||||
message: '登录失效或无权限访问~',
|
||||
};
|
||||
return;
|
||||
throw new CoolCommException('登录失效或无权限访问~');
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
74
src/modules/base/middleware/translate.ts
Normal file
74
src/modules/base/middleware/translate.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { Config, ILogger, Middleware } from '@midwayjs/core';
|
||||
import { NextFunction, Context } from '@midwayjs/koa';
|
||||
import { IMiddleware, Inject } from '@midwayjs/core';
|
||||
import { BaseTranslateService } from '../service/translate';
|
||||
import * as _ from 'lodash';
|
||||
import { RESCODE } from '@cool-midway/core';
|
||||
/**
|
||||
* 翻译中间件
|
||||
*/
|
||||
@Middleware()
|
||||
export class BaseTranslateMiddleware
|
||||
implements IMiddleware<Context, NextFunction>
|
||||
{
|
||||
@Inject()
|
||||
baseTranslateService: BaseTranslateService;
|
||||
|
||||
@Inject()
|
||||
logger: ILogger;
|
||||
|
||||
@Config('cool.i18n')
|
||||
config: {
|
||||
/** 是否开启 */
|
||||
enable: boolean;
|
||||
/** 语言 */
|
||||
languages: string[];
|
||||
/** 翻译服务 */
|
||||
serviceUrl?: string;
|
||||
};
|
||||
|
||||
resolve() {
|
||||
return async (ctx, next: NextFunction) => {
|
||||
const url = ctx.url;
|
||||
const language = 'en';
|
||||
let data;
|
||||
try {
|
||||
data = await next();
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
// 处理翻译消息
|
||||
if (error.name == 'CoolCommException') {
|
||||
if (language && error.message && error.message !== 'success') {
|
||||
ctx.status = 200;
|
||||
ctx.body = {
|
||||
code: RESCODE.COMMFAIL,
|
||||
message: await this.baseTranslateService.translate(
|
||||
'msg',
|
||||
language,
|
||||
error.message
|
||||
),
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this.config.enable) {
|
||||
return;
|
||||
}
|
||||
// 处理菜单翻译
|
||||
if (url == '/admin/base/comm/permmenu') {
|
||||
console.log(data);
|
||||
for (const menu of data.data.menus) {
|
||||
if (menu.name) {
|
||||
menu.name = await this.baseTranslateService.translate(
|
||||
'menu',
|
||||
language,
|
||||
menu.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
295
src/modules/base/service/translate.ts
Normal file
295
src/modules/base/service/translate.ts
Normal file
@ -0,0 +1,295 @@
|
||||
import { I18N } from '@cool-midway/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseSysMenuEntity } from '../entity/sys/menu';
|
||||
import {
|
||||
App,
|
||||
Config,
|
||||
ILogger,
|
||||
IMidwayApplication,
|
||||
Inject,
|
||||
Provide,
|
||||
Scope,
|
||||
ScopeEnum,
|
||||
} from '@midwayjs/core';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import axios from 'axios';
|
||||
/**
|
||||
* 翻译服务
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class BaseTranslateService {
|
||||
@InjectEntityModel(BaseSysMenuEntity)
|
||||
baseSysMenuEntity: Repository<BaseSysMenuEntity>;
|
||||
|
||||
// 基础路径
|
||||
basePath: string;
|
||||
|
||||
@App()
|
||||
app: IMidwayApplication;
|
||||
|
||||
@Inject()
|
||||
logger: ILogger;
|
||||
|
||||
@Config('cool.i18n')
|
||||
config: {
|
||||
/** 是否开启 */
|
||||
enable: boolean;
|
||||
/** 语言 */
|
||||
languages: string[];
|
||||
/** 翻译服务 */
|
||||
serviceUrl?: string;
|
||||
};
|
||||
|
||||
menuMap: Record<string, string> = {};
|
||||
|
||||
msgMap: Record<string, string> = {};
|
||||
|
||||
/**
|
||||
* 检查是否存在锁文件
|
||||
*/
|
||||
private checkLockFile(type: 'menu' | 'msg'): boolean {
|
||||
const lockFile = path.join(this.basePath, type, '.lock');
|
||||
return fs.existsSync(lockFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建锁文件
|
||||
*/
|
||||
private createLockFile(type: 'menu' | 'msg'): void {
|
||||
const lockFile = path.join(this.basePath, type, '.lock');
|
||||
fs.writeFileSync(lockFile, new Date().toISOString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载翻译文件到内存
|
||||
*/
|
||||
async loadTranslations() {
|
||||
if (!this.basePath) {
|
||||
this.basePath = path.join(this.app.getBaseDir(), '..', 'src', 'locales');
|
||||
}
|
||||
|
||||
// 加载菜单翻译
|
||||
const menuDir = path.join(this.basePath, 'menu');
|
||||
if (fs.existsSync(menuDir)) {
|
||||
const files = fs.readdirSync(menuDir);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.json')) {
|
||||
const language = file.replace('.json', '');
|
||||
const content = fs.readFileSync(path.join(menuDir, file), 'utf-8');
|
||||
const translations = JSON.parse(content);
|
||||
for (const [key, value] of Object.entries(translations)) {
|
||||
this.menuMap[`${language}:${key}`] = value as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载消息翻译
|
||||
const msgDir = path.join(this.basePath, 'msg');
|
||||
if (fs.existsSync(msgDir)) {
|
||||
const files = fs.readdirSync(msgDir);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.json')) {
|
||||
const language = file.replace('.json', '');
|
||||
const content = fs.readFileSync(path.join(msgDir, file), 'utf-8');
|
||||
const translations = JSON.parse(content);
|
||||
for (const [key, value] of Object.entries(translations)) {
|
||||
this.msgMap[`${language}:${key}`] = value as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新翻译映射
|
||||
* @param type 类型 menu | msg
|
||||
* @param language 语言
|
||||
*/
|
||||
async updateTranslationMap(type: 'menu' | 'msg', language: string) {
|
||||
const dirPath = path.join(this.basePath, type);
|
||||
const file = path.join(dirPath, `${language}.json`);
|
||||
|
||||
if (fs.existsSync(file)) {
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
const translations = JSON.parse(content);
|
||||
const map = type === 'menu' ? this.menuMap : this.msgMap;
|
||||
|
||||
for (const [key, value] of Object.entries(translations)) {
|
||||
map[`${language}:${key}`] = value as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译
|
||||
* @param type 类型 menu | msg
|
||||
* @param language 语言
|
||||
* @param text 原文
|
||||
* @returns 翻译后的文本
|
||||
*/
|
||||
translate(type: 'menu' | 'msg', language: string, text: string): string {
|
||||
const map = type === 'menu' ? this.menuMap : this.msgMap;
|
||||
const key = `${language}:${text}`;
|
||||
return map[key] || text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查翻译
|
||||
*/
|
||||
async check() {
|
||||
if (this.config?.enable) {
|
||||
this.basePath = path.join(this.app.getBaseDir(), '..', 'src', 'locales');
|
||||
|
||||
const menuLockExists = this.checkLockFile('menu');
|
||||
const msgLockExists = this.checkLockFile('msg');
|
||||
|
||||
if (!menuLockExists || !msgLockExists) {
|
||||
const tasks = [];
|
||||
if (!msgLockExists) {
|
||||
tasks.push(this.genBaseMsg());
|
||||
}
|
||||
if (!menuLockExists) {
|
||||
tasks.push(this.genBaseMenu());
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
this.logger.info('All translations completed successfully');
|
||||
// 更新翻译映射
|
||||
await this.loadTranslations();
|
||||
} else {
|
||||
this.logger.info('Translation lock files exist, skipping translation');
|
||||
// 直接加载翻译文件到内存
|
||||
await this.loadTranslations();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成基础菜单
|
||||
*/
|
||||
async genBaseMenu() {
|
||||
const menus = await this.baseSysMenuEntity.find();
|
||||
const file = path.join(this.basePath, 'menu', 'zh-cn.json');
|
||||
const content = {};
|
||||
for (const menu of menus) {
|
||||
content[menu.name] = menu.name;
|
||||
}
|
||||
// 确保目录存在
|
||||
const msgDir = path.dirname(file);
|
||||
if (!fs.existsSync(msgDir)) {
|
||||
fs.mkdirSync(msgDir, { recursive: true });
|
||||
}
|
||||
const text = JSON.stringify(content, null, 2);
|
||||
fs.writeFileSync(file, text);
|
||||
this.logger.info('base menu generate success');
|
||||
const translatePromises = [];
|
||||
for (const language of this.config.languages) {
|
||||
if (language !== 'zh-cn') {
|
||||
translatePromises.push(
|
||||
this.invokeTranslate(
|
||||
text,
|
||||
language,
|
||||
path.join(this.basePath, 'menu'),
|
||||
'menu'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
await Promise.all(translatePromises);
|
||||
this.createLockFile('menu');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成基础消息
|
||||
*/
|
||||
async genBaseMsg() {
|
||||
const file = path.join(this.basePath, 'msg', 'zh-cn.json');
|
||||
const scanPath = path.join(this.app.getBaseDir(), '..', 'src', 'modules');
|
||||
const messages = {};
|
||||
|
||||
// 递归扫描目录
|
||||
const scanDir = (dir: string) => {
|
||||
const files = fs.readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
scanDir(fullPath);
|
||||
} else if (file.endsWith('.ts')) {
|
||||
const content = fs.readFileSync(fullPath, 'utf-8');
|
||||
const matches = content.match(
|
||||
/throw new CoolCommException\((['"])(.*?)\1\)/g
|
||||
);
|
||||
if (matches) {
|
||||
matches.forEach(match => {
|
||||
const message = match.match(/(['"])(.*?)\1/)[2];
|
||||
messages[message] = message;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 开始扫描
|
||||
scanDir(scanPath);
|
||||
|
||||
// 确保目录存在
|
||||
const msgDir = path.dirname(file);
|
||||
if (!fs.existsSync(msgDir)) {
|
||||
fs.mkdirSync(msgDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
const text = JSON.stringify(messages, null, 2);
|
||||
fs.writeFileSync(file, text);
|
||||
this.logger.info('base msg generate success');
|
||||
|
||||
const translatePromises = [];
|
||||
for (const language of this.config.languages) {
|
||||
if (language !== 'zh-cn') {
|
||||
translatePromises.push(
|
||||
this.invokeTranslate(
|
||||
text,
|
||||
language,
|
||||
path.join(this.basePath, 'msg'),
|
||||
'msg'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
await Promise.all(translatePromises);
|
||||
this.createLockFile('msg');
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用翻译
|
||||
* @param text 文本
|
||||
* @param language 语言
|
||||
* @param dirPath 目录
|
||||
* @param type 类型
|
||||
* @returns
|
||||
*/
|
||||
async invokeTranslate(
|
||||
text: string,
|
||||
language: string,
|
||||
dirPath: string,
|
||||
type: 'menu' | 'msg' = 'msg'
|
||||
) {
|
||||
this.logger.info(`${type} ${language} translate start`);
|
||||
const response = await axios.post(I18N.DEFAULT_SERVICE_URL, {
|
||||
label: 'i18n-node',
|
||||
params: {
|
||||
text,
|
||||
language,
|
||||
},
|
||||
stream: false,
|
||||
});
|
||||
const file = path.join(dirPath, `${language}.json`);
|
||||
fs.writeFileSync(file, response.data.data.result.data);
|
||||
this.logger.info(`${type} ${language} translate success`);
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,8 @@ 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';
|
||||
import { CoolCommException, CoolUrlTagData, TagTypes } from '@cool-midway/core';
|
||||
import { Utils } from '../../../comm/utils';
|
||||
|
||||
/**
|
||||
* 用户
|
||||
@ -24,6 +25,9 @@ export class UserMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
@Config('koa.globalPrefix')
|
||||
prefix;
|
||||
|
||||
@Inject()
|
||||
utils: Utils;
|
||||
|
||||
@Init()
|
||||
async init() {
|
||||
this.ignoreUrls = this.coolUrlTagData.byKey(TagTypes.IGNORE_TOKEN, 'app');
|
||||
@ -37,18 +41,14 @@ export class UserMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
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;
|
||||
throw new CoolCommException('登录失效~');
|
||||
}
|
||||
} catch (error) {}
|
||||
// 使用matchUrl方法来检查URL是否应该被忽略
|
||||
const isIgnored = this.ignoreUrls.some(pattern =>
|
||||
this.matchUrl(pattern, url)
|
||||
this.utils.matchUrl(pattern, url)
|
||||
);
|
||||
if (isIgnored) {
|
||||
await next();
|
||||
@ -56,41 +56,11 @@ export class UserMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
} else {
|
||||
if (!ctx.user) {
|
||||
ctx.status = 401;
|
||||
ctx.body = {
|
||||
code: RESCODE.COMMFAIL,
|
||||
message: '登录失效~',
|
||||
};
|
||||
return;
|
||||
throw new CoolCommException('登录失效~');
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,6 +230,7 @@ export class UserLoginService extends BaseService {
|
||||
nickName: wxUserInfo.nickName,
|
||||
avatarUrl,
|
||||
gender: wxUserInfo.gender,
|
||||
tenantId: userInfo['tenantId'],
|
||||
};
|
||||
await this.userInfoEntity.insert(userInfo);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user