add bullmq and redis

This commit is contained in:
liuyi 2025-07-03 22:26:18 +08:00
parent 769973517b
commit 0d11d2bd8a
9 changed files with 2051 additions and 9 deletions

View File

@ -25,6 +25,7 @@
"dependencies": {
"@casl/ability": "^6.7.3",
"@fastify/static": "^8.2.0",
"@nestjs/bullmq": "^11.0.2",
"@nestjs/common": "^11.1.3",
"@nestjs/core": "^11.1.3",
"@nestjs/jwt": "^11.0.0",
@ -33,6 +34,7 @@
"@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.0",
"bcrypt": "^6.0.0",
"bullmq": "^5.56.0",
"chalk": "^5.4.1",
"chokidar": "^4.0.3",
"class-transformer": "^0.5.1",
@ -40,13 +42,16 @@
"dayjs": "^1.11.13",
"deepmerge": "^4.3.1",
"dotenv": "^16.5.0",
"email-templates": "^12.0.3",
"fastify": "^5.4.0",
"find-up": "^7.0.0",
"fs-extra": "^11.3.0",
"ioredis": "^5.6.1",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"meilisearch": "^0.51.0",
"mysql2": "^3.14.1",
"nodemailer": "^7.0.4",
"ora": "^8.2.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
@ -56,6 +61,7 @@
"rimraf": "^6.0.1",
"rxjs": "^7.8.2",
"sanitize-html": "^2.17.0",
"tencentcloud-sdk-nodejs": "^4.1.67",
"typeorm": "^0.3.24",
"uuid": "^11.1.0",
"validator": "^13.15.15",
@ -73,12 +79,14 @@
"@swc/cli": "^0.7.7",
"@swc/core": "^1.12.1",
"@types/bcrypt": "^5.0.2",
"@types/email-templates": "^10.0.4",
"@types/eslint": "^9.6.1",
"@types/fs-extra": "^11.0.4",
"@types/jest": "29.5.14",
"@types/jsonwebtoken": "^9.0.10",
"@types/lodash": "^4.17.17",
"@types/node": "^24.0.1",
"@types/nodemailer": "^6.4.17",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/sanitize-html": "^2.16.0",

File diff suppressed because it is too large Load Diff

View File

@ -3,3 +3,5 @@ export * from './content.config';
export * from './app.config';
export * from './meili.config';
export * from './api.config';
export * from './sms.config';
export * from './smtp.config';

10
src/config/sms.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { Configure } from '@/modules/config/configure';
import { SmsOptions } from '@/modules/core/types';
export const sms: (configure: Configure) => SmsOptions = (configure) => ({
sign: configure.env.get('SMS_CLOUD_SING', '极客科技'),
region: configure.env.get('SMS_CLOUD_REGION', 'ap-guangzhou'),
appid: configure.env.get('SMS_CLOUD_APPID', '1400437232'),
secretId: configure.env.get('SMS_CLOUD_ID', 'your-secret-id'),
secretKey: configure.env.get('SMS_CLOUD_KEY', 'your-secret-key'),
});

15
src/config/smtp.config.ts Normal file
View File

@ -0,0 +1,15 @@
import path from 'path';
import { Configure } from '@/modules/config/configure';
import { SmtpOptions } from '@/modules/core/types';
export const smtp: (configure: Configure) => SmtpOptions = (configure) => ({
host: configure.env.get('SMTP_HOST', 'localhost'),
user: configure.env.get('SMTP_USER', 'test'),
password: configure.env.get('SMTP_PASSWORD', ''),
from: configure.env.get('SMTP_FROM', '平克小站<support@localhost>'),
port: configure.env.get('SMTP_PORT', (v) => Number(v), 25),
secure: configure.env.get('SMTP_SSL', (v) => JSON.parse(v), false),
// Email模板路径
resource: path.resolve(__dirname, '../../assets/emails'),
});

View File

@ -0,0 +1,71 @@
import { Injectable } from '@nestjs/common';
import * as tencentcloud from 'tencentcloud-sdk-nodejs';
import { deepMerge } from '@/modules/core/helpers';
import { SmsOptions, SmsSendParams } from '@/modules/core/types';
const SmsClient = tencentcloud.sms.v20210111.Client;
/**
*
*/
@Injectable()
export class SmsService {
/**
*
* @param options
*/
constructor(protected options: SmsOptions) {}
/**
*
* @param options
*/
protected makeClient(options: SmsOptions) {
const { secretId, secretKey, region, endpoint } = options;
return new SmsClient({
credential: { secretId, secretKey },
region,
profile: {
httpProfile: { endpoint: endpoint ?? 'sms.tencentcloudapi.com' },
},
});
}
/**
*
* @param params
* @param options
*/
protected transSendParams(params: SmsSendParams, options: SmsOptions) {
const { numbers, template, vars, appid, sign, ...others } = params;
let paramSet: RecordAny = {};
if (vars) {
paramSet = Object.fromEntries(
Object.entries(vars).map(([key, value]) => [key, value.toString()]),
);
}
return {
PhoneNumberSet: numbers.map((n) => {
const phone: string[] = n.split('.');
return `${phone[0]}${phone[1]}`;
}),
TemplateId: template,
SmsSdkAppId: appid ?? options.appid,
SignName: sign ?? options.sign,
TemplateParamSet: Object.values(paramSet),
...(others ?? {}),
};
}
/**
*
* @param params
* @param options ()
*/
async send<T>(params: SmsSendParams & T, options?: SmsSendParams) {
const newOptions = deepMerge(this.options, options ?? {}) as SmsOptions;
const client = this.makeClient(newOptions);
return client.SendSms(this.transSendParams(params, newOptions));
}
}

View File

@ -0,0 +1,93 @@
import path from 'path';
import { Injectable } from '@nestjs/common';
import Email from 'email-templates';
import { pick } from 'lodash';
import mailer from 'nodemailer';
import Mail from 'nodemailer/lib/mailer';
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import { deepMerge } from '@/modules/core/helpers';
import { SmtpOptions, SmtpSendParams } from '@/modules/core/types';
/**
* SMTP邮件发送驱动
*/
@Injectable()
export class SmtpService {
/**
*
* @param options
*/
constructor(protected readonly options: SmtpOptions) {}
/**
*
* @param params
* @param options
*/
async send<T>(params: SmtpSendParams & T, options?: SmtpOptions) {
const newOptions = deepMerge(this.options, options ?? {}) as SmtpOptions;
const client = this.makeClient(newOptions);
return this.makeSend(client, params, newOptions);
}
/**
* NodeMailer客户端
* @param options
*/
protected makeClient(options: SmtpOptions) {
const { host, secure, user, password, port } = options;
const clientOptions: SMTPConnection.Options = {
host,
secure: secure ?? false,
auth: {
user,
pass: password,
},
};
if (!clientOptions.secure) {
clientOptions.port = port ?? 25;
}
return mailer.createTransport(clientOptions);
}
/**
* NodeMailer发送参数
* @param client
* @param params
* @param options
*/
protected async makeSend(client: Mail, params: SmtpSendParams, options: SmtpOptions) {
const tplPath = path.resolve(options.resource, params.name ?? 'custom');
const textOnly = !params.html && params.text;
const noHtmlToText = params.html && params.text;
const configd: Email.EmailConfig = {
preview: params.preview ?? false,
send: !params.preview,
message: { from: params.from ?? options.from ?? options.user },
transport: client,
subjectPrefix: params.subjectPrefix,
textOnly,
juiceResources: {
preserveImportant: true,
webResources: {
relativeTo: tplPath,
},
},
};
if (noHtmlToText) {
configd.htmlToText = false;
}
const email = new Email(configd);
const message = {
...pick(params, ['from', 'to', 'reply', 'attachments', 'subject']),
locals: params.vars,
};
return email.send({
template: tplPath,
message,
});
}
}

View File

@ -3,7 +3,12 @@ import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common';
import { IAuthGuard } from '@nestjs/passport';
import { NestFastifyApplication } from '@nestjs/platform-fastify';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { QueueOptions as BullMQOptions } from 'bullmq';
import dayjs from 'dayjs';
import Email from 'email-templates';
import { RedisOptions as IORedisOptions } from 'ioredis';
import { Attachment } from 'nodemailer/lib/mailer';
import { Ora } from 'ora';
import { StartOptions } from 'pm2';
import { ManyToMany, ManyToOne, OneToMany, OneToOne } from 'typeorm';
@ -142,3 +147,109 @@ export interface DynamicRelation {
| ReturnType<typeof ManyToMany>;
column: string;
}
/**
*
*/
export type NestedRecord = Record<string, Record<string, any>>;
/**
* core模块参数选项
*/
export interface CoreOptions {
database?: () => TypeOrmModuleOptions;
sms?: () => SmsOptions;
}
/**
*
*/
export type SmsOptions<T extends NestedRecord = RecordNever> = {
secretId: string;
secretKey: string;
sign: string;
appid: string;
region: string;
endpoint?: string;
} & T;
/**
*
*/
export interface SmsSendParams {
appid?: string;
numbers: string[];
template: string;
sign?: string;
endpoint?: string;
vars?: Record<string, any>;
ExtendCode?: string;
SessionContext?: string;
SenderId?: string;
}
/**
* SMTP邮件发送配置
*/
export type SmtpOptions<T extends NestedRecord = RecordNever> = {
host: string;
user: string;
password: string;
// Email模板总路径
resource: string;
from?: string;
port?: number;
secure?: boolean;
} & T;
/**
*
*/
export interface SmtpSendParams {
// 模板名称
name?: string;
// 发信地址
from?: string;
// 主题
subject?: string;
// 目标地址
to: string | string[];
// 回信地址
reply?: string;
// 是否加载html模板
html?: boolean;
// 是否加载text模板
text?: boolean;
// 模板变量
vars?: Record<string, any>;
// 是否预览
preview?: boolean | Email.PreviewEmailOpts;
// 主题前缀
subjectPrefix?: string;
// 附件
attachments?: Attachment[];
}
/**
* Redis连接配置
*/
export type RedisOption = Omit<IORedisOptions, 'name'> & { name: string };
/**
* Redis配置
*/
export type RedisOptions = IORedisOptions | Array<RedisOption>;
/**
*
*/
export type QueueOption = Omit<BullMQOptions, 'connection'> & { redis?: string };
/**
*
*/
export type QueueOptions = QueueOption | Array<{ name: string } & QueueOption>;
/**
* BullMQ模块注册配置
*/
export type BullOptions = BullMQOptions | Array<{ name: string } & BullMQOptions>;

View File

@ -13,7 +13,7 @@ import { UserModule } from '@/modules/user/user.module';
import * as configs from './config';
import { ContentModule } from './modules/content/content.module';
import { GlobalExceptionFilter } from './modules/core/filters/global-exception.filter';
import { CreateOptions } from './modules/core/types';
import { CreateOptions, RedisOption, RedisOptions } from './modules/core/types';
import * as dbCommands from './modules/database/commands';
import { DatabaseModule } from './modules/database/database.module';
import { MeiliModule } from './modules/meilisearch/meili.module';
@ -52,12 +52,30 @@ export const createOptions: CreateOptions = {
if (existsSync(join(__dirname, 'metadata.js'))) {
metadata = (await import(join(__dirname, 'metadata.js'))).default;
}
if (existsSync(join(__dirname, 'metadata.ts'))) {
metadata = (await import(join(__dirname, 'metadata.ts'))).default;
}
await restful.factoryDocs(container, metadata);
}
return container;
},
};
/**
* Redis配置
* @param options
*/
export const createRedisOptions = (options: RedisOptions) => {
const config: Array<RedisOption> = Array.isArray(options)
? options
: [{ ...options, name: 'default' }];
if (config.length < 1) {
return undefined;
}
if (isNil(config.find(({ name }) => name === 'default'))) {
config[0].name = 'default';
}
return config.reduce<RedisOption[]>((o, n) => {
const names = o.map(({ name }) => name) as string[];
return names.includes(n.name) ? o : [...o, n];
}, []);
};