feat(redis mysql middleware):

This commit is contained in:
HeXiaoLong:Suanier 2025-05-30 18:37:21 +08:00
parent 16bfea2dc0
commit 9afe6d756f
27 changed files with 2068 additions and 122 deletions

View File

@ -1,6 +1,37 @@
# 系统信息
HOST=0.0.0.0
PORT=3000
# 日志配置 # 日志配置
LOG_LEVEL=debug # 日志级别debug, verbose, info, warn, error LOG_LEVEL=debug # 日志级别debug, verbose, info, warn, error
LOG_MAX_FILES=30d # 保留日志文件的时间 LOG_MAX_FILES=30d # 保留日志文件的时间
LOG_MAX_SIZE=20m # 单个日志文件的最大大小 LOG_MAX_SIZE=20m # 单个日志文件的最大大小
LOG_DIRECTORY=logs # 日志文件存储目录 LOG_DIRECTORY=logs # 日志文件存储目录
LOG_CONSOLE=true # 是否在控制台输出日志 LOG_CONSOLE=true # 是否在控制台输出日志
# 数据库MYSQL配置
DB_HOST=172.16.1.3
DB_PORT=3306
DB_USER=root
DB_NAME=
DB_PASSWORD=docker
# redis配置
REDIS_HOST=172.16.1.3
REDIS_PORT=6379
REDIS_USERNAME=default
REDIS_PASSWORD=docker
REDIS_DATABASE=9
REDIS_CONNECT_NAME=star-tune
REDIS_TTL=3600
# 初始密码
USER_DEFAULT_PASSWORD=startune
# 加密盐值
PASSWORD_SALT=StarTune123
# 迭代次数
PASSWORD_ITERATIONS=15000
# 密钥长度
PASSWORD_KEYLEN=64
哈希算法
PASSWORD_DIGEST=sha512

View File

