团队冲刺第七天:
题库后端继续:
![](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 async setTags(newTagIDs) { 303 let oldTagIDs = (await this.getTags()).map(x => x.id); 304 305 let delTagIDs = oldTagIDs.filter(x => !newTagIDs.includes(x)); 306 let addTagIDs = newTagIDs.filter(x => !oldTagIDs.includes(x)); 307 308 for (let tagID of delTagIDs) { 309 let map = await ProblemTagMap.findOne({ 310 where: { 311 problem_id: this.id, 312 tag_id: tagID 313 } 314 }); 315 316 await map.destroy(); 317 } 318 319 for (let tagID of addTagIDs) { 320 let map = await ProblemTagMap.create({ 321 problem_id: this.id, 322 tag_id: tagID 323 }); 324 325 await map.save(); 326 } 327 328 problemTagCache.set(this.id, newTagIDs); 329 } 330 331 async changeID(id) { 332 const entityManager = TypeORM.getManager(); 333 334 id = parseInt(id); 335 await entityManager.query('UPDATE `problem` SET `id` = ' + id + ' WHERE `id` = ' + this.id); 336 await entityManager.query('UPDATE `judge_state` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); 337 await entityManager.query('UPDATE `problem_tag_map` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); 338 await entityManager.query('UPDATE `article` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); 339 await entityManager.query('UPDATE `submission_statistics` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); 340 341 let contests = await Contest.find(); 342 for (let contest of contests) { 343 let problemIDs = await contest.getProblems(); 344 345 let flag = false; 346 for (let i in problemIDs) { 347 if (problemIDs[i] === this.id) { 348 problemIDs[i] = id; 349 flag = true; 350 } 351 } 352 353 if (flag) { 354 await contest.setProblemsNoCheck(problemIDs); 355 await contest.save(); 356 } 357 } 358 359 let oldTestdataDir = this.getTestdataPath(), oldTestdataZip = this.getTestdataArchivePath(); 360 361 const oldID = this.id; 362 this.id = id; 363 364 // Move testdata 365 let newTestdataDir = this.getTestdataPath(), newTestdataZip = this.getTestdataArchivePath(); 366 if (await syzoj.utils.isDir(oldTestdataDir)) { 367 await fs.move(oldTestdataDir, newTestdataDir); 368 } 369 370 if (await syzoj.utils.isFile(oldTestdataZip)) { 371 await fs.move(oldTestdataZip, newTestdataZip); 372 } 373 374 await this.save(); 375 376 await Problem.deleteFromCache(oldID); 377 await problemTagCache.del(oldID); 378 } 379 380 async delete() { 381 const entityManager = TypeORM.getManager(); 382 383 let oldTestdataDir = this.getTestdataPath(), oldTestdataZip = this.getTestdataPath(); 384 await fs.remove(oldTestdataDir); 385 await fs.remove(oldTestdataZip); 386 387 let submissions = await JudgeState.find({ 388 where: { 389 problem_id: this.id 390 } 391 }), submitCnt = {}, acUsers = new Set(); 392 for (let sm of submissions) { 393 if (sm.status === 'Accepted') acUsers.add(sm.user_id); 394 if (!submitCnt[sm.user_id]) { 395 submitCnt[sm.user_id] = 1; 396 } else { 397 submitCnt[sm.user_id]++; 398 } 399 } 400 401 for (let u in submitCnt) { 402 let user = await User.findById(parseInt(u)); 403 user.submit_num -= submitCnt[u]; 404 if (acUsers.has(parseInt(u))) user.ac_num--; 405 await user.save(); 406 } 407 408 problemTagCache.del(this.id); 409 410 await entityManager.query('DELETE FROM `judge_state` WHERE `problem_id` = ' + this.id); 411 await entityManager.query('DELETE FROM `problem_tag_map` WHERE `problem_id` = ' + this.id); 412 await entityManager.query('DELETE FROM `article` WHERE `problem_id` = ' + this.id); 413 await entityManager.query('DELETE FROM `submission_statistics` WHERE `problem_id` = ' + this.id); 414 415 await this.destroy(); 416 } 417 }