@Aop("redis")
  public CResult addReply(final String topicId, final TopicReply reply, final int userId) {
    if (userId < 1) return _fail("请先登录");
    if (reply == null || reply.getContent() == null || reply.getContent().trim().isEmpty()) {
      return _fail("内容不能为空");
    }
    final String cnt = reply.getContent().trim();
    final Topic topic = dao.fetch(Topic.class, topicId); // TODO 改成只fetch出type属性
    if (topic == null) {
      return _fail("主题不存在");
    }
    if (topic.isLock()) {
      return _fail("该帖子已经锁定,不能回复");
    }
    reply.setTopicId(topicId);
    reply.setUserId(userId);
    reply.setContent(Toolkit.filteContent(reply.getContent()));
    reply.setContentId(bigContentService.put(reply.getContent()));
    reply.setContent(null);
    dao.insert(reply);
    // 更新索引
    topicSearchService.add(topic);
    // 更新topic的时间戳
    Pipeline pipe = jedis().pipelined();
    if (topic.isTop()) {
      pipe.zadd(RKEY_TOPIC_TOP, reply.getCreateTime().getTime(), topicId);
    } else {
      pipe.zadd(RKEY_TOPIC_UPDATE + topic.getType(), reply.getCreateTime().getTime(), topicId);
      pipe.zadd(RKEY_TOPIC_UPDATE_ALL, reply.getCreateTime().getTime(), topicId);
    }
    pipe.zrem(RKEY_TOPIC_NOREPLY, topicId);
    if (topic.getTags() != null) {
      for (String tag : topic.getTags()) {
        pipe.zadd(
            RKEY_TOPIC_TAG + tag.toLowerCase().trim(), reply.getCreateTime().getTime(), topicId);
      }
    }
    pipe.hset(RKEY_REPLY_LAST, topicId, reply.getId());
    pipe.zincrby(RKEY_REPLY_COUNT, 1, topicId);
    pipe.zincrby(RKEY_USER_SCORE, 10, "" + userId);
    pipe.sync();

    notifyUsers(topic, reply, cnt, userId);

    return _ok(reply.getId());
  }
  @Override
  protected void updateStatesToRedis(RedisState redisState, Map<String, String> keyToValue) {
    Jedis jedis = null;
    try {
      jedis = redisState.getJedis();
      Pipeline pipeline = jedis.pipelined();

      for (Map.Entry<String, String> kvEntry : keyToValue.entrySet()) {
        String key = kvEntry.getKey();
        String value = kvEntry.getValue();

        switch (dataType) {
          case STRING:
            if (this.expireIntervalSec > 0) {
              pipeline.setex(key, expireIntervalSec, value);
            } else {
              pipeline.set(key, value);
            }
            break;
          case HASH:
            pipeline.hset(additionalKey, key, value);
            break;
          default:
            throw new IllegalArgumentException("Cannot process such data type: " + dataType);
        }
      }

      // send expire command for hash only once
      // it expires key itself entirely, so use it with caution
      if (dataType == RedisDataTypeDescription.RedisDataType.HASH && this.expireIntervalSec > 0) {
        pipeline.expire(additionalKey, expireIntervalSec);
      }

      pipeline.sync();
    } finally {
      if (jedis != null) {
        redisState.returnJedis(jedis);
      }
    }
  }