/** * @api {get} /yvr/api/v1/topic/:id 获取帖子的详细数据 * @apiGroup Topic * @apiVersion 1.0.0 * @apiParam {String} id 帖子id * @apiParam {boolean} [mdrender=true] 是否渲染Markdown * @apiSuccess {Object[]} data 帖子数据 * @apiSuccess {String} data.id 唯一标示符 * @apiSuccess {String} data.title 标题 * @apiSuccess {String} data.tab 类型 * @apiSuccess {String} data.content 内容 * @apiSuccess {String} [data.last_reply_at] 最后回复时间 * @apiSuccess {boolean} data.top 是否置顶 * @apiSuccess {boolean} data.good 是否为精华帖 * @apiSuccess {int} data.reply_count 总回复数量 * @apiSuccess {int} data.visit_count 总浏览数量 * @apiSuccess {Object} data.author 作者信息 * @apiSuccess {String} data.author.id 作者id * @apiSuccess {String} data.author.loginname 作者登陆名 * @apiSuccess {Object[]} [data.replies] 回复列表 * @apiSuccess {String} data.replies.id 回复id * @apiSuccess {String} data.replies.author 回复的作者 * @apiSuccess {String} data.replies.author.id 回复的作者的id * @apiSuccess {String} data.replies.author.loginname 回复的作者的登陆名称 * @apiSuccess {String} data.replies.content 回复的内容 * @apiSuccess {String[]} data.replies.ups 点赞数 * @apiSuccess {Object} data.replies.author 回帖作者信息 * @apiSuccess {String} data.replies.create_at 回帖时间 * @apiSuccess {String} data.replies.author.id 作者id * @apiSuccess {String} data.replies.author.loginname 作者登陆名 * @apiError 404 The <code>id</code> of the Topic was not found. */ @Aop("redis") @GET @At("/topic/?") public Object topic(String id, @Param("mdrender") String mdrender) { Topic topic = dao.fetch(Topic.class, id); if (id == null) { return HttpStatusView.HTTP_404; } NutMap tp = _topic(topic, new HashMap<Integer, UserProfile>(), mdrender); List<NutMap> replies = new ArrayList<NutMap>(); for (TopicReply reply : dao.query(TopicReply.class, Cnd.where("topicId", "=", id).asc("createTime"))) { dao.fetchLinks(reply, null); reply.setUps(jedis().zrange(RKEY_REPLY_LIKE + reply.getId(), 0, System.currentTimeMillis())); NutMap re = new NutMap(); re.put("id", reply.getId()); re.put("author", _author(reply.getAuthor())); re.put( "content", "false".equals(mdrender) ? reply.getContent() : Markdowns.toHtml(reply.getContent(), urlbase)); re.put("ups", new ArrayList<String>(reply.getUps())); re.put("create_at", _time(reply.getCreateTime())); replies.add(re); } tp.put("replies", replies); jedis().zincrby(RKEY_TOPIC_VISIT, 1, topic.getId()); return _map("data", tp); }
@Aop("redis") public boolean updateTags(String topicId, @Param("tags") Set<String> tags) { if (Strings.isBlank(topicId) || tags == null) { return false; } Topic topic = dao.fetch(Topic.class, topicId); if (topic == null) return false; Set<String> oldTags = topic.getTags(); if (oldTags == null) oldTags = new HashSet<>(); log.debugf("update from '%s' to '%s'", oldTags, tags); topic.setTags(tags); dao.update(topic, "tags"); Set<String> newTags = new HashSet<>(tags); newTags.removeAll(oldTags); Set<String> removeTags = new HashSet<>(oldTags); ; removeTags.remove(tags); fillTopic(topic, null); Date lastReplyTime = topic.getCreateTime(); if (topic.getLastComment() != null) lastReplyTime = topic.getLastComment().getCreateTime(); Pipeline pipe = jedis().pipelined(); for (String tag : removeTags) { pipe.zrem(RKEY_TOPIC_TAG + tag.toLowerCase().trim(), topic.getId()); pipe.zincrby(RKEY_TOPIC_TAG_COUNT, -1, tag.toLowerCase().trim()); } for (String tag : newTags) { pipe.zadd(RKEY_TOPIC_TAG + tag.toLowerCase().trim(), lastReplyTime.getTime(), topic.getId()); pipe.zincrby(RKEY_TOPIC_TAG_COUNT, 1, tag.toLowerCase().trim()); } pipe.sync(); return true; }
@Async protected void notifyUsers(Topic topic, TopicReply reply, String cnt, int userId) { String replyAuthorName = dao.fetch(User.class, userId).getName(); // 通知原本的作者 if (topic.getUserId() != userId) { String alert = replyAuthorName + "回复了您的帖子"; pushUser( topic.getUserId(), alert, topic.getId(), replyAuthorName, topic.getTitle(), PushService.PUSH_TYPE_REPLY); } Set<String> ats = findAt(cnt, 5); for (String at : ats) { User user = dao.fetch(User.class, at); if (user == null) continue; if (topic.getUserId() == user.getId()) continue; // 前面已经发过了 if (userId == user.getId()) continue; // 自己@自己, 忽略 String alert = replyAuthorName + "在帖子回复中@了你"; pushUser( user.getId(), alert, topic.getId(), replyAuthorName, topic.getTitle(), PushService.PUSH_TYPE_AT); } }
@Aop("redis") public Object check(String topicId, int replies) { Topic topic = dao.fetch(Topic.class, topicId); if (topic == null) return ""; Double reply_count = jedis().zscore(RKEY_REPLY_COUNT, topicId); if (reply_count == null) reply_count = Double.valueOf(0); if (reply_count.intValue() == replies) { return ""; } String replyId = jedis().hget(RKEY_REPLY_LAST, topicId); TopicReply reply = dao.fetch(TopicReply.class, replyId); dao.fetchLinks(reply, null); NutMap re = new NutMap().setv("count", reply_count.intValue()); re.put("data", reply.getAuthor().getNickname() + " 回复了帖子:" + topic.getTitle()); re.put("options", new NutMap().setv("tag", topicId)); return re; }
/** * @api {post} /yvr/api/v1/topics 发表帖子, 以json格式提交数据 * @apiGroup Topic * @apiVersion 1.0.0 * @apiUse TOKEN * @apiUse TOKEN_ERROR * @apiParam {String} title 标题 * @apiParam {String} content 内容 * @apiParam {String} [tab=ask] 类型,默认为问答 * @apiSuccess {boolean} success 是否成功 * @apiSuccess {String} [topic_id] 成功时返回帖子的Id * @apiSuccess {String} [message] 失败时返回原因 */ @POST @At("/topics") @AdaptBy(type = WhaleAdaptor.class) @Filters(@By(type = AccessTokenFilter.class)) public Object add( @Param("..") Topic topic, @Attr(scope = Scope.SESSION, value = "me") int userId, @Param("tab") String tab) { if (tab != null) topic.setType(TopicType.valueOf(tab)); CResult re = yvrService.add(topic, userId); if (re.isOk()) { return _map("success", true, "topic_id", re.as(String.class)); } else { return _map("success", false, "message", re.getMsg()); } }
@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()); }
/** 全文输出 */ @At @Ok("raw:xml") public String rss() throws IOException, FeedException { SyndFeed feed = new SyndFeedImpl(); feed.setFeedType("rss_2.0"); String urlbase = conf.get("website.urlbase", "https://nutz.cn"); feed.setLink(urlbase); feed.setTitle(conf.get("website.title", "Nutz社区")); feed.setDescription(conf.get("website.description", "一个有爱的社区")); feed.setAuthor(conf.get("website.author", "wendal")); feed.setEncoding("UTF-8"); feed.setLanguage("zh-cn"); List<SyndEntry> entries = new ArrayList<SyndEntry>(); SyndEntry entry; SyndContent description; List<Topic> list = dao.query(Topic.class, Cnd.orderBy().desc("createTime"), dao.createPager(1, 10)); for (Topic topic : list) { dao.fetchLinks(topic, "author"); entry = new SyndEntryImpl(); entry.setTitle(topic.getTitle()); entry.setLink(urlbase + "/yvr/t/" + topic.getId()); entry.setPublishedDate(topic.getCreateTime()); description = new SyndContentImpl(); description.setType("text/html"); description.setValue(Markdowns.toHtml(topic.getContent(), urlbase)); entry.setDescription(description); entry.setAuthor(topic.getAuthor().getLoginname()); entries.add(entry); } feed.setEntries(entries); if (list.size() > 0) { feed.setPublishedDate(list.get(0).getCreateTime()); } SyndFeedOutput output = new SyndFeedOutput(); return output.outputString(feed, true); }
@Aop("redis") public void fillTopic(Topic topic, Map<Integer, UserProfile> authors) { if (topic.getUserId() == 0) topic.setUserId(1); topic.setAuthor(_cacheFetch(authors, topic.getUserId())); Double reply_count = jedis().zscore(RKEY_REPLY_COUNT, topic.getId()); topic.setReplyCount(reply_count == null ? 0 : reply_count.intValue()); if (topic.getReplyCount() > 0) { String replyId = jedis().hget(RKEY_REPLY_LAST, topic.getId()); TopicReply reply = dao.fetch(TopicReply.class, replyId); if (reply != null) { if (reply.getUserId() == 0) reply.setUserId(1); reply.setAuthor(_cacheFetch(authors, reply.getUserId())); topic.setLastComment(reply); } } Double visited = jedis().zscore(RKEY_TOPIC_VISIT, topic.getId()); topic.setVisitCount((visited == null) ? 0 : visited.intValue()); }
@Aop("redis") public CResult add(Topic topic, int userId) { if (userId < 1) { return _fail("请先登录"); } if (Strings.isBlank(topic.getTitle()) || topic.getTitle().length() > 1024 || topic.getTitle().length() < 5) { return _fail("标题长度不合法"); } if (Strings.isBlank(topic.getContent())) { return _fail("内容不合法"); } if (topic.getTags() != null && topic.getTags().size() > 10) { return _fail("最多只能有10个tag"); } if (0 != dao.count(Topic.class, Cnd.where("title", "=", topic.getTitle().trim()))) { return _fail("相同标题已经发过了"); } topic.setTitle(Strings.escapeHtml(topic.getTitle().trim())); topic.setUserId(userId); topic.setTop(false); topic.setTags(new HashSet<String>()); if (topic.getType() == null) topic.setType(TopicType.ask); topic.setContent(Toolkit.filteContent(topic.getContent())); String oldContent = topic.getContent(); topic.setContentId(bigContentService.put(topic.getContent())); topic.setContent(null); dao.insert(topic); try { topic.setContent(oldContent); topicSearchService.add(topic); } catch (Exception e) { } // 如果是ask类型,把帖子加入到 "未回复"列表 Pipeline pipe = jedis().pipelined(); if (TopicType.ask.equals(topic.getType())) { pipe.zadd(RKEY_TOPIC_NOREPLY, System.currentTimeMillis(), topic.getId()); } pipe.zadd(RKEY_TOPIC_UPDATE + topic.getType(), System.currentTimeMillis(), topic.getId()); if (topic.getType() != TopicType.shortit) pipe.zadd(RKEY_TOPIC_UPDATE_ALL, System.currentTimeMillis(), topic.getId()); pipe.zincrby(RKEY_USER_SCORE, 100, "" + userId); pipe.sync(); String replyAuthorName = dao.fetch(User.class, userId).getName(); for (Integer watcherId : globalWatcherIds) { if (watcherId != userId) pushUser( watcherId, "新帖:" + topic.getTitle(), topic.getId(), replyAuthorName, topic.getTitle(), PushService.PUSH_TYPE_REPLY); } updateTopicTypeCount(); return _ok(topic.getId()); }
public NutMap _topic(Topic topic, Map<Integer, UserProfile> authors, String mdrender) { yvrService.fillTopic(topic, authors); NutMap tp = new NutMap(); tp.put("id", topic.getId()); tp.put("author_id", "" + topic.getAuthor().getUserId()); tp.put("tab", topic.getType().toString()); tp.put( "content", "false".equals(mdrender) ? topic.getContent() : Markdowns.toHtml(topic.getContent(), urlbase)); tp.put("title", StringEscapeUtils.unescapeHtml(topic.getTitle())); if (topic.getLastComment() != null) tp.put("last_reply_at", _time(topic.getLastComment().getCreateTime())); tp.put("good", topic.isGood()); tp.put("top", topic.isTop()); tp.put("reply_count", topic.getReplyCount()); tp.put("visit_count", topic.getVisitCount()); tp.put("create_at", _time(topic.getCreateTime())); UserProfile profile = topic.getAuthor(); if (profile != null) { profile.setScore(yvrService.getUserScore(topic.getUserId())); } tp.put("author", _author(profile)); return tp; }
protected void _add(Topic topic) { if (topic == null) return; // 虽然不太可能,还是预防一下吧 // 暂时不索引评论 dao.fetchLinks(topic, "replies"); Document document; document = new Document(); Field field; FieldType fieldType; // 先加入id fieldType = new FieldType(); fieldType.setIndexed(true); // 索引 fieldType.setStored(true); // 存储 fieldType.setStoreTermVectors(true); fieldType.setTokenized(true); fieldType.setStoreTermVectorPositions(true); // 存储位置 fieldType.setStoreTermVectorOffsets(true); // 存储偏移量 field = new Field("id", topic.getId(), fieldType); document.add(field); // 加入标题 fieldType = new FieldType(); fieldType.setIndexed(true); // 索引 fieldType.setStored(true); // 存储 fieldType.setStoreTermVectors(true); fieldType.setTokenized(true); fieldType.setStoreTermVectorPositions(true); // 存储位置 fieldType.setStoreTermVectorOffsets(true); // 存储偏移量 field = new Field("title", topic.getTitle(), fieldType); document.add(field); // 加入文章内容 fieldType = new FieldType(); fieldType.setIndexed(true); // 索引 fieldType.setStored(false); // 存储 fieldType.setStoreTermVectors(true); fieldType.setTokenized(true); fieldType.setStoreTermVectorPositions(true); // 存储位置 fieldType.setStoreTermVectorOffsets(true); // 存储偏移量 field = new Field("content", topic.getContent(), fieldType); document.add(field); StringBuilder sb = new StringBuilder(); if (topic.getReplies() != null) { for (TopicReply reply : topic.getReplies()) { if (reply == null) continue; bigContentService.fill(reply); if (reply.getContent() != null) { if (sb.length() + reply.getContent().length() > (IndexWriter.MAX_TERM_LENGTH / 3)) { break; } sb.append(reply.getContent()); } } } fieldType = new FieldType(); fieldType.setIndexed(true); // 索引 fieldType.setStored(false); // 存储 fieldType.setStoreTermVectors(true); fieldType.setTokenized(true); fieldType.setStoreTermVectorPositions(true); // 存储位置 fieldType.setStoreTermVectorOffsets(true); // 存储偏移量 field = new Field("reply", sb.toString(), fieldType); document.add(field); try { luceneIndex.writer.addDocument(document); } catch (IOException e) { log.debug("add to index fail : id=" + topic.getId()); } catch (Error e) { log.debug("add to index fail : id=" + topic.getId()); } }