@ -1,98 +1,27 @@
<p align="center"> # star-tune
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 > nestjs fastify drizzle mysql2 redis
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash ```bash
$ npm install npm run dev
npm run start:dev
npm run start:debug
``` ```
## Compile and run the project
```bash ```bash
# development npm start
$ npm run start npm run start
npm run start:prod
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
``` ```
## Run tests ### DB
```
```bash makeSQL
# unit tests makeEntity
$ npm run test syncDB
sqlV
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
``` ```
## Deployment 1. npm i -d
2. 将init.sql同步到数据库
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. 3. npm start
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

View File

@ -0,0 +1,67 @@
-- 用户主表 (移除外键约束)
CREATE TABLE `user` (
`user_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY COMMENT '用户ID雪花ID',
`status` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '状态0=正常 1=禁用',
`user_type` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户类型0=一般用户 1=root',
`user_role` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户角色0默认角色',
`username` VARCHAR(50) NOT NULL COMMENT '唯一用户名',
`email` VARCHAR(320) NOT NULL COMMENT '邮箱',
`nickname` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '昵称',
`gender` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '性别0=未知 1=男 2=女',
`birthdate` DATE NULL DEFAULT NULL COMMENT '出生年月',
`address` VARCHAR(255) NULL DEFAULT '' COMMENT '地址',
`avatar` VARCHAR(255) NULL DEFAULT '' COMMENT '头像URL',
`background_image` VARCHAR(255) NULL DEFAULT '' COMMENT '背景图URL',
`created_by` ENUM('SELF','ADMIN') NOT NULL COMMENT '创建方式SELF=自注册 ADMIN=管理员添加',
`created_at` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '创建时间(毫秒精度)',
`updated_at` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '最后更新时间',
`username_updated_at` DATETIME(3) NULL DEFAULT NULL COMMENT '用户名修改时间',
`is_deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '软删除标记0=正常 1=已删除',
`deleted_at` DATETIME(3) NULL DEFAULT NULL COMMENT '删除时间',
-- 唯一索引确保数据完整性
UNIQUE INDEX `idx_unique_username` (`username`),
-- 复合唯一索引实现删除后邮箱可重用
UNIQUE INDEX `idx_unique_active_email` (`email`, `is_deleted`),
-- 时间索引优化查询
INDEX `idx_created_at` (`created_at`),
INDEX `idx_email` (`email`),
INDEX `idx_status` (`status`),
INDEX `idx_user_type` (`user_type`),
INDEX `idx_username_updated` (`username_updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户主表';
-- 个人简介表 (移除外键)
CREATE TABLE `user_profile` (
`user_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY COMMENT '用户ID',
`profile` MEDIUMTEXT NOT NULL COMMENT '简介内容',
`created_at` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '创建时间',
`updated_at` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '最后更新时间',
-- 添加索引优化关联查询
INDEX `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户简介表';
-- 密码表 (移除外键)
CREATE TABLE `user_password` (
`user_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY COMMENT '用户ID',
`password_hash` VARCHAR(128) NOT NULL COMMENT '加密密码argon2格式',
`previous_password_hash` VARCHAR(128) NULL DEFAULT NULL COMMENT '前次密码',
`last_updated_at` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '最后修改时间',
-- 添加索引
INDEX `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户密码表';
-- 个性签名历史表 (移除外键)
CREATE TABLE `user_signature_history` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`signature` VARCHAR(100) NOT NULL COMMENT '签名内容',
`signature_tag` VARCHAR(100) NOT NULL COMMENT '签名标签',
`created_at` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '创建时间',
-- 优化用户签名查询
INDEX `idx_user_created` (`user_id`, `created_at` DESC),
INDEX `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='签名历史表';

View File

@ -0,0 +1,67 @@
-- 用户主表 (移除外键约束)
CREATE TABLE `user` (
`user_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY COMMENT '用户ID雪花ID',
`status` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '状态0=正常 1=禁用',
`user_type` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户类型0=一般用户 1=root',
`user_role` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户角色0默认角色',
`username` VARCHAR(50) NOT NULL COMMENT '唯一用户名',
`email` VARCHAR(320) NOT NULL COMMENT '邮箱',
`nickname` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '昵称',
`gender` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '性别0=未知 1=男 2=女',
`birthdate` DATE NULL DEFAULT NULL COMMENT '出生年月',
`address` VARCHAR(255) NULL DEFAULT '' COMMENT '地址',
`avatar` VARCHAR(255) NULL DEFAULT '' COMMENT '头像URL',
`background_image` VARCHAR(255) NULL DEFAULT '' COMMENT '背景图URL',
`created_by` ENUM('SELF','ADMIN') NOT NULL COMMENT '创建方式SELF=自注册 ADMIN=管理员添加',
`created_at` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '创建时间(毫秒精度)',
`updated_at` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '最后更新时间',
`username_updated_at` DATETIME(3) NULL DEFAULT NULL COMMENT '用户名修改时间',
`is_deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '软删除标记0=正常 1=已删除',
`deleted_at` DATETIME(3) NULL DEFAULT NULL COMMENT '删除时间',
-- 唯一索引确保数据完整性
UNIQUE INDEX `idx_unique_username` (`username`),
-- 复合唯一索引实现删除后邮箱可重用
UNIQUE INDEX `idx_unique_active_email` (`email`, `is_deleted`),
-- 时间索引优化查询
INDEX `idx_created_at` (`created_at`),
INDEX `idx_email` (`email`),
INDEX `idx_status` (`status`),
INDEX `idx_user_type` (`user_type`),
INDEX `idx_username_updated` (`username_updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户主表';
-- 个人简介表 (移除外键)
CREATE TABLE `user_profile` (
`user_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY COMMENT '用户ID',
`profile` MEDIUMTEXT NOT NULL COMMENT '简介内容',
`created_at` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '创建时间',
`updated_at` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '最后更新时间',
-- 添加索引优化关联查询
INDEX `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户简介表';
-- 密码表 (移除外键)
CREATE TABLE `user_password` (
`user_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY COMMENT '用户ID',
`password_hash` VARCHAR(128) NOT NULL COMMENT '加密密码argon2格式',
`previous_password_hash` VARCHAR(128) NULL DEFAULT NULL COMMENT '前次密码',
`last_updated_at` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '最后修改时间',
-- 添加索引
INDEX `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户密码表';
-- 个性签名历史表 (移除外键)
CREATE TABLE `user_signature_history` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`signature` VARCHAR(100) NOT NULL COMMENT '签名内容',
`signature_tag` VARCHAR(100) NOT NULL COMMENT '签名标签',
`created_at` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '创建时间',
-- 优化用户签名查询
INDEX `idx_user_created` (`user_id`, `created_at` DESC),
INDEX `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='签名历史表';

View File

@ -0,0 +1,75 @@
/**
* Drizzle ORM
*
* schema
*
*/
import type { Config } from 'drizzle-kit';
import * as dotenv from 'dotenv';
// 加载环境变量
dotenv.config();
// 1. `makeSQL` (drizzle-kit generate)
// - 作用:根据 `schema.ts` 文件中的定义生成 SQL 迁移文件
// - 具体功能:
// - 比较当前数据库状态和 schema 定义的差异
// - 在 `src/drizzle` 目录下生成新的迁移文件(如 `0000_green_doorman.sql`
// - 同时更新 `meta/_journal.json` 记录迁移历史
// - 使用场景:当你修改了 `schema.ts` 中的表结构定义后,需要生成对应的 SQL 迁移文件
// - 执行命令:`npm run makeSQL`
// 2. `makeEntity` (drizzle-kit introspect)
// - 作用:从现有数据库反向生成 TypeScript 实体定义
// - 具体功能:
// - 扫描数据库中的表结构
// - 自动生成对应的 TypeScript 类型定义
// - 在 `src/drizzle/schema.ts` 中生成表结构代码
// - 使用场景:
// - 当你有一个现成的数据库,想要快速生成对应的 TypeScript 类型定义
// - 数据库表结构发生变化,需要更新 TypeScript 定义
// - 执行命令:`npm run makeEntity`
// 3. `syncDB` (drizzle-kit migrate)
// - 作用:将生成的 SQL 迁移文件同步到数据库
// - 具体功能:
// - 读取 `src/drizzle` 目录下的迁移文件
// - 按照迁移历史记录的顺序执行 SQL 语句
// - 将数据库结构更新到最新状态
// - 使用场景:
// - 当你生成了新的迁移文件后,需要将这些更改应用到数据库
// - 团队协作时,需要同步其他人的数据库更改
// - 执行命令:`npm run syncDB`
// 典型的工作流程是:
// 1. 修改 `schema.ts` 中的表结构定义
// 2. 运行 `makeSQL` 生成迁移文件
// 3. 检查生成的 SQL 文件是否正确
// 4. 运行 `syncDB` 将更改同步到数据库
// 如果你是从现有数据库开始,可以:
// 1. 运行 `makeEntity` 生成 TypeScript 定义
// 2. 根据需要修改生成的代码
// 3. 运行 `makeSQL` 和 `syncDB` 同步更改
// 这些命令都是通过 `drizzle-kit` 工具提供的,它提供了完整的数据库迁移和类型生成功能。
export default {
schema: './src/drizzle/schema.ts',
out: './src/drizzle',
dialect: 'mysql',
dbCredentials: {
host: process.env.DB_HOST || 'localhost',
port: Number(process.env.DB_PORT) || 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || 'root',
database: process.env.DB_NAME || 'star_tune',
},
verbose: true,
strict: true,
introspect: {
// 启用驼峰命名
casing: 'camel',
},
} satisfies Config;

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,9 @@
"author": "", "author": "",
"private": true, "private": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"engines": {
"node": ">=22.0.0"
},
"scripts": { "scripts": {
"build": "nest build", "build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
@ -18,7 +21,11 @@
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json",
"makeSQL": "drizzle-kit generate",
"makeEntity": "drizzle-kit introspect",
"syncDB": "drizzle-kit migrate",
"sqlV": "drizzle-kit studio"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
@ -26,7 +33,10 @@
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/platform-fastify": "^11.1.2", "@nestjs/platform-fastify": "^11.1.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"drizzle-orm": "^0.44.0",
"mysql2": "^3.14.1",
"nest-winston": "^1.10.2", "nest-winston": "^1.10.2",
"redis": "^5.1.1",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"winston": "^3.17.0", "winston": "^3.17.0",
@ -43,6 +53,7 @@
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"drizzle-kit": "^0.31.1",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2", "eslint-plugin-prettier": "^5.2.2",

View File

@ -6,7 +6,7 @@ export class AppController {
constructor(private readonly appService: AppService) {} constructor(private readonly appService: AppService) {}
@Get() @Get()
getHello(): string { async getHello(): Promise<object> {
return this.appService.getHello(); return await this.appService.getHello();
} }
} }

View File

@ -1,12 +1,38 @@
import { Module } from '@nestjs/common'; import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { AppController } from './app.controller'; import { ConfigModule } from '@nestjs/config';
import { AppService } from './app.service'; import { InitModule } from '@/common/init/init.module';
import { AppConfigModule } from './config/config.module'; import { DatabaseModule } from '@/common/database/database.module';
import { LoggerModule } from './logger/logger.module'; import { LoggerModule } from '@/common/logger/logger.module';
import { UtilsModule } from '@/common/utils/utils.module';
import { RedisModule } from '@/common/redis/redis.module';
import configuration from '@/config/configuration';
import { AppController } from '@/app.controller';
import { AppService } from '@/app.service';
import { RequestLoggerMiddleware } from '@/common/middleware/request-logger.middleware';
@Module({ @Module({
imports: [AppConfigModule, LoggerModule], imports: [
// 配置模块
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
}),
// 日志模块
LoggerModule,
// 数据库模块
DatabaseModule,
// Redis 模块
RedisModule,
// 初始化模块
InitModule,
// 工具模块
UtilsModule,
],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
}) })
export class AppModule {} export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestLoggerMiddleware).forRoutes('*');
}
}

View File

@ -2,7 +2,10 @@ import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class AppService { export class AppService {
getHello(): string { async getHello(): Promise<object> {
return 'Hello World!'; await new Promise(resolve => setTimeout(resolve, 1000));
let str = 'Hello World!';
str = str.repeat(1000);
return {str};
} }
} }

View File

@ -0,0 +1,19 @@
/**
*
*
* 使 @Global() 使
* 使
*/
import { Global, Module } from '@nestjs/common';
import { LoggerModule } from '@/common/logger/logger.module';
import { DatabaseService } from '@/common/database/database.service';
@Global()
@Module({
imports: [LoggerModule],
// 提供数据库服务
providers: [DatabaseService],
// 导出数据库服务,使其可以被其他模块注入使用
exports: [DatabaseService],
})
export class DatabaseModule {}

View File

@ -0,0 +1,75 @@
/**
*
*
*
* OnModuleInit
*/
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { drizzle, type MySql2Database } from 'drizzle-orm/mysql2';
import { createPool, Pool} from 'mysql2/promise';
import * as schema from '@/drizzle/schema';
import { CustomLogger } from '@/common/logger/logger.service';
import { type ResultSetHeader } from 'mysql2';
@Injectable()
export class DatabaseService implements OnModuleInit {
// 数据库实例,使用 drizzle ORM
private db!: MySql2Database<typeof schema>;
// MySQL 连接池实例
private pool!: Pool;
constructor(private configService: ConfigService, private logger: CustomLogger) {}
/**
*
* drizzle ORM
*/
async onModuleInit() {
// 从配置服务获取数据库配置
const dbConfig = this.configService.get('database');
// 先创建临时连接(不指定数据库名)
const tempPool = createPool({
host: dbConfig.host,
port: dbConfig.port,
user: dbConfig.username,
password: dbConfig.password,
});
// 创建数据库(如果不存在)
await tempPool.query(`CREATE DATABASE IF NOT EXISTS \`${dbConfig.database}\``) as [ResultSetHeader, any];
this.logger.log(`数据库 ${dbConfig.database} 初始化成功`, 'InitMySQL');
await tempPool.end();
// 创建正式的数据库连接池
this.pool = createPool({
host: dbConfig.host,
port: dbConfig.port,
user: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database,
});
// 初始化 drizzle ORM使用默认模式
this.db = drizzle(this.pool, { schema, mode: 'default' });
}
/**
*
*
*/
async onModuleDestroy() {
await this.pool.end();
}
/**
*
* @returns drizzle ORM
*/
getDb() {
return this.db;
}
}

View File

@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { InitService } from './init.service';
import { DatabaseModule } from '../database/database.module';
import { LoggerModule } from '../logger/logger.module';
import { UtilsModule } from '../utils/utils.module';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
DatabaseModule,
LoggerModule,
UtilsModule,
ConfigModule,
],
providers: [InitService],
exports: [InitService],
})
export class InitModule {}

