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 @@
"@nestjs/typeorm": "^11.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"crypto-random-string": "3.3.1",
"lodash": "^4.17.21",
"nesties": "^1.1.1",
"nestjs-mycard": "^4.0.2",
"nicot": "^1.1.6",
"nicot": "^1.1.7",
"pg": "^8.14.1",
"pg-native": "^3.3.0",
"reflect-metadata": "^0.2.2",
......@@ -5719,6 +5720,27 @@
"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": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
......@@ -9012,9 +9034,9 @@
}
},
"node_modules/nicot": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/nicot/-/nicot-1.1.6.tgz",
"integrity": "sha512-oYm3rBq6/wsVacpncpAxnB8kJ7lpGX9jYnS1E2QhD4Z4xV+TL8Q5kwCb/3j0lHeCvZJC6+jzkanDNBDW+tgagg==",
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/nicot/-/nicot-1.1.7.tgz",
"integrity": "sha512-vRNyHv42xA7ccIzJAUP7zrL+A6FDdsmgXtcclodxfuM1AX/AECAbx2spu9HmFkDaWKyfauyoofce22ql+R3sFw==",
"license": "MIT",
"dependencies": {
"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';
import { TournamentController } from './tournament/tournament.controller';
import { MatchController } from './match/match.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({
imports: [
......@@ -42,9 +47,21 @@ import { ParticipantController } from './participant/participant.controller';
// 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 {}
......@@ -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({
where: { id },
relations: ['tournament'],
......
......@@ -5,14 +5,17 @@ import {
NotChangeable,
NotColumn,
NotInResult,
NotQueryable,
QueryEqual,
QueryMatchBoolean,
StringColumn,
} from 'nicot';
import { TournamentIdColumn } from '../../utility/decorators/tournament-id-column';
import { Tournament } from '../../tournament/entities/Tournament.entity';
import { MycardUser } from 'nestjs-mycard';
import { Match } from '../../match/entities/match.entity';
import { ApiProperty } from '@nestjs/swagger';
import { IsBase64 } from 'class-validator';
export class ParticipantScore {
@ApiProperty({ description: '排名' })
......@@ -61,10 +64,6 @@ export class Participant extends NamedBase {
@NotInResult()
wonMatches: Match[];
//@NotColumn()
//@ApiProperty({ type: [Match], description: '参与的比赛。' })
//matches: Match[];
@NotColumn({
description: '该选手的成绩。',
required: false,
......@@ -80,7 +79,15 @@ export class Participant extends NamedBase {
//this.matches = this.getMatches();
}
checkPermission(user: MycardUser) {
checkPermission(user: MycardUser | number) {
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, {
return participant.tournament.checkPermission(user);
}
async importParticipants(participants: Participant[], user: MycardUser) {
async importParticipants(
participants: Participant[],
user: MycardUser | number,
) {
return this.importEntities(participants, async (p) => {
try {
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 {
}
static extraQueryForUser(
user: MycardUser,
user: MycardUser | number,
qb: SelectQueryBuilder<any>,
entityName: string,
) {
......@@ -166,22 +166,28 @@ export class Tournament extends DescBase {
`(${entityName}.visibility != :private OR ${entityName}.creator = :self OR :self = ANY(${entityName}.collaborators))`,
{
private: TournamentVisibility.Private,
self: user.id,
self: typeof user === 'number' ? user : user.id,
},
);
}
}
checkPermission(user: MycardUser) {
checkPermissionWithUserId(userId: number) {
if (
this.creator !== user.id &&
!user.admin &&
!this.collaborators.includes(user.id)
!userId ||
(this.creator !== userId && !this.collaborators.includes(userId))
) {
throw new BlankReturnMessageDto(403, '您无权操作该比赛。').toException();
}
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() {
const rule = this.getRuleHandler();
......
......@@ -65,7 +65,7 @@ export class TournamentController {
@Post(':id/start')
@HttpCode(200)
@ApiOperation({ summary: 'Start a tournament' })
@ApiOperation({ summary: '开始比赛' })
@ApiParam({ name: 'id', description: 'Tournament ID' })
@ApiOkResponse({ type: BlankReturnMessageDto })
async start(
......@@ -77,7 +77,7 @@ export class TournamentController {
@Post(':id/reset')
@HttpCode(200)
@ApiOperation({ summary: 'Reset a tournament' })
@ApiOperation({ summary: '重置比赛' })
@ApiParam({ name: 'id', description: 'Tournament ID' })
@ApiOkResponse({ type: BlankReturnMessageDto })
async reset(
......@@ -89,7 +89,7 @@ export class TournamentController {
@Post(':id/finish')
@HttpCode(200)
@ApiOperation({ summary: 'Finish a tournament' })
@ApiOperation({ summary: '结束比赛' })
@ApiParam({ name: 'id', description: 'Tournament ID' })
@ApiOkResponse({ type: BlankReturnMessageDto })
async finish(
......
......@@ -5,6 +5,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { MycardUser } from 'nestjs-mycard';
import { Match, MatchStatus } from '../match/entities/match.entity';
import { In, Repository } from 'typeorm';
import { Participant } from '../participant/entities/participant.entity';
@Injectable()
export class TournamentService extends CrudService(Tournament, {
......@@ -23,7 +24,7 @@ export class TournamentService extends CrudService(Tournament, {
super(repo);
}
async getTournament(id: number, user: MycardUser) {
async getTournament(id: number, user: MycardUser | number) {
const result = await this.findOne(id, (qb) =>
Tournament.extraQueryForUser(user, qb, this.entityAliasName),
);
......@@ -31,14 +32,14 @@ export class TournamentService extends CrudService(Tournament, {
return result;
}
getTournaments(dto: Partial<Tournament>, user: MycardUser) {
getTournaments(dto: Partial<Tournament>, user: MycardUser | number) {
return this.findAll(dto, (qb) =>
Tournament.extraQueryForUser(user, qb, this.entityAliasName),
);
}
createTournament(tournament: Tournament, user: MycardUser) {
tournament.creator = user.id;
createTournament(tournament: Tournament, user: MycardUser | number) {
tournament.creator = typeof user === 'number' ? user : user.id;
return this.create(tournament);
}
......@@ -51,14 +52,14 @@ export class TournamentService extends CrudService(Tournament, {
return this.update(id, dto);
}
async deleteTournament(id: number, user: MycardUser) {
async deleteTournament(id: number, user: MycardUser | number) {
await this.checkPermissionOfTournament(id, user);
return this.delete(id);
}
async checkPermissionOfTournament(
id: number,
user: MycardUser,
user: MycardUser | number,
extraGets: (keyof Tournament)[] = [],
relations: string[] = [],
) {
......@@ -73,7 +74,7 @@ export class TournamentService extends CrudService(Tournament, {
return tournament.checkPermission(user);
}
async canModifyParticipants(id: number, user: MycardUser) {
async canModifyParticipants(id: number, user: MycardUser | number) {
const tournamnt = await this.checkPermissionOfTournament(id, user, [
'status',
]);
......@@ -86,7 +87,7 @@ export class TournamentService extends CrudService(Tournament, {
return tournamnt;
}
async resetTournament(id: number, user: MycardUser) {
async resetTournament(id: number, user: MycardUser | number) {
const tournament = await this.checkPermissionOfTournament(id, user, [
'status',
]);
......@@ -103,7 +104,7 @@ export class TournamentService extends CrudService(Tournament, {
return new BlankReturnMessageDto(200, 'success');
}
async startTournament(id: number, user: MycardUser) {
async startTournament(id: number, user: MycardUser | number) {
const tournament = await this.checkPermissionOfTournament(
id,
user,
......@@ -129,7 +130,7 @@ export class TournamentService extends CrudService(Tournament, {
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, [
'status',
]);
......@@ -157,6 +158,22 @@ export class TournamentService extends CrudService(Tournament, {
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) {
if (
!(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