text: let ExerciseTag = syzoj.model('exercise_tag');
  let Exercise = syzoj.model('exercise');
  
  app.get('/exercises/tag/:id/edit', async (req, res) => {
      try {
          if (!res.locals.user || !await res.locals.user.hasPrivilege('manage_exercise_tag')) throw new ErrorMessage('您没有权限进行此操作。');
  
          let id = parseInt(req.params.id) || 0;
          let tag = await ExerciseTag.findById(id);
  
          if (!tag) {
              tag = await ExerciseTag.create();
              tag.id = id;
          }
  
          res.render('exercise_tag_edit', { tag: tag });
      } catch (e) {
          syzoj.log(e);
          res.render('error', {
              err: e
          });
      }
  });
  
  app.post('/exercises/tag/:id/edit', async (req, res) => {
      try {
          if (!res.locals.user || !await res.locals.user.hasPrivilege('manage_exercise_tag')) throw new ErrorMessage('您没有权限进行此操作。');
  
          let id = parseInt(req.params.id) || 0;
          let tag = await ExerciseTag.findById(id);
  
          if (!tag) {
              tag = await ExerciseTag.create();
              tag.id = id;
          }
  
          req.body.name = req.body.name.trim();
          if (tag.name !== req.body.name && await ExerciseTag.findOne({ where: { name: req.body.name }})) {
              throw new ErrorMessage('标签名称已被使用。');
          }
  
          tag.name = req.body.name;
          tag.color = req.body.color;
  
          await tag.save();
  
          res.redirect(syzoj.utils.makeUrl(['exercises', 'tag', tag.id]));
      } catch (e) {
          syzoj.log(e);
          res.render('error', {
              err: e
          });
      }
  });
  
  app.get('/exercises/tag/:tagIDs', async (req, res) => {
      try { 
          let tagIDs = Array.from(new Set(req.params.tagIDs.split(',').map(x => parseInt(x))));
          let tags = await tagIDs.mapAsync(async tagID => ExerciseTag.findById(tagID));
          const sort = req.query.sort || 'id';
          const order = req.query.order || 'asc'; // TODO: harcode -> config
          if (!['id', 'title'].includes(sort) || !['asc', 'dasc'].includes(order)) {
              throw new ErrorMessage('错误的排序参数。');
          }
          let sortVal = '`exercise`.`' + sort + '`';
  
          for (let tag of tags) {
              if (!tag) {
                  return res.redirect(syzoj.utils.makeUrl(['exercises']));
              }
          }
  
          let sql = 'SELECT `id` FROM `exercise` WHERE\n';
          for (let tagID of tagIDs) {
              if (tagID !== tagIDs[0]) {
                  sql += 'AND\n';
              }
              sql += '`exercise`.`id` IN (SELECT `exercise_id` FROM `exercise_tag_map` WHERE `tag_id` = ' + tagID + ')';
          }
  
          if (!res.locals.user) {
              sql += 'AND (`exercise`.`is_public` = 1)';
          } else if (!await res.locals.user.hasPrivilege('manage_exercise')) {
              sql += 'AND (`exercise`.`is_public` = 1 OR `exercise`.`user_id` = ' + res.locals.user.id + ')';
          }
  
          let paginate = syzoj.utils.paginate(await Exercise.countQuery(sql), req.query.page, 12); // TODO: hardcode -> config
          let exercises = await Exercise.query(sql + `ORDER BY ${sortVal} ${order} ` + paginate.toSQL());
  
          exercises = await exercises.mapAsync(async e => {
              e = await Exercise.findOne(e.id);
              e.tags = await e.getTags();
              e.problems = await e.get_problems();
              await Promise.all(e.problems.map(async p => {
                  p.judge_state = await p.getJudgeState(res.locals.user, true);
              }));
              return e;
          });
  
          res.render('exercises', {
              allowedManageTag: res.locals.user && await res.locals.user.hasPrivilege('manage_exercise_tag'),
              exercises,
              tags,
              paginate,
              curSort: sort,
              curOrder: order === 'asc'
          });
      } catch(e) {
          syzoj.log(e);
          res.render('error', {
              err: e
          });
      }
  });
  