add bullmq and redis
This commit is contained in:
parent
769973517b
commit
0d11d2bd8a
@ -25,6 +25,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@casl/ability": "^6.7.3",
|
"@casl/ability": "^6.7.3",
|
||||||
"@fastify/static": "^8.2.0",
|
"@fastify/static": "^8.2.0",
|
||||||
|
"@nestjs/bullmq": "^11.0.2",
|
||||||
"@nestjs/common": "^11.1.3",
|
"@nestjs/common": "^11.1.3",
|
||||||
"@nestjs/core": "^11.1.3",
|
"@nestjs/core": "^11.1.3",
|
||||||
"@nestjs/jwt": "^11.0.0",
|
"@nestjs/jwt": "^11.0.0",
|
||||||
@ -33,6 +34,7 @@
|
|||||||
"@nestjs/swagger": "^11.2.0",
|
"@nestjs/swagger": "^11.2.0",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
"bullmq": "^5.56.0",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
@ -40,13 +42,16 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
"email-templates": "^12.0.3",
|
||||||
"fastify": "^5.4.0",
|
"fastify": "^5.4.0",
|
||||||
"find-up": "^7.0.0",
|
"find-up": "^7.0.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
|
"ioredis": "^5.6.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"meilisearch": "^0.51.0",
|
"meilisearch": "^0.51.0",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
|
"nodemailer": "^7.0.4",
|
||||||
"ora": "^8.2.0",
|
"ora": "^8.2.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
@ -56,6 +61,7 @@
|
|||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"sanitize-html": "^2.17.0",
|
"sanitize-html": "^2.17.0",
|
||||||
|
"tencentcloud-sdk-nodejs": "^4.1.67",
|
||||||
"typeorm": "^0.3.24",
|
"typeorm": "^0.3.24",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"validator": "^13.15.15",
|
"validator": "^13.15.15",
|
||||||
@ -73,12 +79,14 @@
|
|||||||
"@swc/cli": "^0.7.7",
|
"@swc/cli": "^0.7.7",
|
||||||
"@swc/core": "^1.12.1",
|
"@swc/core": "^1.12.1",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/email-templates": "^10.0.4",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/jest": "29.5.14",
|
"@types/jest": "29.5.14",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/lodash": "^4.17.17",
|
"@types/lodash": "^4.17.17",
|
||||||
"@types/node": "^24.0.1",
|
"@types/node": "^24.0.1",
|
||||||
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/passport-local": "^1.0.38",
|
"@types/passport-local": "^1.0.38",
|
||||||
"@types/sanitize-html": "^2.16.0",
|
"@types/sanitize-html": "^2.16.0",
|
||||||
|
1724
pnpm-lock.yaml
1724
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -3,3 +3,5 @@ export * from './content.config';
|
|||||||
export * from './app.config';
|
export * from './app.config';
|
||||||
export * from './meili.config';
|
export * from './meili.config';
|
||||||
export * from './api.config';
|
export * from './api.config';
|
||||||
|
export * from './sms.config';
|
||||||
|
export * from './smtp.config';
|
||||||
|
10
src/config/sms.config.ts
Normal file
10
src/config/sms.config.ts
Normal 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
15
src/config/smtp.config.ts
Normal 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'),
|
||||||
|
});
|
71
src/modules/core/services/sms.service.ts
Normal file
71
src/modules/core/services/sms.service.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
93
src/modules/core/services/smtp.service.ts
Normal file
93
src/modules/core/services/smtp.service.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,12 @@ import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common';
|
|||||||
import { IAuthGuard } from '@nestjs/passport';
|
import { IAuthGuard } from '@nestjs/passport';
|
||||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||||
|
|
||||||
|
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||||
|
import { QueueOptions as BullMQOptions } from 'bullmq';
|
||||||
import dayjs from 'dayjs';
|
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 { Ora } from 'ora';
|
||||||
import { StartOptions } from 'pm2';
|
import { StartOptions } from 'pm2';
|
||||||
import { ManyToMany, ManyToOne, OneToMany, OneToOne } from 'typeorm';
|
import { ManyToMany, ManyToOne, OneToMany, OneToOne } from 'typeorm';
|
||||||
@ -142,3 +147,109 @@ export interface DynamicRelation {
|
|||||||
| ReturnType<typeof ManyToMany>;
|
| ReturnType<typeof ManyToMany>;
|
||||||
column: string;
|
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>;
|
||||||
|
@ -13,7 +13,7 @@ import { UserModule } from '@/modules/user/user.module';
|
|||||||
import * as configs from './config';
|
import * as configs from './config';
|
||||||
import { ContentModule } from './modules/content/content.module';
|
import { ContentModule } from './modules/content/content.module';
|
||||||
import { GlobalExceptionFilter } from './modules/core/filters/global-exception.filter';
|
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 * as dbCommands from './modules/database/commands';
|
||||||
import { DatabaseModule } from './modules/database/database.module';
|
import { DatabaseModule } from './modules/database/database.module';
|
||||||
import { MeiliModule } from './modules/meilisearch/meili.module';
|
import { MeiliModule } from './modules/meilisearch/meili.module';
|
||||||
@ -52,12 +52,30 @@ export const createOptions: CreateOptions = {
|
|||||||
if (existsSync(join(__dirname, 'metadata.js'))) {
|
if (existsSync(join(__dirname, 'metadata.js'))) {
|
||||||
metadata = (await import(join(__dirname, 'metadata.js'))).default;
|
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);
|
await restful.factoryDocs(container, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
return container;
|
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];
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user