From 71ee2e7393ac1c1178ee4acc796731123cc82757 Mon Sep 17 00:00:00 2001 From: xiaopeng Date: Tue, 21 Jan 2025 01:42:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=A4=9A=E7=A7=9F=E6=88=B7&?= =?UTF-8?q?=E5=9B=BD=E9=99=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/middleware.code-snippets | 2 +- .vscode/service.code-snippets | 3 +- bootstrap.js | 1 - pnpm-lock.yaml | 17 +- src/comm/utils.ts | 41 ++++ src/config/config.default.ts | 20 +- src/config/config.local.ts | 4 +- src/configuration.ts | 8 +- src/modules/base/config.ts | 7 +- src/modules/base/db/tenant.ts | 128 +++++++--- src/modules/base/entity/base.ts | 2 +- src/modules/base/event/menu.ts | 10 + src/modules/base/middleware/authority.ts | 71 +----- src/modules/base/middleware/translate.ts | 74 ++++++ src/modules/base/service/translate.ts | 295 +++++++++++++++++++++++ src/modules/user/event/app.ts | 56 ----- src/modules/user/middleware/app.ts | 48 +--- src/modules/user/service/login.ts | 1 + 18 files changed, 573 insertions(+), 215 deletions(-) create mode 100644 src/modules/base/middleware/translate.ts create mode 100644 src/modules/base/service/translate.ts delete mode 100644 src/modules/user/event/app.ts diff --git a/.vscode/middleware.code-snippets b/.vscode/middleware.code-snippets index 367be90..76230f8 100644 --- a/.vscode/middleware.code-snippets +++ b/.vscode/middleware.code-snippets @@ -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';", "", diff --git a/.vscode/service.code-snippets b/.vscode/service.code-snippets index 54ffe0c..6ebf77a 100644 --- a/.vscode/service.code-snippets +++ b/.vscode/service.code-snippets @@ -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';", "", diff --git a/bootstrap.js b/bootstrap.js index 6444271..8601e1f 100644 --- a/bootstrap.js +++ b/bootstrap.js @@ -1,4 +1,3 @@ -process.env.NODE_ENV = 'local'; const { Bootstrap } = require('@midwayjs/bootstrap'); // 显式以组件方式引入用户代码 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83ab1b6..a77128f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/comm/utils.ts b/src/comm/utils.ts index 075bb24..3c05a7a 100644 --- a/src/comm/utils.ts +++ b/src/comm/utils.ts @@ -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; + } } diff --git a/src/config/config.default.ts b/src/config/config.default.ts index 405c970..5b40f3a 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -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: { diff --git a/src/config/config.local.ts b/src/config/config.local.ts index 5597964..bb22421 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -28,10 +28,10 @@ export default { // 实体与路径,跟生成代码、前端请求、swagger文档相关 注意:线上不建议开启,以免暴露敏感信息 eps: true, // 是否自动导入模块数据库 - initDB: false, + initDB: true, // 判断是否初始化的方式 initJudge: 'db', // 是否自动导入模块菜单 - initMenu: false, + initMenu: true, } as CoolConfig, } as MidwayConfig; diff --git a/src/configuration.ts b/src/configuration.ts index e1a165d..116ab6a 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -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'], diff --git a/src/modules/base/config.ts b/src/modules/base/config.ts index 6b920d0..259caa4 100644 --- a/src/modules/base/config.ts +++ b/src/modules/base/config.ts @@ -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 diff --git a/src/modules/base/db/tenant.ts b/src/modules/base/db/tenant.ts index c5506a8..a8f3b09 100644 --- a/src/modules/base/db/tenant.ts +++ b/src/modules/base/db/tenant.ts @@ -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 { @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 { * @param queryBuilder */ afterSelectQueryBuilder(queryBuilder: SelectQueryBuilder) { + if (!this.tenant.enable) return; const tenantId = this.getTenantId(); if (tenantId) { queryBuilder.where('tenantId = :tenantId', { tenantId }); } } - // /** - // * 插入时添加租户ID - // * @param queryBuilder - // */ - // async afterInsertQueryBuilder(queryBuilder: InsertQueryBuilder) { - // const tenantId = await this.getTenantId(); - // if (tenantId) { - // queryBuilder.values({ tenantId }); - // } - // } + /** + * 插入时添加租户ID + * @param queryBuilder + */ + async afterInsertQueryBuilder(queryBuilder: InsertQueryBuilder) { + if (!this.tenant.enable) return; + const tenantId = await this.getTenantId(); + if (tenantId) { + queryBuilder.values({ tenantId }); + } + } - // /** - // * 更新时添加租户ID和条件 - // * @param queryBuilder - // */ - // async afterUpdateQueryBuilder(queryBuilder: UpdateQueryBuilder) { - // const tenantId = await this.getTenantId(); - // if (tenantId) { - // queryBuilder.set({ tenantId }); - // queryBuilder.where('tenantId = :tenantId', { tenantId }); - // } - // } + /** + * 更新时添加租户ID和条件 + * @param queryBuilder + */ + async afterUpdateQueryBuilder(queryBuilder: UpdateQueryBuilder) { + 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) { - // const tenantId = await this.getTenantId(); - // if (tenantId) { - // queryBuilder.where('tenantId = :tenantId', { tenantId }); - // } - // } + /** + * 删除时添加租户ID和条件 + * @param queryBuilder + */ + async afterDeleteQueryBuilder(queryBuilder: DeleteQueryBuilder) { + if (!this.tenant.enable) return; + const tenantId = await this.getTenantId(); + if (tenantId) { + queryBuilder.where('tenantId = :tenantId', { tenantId }); + } + } } diff --git a/src/modules/base/entity/base.ts b/src/modules/base/entity/base.ts index 3b7506b..a67f41b 100644 --- a/src/modules/base/entity/base.ts +++ b/src/modules/base/entity/base.ts @@ -8,7 +8,7 @@ import { import { CoolBaseEntity } from '@cool-midway/core'; /** - * 模型基类 + * 实体基类 */ export abstract class BaseEntity extends CoolBaseEntity { // 默认自增 diff --git a/src/modules/base/event/menu.ts b/src/modules/base/event/menu.ts index 3257b43..f504eb4 100644 --- a/src/modules/base/event/menu.ts +++ b/src/modules/base/event/menu.ts @@ -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(); } } diff --git a/src/modules/base/middleware/authority.ts b/src/modules/base/middleware/authority.ts index 6f5e967..0ffd4db 100644 --- a/src/modules/base/middleware/authority.ts +++ b/src/modules/base/middleware/authority.ts @@ -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; - } } diff --git a/src/modules/base/middleware/translate.ts b/src/modules/base/middleware/translate.ts new file mode 100644 index 0000000..d94e248 --- /dev/null +++ b/src/modules/base/middleware/translate.ts @@ -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 +{ + @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 + ); + } + } + } + }; + } +} diff --git a/src/modules/base/service/translate.ts b/src/modules/base/service/translate.ts new file mode 100644 index 0000000..0dc910d --- /dev/null +++ b/src/modules/base/service/translate.ts @@ -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; + + // 基础路径 + basePath: string; + + @App() + app: IMidwayApplication; + + @Inject() + logger: ILogger; + + @Config('cool.i18n') + config: { + /** 是否开启 */ + enable: boolean; + /** 语言 */ + languages: string[]; + /** 翻译服务 */ + serviceUrl?: string; + }; + + menuMap: Record = {}; + + msgMap: Record = {}; + + /** + * 检查是否存在锁文件 + */ + 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`); + } +} diff --git a/src/modules/user/event/app.ts b/src/modules/user/event/app.ts deleted file mode 100644 index d7861f6..0000000 --- a/src/modules/user/event/app.ts +++ /dev/null @@ -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); - } - } -} diff --git a/src/modules/user/middleware/app.ts b/src/modules/user/middleware/app.ts index b803a6f..5988fa7 100644 --- a/src/modules/user/middleware/app.ts +++ b/src/modules/user/middleware/app.ts @@ -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 { @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 { 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 { } 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; - } } diff --git a/src/modules/user/service/login.ts b/src/modules/user/service/login.ts index b7708b7..12b53ff 100644 --- a/src/modules/user/service/login.ts +++ b/src/modules/user/service/login.ts @@ -230,6 +230,7 @@ export class UserLoginService extends BaseService { nickName: wxUserInfo.nickName, avatarUrl, gender: wxUserInfo.gender, + tenantId: userInfo['tenantId'], }; await this.userInfoEntity.insert(userInfo); }