feat:随机表情包

This commit is contained in:
Jerry 2025-09-01 16:57:23 +08:00
parent 264cd37dd5
commit c7108833ed
6 changed files with 175 additions and 0 deletions

4
.gitignore vendored
View File

@ -2,6 +2,10 @@
/dist
/node_modules
/build
/private
/public
/logs
# Logs
logs

View File

@ -12,6 +12,7 @@ import { SystemWebModule } from './modules/system/systemWeb.module';
import { BotModule } from './modules/bot/bot.module';
import { CdnModule } from './modules/cdn/cdn.module';
import { WordsModule } from './modules/words/words.module';
import { MemeModule } from './modules/meme/meme.module';
@Module({
imports: [
@ -28,6 +29,7 @@ import { WordsModule } from './modules/words/words.module';
BotModule,
CdnModule,
WordsModule,
MemeModule,
],
})
export class AppModule {}

View File

@ -28,6 +28,8 @@ export class PathService {
package: path.join(this.baseDir, 'package.json'),
modules: path.join(this.baseDir, 'src/modules'),
words: path.join(this.baseDir, 'private/words/src'),
private: path.join(this.baseDir, 'private'),
meme: path.join(this.baseDir, 'private/meme'),
};
return type ? mappings[type] : this.baseDir;
@ -45,6 +47,7 @@ export class PathService {
this.get('temp'),
this.get('public'),
this.get('words'),
this.get('meme'),
];
pathsToInit.forEach((dirPath) => {
@ -105,5 +108,7 @@ export type PathType =
| 'temp'
| 'userData'
| 'package'
| 'meme'
| 'modules'
| 'private'
| 'words';

View File

@ -0,0 +1,69 @@
import {
Controller,
Post,
Body,
Res,
HttpException,
HttpStatus,
Logger,
Inject,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBody, ApiProperty } from '@nestjs/swagger';
import { MemeService } from './meme.service';
import { Response } from 'express';
import * as fs from 'fs';
class MemeRequestDto {
@ApiProperty({ description: '角色名称', example: 'zhenxun', required: false })
character?: string;
@ApiProperty({ description: '状态', example: 'happy', required: false })
status?: string;
}
@Controller('meme')
@ApiTags('Meme')
export class MemeController {
private readonly logger = new Logger(MemeController.name);
constructor(@Inject(MemeService) private readonly memeService: MemeService) {}
@Post('getRandom')
@ApiOperation({ summary: '获取随机表情包' })
@ApiBody({ type: MemeRequestDto })
async getRandomMeme(@Body() dto: MemeRequestDto, @Res() res: Response) {
try {
const memePath = await this.memeService.getRandomMemePath(
dto.character,
dto.status,
);
if (!memePath) {
throw new HttpException(
'没有找到符合条件的表情包',
HttpStatus.NOT_FOUND,
);
}
const stream = fs.createReadStream(memePath);
stream.on('error', () => {
throw new HttpException(
'读取表情包失败',
HttpStatus.INTERNAL_SERVER_ERROR,
);
});
const ext = memePath.split('.').pop()?.toLowerCase();
let contentType = 'image/jpeg';
if (ext === 'png') contentType = 'image/png';
if (ext === 'gif') contentType = 'image/gif';
if (ext === 'webp') contentType = 'image/webp';
res.setHeader('Content-Type', contentType);
stream.pipe(res);
} catch (e) {
this.logger.error(`获取表情包失败: ${e.message}`);
throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { MemeService } from './meme.service';
import { MemeController } from './meme.controller';
import { PathService } from '../../core/path/path.service';
import { PathModule } from '../../core/path/path.module';
@Module({
imports: [PathModule],
providers: [MemeService],
controllers: [MemeController],
})
export class MemeModule {}

View File

@ -0,0 +1,83 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import * as path from 'path';
import * as fs from 'fs/promises';
import { PathService } from '../../core/path/path.service';
@Injectable()
export class MemeService {
private readonly logger = new Logger(MemeService.name);
constructor(@Inject(PathService) private readonly pathService: PathService) {}
/**
*
* @param character
* @param status
*/
async getRandomMemePath(
character?: string,
status?: string,
): Promise<string | null> {
const baseDir = path.join(this.pathService.get('private'), 'meme');
try {
if (!character) {
return this.getRandomFileRecursive(baseDir);
}
const characterDir = path.join(baseDir, character);
if (!status) {
return this.getRandomFileRecursive(characterDir);
}
const statusDir = path.join(characterDir, status);
return this.getRandomFileFromDir(statusDir);
} catch (e) {
this.logger.error(`获取表情包失败: ${e.message}`);
return null;
}
}
/**
*
*/
private async getRandomFileFromDir(dir: string): Promise<string | null> {
try {
const files = await fs.readdir(dir);
const images = files.filter((f) => /\.(jpg|jpeg|png|gif|webp)$/i.test(f));
if (images.length === 0) return null;
const randomFile = images[Math.floor(Math.random() * images.length)];
return path.join(dir, randomFile);
} catch {
return null;
}
}
/**
*
* @param dir
* @private
*/
private async getRandomFileRecursive(dir: string): Promise<string | null> {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files: string[] = [];
const dirs: string[] = [];
for (const entry of entries) {
if (entry.isDirectory()) {
dirs.push(path.join(dir, entry.name));
} else if (/\.(jpg|jpeg|png|gif|webp)$/i.test(entry.name)) {
files.push(path.join(dir, entry.name));
}
}
let allFiles = [...files];
for (const subDir of dirs) {
const subFile = await this.getRandomFileRecursive(subDir);
if (subFile) allFiles.push(subFile);
}
if (allFiles.length === 0) return null;
return allFiles[Math.floor(Math.random() * allFiles.length)];
} catch {
return null;
}
}
}