init: monorepo 初始化 - ygo-sdk + ygo-deck + ygo-deck-server
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.umi/
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "ygo-deck",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build:sdk": "cd packages/ygo-sdk && npm run build",
|
||||
"build:server": "cd packages/ygo-deck-server && npm run build",
|
||||
"build:fe": "cd packages/ygo-deck && npm run build",
|
||||
"build": "npm run build:sdk && npm run build:server && npm run build:fe",
|
||||
"dev:server": "cd packages/ygo-deck-server && npm run start:dev",
|
||||
"dev:fe": "cd packages/ygo-deck && npm start",
|
||||
"install:all": "npm install"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
/file-cache
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# YGO DECK Server 开发文档
|
||||
|
||||
## 初始化
|
||||
|
||||
### 安装前置依赖
|
||||
|
||||
```shell
|
||||
sudo apt-get install imagemagick # 图片处理工具
|
||||
sudo apt install mysql-server
|
||||
```
|
||||
|
||||
安装node依赖
|
||||
|
||||
```shell
|
||||
yarn # 如果报错使用cnpm i
|
||||
```
|
||||
|
||||
### 更新卡片数据库
|
||||
|
||||
更新本地的卡片数据库
|
||||
```shell
|
||||
yarn cdb
|
||||
```
|
||||
|
||||
### 更新卡片图库
|
||||
|
||||
将比较全的卡图文件夹放在public/pics/500里面,要求图片为jpg格式
|
||||
|
||||
你可以使用爬虫来获取最新的卡图
|
||||
|
||||
|
||||
## 开发
|
||||
|
||||
设计数据库的时候记得先学习数据库的五大范式哦
|
||||
|
||||
## 部署
|
||||
|
||||
```shell
|
||||
yarn
|
||||
yarn build
|
||||
pm2 start dist/src/main.js --name BE
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
function parseEnv() {
|
||||
const localEnv = path.resolve('.env');
|
||||
const prodEnv = path.resolve('.env.prod');
|
||||
|
||||
if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
|
||||
throw new Error('缺少环境配置文件');
|
||||
}
|
||||
|
||||
const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
|
||||
return { path: filePath };
|
||||
}
|
||||
export default parseEnv();
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"type": "mysql",
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"username": "root",
|
||||
"password": "18032910ym",
|
||||
"database": "ygo",
|
||||
"entities": ["dist/**/*.entity{.ts,.js}"],
|
||||
"synchronize": true
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "ygo-pics",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist",
|
||||
"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",
|
||||
"start:serve": "git pull origin master && yarn && yarn build && pm2 reload dist/src/main.js --name BE",
|
||||
"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",
|
||||
"cdb": "./scripts/cards.cdb.sh",
|
||||
"pic": "./scripts/get_pics.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^8.0.0",
|
||||
"@nestjs/config": "^2.0.0",
|
||||
"@nestjs/core": "^8.0.0",
|
||||
"@nestjs/jwt": "^8.0.0",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/passport": "^8.2.1",
|
||||
"@nestjs/platform-express": "^8.0.0",
|
||||
"@nestjs/swagger": "^5.2.1",
|
||||
"@nestjs/typeorm": "^8.0.3",
|
||||
"@types/passport": "^1.0.7",
|
||||
"@types/passport-jwt": "^3.0.6",
|
||||
"@types/passport-local": "^1.0.34",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"gm": "^1.23.1",
|
||||
"install": "^0.13.0",
|
||||
"mysql2": "^2.3.3",
|
||||
"passport": "^0.5.2",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.2.0",
|
||||
"stream": "^0.0.2",
|
||||
"typeorm": "^0.3.6",
|
||||
"ygo-card-node": "^1.4.4",
|
||||
"ygo-sdk": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^8.0.0",
|
||||
"@nestjs/schematics": "^8.0.0",
|
||||
"@nestjs/testing": "^8.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^27.2.5",
|
||||
"pm2": "^5.2.0",
|
||||
"prettier": "^2.3.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "^3.10.1",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,3 @@
|
||||
git pull origin master
|
||||
yarn
|
||||
yarn build
|
||||
@@ -0,0 +1,6 @@
|
||||
wget -P ./locales/en-US/ -N https://koishi.pro/ygopro/locales/en-US/cards.cdb
|
||||
wget -P ./locales/es-ES/ -N https://koishi.pro/ygopro/locales/es-ES/cards.cdb
|
||||
wget -P ./locales/ja-JP/ -N https://koishi.pro/ygopro/locales/ja-JP/cards.cdb
|
||||
wget -P ./locales/ko-KR/ -N https://koishi.pro/ygopro/locales/ko-KR/cards.cdb
|
||||
wget -P ./locales/pt-BR/ -N https://koishi.pro/ygopro/locales/pt-BR/cards.cdb
|
||||
wget -P ./locales/zh-CN/ -N https://koishi.pro/ygopro/locales/zh-CN/cards.cdb
|
||||
@@ -0,0 +1,2 @@
|
||||
cd public
|
||||
git clone https://gitee.com/ymssx/pics.git
|
||||
@@ -0,0 +1,2 @@
|
||||
sudo apt-get install imagemagick
|
||||
sudo apt install mysql-server
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const stream = require('stream');
|
||||
import { Controller, Get, Query, Res, Request } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller('api')
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
@Get('data')
|
||||
async getCardData(@Query() query): Promise<any> {
|
||||
const { id, lang = 'zh-CN' } = query;
|
||||
return await this.appService.getCardDataWithHash(id, lang);
|
||||
}
|
||||
|
||||
@Get('search')
|
||||
async searchCardData(@Query() query): Promise<any> {
|
||||
const { keywords } = query;
|
||||
return await this.appService.searchData({ keywords });
|
||||
}
|
||||
|
||||
@Get('data/list')
|
||||
async getMultiCardData(@Query() query): Promise<any> {
|
||||
const { ids, lang = 'zh-CN' } = query;
|
||||
return await this.appService.getMultiCardData(ids, lang);
|
||||
}
|
||||
|
||||
@Get('pic')
|
||||
async getCardPic(@Query() query, @Res() res: any) {
|
||||
const { id, size = 100, hash } = query;
|
||||
const buffer = await this.appService.getCardPic(id, Number(size), hash);
|
||||
const bufferStream = new stream.PassThrough();
|
||||
bufferStream.end(buffer);
|
||||
bufferStream.pipe(res);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { UserModule } from './user/user.module';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { DeckModule } from './deck/deck.module';
|
||||
import { DeckCommentModule } from './deck-comment/deck-comment.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { DeckRelationModule } from './deck-relation/deck-relation.module';
|
||||
import { DeckStatisticsModule } from './deck-statistics/deck-statistics.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRoot(),
|
||||
UserModule,
|
||||
DeckModule,
|
||||
DeckCommentModule,
|
||||
AuthModule,
|
||||
DeckRelationModule,
|
||||
DeckStatisticsModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { queryDataList, getData, getCard, variation } from 'ygo-card-node';
|
||||
import { getPicHash, checkPicHash } from './service/lock';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const gm = require('gm');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { promises } = require('fs');
|
||||
|
||||
// TODO: 这里和下面都要替换成动态的国际化目录 ./locales/{lang}/cards.cdb
|
||||
const DB_PATH = './src/cards.cdb';
|
||||
|
||||
interface Rules {
|
||||
keywords: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
|
||||
async getCardPic(id: string, size = 100, hash: string): Promise<Buffer> {
|
||||
if (size > 1000) {
|
||||
throw new HttpException('size too large', HttpStatus.PAYLOAD_TOO_LARGE);
|
||||
}
|
||||
|
||||
// const checkRes = checkPicHash(hash, id, size);
|
||||
// if (Array.isArray(checkRes)) {
|
||||
// const [reason, code] = checkRes;
|
||||
// throw new HttpException(reason, code);
|
||||
// }
|
||||
// if (checkRes !== true) {
|
||||
// throw new HttpException(
|
||||
// 'Unknown Error',
|
||||
// HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
// );
|
||||
// }
|
||||
|
||||
const filePath = `./file-cache/pic/${id}.jpg`;
|
||||
const exists = await promises
|
||||
.access(filePath)
|
||||
.then(() => true)
|
||||
.catch((_) => false);
|
||||
|
||||
if (exists) {
|
||||
return new Promise((resolve, reject) => {
|
||||
gm(filePath).toBuffer('jpg', (err, buffer) => {
|
||||
if (err) return reject(err);
|
||||
resolve(buffer);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const data = await this.getCardData(id);
|
||||
const sortedData = variation(data);
|
||||
const buffer = await getCard(
|
||||
{
|
||||
...sortedData,
|
||||
name: '',
|
||||
desc: '',
|
||||
lb_desc: '',
|
||||
},
|
||||
{
|
||||
picPath: `./public/pics/500/${id}.jpg`,
|
||||
moldPath: './public/mold/',
|
||||
type: 'image/jpeg',
|
||||
size,
|
||||
},
|
||||
);
|
||||
|
||||
gm(buffer, `${id}.jpg`).write(`./file-cache/pic/${id}.jpg`, function (err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async getCardData(id: string, lang = 'zh-CN'): Promise<any> {
|
||||
if (!id) {
|
||||
throw new HttpException("id can'not be empty", HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const data = await getData(`./locales/${lang}/cards.cdb`, id);
|
||||
if (!data) {
|
||||
throw new HttpException('no data', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getCardDataWithHash(id: string, lang = 'zh-CN'): Promise<any> {
|
||||
const data = await this.getCardData(id, lang);
|
||||
|
||||
return {
|
||||
...data,
|
||||
hash: getPicHash([id], 500),
|
||||
};
|
||||
}
|
||||
|
||||
async getMultiCardData(ids: string, lang = 'zh-CN'): Promise<any[]> {
|
||||
if (!ids) {
|
||||
throw new HttpException("id can'not be empty", HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const list = ids.split(',');
|
||||
|
||||
if (list.length > 100) {
|
||||
throw new HttpException(
|
||||
'list length is too long',
|
||||
HttpStatus.PAYLOAD_TOO_LARGE,
|
||||
);
|
||||
}
|
||||
|
||||
const jobs = list.map((id) => {
|
||||
return getData(`./locales/${lang}/cards.cdb`, id)
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
return data;
|
||||
} else {
|
||||
throw new HttpException(`${id} has no data`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new HttpException(`${err}`, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(jobs);
|
||||
}
|
||||
|
||||
async searchData(rules: Rules) {
|
||||
const { keywords } = rules;
|
||||
const SQL = `
|
||||
SELECT *
|
||||
FROM texts t, datas d
|
||||
WHERE
|
||||
t.id = d.id
|
||||
AND t.name LIKE '%${keywords}%'
|
||||
`;
|
||||
return queryDataList(DB_PATH, SQL);
|
||||
}
|
||||
}
|
||||