init
This commit is contained in:
10
.env.production
Normal file
10
.env.production
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Database Configuration
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=13306
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=MemeMind@2026
|
||||||
|
DB_DATABASE=mememind
|
||||||
|
|
||||||
|
# Application Configuration
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Keep example files
|
||||||
|
!.env.example
|
||||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
98
README.md
Normal file
98
README.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compile and run the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ pnpm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ pnpm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ pnpm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ pnpm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ pnpm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ pnpm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
$ pnpm 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).
|
||||||
35
eslint.config.mjs
Normal file
35
eslint.config.mjs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
|
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
8
nest-cli.json
Normal file
8
nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
78
package.json
Normal file
78
package.json
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"name": "MemeMind-Server",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/config": "^4.0.3",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/swagger": "^11.2.6",
|
||||||
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.15.1",
|
||||||
|
"mysql2": "^3.19.1",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"typeorm": "^0.3.28"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
6708
pnpm-lock.yaml
generated
Normal file
6708
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
src/app.module.ts
Normal file
29
src/app.module.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AppConfigModule } from './config/config.module';
|
||||||
|
import { WechatGameModule } from './modules/wechat-game/wechat-game.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
AppConfigModule,
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
type: 'mysql',
|
||||||
|
host: configService.get<string>('database.host'),
|
||||||
|
port: configService.get<number>('database.port'),
|
||||||
|
username: configService.get<string>('database.username'),
|
||||||
|
password: configService.get<string>('database.password'),
|
||||||
|
database: configService.get<string>('database.database'),
|
||||||
|
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
|
synchronize: configService.get<string>('NODE_ENV') !== 'production',
|
||||||
|
logging: configService.get<string>('NODE_ENV') !== 'production',
|
||||||
|
autoLoadEntities: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
WechatGameModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
30
src/common/dto/api-response.dto.ts
Normal file
30
src/common/dto/api-response.dto.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ApiResponseDto<T> {
|
||||||
|
@ApiProperty({ description: '请求是否成功' })
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '响应数据', nullable: true })
|
||||||
|
data: T | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '错误信息', nullable: true })
|
||||||
|
message: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '响应时间戳' })
|
||||||
|
timestamp: Date;
|
||||||
|
|
||||||
|
constructor(success: boolean, data: T | null, message: string | null = null) {
|
||||||
|
this.success = success;
|
||||||
|
this.data = data;
|
||||||
|
this.message = message;
|
||||||
|
this.timestamp = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
static success<T>(data: T): ApiResponseDto<T> {
|
||||||
|
return new ApiResponseDto(true, data, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
static error<T>(message: string): ApiResponseDto<T | null> {
|
||||||
|
return new ApiResponseDto<T | null>(false, null, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/common/filters/http-exception.filter.ts
Normal file
49
src/common/filters/http-exception.filter.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
ExceptionFilter,
|
||||||
|
Catch,
|
||||||
|
ArgumentsHost,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { ApiResponseDto } from '../dto/api-response.dto';
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class HttpExceptionFilter implements ExceptionFilter {
|
||||||
|
private readonly logger = new Logger(HttpExceptionFilter.name);
|
||||||
|
|
||||||
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
const request = ctx.getRequest<Request>();
|
||||||
|
|
||||||
|
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
let message = 'Internal server error';
|
||||||
|
|
||||||
|
if (exception instanceof HttpException) {
|
||||||
|
status = exception.getStatus();
|
||||||
|
const exceptionResponse = exception.getResponse();
|
||||||
|
|
||||||
|
if (typeof exceptionResponse === 'string') {
|
||||||
|
message = exceptionResponse;
|
||||||
|
} else if (typeof exceptionResponse === 'object') {
|
||||||
|
const responseObj = exceptionResponse as Record<string, unknown>;
|
||||||
|
message = (responseObj.message as string) || exception.message;
|
||||||
|
}
|
||||||
|
} else if (exception instanceof Error) {
|
||||||
|
message = exception.message;
|
||||||
|
this.logger.error(
|
||||||
|
`Internal error: ${exception.message}`,
|
||||||
|
exception.stack,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorResponse = ApiResponseDto.error(message);
|
||||||
|
|
||||||
|
response.status(status).json({
|
||||||
|
...errorResponse,
|
||||||
|
path: request.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/config/config.module.ts
Normal file
18
src/config/config.module.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { databaseConfig } from './database.config';
|
||||||
|
import { validateEnvironment } from './env.validation';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [databaseConfig],
|
||||||
|
validate: validateEnvironment,
|
||||||
|
envFilePath: ['.env.local', '.env'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
exports: [ConfigModule],
|
||||||
|
})
|
||||||
|
export class AppConfigModule {}
|
||||||
34
src/config/database.config.ts
Normal file
34
src/config/database.config.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||||
|
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||||
|
|
||||||
|
export const databaseConfig = registerAs(
|
||||||
|
'database',
|
||||||
|
(): TypeOrmModuleOptions => ({
|
||||||
|
type: 'mysql',
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT || '3306', 10),
|
||||||
|
username: process.env.DB_USERNAME || 'meme_user',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
database: process.env.DB_DATABASE || 'meme_mind',
|
||||||
|
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
|
||||||
|
synchronize: process.env.NODE_ENV !== 'production',
|
||||||
|
logging: process.env.NODE_ENV !== 'production',
|
||||||
|
autoLoadEntities: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const dataSourceOptions: DataSourceOptions = {
|
||||||
|
type: 'mysql',
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT || '3306', 10),
|
||||||
|
username: process.env.DB_USERNAME || 'meme_user',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
database: process.env.DB_DATABASE || 'meme_mind',
|
||||||
|
entities: ['dist/**/*.entity{.ts,.js}'],
|
||||||
|
synchronize: false,
|
||||||
|
logging: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataSource = new DataSource(dataSourceOptions);
|
||||||
|
export default dataSource;
|
||||||
56
src/config/env.validation.ts
Normal file
56
src/config/env.validation.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
IsEnum,
|
||||||
|
IsNumber,
|
||||||
|
IsString,
|
||||||
|
validateSync,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
enum Environment {
|
||||||
|
Development = 'development',
|
||||||
|
Production = 'production',
|
||||||
|
Test = 'test',
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnvironmentVariables {
|
||||||
|
@IsEnum(Environment)
|
||||||
|
NODE_ENV: Environment = Environment.Development;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
PORT: number = 3000;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
DB_HOST: string = 'localhost';
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
DB_PORT: number = 3306;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
DB_USERNAME: string = 'meme_user';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
DB_PASSWORD: string = '';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
DB_DATABASE: string = 'meme_mind';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateEnvironment(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
): EnvironmentVariables {
|
||||||
|
const validatedConfig = plainToInstance(EnvironmentVariables, config, {
|
||||||
|
enableImplicitConversion: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const errors = validateSync(validatedConfig, {
|
||||||
|
skipMissingProperties: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Environment validation failed: ${errors.map((e) => e.toString()).join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return validatedConfig;
|
||||||
|
}
|
||||||
45
src/main.ts
Normal file
45
src/main.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
// 设置全局前缀
|
||||||
|
app.setGlobalPrefix('api');
|
||||||
|
|
||||||
|
// 启用 CORS (支持微信小游戏)
|
||||||
|
app.enableCors({
|
||||||
|
origin: true,
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 全局验证管道
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
transform: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 全局异常过滤器
|
||||||
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
|
|
||||||
|
// Swagger 文档配置
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('MemeMind Server API')
|
||||||
|
.setDescription('微信小游戏 MemeMind 服务端 API 文档')
|
||||||
|
.setVersion('1.0')
|
||||||
|
.build();
|
||||||
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('api/docs', app, document);
|
||||||
|
|
||||||
|
const port = process.env.PORT ?? 3000;
|
||||||
|
await app.listen(port);
|
||||||
|
console.log(`Application is running on: http://localhost:${port}/api`);
|
||||||
|
console.log(`Swagger documentation: http://localhost:${port}/api/docs`);
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
32
src/modules/wechat-game/dto/game-config-response.dto.ts
Normal file
32
src/modules/wechat-game/dto/game-config-response.dto.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class GameConfigResponseDto {
|
||||||
|
@ApiProperty({ description: '配置ID' })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '配置键名' })
|
||||||
|
configKey: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '配置值' })
|
||||||
|
configValue: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '配置描述', nullable: true })
|
||||||
|
description: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否激活' })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新时间' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GameConfigListResponseDto {
|
||||||
|
@ApiProperty({ type: [GameConfigResponseDto], description: '配置列表' })
|
||||||
|
configs: GameConfigResponseDto[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '配置总数' })
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
35
src/modules/wechat-game/entities/game-config.entity.ts
Normal file
35
src/modules/wechat-game/entities/game-config.entity.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('game_configs')
|
||||||
|
export class GameConfig {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'config_key' })
|
||||||
|
configKey: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', name: 'config_value' })
|
||||||
|
configValue: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'varchar',
|
||||||
|
length: 100,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
description: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: true, name: 'is_active' })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { GameConfig } from '../entities/game-config.entity';
|
||||||
|
|
||||||
|
export interface IGameConfigRepository {
|
||||||
|
findAll(): Promise<GameConfig[]>;
|
||||||
|
findById(id: string): Promise<GameConfig | null>;
|
||||||
|
findByKey(key: string): Promise<GameConfig | null>;
|
||||||
|
findActiveConfigs(): Promise<GameConfig[]>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { GameConfig } from '../entities/game-config.entity';
|
||||||
|
import { IGameConfigRepository } from './game-config.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GameConfigRepository implements IGameConfigRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(GameConfig)
|
||||||
|
private readonly repository: Repository<GameConfig>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findAll(): Promise<GameConfig[]> {
|
||||||
|
return this.repository.find();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<GameConfig | null> {
|
||||||
|
return this.repository.findOne({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByKey(key: string): Promise<GameConfig | null> {
|
||||||
|
return this.repository.findOne({ where: { configKey: key } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findActiveConfigs(): Promise<GameConfig[]> {
|
||||||
|
return this.repository.find({ where: { isActive: true } });
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/modules/wechat-game/wechat-game.controller.ts
Normal file
33
src/modules/wechat-game/wechat-game.controller.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Controller, Get, Param } from '@nestjs/common';
|
||||||
|
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { WechatGameService } from './wechat-game.service';
|
||||||
|
import {
|
||||||
|
GameConfigResponseDto,
|
||||||
|
GameConfigListResponseDto,
|
||||||
|
} from './dto/game-config-response.dto';
|
||||||
|
import { ApiResponseDto } from '../../common/dto/api-response.dto';
|
||||||
|
|
||||||
|
@ApiTags('微信小游戏')
|
||||||
|
@Controller('v1/wechat-game')
|
||||||
|
export class WechatGameController {
|
||||||
|
constructor(private readonly wechatGameService: WechatGameService) {}
|
||||||
|
|
||||||
|
@Get('configs')
|
||||||
|
@ApiOperation({ summary: '获取所有游戏配置', description: '获取所有激活的游戏配置列表' })
|
||||||
|
@ApiResponse({ status: 200, description: '成功获取配置列表' })
|
||||||
|
async getAllConfigs(): Promise<ApiResponseDto<GameConfigListResponseDto>> {
|
||||||
|
const data = await this.wechatGameService.getAllConfigs();
|
||||||
|
return ApiResponseDto.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('configs/:key')
|
||||||
|
@ApiOperation({ summary: '根据key获取配置', description: '根据配置键名获取单个游戏配置' })
|
||||||
|
@ApiResponse({ status: 200, description: '成功获取配置' })
|
||||||
|
@ApiResponse({ status: 404, description: '配置不存在' })
|
||||||
|
async getConfigByKey(
|
||||||
|
@Param('key') key: string,
|
||||||
|
): Promise<ApiResponseDto<GameConfigResponseDto>> {
|
||||||
|
const data = await this.wechatGameService.getConfigByKey(key);
|
||||||
|
return ApiResponseDto.success(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/modules/wechat-game/wechat-game.module.ts
Normal file
14
src/modules/wechat-game/wechat-game.module.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { WechatGameController } from './wechat-game.controller';
|
||||||
|
import { WechatGameService } from './wechat-game.service';
|
||||||
|
import { GameConfig } from './entities/game-config.entity';
|
||||||
|
import { GameConfigRepository } from './repositories/game-config.repository';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([GameConfig])],
|
||||||
|
controllers: [WechatGameController],
|
||||||
|
providers: [WechatGameService, GameConfigRepository],
|
||||||
|
exports: [WechatGameService],
|
||||||
|
})
|
||||||
|
export class WechatGameModule {}
|
||||||
86
src/modules/wechat-game/wechat-game.service.spec.ts
Normal file
86
src/modules/wechat-game/wechat-game.service.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { WechatGameService } from './wechat-game.service';
|
||||||
|
import { GameConfigRepository } from './repositories/game-config.repository';
|
||||||
|
import { NotFoundException } from '@nestjs/common';
|
||||||
|
import { GameConfig } from './entities/game-config.entity';
|
||||||
|
|
||||||
|
describe('WechatGameService', () => {
|
||||||
|
let service: WechatGameService;
|
||||||
|
let repository: GameConfigRepository;
|
||||||
|
|
||||||
|
const mockGameConfig: GameConfig = {
|
||||||
|
id: 'test-uuid',
|
||||||
|
configKey: 'game_speed',
|
||||||
|
configValue: '1.5',
|
||||||
|
description: 'Game speed multiplier',
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockRepository = {
|
||||||
|
findActiveConfigs: jest.fn(),
|
||||||
|
findByKey: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
WechatGameService,
|
||||||
|
{
|
||||||
|
provide: GameConfigRepository,
|
||||||
|
useValue: mockRepository,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<WechatGameService>(WechatGameService);
|
||||||
|
repository = module.get<GameConfigRepository>(GameConfigRepository);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllConfigs', () => {
|
||||||
|
it('should return all active configs', async () => {
|
||||||
|
mockRepository.findActiveConfigs.mockResolvedValue([mockGameConfig]);
|
||||||
|
|
||||||
|
const result = await service.getAllConfigs();
|
||||||
|
|
||||||
|
expect(result.configs).toHaveLength(1);
|
||||||
|
expect(result.total).toBe(1);
|
||||||
|
expect(result.configs[0].configKey).toBe('game_speed');
|
||||||
|
expect(mockRepository.findActiveConfigs).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when no configs found', async () => {
|
||||||
|
mockRepository.findActiveConfigs.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const result = await service.getAllConfigs();
|
||||||
|
|
||||||
|
expect(result.configs).toHaveLength(0);
|
||||||
|
expect(result.total).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConfigByKey', () => {
|
||||||
|
it('should return config by key', async () => {
|
||||||
|
mockRepository.findByKey.mockResolvedValue(mockGameConfig);
|
||||||
|
|
||||||
|
const result = await service.getConfigByKey('game_speed');
|
||||||
|
|
||||||
|
expect(result.configKey).toBe('game_speed');
|
||||||
|
expect(result.configValue).toBe('1.5');
|
||||||
|
expect(mockRepository.findByKey).toHaveBeenCalledWith('game_speed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw NotFoundException when config not found', async () => {
|
||||||
|
mockRepository.findByKey.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.getConfigByKey('nonexistent')).rejects.toThrow(
|
||||||
|
NotFoundException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
42
src/modules/wechat-game/wechat-game.service.ts
Normal file
42
src/modules/wechat-game/wechat-game.service.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { GameConfigRepository } from './repositories/game-config.repository';
|
||||||
|
import {
|
||||||
|
GameConfigResponseDto,
|
||||||
|
GameConfigListResponseDto,
|
||||||
|
} from './dto/game-config-response.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WechatGameService {
|
||||||
|
constructor(private readonly gameConfigRepository: GameConfigRepository) {}
|
||||||
|
|
||||||
|
async getAllConfigs(): Promise<GameConfigListResponseDto> {
|
||||||
|
const configs = await this.gameConfigRepository.findActiveConfigs();
|
||||||
|
|
||||||
|
return {
|
||||||
|
configs: configs.map((config) => this.toResponseDto(config)),
|
||||||
|
total: configs.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfigByKey(key: string): Promise<GameConfigResponseDto> {
|
||||||
|
const config = await this.gameConfigRepository.findByKey(key);
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new NotFoundException(`Game config with key "${key}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.toResponseDto(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toResponseDto(config: import('./entities/game-config.entity').GameConfig): GameConfigResponseDto {
|
||||||
|
return {
|
||||||
|
id: config.id,
|
||||||
|
configKey: config.configKey,
|
||||||
|
configValue: config.configValue,
|
||||||
|
description: config.description,
|
||||||
|
isActive: config.isActive,
|
||||||
|
createdAt: config.createdAt,
|
||||||
|
updatedAt: config.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
25
test/app.e2e-spec.ts
Normal file
25
test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { App } from 'supertest/types';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication<App>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
9
test/jest-e2e.json
Normal file
9
test/jest-e2e.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"resolvePackageJsonExports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2023",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user