View File

@ -0,0 +1,85 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { DatabaseService } from '@/common/database/database.service';
import { CustomLogger } from '@/common/logger/logger.service';
import { eq } from 'drizzle-orm';
import { user, userPassword, userProfile } from '@/drizzle/schema';
import { UtilsServer } from '@/common/utils/utils.server';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class InitService implements OnModuleInit {
constructor(
private readonly dbService: DatabaseService,
private readonly logger: CustomLogger,
private readonly utilsServer: UtilsServer,
private readonly configService: ConfigService,
) {}
async onModuleInit() {
await this.initRootUser();
}
/**
* root
*
*/
private async initRootUser() {
const db = this.dbService.getDb();
try {
// 检查 root 用户是否存在
const rootUser = await db.query.user.findFirst({
where: eq(user.userId, 0),
});
if (!rootUser) {
this.logger.log('正在创建 root 用户...', 'InitService');
// 生成密码哈希
const rootPassword = this.configService.get<string>('password.userDefaultPassword'); // 默认密码
const passwordHash = await this.utilsServer.hashPassword(rootPassword as string);
const now = new Date().toISOString().slice(0, 23).replace('T', ' ');
// 创建 root 用户
await db.transaction(async (tx) => {
// 插入用户基本信息
await tx.insert(user).values({
userId: 0,
status: 0,
userType: 1, // root 用户
userRole: 0,
username: 'root',
email: 'root@star-tune.com',
nickname: '系统管理员',
gender: 0,
createdBy: 'ADMIN',
createdAt: now,
updatedAt: now,
isDeleted: 0,
});
// 插入用户密码
await tx.insert(userPassword).values({
userId: 0,
passwordHash,
lastUpdatedAt: now,
});
// 插入用户简介
await tx.insert(userProfile).values({
userId: 0,
profile: '系统管理员账号',
createdAt: now,
updatedAt: now,
});
});
this.logger.log('root 用户创建成功', 'InitService');
} else {
this.logger.log('root 用户已存在', 'InitService');
}
} catch (error) {
this.logger.error('初始化 root 用户失败', error, 'InitService');
throw error;
}
}
}

