CodingSouls团队项目冲刺(8)-个人概况

团队冲刺第八天:

  题库后端收尾:

  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 }
完整

 

posted @ 2020-04-22 19:30  DemonSlayer  阅读(163)  评论(0编辑  收藏  举报