完善多租户&国际化

This commit is contained in:
xiaopeng 2025-01-21 01:42:00 +08:00
parent b165e97500
commit 71ee2e7393
18 changed files with 573 additions and 215 deletions

View File

@ -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';",
"",

View File

@ -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
View File

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

17
pnpm-lock.yaml generated
View File

@ -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

View File

@ -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;
}
}

View File

@ -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: {

View File

@ -28,10 +28,10 @@ export default {
// 实体与路径跟生成代码、前端请求、swagger文档相关 注意:线上不建议开启,以免暴露敏感信息
eps: true,
// 是否自动导入模块数据库
initDB: false,
initDB: true,
// 判断是否初始化的方式
initJudge: 'db',
// 是否自动导入模块菜单
initMenu: false,
initMenu: true,
} as CoolConfig,
} as MidwayConfig;

View File

@ -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'],

View File

@ -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

View File

@ -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 });
}
}
}

View File

@ -8,7 +8,7 @@ import {
import { CoolBaseEntity } from '@cool-midway/core';
/**
*
*
*/
export abstract class BaseEntity extends CoolBaseEntity {
// 默认自增

View File

@ -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();
}
}

View File

@ -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;
}
}

View 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
);
}
}
}
};
}
}

View 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`);
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -230,6 +230,7 @@ export class UserLoginService extends BaseService {
nickName: wxUserInfo.nickName,
avatarUrl,
gender: wxUserInfo.gender,
tenantId: userInfo['tenantId'],
};
await this.userInfoEntity.insert(userInfo);
}