feat(redis mysql middleware):
This commit is contained in:
parent
16bfea2dc0
commit
9afe6d756f
@ -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
|
@ -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>
|
|
||||||
<!--[](https://opencollective.com/nest#backer)
|
|
||||||
[](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).
|
|
67
star-tune/dbDesign/init.sql
Normal file
67
star-tune/dbDesign/init.sql
Normal 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='签名历史表';
|
67
star-tune/dbDesign/user.01.sql
Normal file
67
star-tune/dbDesign/user.01.sql
Normal 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='签名历史表';
|
75
star-tune/drizzle.config.ts
Normal file
75
star-tune/drizzle.config.ts
Normal 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;
|
1227
star-tune/package-lock.json
generated
1227
star-tune/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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('*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
star-tune/src/common/database/database.module.ts
Normal file
19
star-tune/src/common/database/database.module.ts
Normal 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 {}
|
75
star-tune/src/common/database/database.service.ts
Normal file
75
star-tune/src/common/database/database.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
17
star-tune/src/common/init/init.module.ts
Normal file
17
star-tune/src/common/init/init.module.ts
Normal 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 {}
|
85
star-tune/src/common/init/init.service.ts
Normal file
85
star-tune/src/common/init/init.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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}`;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
47
star-tune/src/common/middleware/request-logger.middleware.ts
Normal file
47
star-tune/src/common/middleware/request-logger.middleware.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
11
star-tune/src/common/redis/redis.module.ts
Normal file
11
star-tune/src/common/redis/redis.module.ts
Normal 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 {}
|
84
star-tune/src/common/redis/redis.service.ts
Normal file
84
star-tune/src/common/redis/redis.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
9
star-tune/src/common/utils/utils.module.ts
Normal file
9
star-tune/src/common/utils/utils.module.ts
Normal 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 {}
|
57
star-tune/src/common/utils/utils.server.ts
Normal file
57
star-tune/src/common/utils/utils.server.ts
Normal 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]}`;
|
||||||
|
}
|
||||||
|
}
|
@ -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: [
|
||||||
|
@ -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',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
62
star-tune/src/drizzle/0000_tearful_menace.sql
Normal file
62
star-tune/src/drizzle/0000_tearful_menace.sql
Normal 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`);
|
||||||
|
*/
|
8
star-tune/src/drizzle/0001_silent_natasha_romanoff.sql
Normal file
8
star-tune/src/drizzle/0001_silent_natasha_romanoff.sql
Normal 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);
|
@ -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',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user