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_MAX_FILES=30d # 保留日志文件的时间
|
||||
LOG_MAX_SIZE=20m # 单个日志文件的最大大小
|
||||
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">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
# star-tune
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[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
|
||||
> nestjs fastify drizzle mysql2 redis
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
npm run dev
|
||||
npm run start:dev
|
||||
npm run start:debug
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
npm start
|
||||
npm run start
|
||||
npm run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
### DB
|
||||
```
|
||||
makeSQL
|
||||
makeEntity
|
||||
syncDB
|
||||
sqlV
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
1. npm i -d
|
||||
2. 将init.sql同步到数据库
|
||||
3. npm start
|
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": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
@ -18,7 +21,11 @@
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"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": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
@ -26,7 +33,10 @@
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-fastify": "^11.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"drizzle-orm": "^0.44.0",
|
||||
"mysql2": "^3.14.1",
|
||||
"nest-winston": "^1.10.2",
|
||||
"redis": "^5.1.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"winston": "^3.17.0",
|
||||
@ -43,6 +53,7 @@
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
|
@ -6,7 +6,7 @@ export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
async getHello(): Promise<object> {
|
||||
return await this.appService.getHello();
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,38 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { AppConfigModule } from './config/config.module';
|
||||
import { LoggerModule } from './logger/logger.module';
|
||||
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { InitModule } from '@/common/init/init.module';
|
||||
import { DatabaseModule } from '@/common/database/database.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({
|
||||
imports: [AppConfigModule, LoggerModule],
|
||||
imports: [
|
||||
// 配置模块
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [configuration],
|
||||
}),
|
||||
// 日志模块
|
||||
LoggerModule,
|
||||
// 数据库模块
|
||||
DatabaseModule,
|
||||
// Redis 模块
|
||||
RedisModule,
|
||||
// 初始化模块
|
||||
InitModule,
|
||||
// 工具模块
|
||||
UtilsModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
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()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
async getHello(): Promise<object> {
|
||||
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 DailyRotateFile from 'winston-daily-rotate-file';
|
||||
import { join } from 'path';
|
||||
import { CustomLogger } from './custom.logger';
|
||||
import { CustomLogger } from '@/common/logger/logger.service';
|
||||
|
||||
// 定义配置接口
|
||||
interface LogConfig {
|
||||
@ -44,21 +44,35 @@ const getPaddedPid = () => process.pid.toString().padStart(5, '0');
|
||||
// 创建自定义的 winston 格式化器
|
||||
const createCustomFormat = (environment: Environment) => {
|
||||
return winston.format.printf(
|
||||
({
|
||||
timestamp,
|
||||
level,
|
||||
message,
|
||||
context,
|
||||
}: {
|
||||
(info: {
|
||||
timestamp: string;
|
||||
level: string;
|
||||
message: string;
|
||||
message: string | Error;
|
||||
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 levelColor = levelColors[level] || '\x1b[37m';
|
||||
// 粗体
|
||||
const bold = '\x1b[1m';
|
||||
// 重置颜色的代码
|
||||
const reset = '\x1b[0m';
|
||||
|
||||
@ -82,10 +96,18 @@ const createCustomFormat = (environment: Environment) => {
|
||||
contextContent.length > FORMAT_CONFIG.CONTEXT_LENGTH
|
||||
? contextContent.slice(0, 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 { ConfigModule } from '@nestjs/config';
|
||||
import configuration from './configuration';
|
||||
import configuration from '@/config/configuration';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -4,13 +4,25 @@ export default () => {
|
||||
server: {
|
||||
port: parseInt(process.env.PORT || '3000', 10),
|
||||
host: process.env.HOST || 'localhost',
|
||||
serviceName: 'StarTune',
|
||||
description: 'A music platform',
|
||||
version: '1.0.0',
|
||||
},
|
||||
database: {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '3306', 10),
|
||||
username: process.env.DB_USERNAME || '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: {
|
||||
secret: process.env.JWT_SECRET || 'your-secret-key',
|
||||
@ -23,5 +35,12 @@ export default () => {
|
||||
directory: process.env.LOG_DIRECTORY || 'logs',
|
||||
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,
|
||||
} from '@nestjs/platform-fastify';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AppModule } from './app.module';
|
||||
import { CustomLogger } from './logger/custom.logger';
|
||||
import { AppModule } from '@/app.module';
|
||||
import { CustomLogger } from '@/common/logger/logger.service';
|
||||
|
||||
interface ServerConfig {
|
||||
port: number;
|
||||
@ -15,7 +15,9 @@ interface ServerConfig {
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter(),
|
||||
new FastifyAdapter({
|
||||
logger: false
|
||||
}),
|
||||
);
|
||||
|
||||
const configService = app.get(ConfigService);
|
||||
@ -41,7 +43,7 @@ async function bootstrap() {
|
||||
await app.listen(port, host);
|
||||
const environment = configService.get<string>('environment');
|
||||
logger.info(
|
||||
`应用程序正在运行: http://${host}:${port}, 环境: ${environment}`,
|
||||
`应用程序正在运行: http://${host==='0.0.0.0'?'127.0.0.1':host}:${port}, 环境: ${environment}`,
|
||||
'Bootstrap',
|
||||
);
|
||||
}
|
||||
|
@ -10,6 +10,9 @@
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
|
Loading…
Reference in New Issue
Block a user