团队冲刺第八天:
题库后端收尾:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 import * as TypeORM from "typeorm"; 2 import Model from "./common"; 3 4 declare var syzoj, ErrorMessage: any; 5 6 import User from "./user"; 7 import File from "./file"; 8 import JudgeState from "./judge_state"; 9 import Contest from "./contest"; 10 import ProblemTag from "./problem_tag"; 11 import ProblemTagMap from "./problem_tag_map"; 12 import SubmissionStatistics, { StatisticsType } from "./submission_statistics"; 13 14 import * as fs from "fs-extra"; 15 import * as path from "path"; 16 import * as util from "util"; 17 import * as LRUCache from "lru-cache"; 18 import * as DeepCopy from "deepcopy"; 19 20 const problemTagCache = new LRUCache<number, number[]>({ 21 max: syzoj.config.db.cache_size 22 }); 23 24 enum ProblemType { 25 Traditional = "traditional", 26 SubmitAnswer = "submit-answer", 27 Interaction = "interaction" 28 } 29 30 const statisticsTypes = { 31 fastest: ['total_time', 'ASC'], 32 slowest: ['total_time', 'DESC'], 33 shortest: ['code_length', 'ASC'], 34 longest: ['code_length', 'DESC'], 35 min: ['max_memory', 'ASC'], 36 max: ['max_memory', 'DESC'], 37 earliest: ['submit_time', 'ASC'] 38 }; 39 40 const statisticsCodeOnly = ["fastest", "slowest", "min", "max"]; 41 42 @TypeORM.Entity() 43 export default class Problem extends Model { 44 static cache = true; 45 46 @TypeORM.PrimaryGeneratedColumn() 47 id: number; 48 49 @TypeORM.Column({ nullable: true, type: "varchar", length: 80 }) 50 title: string; 51 52 @TypeORM.Index() 53 @TypeORM.Column({ nullable: true, type: "integer" }) 54 user_id: number; 55 56 @TypeORM.Column({ nullable: true, type: "integer" }) 57 publicizer_id: number; 58 59 @TypeORM.Column({ nullable: true, type: "boolean" }) 60 is_anonymous: boolean; 61 62 @TypeORM.Column({ nullable: true, type: "text" }) 63 description: string; 64 65 @TypeORM.Column({ nullable: true, type: "text" }) 66 input_format: string; 67 68 @TypeORM.Column({ nullable: true, type: "text" }) 69 output_format: string; 70 71 @TypeORM.Column({ nullable: true, type: "text" }) 72 example: string; 73 74 @TypeORM.Column({ nullable: true, type: "text" }) 75 limit_and_hint: string; 76 77 @TypeORM.Column({ nullable: true, type: "integer" }) 78 time_limit: number; 79 80 @TypeORM.Column({ nullable: true, type: "integer" }) 81 memory_limit: number; 82 83 @TypeORM.Column({ nullable: true, type: "integer" }) 84 additional_file_id: number; 85 86 @TypeORM.Column({ nullable: true, type: "integer" }) 87 ac_num: number; 88 89 @TypeORM.Column({ nullable: true, type: "integer" }) 90 submit_num: number; 91 92 @TypeORM.Index() 93 @TypeORM.Column({ nullable: true, type: "boolean" }) 94 is_public: boolean; 95 96 @TypeORM.Column({ nullable: true, type: "boolean" }) 97 file_io: boolean; 98 99 @TypeORM.Column({ nullable: true, type: "text" }) 100 file_io_input_name: string; 101 102 @TypeORM.Column({ nullable: true, type: "text" }) 103 file_io_output_name: string; 104 105 @TypeORM.Index() 106 @TypeORM.Column({ nullable: true, type: "datetime" }) 107 publicize_time: Date; 108 109 @TypeORM.Column({ nullable: true, 110 type: "enum", 111 enum: ProblemType, 112 default: ProblemType.Traditional 113 }) 114 type: ProblemType; 115 116 user?: User; 117 publicizer?: User; 118 additional_file?: File; 119 120 async loadRelationships() { 121 this.user = await User.findById(this.user_id); 122 this.publicizer = await User.findById(this.publicizer_id); 123 this.additional_file = await File.findById(this.additional_file_id); 124 } 125 126 async isAllowedEditBy(user) { 127 if (!user) return false; 128 if (await user.hasPrivilege('manage_problem')) return true; 129 return this.user_id === user.id; 130 } 131 132 async isAllowedUseBy(user) { 133 if (this.is_public) return true; 134 if (!user) return false; 135 if (await user.hasPrivilege('manage_problem')) return true; 136 return this.user_id === user.id; 137 } 138 139 async isAllowedManageBy(user) { 140 if (!user) return false; 141 if (await user.hasPrivilege('manage_problem')) return true; 142 return user.is_admin; 143 } 144 145 getTestdataPath() { 146 return syzoj.utils.resolvePath(syzoj.config.upload_dir, 'testdata', this.id.toString()); 147 } 148 149 getTestdataArchivePath() { 150 return syzoj.utils.resolvePath(syzoj.config.upload_dir, 'testdata-archive', this.id.toString() + '.zip'); 151 } 152 153 async updateTestdata(path, noLimit) { 154 await syzoj.utils.lock(['Problem::Testdata', this.id], async () => { 155 let unzipSize = 0, unzipCount = 0; 156 let p7zip = new (require('node-7z')); 157 await p7zip.list(path).progress(files => { 158 unzipCount += files.length; 159 for (let file of files) unzipSize += file.size; 160 }); 161 if (!noLimit && unzipCount > syzoj.config.limit.testdata_filecount) throw new ErrorMessage('数据包中的文件太多。'); 162 if (!noLimit && unzipSize > syzoj.config.limit.testdata) throw new ErrorMessage('数据包太大。'); 163 164 let dir = this.getTestdataPath(); 165 await fs.remove(dir); 166 await fs.ensureDir(dir); 167 168 let execFileAsync = util.promisify(require('child_process').execFile); 169 await execFileAsync(__dirname + '/../bin/unzip', ['-j', '-o', '-d', dir, path]); 170 await fs.move(path, this.getTestdataArchivePath(), { overwrite: true }); 171 }); 172 } 173 174 async uploadTestdataSingleFile(filename, filepath, size, noLimit) { 175 await syzoj.utils.lock(['Promise::Testdata', this.id], async () => { 176 let dir = this.getTestdataPath(); 177 await fs.ensureDir(dir); 178 179 let oldSize = 0, list = await this.listTestdata(), replace = false, oldCount = 0; 180 if (list) { 181 oldCount = list.files.length; 182 for (let file of list.files) { 183 if (file.filename !== filename) oldSize += file.size; 184 else replace = true; 185 } 186 } 187 188 if (!noLimit && oldSize + size > syzoj.config.limit.testdata) throw new ErrorMessage('数据包太大。'); 189 if (!noLimit && oldCount + (!replace as any as number) > syzoj.config.limit.testdata_filecount) throw new ErrorMessage('数据包中的文件太多。'); 190 191 await fs.move(filepath, path.join(dir, filename), { overwrite: true }); 192 193 let execFileAsync = util.promisify(require('child_process').execFile); 194 try { await execFileAsync('dos2unix', [path.join(dir, filename)]); } catch (e) {} 195 196 await fs.remove(this.getTestdataArchivePath()); 197 }); 198 } 199 200 async deleteTestdataSingleFile(filename) { 201 await syzoj.utils.lock(['Promise::Testdata', this.id], async () => { 202 await fs.remove(path.join(this.getTestdataPath(), filename)); 203 await fs.remove(this.getTestdataArchivePath()); 204 }); 205 } 206 207 async makeTestdataZip() { 208 await syzoj.utils.lock(['Promise::Testdata', this.id], async () => { 209 let dir = this.getTestdataPath(); 210 if (!await syzoj.utils.isDir(dir)) throw new ErrorMessage('无测试数据。'); 211 212 let p7zip = new (require('node-7z')); 213 214 let list = await this.listTestdata(), pathlist = list.files.map(file => path.join(dir, file.filename)); 215 if (!pathlist.length) throw new ErrorMessage('无测试数据。'); 216 await fs.ensureDir(path.resolve(this.getTestdataArchivePath(), '..')); 217 await p7zip.add(this.getTestdataArchivePath(), pathlist); 218 }); 219 } 220 221 async hasSpecialJudge() { 222 try { 223 let dir = this.getTestdataPath(); 224 let list = await fs.readdir(dir); 225 return list.includes('spj.js') || list.find(x => x.startsWith('spj_')) !== undefined; 226 } catch (e) { 227 return false; 228 } 229 } 230 231 async listTestdata() { 232 try { 233 let dir = this.getTestdataPath(); 234 let filenameList = await fs.readdir(dir); 235 let list = await Promise.all(filenameList.map(async x => { 236 let stat = await fs.stat(path.join(dir, x)); 237 if (!stat.isFile()) return undefined; 238 return { 239 filename: x, 240 size: stat.size 241 }; 242 })); 243 244 list = list.filter(x => x); 245 246 let res = { 247 files: list, 248 zip: null 249 }; 250 251 try { 252 let stat = await fs.stat(this.getTestdataArchivePath()); 253 if (stat.isFile()) { 254 res.zip = { 255 size: stat.size 256 }; 257 } 258 } catch (e) { 259 if (list) { 260 res.zip = { 261 size: null 262 }; 263 } 264 } 265 266 return res; 267 } catch (e) { 268 return null; 269 } 270 } 271 272 async updateFile(path, type, noLimit) { 273 let file = await File.upload(path, type, noLimit); 274 275 if (type === 'additional_file') { 276 this.additional_file_id = file.id; 277 } 278 279 await this.save(); 280 } 281 282 async validate() { 283 if (this.time_limit <= 0) return 'Invalid time limit'; 284 if (this.time_limit > syzoj.config.limit.time_limit) return 'Time limit too large'; 285 if (this.memory_limit <= 0) return 'Invalid memory limit'; 286 if (this.memory_limit > syzoj.config.limit.memory_limit) return 'Memory limit too large'; 287 if (!['traditional', 'submit-answer', 'interaction'].includes(this.type)) return 'Invalid problem type'; 288 289 if (this.type === 'traditional') { 290 let filenameRE = /^[w -+.]*$/; 291 if (this.file_io_input_name && !filenameRE.test(this.file_io_input_name)) return 'Invalid input file name'; 292 if (this.file_io_output_name && !filenameRE.test(this.file_io_output_name)) return 'Invalid output file name'; 293 294 if (this.file_io) { 295 if (!this.file_io_input_name) return 'No input file name'; 296 if (!this.file_io_output_name) return 'No output file name'; 297 } 298 } 299 300 return null; 301 } 302 303 async getJudgeState(user, acFirst) { 304 if (!user) return null; 305 306 let where: any = { 307 user_id: user.id, 308 problem_id: this.id 309 }; 310 311 if (acFirst) { 312 where.status = 'Accepted'; 313 314 let state = await JudgeState.findOne({ 315 where: where, 316 order: { 317 submit_time: 'DESC' 318 } 319 }); 320 321 if (state) return state; 322 } 323 324 if (where.status) delete where.status; 325 326 return await JudgeState.findOne({ 327 where: where, 328 order: { 329 submit_time: 'DESC' 330 } 331 }); 332 } 333 334 async resetSubmissionCount() { 335 await syzoj.utils.lock(['Problem::resetSubmissionCount', this.id], async () => { 336 this.submit_num = await JudgeState.count({ problem_id: this.id, type: TypeORM.Not(1) }); 337 this.ac_num = await JudgeState.count({ score: 100, problem_id: this.id, type: TypeORM.Not(1) }); 338 await this.save(); 339 }); 340 } 341 342 async updateStatistics(user_id) { 343 await Promise.all(Object.keys(statisticsTypes).map(async type => { 344 if (this.type === ProblemType.SubmitAnswer && statisticsCodeOnly.includes(type)) return; 345 346 await syzoj.utils.lock(['Problem::UpdateStatistics', this.id, type], async () => { 347 const [column, order] = statisticsTypes[type]; 348 const result = await JudgeState.createQueryBuilder() 349 .select([column, "id"]) 350 .where("user_id = :user_id", { user_id }) 351 .andWhere("status = :status", { status: "Accepted" }) 352 .andWhere("problem_id = :problem_id", { problem_id: this.id }) 353 .orderBy({ [column]: order }) 354 .take(1) 355 .getRawMany(); 356 const resultRow = result[0]; 357 358 let toDelete = false; 359 if (!resultRow || resultRow[column] == null) { 360 toDelete = true; 361 } 362 363 const baseColumns = { 364 user_id, 365 problem_id: this.id, 366 type: type as StatisticsType 367 }; 368 369 let record = await SubmissionStatistics.findOne(baseColumns); 370 371 if (toDelete) { 372 if (record) { 373 await record.destroy(); 374 } 375 376 return; 377 } 378 379 if (!record) { 380 record = SubmissionStatistics.create(baseColumns); 381 } 382 383 record.key = resultRow[column]; 384 record.submission_id = resultRow["id"]; 385 386 await record.save(); 387 }); 388 })); 389 } 390 391 async countStatistics(type) { 392 if (!statisticsTypes[type] || this.type === ProblemType.SubmitAnswer && statisticsCodeOnly.includes(type)) { 393 return null; 394 } 395 396 return await SubmissionStatistics.count({ 397 problem_id: this.id, 398 type: type 399 }); 400 } 401 402 async getStatistics(type, paginate) { 403 if (!statisticsTypes[type] || this.type === ProblemType.SubmitAnswer && statisticsCodeOnly.includes(type)) { 404 return null; 405 } 406 407 const statistics = { 408 type: type, 409 judge_state: null, 410 scoreDistribution: null, 411 prefixSum: null, 412 suffixSum: null 413 }; 414 415 const order = statisticsTypes[type][1]; 416 const ids = (await SubmissionStatistics.queryPage(paginate, { 417 problem_id: this.id, 418 type: type 419 }, { 420 '`key`': order 421 })).map(x => x.submission_id); 422 423 statistics.judge_state = ids.length ? await JudgeState.createQueryBuilder() 424 .whereInIds(ids) 425 .orderBy(`FIELD(id,${ids.join(',')})`) 426 .getMany() 427 : []; 428 429 const a = await JudgeState.createQueryBuilder() 430 .select('score') 431 .addSelect('COUNT(*)', 'count') 432 .where('problem_id = :problem_id', { problem_id: this.id }) 433 .andWhere('type = 0') 434 .andWhere('pending = false') 435 .groupBy('score') 436 .getRawMany(); 437 438 let scoreCount = []; 439 for (let score of a) { 440 score.score = Math.min(Math.round(score.score), 100); 441 scoreCount[score.score] = score.count; 442 } 443 if (scoreCount[0] === undefined) scoreCount[0] = 0; 444 if (scoreCount[100] === undefined) scoreCount[100] = 0; 445 446 if (a[null as any]) { 447 a[0] += a[null as any]; 448 delete a[null as any]; 449 } 450 451 statistics.scoreDistribution = []; 452 for (let i = 0; i < scoreCount.length; i++) { 453 if (scoreCount[i] !== undefined) statistics.scoreDistribution.push({ score: i, count: parseInt(scoreCount[i]) }); 454 } 455 456 statistics.prefixSum = DeepCopy(statistics.scoreDistribution); 457 statistics.suffixSum = DeepCopy(statistics.scoreDistribution); 458 459 for (let i = 1; i < statistics.prefixSum.length; i++) { 460 statistics.prefixSum[i].count += statistics.prefixSum[i - 1].count; 461 } 462 463 for (let i = statistics.prefixSum.length - 1; i >= 1; i--) { 464 statistics.suffixSum[i - 1].count += statistics.suffixSum[i].count; 465 } 466 467 return statistics; 468 } 469 470 async getTags() { 471 let tagIDs; 472 if (problemTagCache.has(this.id)) { 473 tagIDs = problemTagCache.get(this.id); 474 } else { 475 let maps = await ProblemTagMap.find({ 476 where: { 477 problem_id: this.id 478 } 479 }); 480 481 tagIDs = maps.map(x => x.tag_id); 482 problemTagCache.set(this.id, tagIDs); 483 } 484 485 let res = await (tagIDs as any).mapAsync(async tagID => { 486 return ProblemTag.findById(tagID); 487 }); 488 489 res.sort((a, b) => { 490 return a.color > b.color ? 1 : -1; 491 }); 492 493 return res; 494 } 495 496 async setTags(newTagIDs) { 497 let oldTagIDs = (await this.getTags()).map(x => x.id); 498 499 let delTagIDs = oldTagIDs.filter(x => !newTagIDs.includes(x)); 500 let addTagIDs = newTagIDs.filter(x => !oldTagIDs.includes(x)); 501 502 for (let tagID of delTagIDs) { 503 let map = await ProblemTagMap.findOne({ 504 where: { 505 problem_id: this.id, 506 tag_id: tagID 507 } 508 }); 509 510 await map.destroy(); 511 } 512 513 for (let tagID of addTagIDs) { 514 let map = await ProblemTagMap.create({ 515 problem_id: this.id, 516 tag_id: tagID 517 }); 518 519 await map.save(); 520 } 521 522 problemTagCache.set(this.id, newTagIDs); 523 } 524 525 async changeID(id) { 526 const entityManager = TypeORM.getManager(); 527 528 id = parseInt(id); 529 await entityManager.query('UPDATE `problem` SET `id` = ' + id + ' WHERE `id` = ' + this.id); 530 await entityManager.query('UPDATE `judge_state` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); 531 await entityManager.query('UPDATE `problem_tag_map` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); 532 await entityManager.query('UPDATE `article` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); 533 await entityManager.query('UPDATE `submission_statistics` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); 534 535 let contests = await Contest.find(); 536 for (let contest of contests) { 537 let problemIDs = await contest.getProblems(); 538 539 let flag = false; 540 for (let i in problemIDs) { 541 if (problemIDs[i] === this.id) { 542 problemIDs[i] = id; 543 flag = true; 544 } 545 } 546 547 if (flag) { 548 await contest.setProblemsNoCheck(problemIDs); 549 await contest.save(); 550 } 551 } 552 553 let oldTestdataDir = this.getTestdataPath(), oldTestdataZip = this.getTestdataArchivePath(); 554 555 const oldID = this.id; 556 this.id = id; 557 558 // Move testdata 559 let newTestdataDir = this.getTestdataPath(), newTestdataZip = this.getTestdataArchivePath(); 560 if (await syzoj.utils.isDir(oldTestdataDir)) { 561 await fs.move(oldTestdataDir, newTestdataDir); 562 } 563 564 if (await syzoj.utils.isFile(oldTestdataZip)) { 565 await fs.move(oldTestdataZip, newTestdataZip); 566 } 567 568 await this.save(); 569 570 await Problem.deleteFromCache(oldID); 571 await problemTagCache.del(oldID); 572 } 573 574 async delete() { 575 const entityManager = TypeORM.getManager(); 576 577 let oldTestdataDir = this.getTestdataPath(), oldTestdataZip = this.getTestdataPath(); 578 await fs.remove(oldTestdataDir); 579 await fs.remove(oldTestdataZip); 580 581 let submissions = await JudgeState.find({ 582 where: { 583 problem_id: this.id 584 } 585 }), submitCnt = {}, acUsers = new Set(); 586 for (let sm of submissions) { 587 if (sm.status === 'Accepted') acUsers.add(sm.user_id); 588 if (!submitCnt[sm.user_id]) { 589 submitCnt[sm.user_id] = 1; 590 } else { 591 submitCnt[sm.user_id]++; 592 } 593 } 594 595 for (let u in submitCnt) { 596 let user = await User.findById(parseInt(u)); 597 user.submit_num -= submitCnt[u]; 598 if (acUsers.has(parseInt(u))) user.ac_num--; 599 await user.save(); 600 } 601 602 problemTagCache.del(this.id); 603 604 await entityManager.query('DELETE FROM `judge_state` WHERE `problem_id` = ' + this.id); 605 await entityManager.query('DELETE FROM `problem_tag_map` WHERE `problem_id` = ' + this.id); 606 await entityManager.query('DELETE FROM `article` WHERE `problem_id` = ' + this.id); 607 await entityManager.query('DELETE FROM `submission_statistics` WHERE `problem_id` = ' + this.id); 608 609 await this.destroy(); 610 } 611 }