Commit c9131dce authored by nanahira's avatar nanahira

logic left

parent eed3242a
...@@ -5,6 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; ...@@ -5,6 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { TournamentModule } from './tournament/tournament.module'; import { TournamentModule } from './tournament/tournament.module';
import { MycardAuthModule } from 'nestjs-mycard'; import { MycardAuthModule } from 'nestjs-mycard';
import { ParticipantModule } from './participant/participant.module'; import { ParticipantModule } from './participant/participant.module';
import { MatchModule } from './match/match.module';
@Module({ @Module({
imports: [ imports: [
...@@ -36,6 +37,7 @@ import { ParticipantModule } from './participant/participant.module'; ...@@ -36,6 +37,7 @@ import { ParticipantModule } from './participant/participant.module';
}), }),
TournamentModule, TournamentModule,
ParticipantModule, ParticipantModule,
MatchModule,
], ],
}) })
export class AppModule {} export class AppModule {}
import { Entity, ManyToOne } from 'typeorm';
import { EnumColumn, IdBase, IntColumn, NotChangeable, NotColumn } from 'nicot';
import { TournamentIdColumn } from '../../utility/decorators/tournament-id-column';
import { Tournament } from '../../tournament/entities/Tournament.entity';
import { Participant } from '../../participant/entities/participant.entity';
enum MatchStatus {
Pending = 'Pending',
Running = 'Running',
Finished = 'Finished',
}
@Entity()
export class Match extends IdBase() {
@NotChangeable()
@TournamentIdColumn(true)
tournamentId: number;
@NotColumn()
@ManyToOne(() => Tournament, (tournament) => tournament.matches)
tournament: Tournament;
@NotChangeable()
@IntColumn('smallint', {
unsigned: true,
required: true,
description: '比赛轮次。',
})
round: number;
@EnumColumn(MatchStatus, {
default: MatchStatus.Pending,
description: '比赛状态',
})
@NotChangeable()
status: MatchStatus;
@NotChangeable()
@IntColumn('bigint', {
unsigned: true,
description: '玩家 1 ID',
})
player1Id: number;
@IntColumn('mediumint', { description: '玩家 1 分数', required: false })
player1Score: number;
@NotColumn()
@ManyToOne(() => Participant, (participant) => participant.matches1)
player1: Participant;
@NotChangeable()
@IntColumn('bigint', {
unsigned: true,
description: '玩家 2 ID',
})
player2Id: number;
@IntColumn('mediumint', { description: '玩家 2 分数', required: false })
player2Score: number;
@NotColumn()
@ManyToOne(() => Participant, (participant) => participant.matches2)
player2: Participant;
@IntColumn('bigint', {
unsigned: true,
description: '胜者 ID',
})
winnerId: number;
@NotColumn()
@ManyToOne(() => Participant, (participant) => participant.wonMatches)
winner: Participant;
@NotChangeable()
@IntColumn('bigint', {
unsigned: true,
description: '晋级通往的比赛 ID',
required: false,
})
childMatchId: number;
@NotColumn()
@ManyToOne(() => Match, (match) => match.parentMatches)
childMatch: Match;
@NotColumn()
@ManyToOne(() => Match, (match) => match.childMatch)
parentMatches: Match[];
}
import { Test, TestingModule } from '@nestjs/testing';
import { MatchController } from './match.controller';
import { MatchService } from './match.service';
describe('MatchController', () => {
let controller: MatchController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MatchController],
providers: [MatchService],
}).compile();
controller = module.get<MatchController>(MatchController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
import { Controller } from '@nestjs/common';
import { MatchService } from './match.service';
import { RestfulFactory } from 'nicot';
import { Match } from './entities/match.entity';
import { ApiHeader, ApiTags } from '@nestjs/swagger';
import { MycardUser, PutMycardUser } from 'nestjs-mycard';
const factory = new RestfulFactory(Match);
class FindMatchDto extends factory.findAllDto {}
class UpdateMatchDto extends factory.updateDto {}
@Controller('match')
@ApiHeader({ name: 'Authorization', required: false, description: 'MC Token' })
@ApiTags('match')
export class MatchController {
constructor(private readonly matchService: MatchService) {}
@factory.findOne()
findOne(@factory.idParam() id: number, @PutMycardUser() user: MycardUser) {
return this.matchService.getMatch(id, user);
}
@factory.findAll()
findAll(
@factory.findAllParam() dto: FindMatchDto,
@PutMycardUser() user: MycardUser,
) {
return this.matchService.getMatches(dto, user);
}
}
import { Module } from '@nestjs/common';
import { MatchService } from './match.service';
import { MatchController } from './match.controller';
import { TournamentModule } from '../tournament/tournament.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Match } from './entities/match.entity';
@Module({
controllers: [MatchController],
providers: [MatchService],
imports: [TypeOrmModule.forFeature([Match]), TournamentModule],
})
export class MatchModule {}
import { Test, TestingModule } from '@nestjs/testing';
import { MatchService } from './match.service';
describe('MatchService', () => {
let service: MatchService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [MatchService],
}).compile();
service = module.get<MatchService>(MatchService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable } from '@nestjs/common';
import { CrudService, Inner } from 'nicot';
import { Match } from './entities/match.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { MycardUser } from 'nestjs-mycard';
import { Tournament } from '../tournament/entities/Tournament.entity';
import { Participant } from '../participant/entities/participant.entity';
@Injectable()
export class MatchService extends CrudService(Match, {
relations: [Inner('tournament'), 'player1', 'player2', 'winner'],
}) {
constructor(@InjectRepository(Match) repo) {
super(repo);
}
getMatch(id: number, user: MycardUser) {
return this.findOne(id, (qb) =>
Tournament.extraQueryForUser(user, qb, 'tournament'),
);
}
getMatches(dto: Partial<Participant>, user: MycardUser) {
return this.findAll(dto, (qb) =>
Tournament.extraQueryForUser(user, qb, 'tournament'),
);
}
}
import { Entity, ManyToOne, SelectQueryBuilder } from 'typeorm'; import { Entity, ManyToOne, OneToMany, SelectQueryBuilder } from 'typeorm';
import { NamedBase } from '../../utility/NamedBase.entity'; import { NamedBase } from '../../utility/NamedBase.entity';
import { import {
applyQueryProperty, applyQueryProperty,
...@@ -12,6 +12,8 @@ import { ...@@ -12,6 +12,8 @@ import {
TournamentVisibility, TournamentVisibility,
} from '../../tournament/entities/Tournament.entity'; } from '../../tournament/entities/Tournament.entity';
import { MycardUser } from 'nestjs-mycard'; import { MycardUser } from 'nestjs-mycard';
import { Match } from '../../match/entities/match.entity';
import { ApiProperty } from '@nestjs/swagger';
@Entity() @Entity()
export class Participant extends NamedBase { export class Participant extends NamedBase {
...@@ -26,6 +28,31 @@ export class Participant extends NamedBase { ...@@ -26,6 +28,31 @@ export class Participant extends NamedBase {
@ManyToOne(() => Tournament, (tournament) => tournament.participants) @ManyToOne(() => Tournament, (tournament) => tournament.participants)
tournament: Tournament; tournament: Tournament;
@NotColumn()
@OneToMany(() => Match, (match) => match.player1)
matches1: Match[];
@NotColumn()
@OneToMany(() => Match, (match) => match.player2)
matches2: Match[];
@NotColumn()
@OneToMany(() => Match, (match) => match.winner)
wonMatches: Match[];
@NotColumn()
@ApiProperty({ type: [Match], description: '参与的比赛。' })
matches: Match[];
getMatches() {
return this.matches1.concat(this.matches2);
}
async afterGet() {
await super.afterGet();
this.matches = this.getMatches();
}
checkPermission(user: MycardUser) { checkPermission(user: MycardUser) {
return this.tournament?.checkPermission(user); return this.tournament?.checkPermission(user);
} }
......
...@@ -13,7 +13,8 @@ import { MycardUser } from 'nestjs-mycard'; ...@@ -13,7 +13,8 @@ import { MycardUser } from 'nestjs-mycard';
import { DescBase } from '../../utility/NamedBase.entity'; import { DescBase } from '../../utility/NamedBase.entity';
import { Participant } from '../../participant/entities/participant.entity'; import { Participant } from '../../participant/entities/participant.entity';
import { IsArray, IsInt, IsPositive } from 'class-validator'; import { IsArray, IsInt, IsPositive } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty, PartialType } from '@nestjs/swagger';
import { Match } from '../../match/entities/match.entity';
export enum TournamentRule { export enum TournamentRule {
SingleElimination = 'SingleElimination', SingleElimination = 'SingleElimination',
...@@ -32,6 +33,23 @@ export enum TournamentStatus { ...@@ -32,6 +33,23 @@ export enum TournamentStatus {
Finished = 'Finished', Finished = 'Finished',
} }
export class RuleSettings {
@ApiProperty({ description: '瑞士轮局数。' })
rounds: number;
@ApiProperty({ description: '瑞士轮胜利分。' })
winScore: number;
@ApiProperty({ description: '瑞士轮平局分。' })
drawScore: number;
@ApiProperty({ description: '瑞士轮轮空分。' })
byeScore: number;
@ApiProperty({ description: '淘汰赛中是否有季军塞' })
hasThirdPlaceMatch: boolean;
}
@Entity() @Entity()
export class Tournament extends DescBase { export class Tournament extends DescBase {
@NotChangeable() @NotChangeable()
...@@ -41,6 +59,11 @@ export class Tournament extends DescBase { ...@@ -41,6 +59,11 @@ export class Tournament extends DescBase {
}) })
rule: TournamentRule; rule: TournamentRule;
@NotChangeable()
@Column('jsonb', { comment: '规则设定', default: {} })
@ApiProperty({ type: PartialType(RuleSettings), required: false })
ruleSettings: RuleSettings;
@EnumColumn(TournamentVisibility, { @EnumColumn(TournamentVisibility, {
default: TournamentVisibility.Public, default: TournamentVisibility.Public,
description: '可见性', description: '可见性',
...@@ -80,6 +103,10 @@ export class Tournament extends DescBase { ...@@ -80,6 +103,10 @@ export class Tournament extends DescBase {
@OneToMany(() => Participant, (participant) => participant.tournament) @OneToMany(() => Participant, (participant) => participant.tournament)
participants: Participant[]; participants: Participant[];
@NotColumn()
@OneToMany(() => Match, (match) => match.tournament)
matches: Match[];
async beforeCreate() { async beforeCreate() {
this.createdAt = new Date(); this.createdAt = new Date();
} }
......
...@@ -3,11 +3,12 @@ import { TournamentService } from './tournament.service'; ...@@ -3,11 +3,12 @@ import { TournamentService } from './tournament.service';
import { TournamentController } from './tournament.controller'; import { TournamentController } from './tournament.controller';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { Tournament } from './entities/Tournament.entity'; import { Tournament } from './entities/Tournament.entity';
import { Match } from '../match/entities/match.entity';
@Module({ @Module({
controllers: [TournamentController], controllers: [TournamentController],
providers: [TournamentService], providers: [TournamentService],
imports: [TypeOrmModule.forFeature([Tournament])], imports: [TypeOrmModule.forFeature([Tournament, Match])],
exports: [TournamentService], exports: [TournamentService],
}) })
export class TournamentModule {} export class TournamentModule {}
...@@ -3,12 +3,23 @@ import { BlankReturnMessageDto, CrudService } from 'nicot'; ...@@ -3,12 +3,23 @@ import { BlankReturnMessageDto, CrudService } from 'nicot';
import { Tournament, TournamentStatus } from './entities/Tournament.entity'; import { Tournament, TournamentStatus } from './entities/Tournament.entity';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { MycardUser } from 'nestjs-mycard'; import { MycardUser } from 'nestjs-mycard';
import { Match } from '../match/entities/match.entity';
import { Repository } from 'typeorm';
@Injectable() @Injectable()
export class TournamentService extends CrudService(Tournament, { export class TournamentService extends CrudService(Tournament, {
relations: ['participants'], relations: [
'participants',
'matches',
'matches.player1',
'matches.player2',
'matches.winner',
],
}) { }) {
constructor(@InjectRepository(Tournament) repo) { constructor(
@InjectRepository(Tournament) repo,
@InjectRepository(Match) private readonly matchRepo: Repository<Match>,
) {
super(repo); super(repo);
} }
...@@ -43,10 +54,14 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -43,10 +54,14 @@ export class TournamentService extends CrudService(Tournament, {
return this.delete(id); return this.delete(id);
} }
async checkPermissionOfTournament(id: number, user: MycardUser) { async checkPermissionOfTournament(
id: number,
user: MycardUser,
extraGets: (keyof Tournament)[] = [],
) {
const tournament = await this.repo.findOne({ const tournament = await this.repo.findOne({
where: { id }, where: { id },
select: ['id', 'creator', 'collaborators'], select: ['id', 'creator', 'collaborators', ...extraGets],
}); });
if (!tournament) { if (!tournament) {
throw new BlankReturnMessageDto(404, '未找到该比赛。').toException(); throw new BlankReturnMessageDto(404, '未找到该比赛。').toException();
...@@ -64,4 +79,49 @@ export class TournamentService extends CrudService(Tournament, { ...@@ -64,4 +79,49 @@ export class TournamentService extends CrudService(Tournament, {
} }
return tournamnt; return tournamnt;
} }
async resetTournament(id: number, user: MycardUser) {
const tournament = await this.checkPermissionOfTournament(id, user, [
'status',
]);
if (tournament.status === TournamentStatus.Ready) {
throw new BlankReturnMessageDto(
403,
'比赛还未开始,不能重置。',
).toException();
}
await this.repo.update(id, { status: TournamentStatus.Ready });
await this.matchRepo.softDelete({ tournamentId: id });
return new BlankReturnMessageDto(200, 'success');
}
async startTournament(id: number, user: MycardUser) {
const tournament = await this.checkPermissionOfTournament(id, user, [
'status',
]);
if (tournament.status !== TournamentStatus.Ready) {
throw new BlankReturnMessageDto(
403,
'比赛已经开始,不能重复开始。',
).toException();
}
await this.repo.update(id, { status: TournamentStatus.Running });
// TODO: generate matches
return new BlankReturnMessageDto(200, 'success');
}
async endTournament(id: number, user: MycardUser) {
const tournament = await this.checkPermissionOfTournament(id, user, [
'status',
]);
if (tournament.status !== TournamentStatus.Running) {
throw new BlankReturnMessageDto(
403,
'比赛还未开始,不能结束。',
).toException();
}
await this.repo.update(id, { status: TournamentStatus.Finished });
// TODO: calculate results
return new BlankReturnMessageDto(200, 'success');
}
} }
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