From 645065b4fba5945ee845ec113e138ef16bcce858 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Sat, 11 Oct 2025 22:58:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 - pnpm-lock.yaml | 80 +++-------------- src/modules/meme/meme.controller.ts | 131 +++++++++++++++++++++++++++- src/modules/meme/meme.module.ts | 1 + 4 files changed, 144 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index 92aab2c..71e363d 100755 --- a/package.json +++ b/package.json @@ -51,8 +51,6 @@ "@types/supertest": "^6.0.2", "@types/ws": "^8.18.1", "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-prettier": "^5.2.2", "globals": "^16.0.0", "jest": "^29.7.0", "prettier": "^3.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 049883f..a413337 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: moment: specifier: ^2.30.1 version: 2.30.1 + multer: + specifier: ^2.0.2 + version: 2.0.2 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -96,6 +99,9 @@ importers: '@types/jest': specifier: ^29.5.14 version: 29.5.14 + '@types/multer': + specifier: ^2.0.0 + version: 2.0.0 '@types/node': specifier: ^22.16.4 version: 22.16.5 @@ -111,12 +117,6 @@ importers: eslint: specifier: ^9.18.0 version: 9.31.0 - eslint-config-prettier: - specifier: ^10.0.1 - version: 10.1.8(eslint@9.31.0) - eslint-plugin-prettier: - specifier: ^5.2.2 - version: 5.5.3(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.31.0))(eslint@9.31.0)(prettier@3.6.2) globals: specifier: ^16.0.0 version: 16.3.0 @@ -931,10 +931,6 @@ packages: '@paralleldrive/cuid2@2.2.2': resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} - '@pkgr/core@0.2.9': - resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} @@ -1135,6 +1131,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/multer@2.0.0': + resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==} + '@types/node@22.16.5': resolution: {integrity: sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==} @@ -1908,26 +1907,6 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-prettier@10.1.8: - resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-plugin-prettier@5.5.3: - resolution: {integrity: sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - '@types/eslint': '>=8.0.0' - eslint: '>=8.0.0' - eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' - prettier: '>=3.0.0' - peerDependenciesMeta: - '@types/eslint': - optional: true - eslint-config-prettier: - optional: true - eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -2026,9 +2005,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} @@ -2999,10 +2975,6 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} - prettier@3.6.2: resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} @@ -3377,10 +3349,6 @@ packages: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} - synckit@0.11.11: - resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} - engines: {node: ^14.18.0 || >=16.0.0} - tapable@2.2.2: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} engines: {node: '>=6'} @@ -4631,8 +4599,6 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 - '@pkgr/core@0.2.9': {} - '@scarf/scarf@1.4.0': {} '@sec-ant/readable-stream@0.4.1': {} @@ -4830,6 +4796,10 @@ snapshots: '@types/mime@1.3.5': {} + '@types/multer@2.0.0': + dependencies: + '@types/express': 5.0.3 + '@types/node@22.16.5': dependencies: undici-types: 6.21.0 @@ -5679,20 +5649,6 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.31.0): - dependencies: - eslint: 9.31.0 - - eslint-plugin-prettier@5.5.3(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.31.0))(eslint@9.31.0)(prettier@3.6.2): - dependencies: - eslint: 9.31.0 - prettier: 3.6.2 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.11 - optionalDependencies: - '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@9.31.0) - eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -5856,8 +5812,6 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-diff@1.3.0: {} - fast-fifo@1.3.2: {} fast-glob@3.3.3: @@ -6947,10 +6901,6 @@ snapshots: prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.0: - dependencies: - fast-diff: 1.3.0 - prettier@3.6.2: {} pretty-format@29.7.0: @@ -7364,10 +7314,6 @@ snapshots: symbol-observable@4.0.0: {} - synckit@0.11.11: - dependencies: - '@pkgr/core': 0.2.9 - tapable@2.2.2: {} tar-stream@3.1.7: diff --git a/src/modules/meme/meme.controller.ts b/src/modules/meme/meme.controller.ts index b6f2280..1dff80c 100644 --- a/src/modules/meme/meme.controller.ts +++ b/src/modules/meme/meme.controller.ts @@ -10,8 +10,18 @@ import { Logger, Inject, Ip, + UseInterceptors, + UploadedFile, + UseGuards, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBody, ApiQuery } from '@nestjs/swagger'; +import { + ApiTags, + ApiOperation, + ApiBody, + ApiQuery, + ApiConsumes, + ApiHeader, +} from '@nestjs/swagger'; import { MemeService } from './meme.service'; import { Response } from 'express'; import * as fs from 'fs'; @@ -19,6 +29,12 @@ import { Throttle } from 'stream-throttle'; import { ToolsService } from '../../core/tools/tools.service'; import { RedisService } from '../../core/redis/redis.service'; import imageType from 'image-type'; +import { FileInterceptor } from '@nestjs/platform-express'; +import * as path from 'path'; +import { OpenListService } from '../../core/openlist/openlist.service'; +import { PathService } from '../../core/path/path.service'; +import { TokenAuthGuard } from '../../core/tools/token-auth.guard'; +import { AppConfigService } from '../../config/config.service'; class MemeRequestDto { character?: string; @@ -38,6 +54,12 @@ export class MemeController { private readonly toolsService: ToolsService, @Inject(RedisService) private readonly redisService: RedisService, + @Inject(OpenListService) + private readonly openListService: OpenListService, + @Inject(PathService) + private readonly pathService: PathService, + @Inject(AppConfigService) + private readonly configService: AppConfigService, ) {} @Post('get') @@ -158,4 +180,111 @@ export class MemeController { throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR); } } + + /** + * 上传文件 + * @param file + * @param character + * @param status + * @param token + * @param res + */ + @Post('upload') + @ApiOperation({ summary: '上传表情包并同步' }) + @ApiConsumes('multipart/form-data') + @UseInterceptors(FileInterceptor('file')) + @ApiHeader({ name: 'x-token', description: '身份验证token', required: true }) + @UseGuards(TokenAuthGuard) + @ApiBody({ + description: '上传表情包文件', + schema: { + type: 'object', + properties: { + file: { type: 'string', format: 'binary' }, + character: { type: 'string', description: '角色名称' }, + status: { type: 'string', description: '状态' }, + }, + }, + }) + public async uploadMeme( + @UploadedFile() file: Express.Multer.File, + @Body('character') character: string, + @Body('status') status: string, + @Body('token') token: string, + @Res() res: Response, + ) { + if (!file) { + throw new HttpException('未检测到上传文件', HttpStatus.BAD_REQUEST); + } + + try { + const buffer = file.buffer; + const imgType = await imageType(buffer); + if (!imgType || !['jpg', 'png', 'gif', 'webp'].includes(imgType.ext)) { + throw new HttpException( + '不支持的图片格式', + HttpStatus.UNSUPPORTED_MEDIA_TYPE, + ); + } + + const fsp = fs.promises; + const safeCharacter = character?.trim() || 'unknown'; + const safeStatus = status?.trim() || 'default'; + const tempDir = path.join(this.pathService.get('temp'), 'meme'); + await fsp.mkdir(tempDir, { recursive: true }); + const remoteMemePath = this.configService.get('OPENLIST_API_MEME_PATH'); + + const remoteDir = `${remoteMemePath}/${safeCharacter}/${safeStatus}/`; + let fileList: string[] = []; + try { + const listResult = await this.openListService.listFiles(remoteDir); + if ( + listResult?.code === 200 && + Array.isArray(listResult.data?.content) + ) { + fileList = listResult.data.content.map((f) => f.name); + } else { + this.logger.warn(`目录为空或返回结构异常:${remoteDir}`); + } + } catch (err) { + this.logger.warn(`获取远程目录失败(${remoteDir}),将自动创建`); + } + + const usedNumbers = fileList + .map((name) => { + const match = name.match(/^(\d+)\./); + return match ? parseInt(match[1], 10) : null; + }) + .filter((n) => n !== null) as number[]; + + const nextNumber = + usedNumbers.length > 0 ? Math.max(...usedNumbers) + 1 : 1; + const filename = `${nextNumber}.${imgType.ext}`; + const tempFilePath = path.join(tempDir, filename); + await fsp.writeFile(tempFilePath, buffer); + //const openlistBasePath = this.configService.get('OPENLIST_API_BASE_PATH'); + + const openListTargetPath = `${remoteDir}${filename}`; + const fileStream = fs.createReadStream(tempFilePath); + await this.openListService.uploadFile( + tempFilePath, + fileStream, + openListTargetPath, + ); + + await fsp.unlink(tempFilePath); + this.logger.log(`表情包上传成功: ${openListTargetPath}`); + return res.status(200).json({ + message: '表情上传成功!', + path: openListTargetPath, + filename, + }); + } catch (error) { + this.logger.error('表情包上传失败:', error); + throw new HttpException( + `上传失败: ${error.message || error}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/modules/meme/meme.module.ts b/src/modules/meme/meme.module.ts index 0d053be..df6fe88 100644 --- a/src/modules/meme/meme.module.ts +++ b/src/modules/meme/meme.module.ts @@ -16,6 +16,7 @@ import { FilesModule } from '../../core/files/files.module'; RedisModule, AppConfigModule, FilesModule, + PathModule, ], providers: [MemeService], controllers: [MemeController],