diff --git a/src/app.module.ts b/src/app.module.ts index 6973f2c..4d81902 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,7 +12,9 @@ import { TimeSheetController, UserController, InformController, - CodeController, + LanguageController, + QuestionBankController, + QuestionAnswerController, } from './controllers'; import { jwtModuleOptions, @@ -29,15 +31,18 @@ import { UserService, InformService, TimeSheetService, - CodeService, + LanguageService, + QuestionBankService, + QuestionAnswerService, } from './services'; import { JwtStrategy, WsGuard } from './strategys'; import { DataResource, DataDepartment, UserTimesheet, - Language, QuestionBank, + QuestionAnswer, + QuestionContributor, } from './entities'; @Module({ @@ -52,6 +57,8 @@ import { DataDepartment, UserTimesheet, QuestionBank, + QuestionAnswer, + QuestionContributor, ]), ], controllers: [ @@ -62,7 +69,9 @@ import { TimeSheetController, UserController, InformController, - CodeController, + LanguageController, + QuestionAnswerController, + QuestionBankController, ], providers: [ WsGuard, @@ -73,7 +82,9 @@ import { AuthService, UserService, InformService, - CodeService, + LanguageService, + QuestionBankService, + QuestionAnswerService, TimeSheetService, TimeSheetSocket, TimeSheetSchedule, diff --git a/src/config/config.ts b/src/config/config.ts index 0ed3b8f..fdeeb29 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -7,7 +7,7 @@ interface IConfig { environment: ServerEnvironment; }; services: { - codeService: string; + languageService: string; }; dingTalk: { bossId: string; diff --git a/src/constants/resources.ts b/src/constants/resources.ts new file mode 100644 index 0000000..3853d08 --- /dev/null +++ b/src/constants/resources.ts @@ -0,0 +1,9 @@ +export enum Resources { + 'dashboard' = '1', + 'timesheet' = '2', + 'attendance' = '3', + 'questionBank' = '4', + 'codeOnline' = '5', + 'createQuestion' = '6', + 'updateQuestion' = '7', +} diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 1eb4959..48e0f8c 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -4,4 +4,6 @@ export * from './auth.controller'; export * from './timesheet.controller'; export * from './user.controller'; export * from './inform.controller'; -export * from './code.controller'; +export * from './language.controller'; +export * from './questionBank.controller'; +export * from './questionAnswer.controller'; diff --git a/src/controllers/code.controller.ts b/src/controllers/language.controller.ts similarity index 64% rename from src/controllers/code.controller.ts rename to src/controllers/language.controller.ts index ceecd9f..02e238e 100644 --- a/src/controllers/code.controller.ts +++ b/src/controllers/language.controller.ts @@ -1,22 +1,34 @@ -import { ICodeRunBody, IQuestionDto } from '@dtos/code'; -import { Controller, UseGuards, Post, Body, Get, Param } from '@nestjs/common'; +import { ICodeRunBody } from '@dtos/code'; +import { + Controller, + UseGuards, + Post, + Body, + Get, + Request, +} from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { CodeService } from '@services/code.service'; +import { LanguageService } from '@services/language.service'; import recorder from '@utils/recorder'; import { sleep } from '@utils/utils'; import { v4 as uuid } from 'uuid'; import { ICodeLanguageDto } from '@dtos/code'; +import { NestRes } from '@interfaces/nestbase'; const MAX_WAITING_TIME = 360; @UseGuards(AuthGuard('jwt')) -@Controller('code') -export class CodeController { - constructor(private readonly codeService: CodeService) {} +@Controller('language') +export class LanguageController { + constructor(private readonly languageService: LanguageService) {} @Post('run/case') - async runByCase(@Body() body: ICodeRunBody): Promise { + async runByCase( + @Body() body: ICodeRunBody, + @Request() req: NestRes, + ): Promise { const id = uuid(); let waitTime = 0; let result = null; + body.userId = req.user.userId; recorder.add({ id: id, @@ -42,7 +54,7 @@ export class CodeController { return ''; } - return await this.codeService.runCodeByCase(body); + return await this.languageService.runCodeByCase(body); } @Post('run') @@ -71,12 +83,12 @@ export class CodeController { async runImplement(body: ICodeRunBody) { const { code, languageId } = body; - return await this.codeService.run(languageId, code); + return await this.languageService.run(languageId, code); } @Get('languages') async getLanguages() { - const data = await this.codeService.getLanguages(); + const data = await this.languageService.getLanguageList(); return data.map((x) => { return { id: x.id, @@ -86,15 +98,4 @@ export class CodeController { } as ICodeLanguageDto; }); } - - @Get('question/:questionId') - async getQuestion(@Param('questionId') questionId: string) { - const data = await this.codeService.getQuestion(questionId); - return { - id: data.id, - name: data.name, - desribe: data.describe, - code: data.code, - } as IQuestionDto; - } } diff --git a/src/controllers/questionAnswer.controller.ts b/src/controllers/questionAnswer.controller.ts new file mode 100644 index 0000000..03f94eb --- /dev/null +++ b/src/controllers/questionAnswer.controller.ts @@ -0,0 +1,25 @@ +import { Controller, UseGuards, Get, Param, Request } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { NestRes } from '@interfaces/nestbase'; +import { QuestionAnswerService } from '@services/questionAnswer.service'; + +@UseGuards(AuthGuard('jwt')) +@Controller('answer') +export class QuestionAnswerController { + constructor(private readonly questionAnswerService: QuestionAnswerService) {} + + @Get('/last/:questionId/:languageId') + async getUserLastQuestionAnswer( + @Param('questionId') questionId: string, + @Param('languageId') languageId: number, + @Request() req: NestRes, + ) { + const data = await this.questionAnswerService.getUserLastQuestionAnswer( + questionId, + req.user.userId, + languageId, + ); + + return data?.code || null; + } +} diff --git a/src/controllers/questionBank.controller.ts b/src/controllers/questionBank.controller.ts new file mode 100644 index 0000000..e210646 --- /dev/null +++ b/src/controllers/questionBank.controller.ts @@ -0,0 +1,78 @@ +import { + Controller, + UseGuards, + Post, + Body, + Get, + Param, + Put, + Request, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { + ICreateQuestionBankBody, + IUpdateQuestionBankBody, +} from '@models/questionBank.model'; +import { QuestionBankService } from '@services/questionBank.service'; +import { NestRes } from '@interfaces/nestbase'; +import { QuestionAnswerService } from '@services/questionAnswer.service'; +import { Resources } from '@constants/resources'; + +@UseGuards(AuthGuard('jwt')) +@Controller('question') +export class QuestionBankController { + constructor( + private readonly questionAnswerService: QuestionAnswerService, + private readonly questionBankService: QuestionBankService, + ) {} + + @Get('/list') + async getQuestionList(@Request() req: NestRes) { + const authorized = req.user.resources.includes(Resources.updateQuestion); + return await this.questionBankService.getQuestionList( + req.user.userId, + authorized, + ); + } + + @Get('/:questionId') + async getQuestion(@Param('questionId') questionId: string) { + return await this.questionBankService.getQuestion(questionId); + } + + verifyQuestion(question: ICreateQuestionBankBody) { + if (!question.name) { + throw new HttpException('题目名称不能为空', HttpStatus.BAD_REQUEST); + } + try { + JSON.parse(JSON.stringify(question.cases)); + JSON.parse(JSON.stringify(question.entryCodes)); + } catch (error) { + throw new HttpException(JSON.stringify(error), HttpStatus.BAD_REQUEST); + } + } + + @Post() + async createQuestion( + @Body() body: ICreateQuestionBankBody, + @Request() req: NestRes, + ) { + this.verifyQuestion(body); + body.userId = req.user.userId; + body.userName = req.user.username; + return await this.questionBankService.createQuestion(body); + } + + @Put() + async updateQuestion( + @Body() body: IUpdateQuestionBankBody, + @Request() req: NestRes, + ) { + this.verifyQuestion(body); + body.userId = req.user.userId; + body.userName = req.user.username; + return await this.questionBankService.updateQuestion(body); + } +} diff --git a/src/dtos/code.ts b/src/dtos/code.ts index 3707b98..b3d7215 100644 --- a/src/dtos/code.ts +++ b/src/dtos/code.ts @@ -1,8 +1,11 @@ +import { IEntryCode } from '@entities/questionBank.entity'; + export interface ICodeRunBody { once?: boolean; code: string; questionId: string; languageId: number; + userId: string; } export interface ICodeLanguageDto { @@ -16,7 +19,9 @@ export interface IQuestionDto { id: string; name: string; desribe: string; - code: string; + createTime: string; + level: number; + entrys: IEntryCode[]; } export interface ICodeRunResult { diff --git a/src/entities/index.ts b/src/entities/index.ts index d9a35e0..afe4552 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -3,3 +3,5 @@ export * from './data.permission.entity'; export * from './timesheet.enetity'; export * from './language.entity'; export * from './questionBank.entity'; +export * from './questionAnswer.entity'; +export * from './questionContributor.entity'; diff --git a/src/entities/questionAnswer.entity.ts b/src/entities/questionAnswer.entity.ts new file mode 100644 index 0000000..c4a21ef --- /dev/null +++ b/src/entities/questionAnswer.entity.ts @@ -0,0 +1,23 @@ +import { Column, Entity } from 'typeorm'; + +@Entity({ name: 'question_answer' }) +export class QuestionAnswer { + @Column({ primary: true, generated: 'uuid' }) + id: string; + @Column('varchar', { length: 50, nullable: true }) + questionId: string; + @Column('int2', { nullable: true }) + languageId: number; + @Column('varchar', { length: 50, nullable: true }) + userId: string; + @Column('text', { nullable: true }) + code: string; + @Column('text', { nullable: true }) + result: string; + @Column('boolean', { nullable: true }) + isPassed: boolean; + @Column('double precision', { nullable: true }) + elapsedTime: number; + @Column('timestamp') + createTime: string; +} diff --git a/src/entities/questionBank.entity.ts b/src/entities/questionBank.entity.ts index aee786c..1581acf 100644 --- a/src/entities/questionBank.entity.ts +++ b/src/entities/questionBank.entity.ts @@ -6,22 +6,29 @@ export class QuestionBank { id: string; @Column('varchar', { length: 100 }) name: string; // 题目名称 - @Column('varchar', { length: 50 }) - entry: string; // 入口方法 - @Column('text') - code: string; // 默认填充代码 - @Column('text') + @Column('smallint', { nullable: true }) + level: number; // 难度级别 + @Column('jsonb', { array: false, default: () => "'[]'", nullable: true }) + entryCodes: IEntryCode[]; // 默认填充代码 + @Column('text', { nullable: true }) describe?: string; // 题目描述 @Column('jsonb', { array: false, default: () => "'[]'", nullable: true }) cases?: IQuestionCase[]; // 测试cases - @Column('jsonb', { array: false, default: () => "'[]'", nullable: true }) - contributor?: string[]; // 贡献者 可以多个 - @Column('date') + @Column('boolean', { nullable: true }) + enabled: boolean; // 是否展示 + @Column('timestamp') createTime: string; } export class IQuestionCase { + languageId: number; comments: string; input: any; output: any; } + +export class IEntryCode { + languageId: number; + function: string; // 入口方法 + code: string; +} diff --git a/src/entities/questionContributor.entity.ts b/src/entities/questionContributor.entity.ts new file mode 100644 index 0000000..6f37950 --- /dev/null +++ b/src/entities/questionContributor.entity.ts @@ -0,0 +1,15 @@ +import { Column, Entity } from 'typeorm'; + +@Entity({ name: 'question_contributor' }) +export class QuestionContributor { + @Column({ primary: true, generated: 'uuid' }) + id: string; + @Column('varchar', { length: 50, nullable: true }) + questionId: string; + @Column('varchar', { length: 50, nullable: true }) + userId: string; + @Column('varchar', { length: 50, nullable: true }) + userName: string; + @Column('timestamp') + createTime: string; +} diff --git a/src/models/questionBank.model.ts b/src/models/questionBank.model.ts new file mode 100644 index 0000000..a40d916 --- /dev/null +++ b/src/models/questionBank.model.ts @@ -0,0 +1,29 @@ +import { IEntryCode, IQuestionCase } from '@entities/questionBank.entity'; + +export interface ICreateQuestionBankBody { + userId?: string; + userName?: string; + name: string; + level?: number; + describe?: string; + entryCodes?: IEntryCode[]; + cases?: IQuestionCase[]; +} + +export interface IUpdateQuestionBankBody extends ICreateQuestionBankBody { + id: string; + enabled: boolean; +} + +export interface IQuestionListModel { + id: string; + name: string; + createTime: string; + level: number; + describe?: string; + isPassed: boolean; + elapsedTime: number; + cases?: IQuestionCase[]; + entryCodes?: IEntryCode[]; + enabled: boolean; +} diff --git a/src/services/code.service.ts b/src/services/code.service.ts deleted file mode 100644 index 546a555..0000000 --- a/src/services/code.service.ts +++ /dev/null @@ -1,96 +0,0 @@ -import config from '@config/config'; -import { ICodeRunResult, ICodeRunBody, IRunCaseResult } from '@dtos/code'; -import { IQuestionCase, QuestionBank } from '@entities/questionBank.entity'; -import { LanguageModel } from '@models/language.model'; -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { httpGet, httpPost } from '@utils/httpRequest'; -import { matchCaseResult } from '@utils/utils'; -import { Repository } from 'typeorm'; - -@Injectable() -export class CodeService { - constructor( - @InjectRepository(QuestionBank) - private readonly questionRepository: Repository, - ) {} - - async getQuestion(questionId: string) { - return await this.questionRepository.findOneBy({ - id: questionId, - }); - } - - async getLanguage(languageId: number) { - const languages = await this.getLanguages(); - return languages.find((x) => x.id == languageId); - } - - async getLanguages() { - return await httpGet( - `${config.services.codeService}/languages`, - ); - } - - async prepareCodeCase( - language: LanguageModel, - code: string, - entry: string, - cases: IQuestionCase, - ) { - const testCaseCode = language.captureCode - .replace('${entry}', entry) - .replace('${input}', cases.input); - code += testCaseCode; - return code; - } - - extractCaseResult(value: string) { - value = value ?? ''; - return { - elapsedTime: matchCaseResult('ElapsedTime', value), - codeOutput: matchCaseResult('Output', value), - }; - } - - async runCodeByCase(params: ICodeRunBody) { - const { code, questionId, languageId, once } = params; - - const question = await this.getQuestion(questionId); - const language = await this.getLanguage(languageId); - - const result = [] as IRunCaseResult[]; - for (const testcase of question.cases) { - const codeCommand = await this.prepareCodeCase( - language, - code, - question.entry, - testcase, - ); - - const _result = await this.run(languageId, codeCommand); - result.push({ - ...testcase, - ...this.extractCaseResult(_result.data), - logs: _result.data, - }); - - if (once || !_result.isSuccess) { - break; - } - } - return result; - } - - async run(languageId: number, code: string): Promise { - return await httpPost( - `${config.services.codeService}/run`, - { - body: JSON.stringify({ - languageId: languageId, - code: code, - }), - }, - ); - } -} diff --git a/src/services/index.ts b/src/services/index.ts index faae119..66c31fc 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -5,4 +5,6 @@ export * from './auth.service'; export * from './user.service'; export * from './inform.service'; export * from './timesheet.service'; -export * from './code.service'; +export * from './language.service'; +export * from './questionBank.service'; +export * from './questionAnswer.service'; diff --git a/src/services/language.service.ts b/src/services/language.service.ts new file mode 100644 index 0000000..0505e2c --- /dev/null +++ b/src/services/language.service.ts @@ -0,0 +1,136 @@ +import config from '@config/config'; +import { ICodeRunResult, ICodeRunBody, IRunCaseResult } from '@dtos/code'; +import { QuestionAnswer } from '@entities/questionAnswer.entity'; +import { IQuestionCase } from '@entities/questionBank.entity'; +import { LanguageModel } from '@models/language.model'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { httpGet, httpPost } from '@utils/httpRequest'; +import { matchCaseResult, now } from '@utils/utils'; +import { Repository } from 'typeorm'; +import { QuestionBankService } from './questionBank.service'; + +@Injectable() +export class LanguageService { + constructor( + @InjectRepository(QuestionAnswer) + private readonly answerRepository: Repository, + private readonly questionBankService: QuestionBankService, + ) {} + + async getLanguage(languageId: number) { + const languages = await this.getLanguageList(); + return languages.find((x) => x.id == languageId); + } + + async getLanguageList() { + return await httpGet( + `${config.services.languageService}/languages`, + ); + } + + async prepareCodeCase( + language: LanguageModel, + code: string, + entry: string, + cases: IQuestionCase, + ) { + const testCaseCode = language.captureCode + .replace('${entry}', entry) + .replace('${input}', cases.input); + code += testCaseCode; + + return code; + } + + extractCaseResult(value: string) { + value = value ?? ''; + return { + elapsedTime: matchCaseResult('ElapsedTime', value), + codeOutput: matchCaseResult('Output', value), + }; + } + + async runCodeByCase(params: ICodeRunBody) { + const { code, questionId, languageId, once, userId } = params; + + const question = await this.questionBankService.getQuestion(questionId); + + if (!question.enabled) { + return { isSuccess: false, message: '题目暂时不能作答,请稍后再试' }; + } + + const language = await this.getLanguage(languageId); + const entry = question.entryCodes.find((x) => x.languageId === languageId); + const cases = question.cases.filter((x) => x.languageId === languageId); + + const result = [] as IRunCaseResult[]; + const recordResults = []; + let isPassed = false, + elapsedTime = 0; + + for (const testcase of cases) { + const codeCommand = await this.prepareCodeCase( + language, + code, + entry.function, + testcase, + ); + + const _result = await this.run(languageId, codeCommand); + + result.push({ + ...testcase, + ...this.extractCaseResult(_result.data), + logs: _result.data, + }); + + recordResults.push({ + ...testcase, + ...this.extractCaseResult(_result.data), + }); + + if (once || !_result.isSuccess) { + break; + } + } + if (!once) { + const passedResult = result.filter((x) => { + elapsedTime += parseFloat(x.elapsedTime); + return ( + JSON.stringify(`${x.output}`.replace(/\s/g, '')) === + JSON.stringify(`${x.codeOutput}`.replace(/\s/g, '')) + ); + }); + + isPassed = passedResult.length === result.length; + + await this.answerRepository.save([ + { + userId: userId, + questionId: questionId, + code: code, + languageId: languageId, + createTime: now(), + result: JSON.stringify(recordResults), + elapsedTime, + isPassed, + }, + ]); + } + + return result; + } + + async run(languageId: number, code: string): Promise { + return await httpPost( + `${config.services.languageService}/run`, + { + body: JSON.stringify({ + languageId: languageId, + code: code, + }), + }, + ); + } +} diff --git a/src/services/questionAnswer.service.ts b/src/services/questionAnswer.service.ts new file mode 100644 index 0000000..6b2174a --- /dev/null +++ b/src/services/questionAnswer.service.ts @@ -0,0 +1,32 @@ +import { QuestionAnswer } from '@entities/questionAnswer.entity'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +@Injectable() +export class QuestionAnswerService { + constructor( + @InjectRepository(QuestionAnswer) + private readonly answerRepository: Repository, + ) {} + async getUserLastQuestionAnswer( + questionId: string, + userId: string, + languageId?: number, + ) { + const where = { + questionId, + languageId, + userId, + }; + + if (!languageId) { + delete where.languageId; + } + + return await this.answerRepository.findOne({ + where: where, + order: { createTime: 'DESC' }, + }); + } +} diff --git a/src/services/questionBank.service.ts b/src/services/questionBank.service.ts new file mode 100644 index 0000000..e0aca02 --- /dev/null +++ b/src/services/questionBank.service.ts @@ -0,0 +1,109 @@ +import { QuestionBank } from '@entities/questionBank.entity'; +import { QuestionContributor } from '@entities/questionContributor.entity'; +import { + ICreateQuestionBankBody, + IQuestionListModel, + IUpdateQuestionBankBody, +} from '@models/questionBank.model'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { now } from '@utils/utils'; +import { Repository } from 'typeorm'; +import { QuestionAnswerService } from './questionAnswer.service'; + +@Injectable() +export class QuestionBankService { + constructor( + @InjectRepository(QuestionBank) + private readonly questionRepository: Repository, + @InjectRepository(QuestionContributor) + private readonly contributorRepository: Repository, + private readonly snswerService: QuestionAnswerService, + ) {} + + async getQuestion(questionId: string) { + return await this.questionRepository.findOneBy({ + id: questionId, + }); + } + + async getQuestionList(userId: string, showAll: boolean) { + const where = { enabled: true }; + if (showAll) { + delete where.enabled; + } + + const questionList = await this.questionRepository.find({ + where: where, + order: { createTime: 'ASC' }, + }); + + const list = [] as IQuestionListModel[]; + for (const question of questionList) { + const questionAnswer = await this.snswerService.getUserLastQuestionAnswer( + question.id, + userId, + ); + + list.push({ + id: question.id, + name: question.name, + level: question.level, + enabled: question.enabled, + isPassed: questionAnswer?.isPassed, + elapsedTime: questionAnswer?.elapsedTime, + createTime: question.createTime, + }); + } + return list; + } + + async updateQuestion(body: IUpdateQuestionBankBody) { + const question = await this.questionRepository.findOneBy({ + id: body.id, + }); + + question.cases = body.cases; + question.describe = body.describe; + question.enabled = body.enabled; + question.entryCodes = body.entryCodes; + question.level = body.level; + question.name = body.name; + + const data = await this.questionRepository.update(question.id, question); + await this.contributorRepository.save({ + questionId: question.id, + userId: body.userId, + userName: body.userName, + createTime: now(), + }); + + if (data.affected > 0) { + return question; + } + return null; + } + + async createQuestion(body: ICreateQuestionBankBody) { + const data = await this.questionRepository.save({ + name: body.name, + level: body.level, + cases: body.cases, + describe: body.describe, + entryCodes: body.entryCodes, + createTime: now(), + status: false, + }); + + if (data.id) { + await this.contributorRepository.save({ + questionId: data.id, + userId: body.userId, + userName: body.userName, + createTime: now(), + }); + } + + return data; + } +} diff --git a/src/utils/httpRequest.ts b/src/utils/httpRequest.ts index 1036405..a144af0 100644 --- a/src/utils/httpRequest.ts +++ b/src/utils/httpRequest.ts @@ -1,4 +1,5 @@ import fetch from 'node-fetch'; + export async function httpRequest( input: RequestInfo | URL, init?: RequestInit,