View File

@ -4,7 +4,7 @@ import { ConfigService } from '@nestjs/config';
import * as winston from 'winston'; import * as winston from 'winston';
import * as DailyRotateFile from 'winston-daily-rotate-file'; import * as DailyRotateFile from 'winston-daily-rotate-file';
import { join } from 'path'; import { join } from 'path';
import { CustomLogger } from './custom.logger'; import { CustomLogger } from '@/common/logger/logger.service';
// 定义配置接口 // 定义配置接口
interface LogConfig { interface LogConfig {
@ -44,21 +44,35 @@ const getPaddedPid = () => process.pid.toString().padStart(5, '0');
// 创建自定义的 winston 格式化器 // 创建自定义的 winston 格式化器
const createCustomFormat = (environment: Environment) => { const createCustomFormat = (environment: Environment) => {
return winston.format.printf( return winston.format.printf(
({ (info: {
timestamp,
level,
message,
context,
}: {
timestamp: string; timestamp: string;
level: string; level: string;
message: string; message: string | Error;
context?: string; context?: string;
}) => { }) => {
const { timestamp, level, message, context} = info;
// 处理错误信息
let errorMessage = message;
let errorStack: string | undefined;
// 如果消息是错误对象或包含堆栈信息
if (message instanceof Error) {
errorMessage = message.message;
errorStack = message.stack;
} else if (typeof message === 'string' && message.includes('at ')) {
// 如果消息字符串中包含堆栈信息
const parts = message.split('\n');
errorMessage = parts[0];
errorStack = parts.slice(1).join('\n');
}
// 获取环境颜色,默认白色 // 获取环境颜色,默认白色
const envColor = envColors[environment] || '\x1b[37m'; const envColor = envColors[environment] || '\x1b[37m';
// 获取日志级别颜色,默认白色 // 获取日志级别颜色,默认白色
const levelColor = levelColors[level] || '\x1b[37m'; const levelColor = levelColors[level] || '\x1b[37m';
// 粗体
const bold = '\x1b[1m';
// 重置颜色的代码 // 重置颜色的代码
const reset = '\x1b[0m'; const reset = '\x1b[0m';
@ -82,10 +96,18 @@ const createCustomFormat = (environment: Environment) => {
contextContent.length > FORMAT_CONFIG.CONTEXT_LENGTH contextContent.length > FORMAT_CONFIG.CONTEXT_LENGTH
? contextContent.slice(0, FORMAT_CONFIG.CONTEXT_LENGTH) ? contextContent.slice(0, FORMAT_CONFIG.CONTEXT_LENGTH)
: contextContent.padEnd(FORMAT_CONFIG.CONTEXT_LENGTH, ' '); : contextContent.padEnd(FORMAT_CONFIG.CONTEXT_LENGTH, ' ');
const paddedContext = `[${formattedContext}]`;
// 处理错误堆栈信息
const stackInfo = errorStack ? `\n${levelColor}${errorStack}${reset}` : '';
// 返回格式化的日志字符串 // 返回格式化的日志字符串
return `${envColor}[${envTag}] ${getPaddedPid()}${reset} - ${timestamp} ${levelColor}${upperLevel} ${reset}${paddedContext} ${levelColor}${message}${reset}`; let paddedContext = formattedContext;
if(formattedContext.trim() === 'RequestLogger') {
paddedContext = `[${levelColors['debug']}${bold}${formattedContext}${reset}]`;
}else{
paddedContext = `[${formattedContext}]`;
}
return `${envColor}[${envTag}] ${getPaddedPid()}${reset} - ${timestamp} ${levelColor}${upperLevel} ${reset}${paddedContext} ${levelColor}${errorMessage}${reset}${stackInfo}`;
}, },
); );
}; };

View File

@ -0,0 +1,47 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { FastifyRequest, FastifyReply } from 'fastify';
import { CustomLogger } from '@/common/logger/logger.service';
import { ConfigService } from '@nestjs/config';
import { UtilsServer } from '@/common/utils/utils.server';
interface ResponseWithHeader {
_header?: string;
}
@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
constructor(
private readonly logger: CustomLogger,
private readonly configService: ConfigService,
private readonly utilsServer: UtilsServer,
) {
const environment = this.configService.get<string>('environment');
// 只在开发环境下记录请求日志
if (environment === 'development') {
this.use = this.useBack;
}
}
use(req: FastifyRequest, res: FastifyReply, next: () => void) {
}
useBack(req: FastifyRequest & FastifyRequest['raw'], res: FastifyReply & FastifyReply['raw'], next: () => void) {
const requestStart = process.hrtime();
const ip = req.id?.slice(4).padEnd(6).toUpperCase();
this.logger.debug(`<=== ${ip} | ${req.method} ${req.url} ${req.ip}`, 'RequestLogger');
res.on('finish', () => {
const [seconds, nanoseconds] = process.hrtime(requestStart);
const duration = seconds * 1000 + nanoseconds / 1000000;
const header = (res as unknown as ResponseWithHeader)._header;
const contentLength = header?.match(/content-length: (\d+)/i)?.[1];
const size = contentLength ? this.utilsServer.formatFileSize(parseInt(contentLength, 10)) : '0 B';
const contentType = header?.match(/content-type: (\w+\/\w+)/i)?.[1];
this.logger.debug(
`===> ${ip} | ${req.method} ${req.url} ${res.statusCode} ${duration.toFixed(2)}ms [${size}] [${contentType}]`,
'RequestLogger'
);
})
next()
}
}

View File

@ -0,0 +1,11 @@
import { Global, Module } from '@nestjs/common';
import { RedisService } from './redis.service';
import { LoggerModule } from '../logger/logger.module';
@Global()
@Module({
imports: [LoggerModule],
providers: [RedisService],
exports: [RedisService],
})
export class RedisModule {}

View File

@ -0,0 +1,84 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createClient, RedisClientType } from 'redis';
import { CustomLogger } from '../logger/logger.service';
@Injectable()
export class RedisService implements OnModuleInit, OnModuleDestroy {
public client: RedisClientType;
constructor(
private configService: ConfigService,
private logger: CustomLogger,
) {
const config = this.configService.get('redis');
this.client = createClient({
name: config.connectName,
username: config.username,
password: config.password,
database: config.database,
url: `redis://${config.username}:${config.password}@${config.host}:${config.port}/${config.database}`,
});
this.client.on('connect', async () => {
this.logger.log(await this.client.set('SI HI', this.configService.get('redis.connectName') as string) || '', 'InitRedis');
});
this.client.on('error', (err) => {
this.logger.error(err.message, err.stack, 'InitRedis');
});
}
async onModuleInit() {
await this.client.connect();
}
async onModuleDestroy() {
await this.client.quit();
}
/**
*
* @param key
* @param value
* @param ttl
*/
async set(key: string, value: string, ttl?: number): Promise<void> {
const expireTime = ttl || this.configService.get('redis.ttl');
await this.client.set(key, value, { EX: expireTime });
}
/**
*
* @param key
* @returns
*/
async get(key: string): Promise<string | null> {
return await this.client.get(key);
}
/**
*
* @param key
*/
async del(key: string): Promise<void> {
await this.client.del(key);
}
/**
*
* @param key
* @returns
*/
async exists(key: string): Promise<boolean> {
return (await this.client.exists(key)) === 1;
}
/**
*
* @param key
* @param ttl
*/
async expire(key: string, ttl: number): Promise<void> {
await this.client.expire(key, ttl);
}
}

View File

@ -0,0 +1,9 @@
import { Module, Global } from '@nestjs/common';
import { UtilsServer } from './utils.server';
@Global()
@Module({
providers: [UtilsServer],
exports: [UtilsServer],
})
export class UtilsModule {}

View File

@ -0,0 +1,57 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as crypto from 'crypto';
@Injectable()
export class UtilsServer {
constructor(private readonly configService: ConfigService) {}
/**
*
* @param password
* @returns
*/
async hashPassword(password: string): Promise<string> {
const salt = this.configService.get<string>('password.salt');
const iterations = this.configService.get<number>('password.iterations');
// 我们设置了 keylen: 128这表示 128 字节
// 每个字节转换为 2 个十六进制字符
// 所以最终输出长度是 128 * 2 = 256 个字符
const keylen = this.configService.get<number>('password.keylen');
const digest = this.configService.get<string>('password.digest');
return new Promise((resolve, reject) => {
crypto.pbkdf2(password, salt as string, iterations as number, keylen as number, digest as string, (err, derivedKey) => {
if (err) reject(err);
resolve(derivedKey.toString('hex'));
});
});
}
/**
*
* @param password
* @param hashedPassword
* @returns
*/
async verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
const hashedInput = await this.hashPassword(password);
return crypto.timingSafeEqual(
Buffer.from(hashedInput, 'hex'),
Buffer.from(hashedPassword, 'hex')
);
}
formatFileSize(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
}

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import configuration from './configuration'; import configuration from '@/config/configuration';
@Module({ @Module({
imports: [ imports: [

View File

@ -4,13 +4,25 @@ export default () => {
server: { server: {
port: parseInt(process.env.PORT || '3000', 10), port: parseInt(process.env.PORT || '3000', 10),
host: process.env.HOST || 'localhost', host: process.env.HOST || 'localhost',
serviceName: 'StarTune',
description: 'A music platform',
version: '1.0.0',
}, },
database: { database: {
host: process.env.DB_HOST || 'localhost', host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306', 10), port: parseInt(process.env.DB_PORT || '3306', 10),
username: process.env.DB_USERNAME || 'root', username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || 'root', password: process.env.DB_PASSWORD || 'root',
database: process.env.DB_DATABASE || 'star_cyan', database: process.env.DB_NAME || 'star_tune',
},
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
username: process.env.REDIS_USERNAME || 'default',
password: process.env.REDIS_PASSWORD || '',
database: parseInt(process.env.REDIS_DATABASE || '0', 10),
connectName: process.env.REDIS_CONNECT_NAME || 'star-tune',
ttl: parseInt(process.env.REDIS_TTL || '3600', 10),
}, },
jwt: { jwt: {
secret: process.env.JWT_SECRET || 'your-secret-key', secret: process.env.JWT_SECRET || 'your-secret-key',
@ -23,5 +35,12 @@ export default () => {
directory: process.env.LOG_DIRECTORY || 'logs', directory: process.env.LOG_DIRECTORY || 'logs',
console: (process.env.LOG_CONSOLE || 'true') === 'true', console: (process.env.LOG_CONSOLE || 'true') === 'true',
}, },
password: {
userDefaultPassword: (process.env.USER_DEFAULT_PASSWORD || 'StarTune123') as string,
salt: (process.env.PASSWORD_SALT || 'star-tune-salt') as string,
iterations: parseInt(process.env.PASSWORD_ITERATIONS || '10000', 10),
keylen: parseInt(process.env.PASSWORD_KEYLEN || '64', 10),
digest: process.env.PASSWORD_DIGEST || 'sha512',
},
}; };
}; };

View File

@ -0,0 +1,62 @@
-- Current sql file was generated after introspecting the database
-- If you want to run this migration please uncomment this code before executing migrations
/*
CREATE TABLE `user` (
`user_id` bigint unsigned NOT NULL,
`status` tinyint unsigned NOT NULL DEFAULT 0,
`user_type` tinyint unsigned NOT NULL DEFAULT 0,
`user_role` int unsigned NOT NULL DEFAULT 0,
`username` varchar(50) NOT NULL,
`email` varchar(320) NOT NULL,
`nickname` varchar(50) NOT NULL DEFAULT '',
`gender` tinyint unsigned NOT NULL DEFAULT 0,
`birthdate` date,
`address` varchar(255) DEFAULT '',
`avatar` varchar(255) DEFAULT '',
`background_image` varchar(255) DEFAULT '',
`created_by` enum('SELF','ADMIN') NOT NULL,
`created_at` datetime(3) NOT NULL,
`updated_at` datetime(3) NOT NULL,
`username_updated_at` datetime(3),
`is_deleted` tinyint unsigned NOT NULL DEFAULT 0,
`deleted_at` datetime(3),
CONSTRAINT `user_user_id` PRIMARY KEY(`user_id`),
CONSTRAINT `idx_unique_username` UNIQUE(`username`),
CONSTRAINT `idx_unique_active_email` UNIQUE(`email`,`is_deleted`)
);
--> statement-breakpoint
CREATE TABLE `user_password` (
`user_id` bigint unsigned NOT NULL,
`password_hash` char(97) NOT NULL,
`previous_password_hash` char(97),
`last_updated_at` datetime(3) NOT NULL,
CONSTRAINT `user_password_user_id` PRIMARY KEY(`user_id`)
);
--> statement-breakpoint
CREATE TABLE `user_profile` (
`user_id` bigint unsigned NOT NULL,
`profile` mediumtext NOT NULL,
`created_at` datetime(3) NOT NULL,
`updated_at` datetime(3) NOT NULL,
CONSTRAINT `user_profile_user_id` PRIMARY KEY(`user_id`)
);
--> statement-breakpoint
CREATE TABLE `user_signature_history` (
`id` bigint unsigned AUTO_INCREMENT NOT NULL,
`user_id` bigint unsigned NOT NULL,
`signature` varchar(100) NOT NULL,
`signature_tag` varchar(100) NOT NULL,
`created_at` datetime(3) NOT NULL,
CONSTRAINT `user_signature_history_id` PRIMARY KEY(`id`)
);
--> statement-breakpoint
CREATE INDEX `idx_created_at` ON `user` (`created_at`);--> statement-breakpoint
CREATE INDEX `idx_email` ON `user` (`email`);--> statement-breakpoint
CREATE INDEX `idx_status` ON `user` (`status`);--> statement-breakpoint
CREATE INDEX `idx_user_type` ON `user` (`user_type`);--> statement-breakpoint
CREATE INDEX `idx_username_updated` ON `user` (`username_updated_at`);--> statement-breakpoint
CREATE INDEX `idx_user_id` ON `user_password` (`user_id`);--> statement-breakpoint
CREATE INDEX `idx_user_id` ON `user_profile` (`user_id`);--> statement-breakpoint
CREATE INDEX `idx_user_created` ON `user_signature_history` (`user_id`,`created_at`);--> statement-breakpoint
CREATE INDEX `idx_user_id` ON `user_signature_history` (`user_id`);
*/

View File

@ -0,0 +1,8 @@
ALTER TABLE `user` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint
ALTER TABLE `user` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint
ALTER TABLE `user_password` MODIFY COLUMN `password_hash` varchar(128) NOT NULL;--> statement-breakpoint
ALTER TABLE `user_password` MODIFY COLUMN `previous_password_hash` varchar(128);--> statement-breakpoint
ALTER TABLE `user_password` MODIFY COLUMN `last_updated_at` datetime(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint
ALTER TABLE `user_profile` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint
ALTER TABLE `user_profile` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint
ALTER TABLE `user_signature_history` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP);

View File

@ -4,8 +4,8 @@ import {
NestFastifyApplication, NestFastifyApplication,
} from '@nestjs/platform-fastify'; } from '@nestjs/platform-fastify';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module'; import { AppModule } from '@/app.module';
import { CustomLogger } from './logger/custom.logger'; import { CustomLogger } from '@/common/logger/logger.service';
interface ServerConfig { interface ServerConfig {
port: number; port: number;
@ -15,7 +15,9 @@ interface ServerConfig {
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>( const app = await NestFactory.create<NestFastifyApplication>(
AppModule, AppModule,
new FastifyAdapter(), new FastifyAdapter({
logger: false
}),
); );
const configService = app.get(ConfigService); const configService = app.get(ConfigService);
@ -41,7 +43,7 @@ async function bootstrap() {
await app.listen(port, host); await app.listen(port, host);
const environment = configService.get<string>('environment'); const environment = configService.get<string>('environment');
logger.info( logger.info(
`应用程序正在运行: http://${host}:${port}, 环境: ${environment}`, `应用程序正在运行: http://${host==='0.0.0.0'?'127.0.0.1':host}:${port}, 环境: ${environment}`,
'Bootstrap', 'Bootstrap',
); );
} }

View File

@ -10,6 +10,9 @@
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
"baseUrl": "./", "baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"incremental": true, "incremental": true,
"skipLibCheck": true, "skipLibCheck": true,
"strictNullChecks": true, "strictNullChecks": true,