Commit f980d71e authored by nanahira's avatar nanahira

add srvpro things

parent 74ea7332
Pipeline #35438 passed with stages
in 2 minutes and 37 seconds
...@@ -18,10 +18,11 @@ ...@@ -18,10 +18,11 @@
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"crypto-random-string": "3.3.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nesties": "^1.1.1", "nesties": "^1.1.1",
"nestjs-mycard": "^4.0.2", "nestjs-mycard": "^4.0.2",
"nicot": "^1.1.6", "nicot": "^1.1.7",
"pg": "^8.14.1", "pg": "^8.14.1",
"pg-native": "^3.3.0", "pg-native": "^3.3.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
...@@ -5719,6 +5720,27 @@ ...@@ -5719,6 +5720,27 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/crypto-random-string": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.3.1.tgz",
"integrity": "sha512-5j88ECEn6h17UePrLi6pn1JcLtAiANa3KExyr9y9Z5vo2mv56Gh3I4Aja/B9P9uyMwyxNHAHWv+nE72f30T5Dg==",
"license": "MIT",
"dependencies": {
"type-fest": "^0.8.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/crypto-random-string/node_modules/type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=8"
}
},
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.11.13", "version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
...@@ -9012,9 +9034,9 @@ ...@@ -9012,9 +9034,9 @@
} }
}, },
"node_modules/nicot": { "node_modules/nicot": {
"version": "1.1.6", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/nicot/-/nicot-1.1.6.tgz", "resolved": "https://registry.npmjs.org/nicot/-/nicot-1.1.7.tgz",
"integrity": "sha512-oYm3rBq6/wsVacpncpAxnB8kJ7lpGX9jYnS1E2QhD4Z4xV+TL8Q5kwCb/3j0lHeCvZJC6+jzkanDNBDW+tgagg==", "integrity": "sha512-vRNyHv42xA7ccIzJAUP7zrL+A6FDdsmgXtcclodxfuM1AX/AECAbx2spu9HmFkDaWKyfauyoofce22ql+R3sFw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lodash": "^4.17.21", "lodash": "^4.17.21",
......
import { Test, TestingModule } from '@nestjs/testing';
import { ApiKeyController } from './api-key.controller';
describe('ApiKeyController', () => {
let controller: ApiKeyController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ApiKeyController],
}).compile();
controller = module.get<ApiKeyController>(ApiKeyController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiMycardUser, MycardUser, PutMycardUser } from 'nestjs-mycard';
import { ApiKeyService } from './api-key.service';
import { RestfulFactory } from 'nicot';
import { ApiKey } from './entities/api-key.entity';
const factory = new RestfulFactory(ApiKey);
class CreateApiKeyDto extends factory.createDto {}
class FindApiKeyDto extends factory.findAllDto {}
class UpdateApiKeyDto extends factory.updateDto {}
@ApiTags('api-key')
@Controller('api-key')
@ApiMycardUser()
export class ApiKeyController {
constructor(private service: ApiKeyService) {}
@factory.create()
async create(
@factory.createParam() dto: CreateApiKeyDto,
@PutMycardUser() user: MycardUser,
) {
dto.userId = user.id;
return this.service.create(dto);
}
@factory.findOne()
async findOne(
@factory.idParam() id: number,
@PutMycardUser() user: MycardUser,
) {
return this.service.findOne(id, (qb) =>
qb.andWhere(`${this.service.entityAliasName}.userId = :currentUserId`, {
currentUserId: user.id,
}),
);
}
@factory.findAll()
async findAll(
@factory.findAllParam() dto: FindApiKeyDto,
@PutMycardUser() user: MycardUser,
) {
return this.service.findAll(dto, (qb) =>
qb.andWhere(`${this.service.entityAliasName}.userId = :currentUserId`, {
currentUserId: user.id,
}),
);
}
@factory.update()
async update(
@factory.idParam() id: number,
@factory.updateParam() dto: UpdateApiKeyDto,
@PutMycardUser() user: MycardUser,
) {
return this.service.update(id, dto, { userId: user.id });
}
@factory.delete()
async delete(
@factory.idParam() id: number,
@PutMycardUser() user: MycardUser,
) {
return this.service.delete(id, { userId: user.id });
}
}
import { ApiKeyPipe } from './api-key.pipe';
describe('ApiKeyPipe', () => {
it('should be defined', () => {
expect(new ApiKeyPipe()).toBeDefined();
});
});
import {
ArgumentMetadata,
createParamDecorator,
ExecutionContext,
Injectable,
PipeTransform,
} from '@nestjs/common';
import { ApiKeyService } from './api-key.service';
import { Request } from 'express';
@Injectable()
export class ApiKeyPipe implements PipeTransform {
constructor(private apiKeyService: ApiKeyService) {}
transform(value: string, metadata: ArgumentMetadata) {
return this.apiKeyService.findUserIdWithApiKey(value);
}
}
const __possibleKeys = createParamDecorator((_, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest<Request>();
const fields = ['query', 'body'] as const;
for (const field of fields) {
const res = req?.[field]?.['api_key'];
if (res) {
return res as string;
}
}
return '';
});
export const PutApiKeyUserId = () => __possibleKeys(ApiKeyPipe);
import { Test, TestingModule } from '@nestjs/testing';
import { ApiKeyService } from './api-key.service';
describe('ApiKeyService', () => {
let service: ApiKeyService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ApiKeyService],
}).compile();
service = module.get<ApiKeyService>(ApiKeyService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable } from '@nestjs/common';
import { BlankReturnMessageDto, CrudService } from 'nicot';
import { ApiKey } from './entities/api-key.entity';
import { InjectRepository } from '@nestjs/typeorm';
class _base extends CrudService(ApiKey) {}
@Injectable()
export class ApiKeyService extends _base {
constructor(@InjectRepository(ApiKey) repo) {
super(repo);
}
async findUserIdWithApiKey(value: string) {
const apiKey = await this.repo.findOne({
where: { key: value },
select: ['id', 'userId', 'expireAt'],
});
if (!apiKey) {
throw new BlankReturnMessageDto(401, 'Invalid API Key.').toException();
}
if (apiKey.expireAt && apiKey.expireAt < new Date()) {
throw new BlankReturnMessageDto(401, 'API Key expired.').toException();
}
return apiKey.userId;
}
}
import { ApiError } from 'nicot';
import { ApiQuery } from '@nestjs/swagger';
export const ApiKeyError = () => ApiError(401, 'API Key 无效');
export const ApiKeyQuery = () =>
ApiQuery({
name: 'api_key',
description: 'API Key',
required: true,
type: String,
});
import { DescBase } from '../../utility/NamedBase.entity';
import {
DateColumn,
IntColumn,
NotQueryable,
NotWritable,
StringColumn,
} from 'nicot';
import { Entity, Index } from 'typeorm';
import cryptoRandomString from 'crypto-random-string';
@Entity()
export class ApiKey extends DescBase {
@NotWritable()
@Index()
@IntColumn('int', {
description: '创建者 MC ID',
unsigned: true,
columnExtras: { nullable: false },
})
userId: number;
@Index({ unique: true })
@NotQueryable()
@NotWritable()
@StringColumn(64, {
description: 'API Key 本体',
columnExtras: { nullable: false },
})
key: string;
@Index()
@DateColumn({
description: 'API Key 过期时间',
})
@NotQueryable()
expireAt: Date;
async beforeCreate() {
this.key = cryptoRandomString({ length: 64, type: 'alphanumeric' });
}
}
...@@ -12,6 +12,11 @@ import { ParticipantService } from './participant/participant.service'; ...@@ -12,6 +12,11 @@ import { ParticipantService } from './participant/participant.service';
import { TournamentController } from './tournament/tournament.controller'; import { TournamentController } from './tournament/tournament.controller';
import { MatchController } from './match/match.controller'; import { MatchController } from './match/match.controller';
import { ParticipantController } from './participant/participant.controller'; import { ParticipantController } from './participant/participant.controller';
import { ApiKeyService } from './api-key/api-key.service';
import { ApiKey } from './api-key/entities/api-key.entity';
import { ApiKeyController } from './api-key/api-key.controller';
import { SrvproService } from './srvpro/srvpro.service';
import { SrvproController } from './srvpro/srvpro.controller';
@Module({ @Module({
imports: [ imports: [
...@@ -42,9 +47,21 @@ import { ParticipantController } from './participant/participant.controller'; ...@@ -42,9 +47,21 @@ import { ParticipantController } from './participant/participant.controller';
// logging: true, // logging: true,
}), }),
}), }),
TypeOrmModule.forFeature([Tournament, Match, Participant]), TypeOrmModule.forFeature([Tournament, Match, Participant, ApiKey]),
],
providers: [
TournamentService,
MatchService,
ParticipantService,
ApiKeyService,
SrvproService,
],
controllers: [
TournamentController,
MatchController,
ParticipantController,
ApiKeyController,
SrvproController,
], ],
providers: [TournamentService, MatchService, ParticipantService],
controllers: [TournamentController, MatchController, ParticipantController],
}) })
export class AppModule {} export class AppModule {}
...@@ -34,7 +34,11 @@ export class MatchService extends CrudService(Match, { ...@@ -34,7 +34,11 @@ export class MatchService extends CrudService(Match, {
); );
} }
async updateMatch(id: number, dto: Partial<Match>, user: MycardUser) { async updateMatch(
id: number,
dto: Partial<Match>,
user: MycardUser | number,
) {
const match = await this.repo.findOne({ const match = await this.repo.findOne({
where: { id }, where: { id },
relations: ['tournament'], relations: ['tournament'],
......
...@@ -5,14 +5,17 @@ import { ...@@ -5,14 +5,17 @@ import {
NotChangeable, NotChangeable,
NotColumn, NotColumn,
NotInResult, NotInResult,
NotQueryable,
QueryEqual, QueryEqual,
QueryMatchBoolean, QueryMatchBoolean,
StringColumn,
} from 'nicot'; } from 'nicot';
import { TournamentIdColumn } from '../../utility/decorators/tournament-id-column'; import { TournamentIdColumn } from '../../utility/decorators/tournament-id-column';
import { Tournament } from '../../tournament/entities/Tournament.entity'; import { Tournament } from '../../tournament/entities/Tournament.entity';
import { MycardUser } from 'nestjs-mycard'; import { MycardUser } from 'nestjs-mycard';
import { Match } from '../../match/entities/match.entity'; import { Match } from '../../match/entities/match.entity';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsBase64 } from 'class-validator';
export class ParticipantScore { export class ParticipantScore {
@ApiProperty({ description: '排名' }) @ApiProperty({ description: '排名' })
...@@ -61,10 +64,6 @@ export class Participant extends NamedBase { ...@@ -61,10 +64,6 @@ export class Participant extends NamedBase {
@NotInResult() @NotInResult()
wonMatches: Match[]; wonMatches: Match[];
//@NotColumn()
//@ApiProperty({ type: [Match], description: '参与的比赛。' })
//matches: Match[];
@NotColumn({ @NotColumn({
description: '该选手的成绩。', description: '该选手的成绩。',
required: false, required: false,
...@@ -80,7 +79,15 @@ export class Participant extends NamedBase { ...@@ -80,7 +79,15 @@ export class Participant extends NamedBase {
//this.matches = this.getMatches(); //this.matches = this.getMatches();
} }
checkPermission(user: MycardUser) { checkPermission(user: MycardUser | number) {
return this.tournament?.checkPermission(user); return this.tournament?.checkPermission(user);
} }
@IsBase64()
@NotQueryable()
@StringColumn(1024, {
description:
'卡组 base64,影响 srvpro 的卡组。用库 ygopro-deck-encode 生成。',
})
deckbuf?: string;
} }
...@@ -90,7 +90,10 @@ export class ParticipantService extends CrudService(Participant, { ...@@ -90,7 +90,10 @@ export class ParticipantService extends CrudService(Participant, {
return participant.tournament.checkPermission(user); return participant.tournament.checkPermission(user);
} }
async importParticipants(participants: Participant[], user: MycardUser) { async importParticipants(
participants: Participant[],
user: MycardUser | number,
) {
return this.importEntities(participants, async (p) => { return this.importEntities(participants, async (p) => {
try { try {
await this.tournamentService.canModifyParticipants( await this.tournamentService.canModifyParticipants(
......
import { Test, TestingModule } from '@nestjs/testing';
import { SrvproController } from './srvpro.controller';
describe('SrvproController', () => {
let controller: SrvproController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SrvproController],
}).compile();
controller = module.get<SrvproController>(SrvproController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
import {
Controller,
Delete,
Get,
HttpCode,
Param,
ParseIntPipe,
Post,
Put,
} from '@nestjs/common';
import {
ApiOkResponse,
ApiOperation,
ApiParam,
ApiTags,
} from '@nestjs/swagger';
import { ApiKeyError, ApiKeyQuery } from '../api-key/api-require-key';
import { SrvproService } from './srvpro.service';
import {
participantRestfulFactory,
SRVProTournamentDto,
SRVProUploadMatchDto,
SRVProUploadParticipantDto,
} from './srvpro.dto';
import { ApiBlankResponse, ApiError, DataBody } from 'nicot';
import { PutApiKeyUserId } from '../api-key/api-key.pipe';
const ApiTournamentIdParam = () =>
ApiParam({
name: 'tournamentId',
description: '赛事 ID',
required: true,
type: Number,
});
@ApiTags('srvpro')
@Controller('srvpro/v1')
@ApiKeyError()
@ApiError(404, '信息不存在')
@ApiError(403, '无法操作或没有权限')
export class SrvproController {
constructor(private service: SrvproService) {}
// GET /v1/tournaments/${tournament_id}.json?api_key=xxx&include_participants=1&include_matches=1 returns { tournament: Tournament }
@Get('tournaments/:tournamentId.json')
@ApiOperation({
summary: '获取赛事信息',
})
@ApiKeyQuery()
@ApiTournamentIdParam()
@ApiOkResponse({
type: SRVProTournamentDto,
})
async getTournament(
@Param('tournamentId', ParseIntPipe) tournamentId: number,
@PutApiKeyUserId() userId: number,
) {
return this.service.getTournament(tournamentId, userId);
}
// PUT /v1/tournaments/${tournament_id}/matches/${match_id}.json { api_key: string, match: MatchPost } returns ANY
@Put('tournaments/:tournamentId/matches/:matchId.json')
@HttpCode(200)
@ApiOperation({
summary: '上传赛事成绩',
})
@ApiTournamentIdParam()
@ApiParam({
name: 'matchId',
description: '对局 ID',
required: true,
type: Number,
})
@ApiBlankResponse()
async putScore(
@Param('tournamentId', ParseIntPipe) tournamentId: number,
@Param('matchId', ParseIntPipe) matchId: number,
@DataBody() body: SRVProUploadMatchDto,
@PutApiKeyUserId() userId: number,
) {
return this.service.putScore(tournamentId, matchId, body, userId);
}
// DELETE /v1/tournaments/${tournament_id}/participants/clear.json?api_key=xxx returns ANY
@Delete('tournaments/:tournamentId/participants/clear.json')
@HttpCode(200)
@ApiOperation({
summary: '清空赛事选手',
})
@ApiKeyQuery()
@ApiTournamentIdParam()
@ApiBlankResponse()
async clearParticipants(
@Param('tournamentId', ParseIntPipe) tournamentId: number,
@PutApiKeyUserId() userId: number,
) {
return this.service.clearParticipants(tournamentId, userId);
}
// POST /v1/tournaments/${tournament_id}/participants/bulk_add.json { api_key: string, participants: { name: string }[] } returns ANY
@Post('tournaments/:tournamentId/participants/bulk_add.json')
@HttpCode(200)
@ApiOperation({
summary: '批量上传选手',
})
@ApiTournamentIdParam()
@ApiOkResponse({
type: participantRestfulFactory.importReturnMessageDto,
})
async uploadParticipants(
@Param('tournamentId', ParseIntPipe) tournamentId: number,
@DataBody() body: SRVProUploadParticipantDto,
@PutApiKeyUserId() userId: number,
) {
return this.service.uploadParticipants(tournamentId, body, userId);
}
}
import {
IsArray,
IsNotEmpty,
IsOptional,
IsString,
Matches,
MaxLength,
MinLength,
Validate,
ValidateNested,
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { ApiProperty, OmitType } from '@nestjs/swagger';
import _ from 'lodash';
import { Participant } from '../participant/entities/participant.entity';
import { Type } from 'class-transformer';
import { RestfulFactory } from 'nicot';
import { Match, MatchStatus } from '../match/entities/match.entity';
import { Tournament } from '../tournament/entities/Tournament.entity';
export const participantRestfulFactory = new RestfulFactory(Participant);
export class WithApiKeyBody {
@IsString()
@IsNotEmpty()
@MaxLength(64)
@MinLength(64)
@ApiProperty({
description: 'API Key',
type: String,
example: _.range(64)
.map(() => 'x')
.join(''),
})
api_key: string;
}
@ValidatorConstraint({ name: 'IsWinnerId', async: false })
export class IsWinnerId implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
if (value === 'tie') return true; // 'tie' 是合法的
if (typeof value === 'number' && value > 0) return true; // 正数也是合法的
return false; // 其他情况不合法
}
defaultMessage(args: ValidationArguments) {
return `${args.property} must be either "tie" or a positive number`;
}
}
class SRVProCreateParticipantDto extends OmitType(
participantRestfulFactory.createDto,
['tournamentId', 'quit'],
) {}
export class SRVProUploadParticipantDto extends WithApiKeyBody {
@IsArray()
@ValidateNested({
each: true,
})
@Type(() => SRVProCreateParticipantDto)
@ApiProperty({
type: () => [SRVProCreateParticipantDto],
description: '参与者列表',
})
participants: Participant[];
}
const WinnderIdProperty = () =>
ApiProperty({
description: '比赛胜者 ID。不写代表比赛还没结束,写 tie 代表平局',
required: false,
oneOf: [
{ type: 'string', example: 'tie', enum: ['tie'] },
{ type: 'number', example: 1, minimum: 1 },
],
});
export class SRVProUploadMatch {
@IsString()
@Matches(/^-?\d+--?\d+$/)
@ApiProperty({
description: '比赛比分',
example: '2-1',
})
scores_csv: string;
@IsOptional()
@Validate(IsWinnerId)
@WinnderIdProperty()
winner_id?: 'tie' | number;
}
export const srvproUploadMatchToUpdateMatch = (
srvproMatch: SRVProUploadMatch,
): Partial<Match> => {
const csvMatch = srvproMatch.scores_csv.match(/^(-?\d+)-(-?\d+)$/);
const player1Score = parseInt(csvMatch[1]);
const player2Score = parseInt(csvMatch[2]);
return {
winnerId:
srvproMatch.winner_id === 'tie'
? null
: srvproMatch.winner_id
? srvproMatch.winner_id
: undefined,
player1Score,
player2Score,
};
};
export class SRVProUploadMatchDto extends WithApiKeyBody {
@ValidateNested()
@ApiProperty({
description: '比分信息',
})
match: SRVProUploadMatch;
}
export class SRVProParticipant extends OmitType(
participantRestfulFactory.entityResultDto,
['tournament'],
) {}
export class SRVProParticipantDto {
@ApiProperty({
type: () => SRVProParticipant,
})
participant: Participant;
constructor(participant: Participant) {
this.participant = participant;
}
}
export class SRVProMatch {
@ApiProperty({
description: '比赛 ID',
})
id: number;
@ApiProperty({
description: '比赛状态',
enum: ['pending', 'open', 'complete'],
type: String,
})
state: 'pending' | 'open' | 'complete'; // pending: 还未开始,open: 进行中,complete: 已结束
@ApiProperty({
description: '玩家 1 ID',
})
player1_id: number;
@ApiProperty({
description: '玩家 2 ID',
})
player2_id: number;
@WinnderIdProperty()
winner_id?: number | 'tie'; // 如果存在,则代表该比赛已经结束
@ApiProperty({
description: '比赛的比分',
})
scores_csv?: string; // 比分,2-1 这样的格式,请保证和上传的情况相同
constructor(match: Match) {
this.id = match.id;
this.state =
match.status === MatchStatus.Pending
? 'pending'
: match.status === MatchStatus.Running
? 'open'
: 'complete';
this.player1_id = match.player1Id;
this.player2_id = match.player2Id;
this.winner_id =
match.winnerId ||
(match.status === MatchStatus.Finished ? 'tie' : undefined);
this.scores_csv =
match.player1Score && match.player2Score
? `${match.player1Score}-${match.player2Score}`
: undefined;
}
}
export class SRVProMatchDto {
@ApiProperty({
description: '比赛信息',
})
match: SRVProMatch;
constructor(match: Match) {
this.match = new SRVProMatch(match);
}
}
export class SRVProTournament {
@ApiProperty({
description: '比赛 ID',
})
id: number;
@ApiProperty({ type: () => [SRVProParticipantDto] })
participants: SRVProParticipantDto[];
@ApiProperty({ type: () => [SRVProMatchDto] })
matches: SRVProMatchDto[];
constructor(tournament: Tournament) {
this.id = tournament.id;
this.participants =
tournament.participants?.map((p) => new SRVProParticipantDto(p)) || [];
this.matches = tournament.matches?.map((m) => new SRVProMatchDto(m)) || [];
}
}
export class SRVProTournamentDto {
@ApiProperty({
description: '比赛信息',
})
tournament: SRVProTournament;
constructor(tournament: Tournament) {
this.tournament = new SRVProTournament(tournament);
}
}
import { Test, TestingModule } from '@nestjs/testing';
import { SrvproService } from './srvpro.service';
describe('SrvproService', () => {
let service: SrvproService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SrvproService],
}).compile();
service = module.get<SrvproService>(SrvproService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable } from '@nestjs/common';
import { TournamentService } from '../tournament/tournament.service';
import { ParticipantService } from '../participant/participant.service';
import { MatchService } from '../match/match.service';
import {
SRVProTournamentDto,
SRVProUploadMatchDto,
srvproUploadMatchToUpdateMatch,
SRVProUploadParticipantDto,
} from './srvpro.dto';
import { Participant } from '../participant/entities/participant.entity';
@Injectable()
export class SrvproService {
constructor(
private tournamentService: TournamentService,
private participantService: ParticipantService,
private matchService: MatchService,
) {}
async getTournament(tournamentId: number, userId: number) {
const res = await this.tournamentService.getTournament(
tournamentId,
userId,
);
return new SRVProTournamentDto(res.data);
}
async putScore(
tournamentId: number,
matchId: number,
body: SRVProUploadMatchDto,
userId: number,
) {
return this.matchService.updateMatch(
matchId,
srvproUploadMatchToUpdateMatch(body.match),
userId,
);
}
async clearParticipants(tournamentId: number, userId: number) {
return this.tournamentService.clearTournamentParticipants(
tournamentId,
userId,
);
}
async uploadParticipants(
tournamentId: number,
body: SRVProUploadParticipantDto,
userId: number,
) {
return this.participantService.importParticipants(
body.participants.map(
(p) =>
({
...p,
tournamentId,
}) as Participant,
),
userId,
);
}
}
...@@ -153,7 +153,7 @@ export class Tournament extends DescBase { ...@@ -153,7 +153,7 @@ export class Tournament extends DescBase {
} }
static extraQueryForUser( static extraQueryForUser(
user: MycardUser, user: MycardUser | number,
qb: SelectQueryBuilder<any>, qb: SelectQueryBuilder<any>,
entityName: string, entityName: string,
) { ) {
...@@ -166,22 +166,28 @@ export class Tournament extends DescBase { ...@@ -166,22 +166,28 @@ export class Tournament extends DescBase {
`(${entityName}.visibility != :private OR ${entityName}.creator = :self OR :self = ANY(${entityName}.collaborators))`, `(${entityName}.visibility != :private OR ${entityName}.creator = :self OR :self = ANY(${entityName}.collaborators))`,
{ {
private: TournamentVisibility.Private, private: TournamentVisibility.Private,
self: user.id, self: typeof user === 'number' ? user : user.id,
}, },
); );
} }
} }
checkPermission(user: MycardUser) { checkPermissionWithUserId(userId: number) {
if ( if (
this.creator !== user.id && !userId ||
!user.admin && (this.creator !== userId && !this.collaborators.includes(userId))
!this.collaborators.includes(user.id)
) { ) {
throw new BlankReturnMessageDto(403, '您无权操作该比赛。').toException(); throw new BlankReturnMessageDto(403, '您无权操作该比赛。').toException();
} }
return this; return this;
} }
checkPermission(user: MycardUser | number) {
if (typeof user === 'number') {
return this.checkPermissionWithUserId(user);
}
if (user.admin) return this;
return this.checkPermissionWithUserId(user.id);
}
calculateScore() { calculateScore() {
const rule = this.getRuleHandler(); const rule = this.getRuleHandler();
......
...@@ -65,7 +65,7 @@ export class TournamentController { ...@@ -65,7 +65,7 @@ export class TournamentController {
@Post(':id/start') @Post(':id/start')
@HttpCode(200) @HttpCode(200)
@ApiOperation({ summary: 'Start a tournament' }) @ApiOperation({ summary: '开始比赛' })
@ApiParam({ name: 'id', description: 'Tournament ID' }) @ApiParam({ name: 'id', description: 'Tournament ID' })
@ApiOkResponse({ type: BlankReturnMessageDto }) @ApiOkResponse({ type: BlankReturnMessageDto })
async start( async start(
...@@ -77,7 +77,7 @@ export class TournamentController { ...@@ -77,7 +77,7 @@ export class TournamentController {
@Post(':id/reset') @Post(':id/reset')
@HttpCode(200) @HttpCode(200)
@ApiOperation({ summary: 'Reset a tournament' }) @ApiOperation({ summary: '重置比赛' })
@ApiParam({ name: 'id', description: 'Tournament ID' }) @ApiParam({ name: 'id', description: 'Tournament ID' })
@ApiOkResponse({ type: BlankReturnMessageDto }) @ApiOkResponse({ type: BlankReturnMessageDto })
async reset( async reset(
...@@ -89,7 +89,7 @@ export class TournamentController { ...@@ -89,7 +89,7 @@ export class TournamentController {
@Post(':id/finish') @Post(':id/finish')
@HttpCode(200) @HttpCode(200)
@ApiOperation({ summary: 'Finish a tournament' }) @ApiOperation({ summary: '结束比赛' })
@ApiParam({ name: 'id', description: 'Tournament ID' }) @ApiParam({ name: 'id', description: 'Tournament ID' })
@ApiOkResponse({ type: BlankReturnMessageDto }) @ApiOkResponse({ type: BlankReturnMessageDto })
async finish( async finish(
......
...@@ -5,6 +5,7 @@ import { InjectRepository } from '@nestjs/typeorm'; ...@@ -5,6 +5,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { MycardUser } from 'nestjs-mycard'; import { MycardUser } from 'nestjs-mycard';
import { Match, MatchStatus } from '../match/entities/match.entity'; import { Match, MatchStatus } from '../match/entities/match.entity';
import { In, Repository } from 'typeorm'; import { In, Repository } from 'typeorm';
import { Participant } from '../participant/entities/participant.entity';
@Injectable() @Injectable()
export class TournamentService extends CrudService(Tournament, { export class TournamentService extends CrudService(Tournament, {
...@@ -23,7 +24,7 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -23,7 +24,7 @@ export class TournamentService extends CrudService(Tournament, {
super(repo); super(repo);
} }
async getTournament(id: number, user: MycardUser) { async getTournament(id: number, user: MycardUser | number) {
const result = await this.findOne(id, (qb) => const result = await this.findOne(id, (qb) =>
Tournament.extraQueryForUser(user, qb, this.entityAliasName), Tournament.extraQueryForUser(user, qb, this.entityAliasName),
); );
...@@ -31,14 +32,14 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -31,14 +32,14 @@ export class TournamentService extends CrudService(Tournament, {
return result; return result;
} }
getTournaments(dto: Partial<Tournament>, user: MycardUser) { getTournaments(dto: Partial<Tournament>, user: MycardUser | number) {
return this.findAll(dto, (qb) => return this.findAll(dto, (qb) =>
Tournament.extraQueryForUser(user, qb, this.entityAliasName), Tournament.extraQueryForUser(user, qb, this.entityAliasName),
); );
} }
createTournament(tournament: Tournament, user: MycardUser) { createTournament(tournament: Tournament, user: MycardUser | number) {
tournament.creator = user.id; tournament.creator = typeof user === 'number' ? user : user.id;
return this.create(tournament); return this.create(tournament);
} }
...@@ -51,14 +52,14 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -51,14 +52,14 @@ export class TournamentService extends CrudService(Tournament, {
return this.update(id, dto); return this.update(id, dto);
} }
async deleteTournament(id: number, user: MycardUser) { async deleteTournament(id: number, user: MycardUser | number) {
await this.checkPermissionOfTournament(id, user); await this.checkPermissionOfTournament(id, user);
return this.delete(id); return this.delete(id);
} }
async checkPermissionOfTournament( async checkPermissionOfTournament(
id: number, id: number,
user: MycardUser, user: MycardUser | number,
extraGets: (keyof Tournament)[] = [], extraGets: (keyof Tournament)[] = [],
relations: string[] = [], relations: string[] = [],
) { ) {
...@@ -73,7 +74,7 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -73,7 +74,7 @@ export class TournamentService extends CrudService(Tournament, {
return tournament.checkPermission(user); return tournament.checkPermission(user);
} }
async canModifyParticipants(id: number, user: MycardUser) { async canModifyParticipants(id: number, user: MycardUser | number) {
const tournamnt = await this.checkPermissionOfTournament(id, user, [ const tournamnt = await this.checkPermissionOfTournament(id, user, [
'status', 'status',
]); ]);
...@@ -86,7 +87,7 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -86,7 +87,7 @@ export class TournamentService extends CrudService(Tournament, {
return tournamnt; return tournamnt;
} }
async resetTournament(id: number, user: MycardUser) { async resetTournament(id: number, user: MycardUser | number) {
const tournament = await this.checkPermissionOfTournament(id, user, [ const tournament = await this.checkPermissionOfTournament(id, user, [
'status', 'status',
]); ]);
...@@ -103,7 +104,7 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -103,7 +104,7 @@ export class TournamentService extends CrudService(Tournament, {
return new BlankReturnMessageDto(200, 'success'); return new BlankReturnMessageDto(200, 'success');
} }
async startTournament(id: number, user: MycardUser) { async startTournament(id: number, user: MycardUser | number) {
const tournament = await this.checkPermissionOfTournament( const tournament = await this.checkPermissionOfTournament(
id, id,
user, user,
...@@ -129,7 +130,7 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -129,7 +130,7 @@ export class TournamentService extends CrudService(Tournament, {
return new BlankReturnMessageDto(200, 'success'); return new BlankReturnMessageDto(200, 'success');
} }
async endTournament(id: number, user: MycardUser) { async endTournament(id: number, user: MycardUser | number) {
const tournament = await this.checkPermissionOfTournament(id, user, [ const tournament = await this.checkPermissionOfTournament(id, user, [
'status', 'status',
]); ]);
...@@ -157,6 +158,22 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -157,6 +158,22 @@ export class TournamentService extends CrudService(Tournament, {
return new BlankReturnMessageDto(200, 'success'); return new BlankReturnMessageDto(200, 'success');
} }
async clearTournamentParticipants(id: number, user: MycardUser | number) {
const tournament = await this.checkPermissionOfTournament(id, user, [
'status',
]);
if (tournament.status !== TournamentStatus.Ready) {
throw new BlankReturnMessageDto(
403,
'比赛已经开始,不能清空参赛者。',
).toException();
}
await this.repo.manager.softDelete(Participant, {
tournamentId: id,
});
return new BlankReturnMessageDto(200, 'success');
}
async afterMatchUpdate(id: number) { async afterMatchUpdate(id: number) {
if ( if (
!(await this.matchRepo.exists({ !(await this.matchRepo.exists({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment