package com.apobates.forum.core.impl.service;

import com.apobates.forum.attention.core.decorater.ForumEncoder;
import com.apobates.forum.attention.core.decorater.posts.ForumPostsDecorator;
import com.apobates.forum.core.AnonyActionEventCulpritor;
import com.apobates.forum.core.ImageIOMeta;
import com.apobates.forum.core.TagRelateTopic;
import com.apobates.forum.core.dao.AlbumDao;
import com.apobates.forum.core.dao.BoardDao;
import com.apobates.forum.core.dao.BoardGroupDao;
import com.apobates.forum.core.dao.BoardStatsDao;
import com.apobates.forum.core.dao.PostsDao;
import com.apobates.forum.core.dao.TopicActionCollectionDao;
import com.apobates.forum.core.dao.TopicCategoryDao;
import com.apobates.forum.core.dao.TopicConfigDao;
import com.apobates.forum.core.dao.TopicDao;
import com.apobates.forum.core.dao.TopicStatsDao;
import com.apobates.forum.core.dao.TopicTagDao;
import com.apobates.forum.core.entity.Album;
import com.apobates.forum.core.entity.AlbumPicture;
import com.apobates.forum.core.entity.Board;
import com.apobates.forum.core.entity.BoardGroup;
import com.apobates.forum.core.entity.ForumEntityStatusEnum;
import com.apobates.forum.core.entity.Posts;
import com.apobates.forum.core.entity.Topic;
import com.apobates.forum.core.entity.TopicCategory;
import com.apobates.forum.core.entity.TopicConfig;
import com.apobates.forum.core.entity.TopicStats;
import com.apobates.forum.core.entity.TopicTag;
import com.apobates.forum.core.entity.proxy.PostsReplica;
import com.apobates.forum.core.entity.proxy.TopicReplica;
import com.apobates.forum.core.impl.ActionDescriptor;
import com.apobates.forum.core.impl.event.PlugTopicPublishEvent;
import com.apobates.forum.core.impl.event.TopicMoveEvent;
import com.apobates.forum.core.impl.event.TopicPublishEvent;
import com.apobates.forum.core.nlp.tag.AbstractTagRelateQueryExecutor;
import com.apobates.forum.core.nlp.tag.RelateWordStats;
import com.apobates.forum.core.nlp.tag.TagExtraction;
import com.apobates.forum.core.nlp.tag.TagRelateResult;
import com.apobates.forum.core.nlp.tag.impl.TagNLPExtraction;
import com.apobates.forum.core.nlp.tag.impl.WordNatureEnum;
import com.apobates.forum.core.nlp.tag.impl.relate.TagRelateDistance;
import com.apobates.forum.core.plug.AbstractPlugTopic;
import com.apobates.forum.core.service.TopicService;
import com.apobates.forum.event.elderly.ActionEventCulpritor;
import com.apobates.forum.event.elderly.ForumActionEnum;
import com.apobates.forum.utils.Commons;
import com.apobates.forum.utils.DateTimeUtils;
import com.apobates.forum.utils.lang.ReplicableException;
import com.apobates.forum.utils.persistence.Page;
import com.apobates.forum.utils.persistence.Pageable;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.github.davidmarquis.redisq.producer.MessageProducer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import com.apobates.forum.core.plug.PlugTopic;
import com.apobates.forum.utils.lang.TriFunction;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;

/**
 *
 * @author xiaofanku
 * @since 20200514
 */
@Service
@CacheConfig(cacheNames = "topicCache")
public class TopicServiceImpl implements TopicService{
    @Autowired
    private TopicDao topicDao;
    @Autowired
    private PostsDao postsDao;
    @Autowired
    private TopicStatsDao topicStatsDao;
    @Autowired
    private TopicConfigDao topicConfigDao;
    @Autowired
    private BoardDao boardDao;
    @Autowired
    private BoardGroupDao boardGroupDao;
    @Autowired
    private BoardStatsDao boardStatsDao;
    @Autowired
    private TopicActionCollectionDao topicActionCollectionDao;
    @Autowired
    private AlbumDao albumDao;
    @Autowired
    private TopicTagDao topicTagDao;
    @Autowired
    private TopicCategoryDao topicCategoryDao;
    @Autowired @Qualifier("topicPublishProducer")
    private MessageProducer<TopicPublishEvent> topicPublishProducer;
    @Autowired @Qualifier("topicMoveProducer")
    private MessageProducer<TopicMoveEvent> topicMoveProducer;
    @Autowired @Qualifier("plugTopicPublishProducer")
    private MessageProducer<PlugTopicPublishEvent> plugTopicPublishProducer;
    private final static Logger logger = LoggerFactory.getLogger(TopicServiceImpl.class);
    
    @Cacheable(key="#id", unless="#result==null")
    @Override
    public Optional<Topic> get(long id) {
        if(id>0){
            return topicDao.findOne(id);
        }
        return Optional.empty();
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_TOP)
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> editTop(long id, ActionEventCulpritor culpritor) { //ASP-T1
        return topicDao.editTop(id, true);
    }
    
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> removeTop(long id, ActionEventCulpritor culpritor) {
        return topicDao.editTop(id, false);
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_BEST)
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> editGoods(long id, ActionEventCulpritor culpritor) { //ASP-T2
        return topicDao.editGoods(id, true);
    }
    
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> removeGoods(long id, ActionEventCulpritor culpritor) {
        return topicDao.editGoods(id, false);
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_LOCK)
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> lock(long id, ActionEventCulpritor culpritor) { //ASP-T5
        return topicDao.editStatus(id, ForumEntityStatusEnum.LOCKED);
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_UNLOCK)
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> releaseLock(long id, ActionEventCulpritor culpritor) { //ASP-T6
        return topicDao.editStatus(id, ForumEntityStatusEnum.ACTIVE);
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_DEL)
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> remove(long id, ActionEventCulpritor culpritor) { //ASP-T7
        return topicDao.editStatus(id, ForumEntityStatusEnum.DELETE);
    }
    
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> edit(long id, String title, String content, String[] keywords, ImageIOMeta imageIO, ActionEventCulpritor culpritor) throws IllegalArgumentException, IllegalStateException {
        Topic topic = get(id).orElseThrow(()->new IllegalArgumentException("指定的话题不存在或暂时无法访问"));
        //无HTML tag,Emoji安全
        final String encodeTitle = new ForumEncoder(title).noneHtmlTag().parseEmoji().getContent(); //ST1
        final ForumEncoder postsEncode = new ForumEncoder(content).encodeUploadImage(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
        //话题编辑时在有图->无图|无图->有图切换时错误
        String modifyTopicContent = postsEncode.parseEmoji().getContent(); //ST2
        //回复像册
        Album album = null;
        List<AlbumPicture> aps = postsEncode.extractUploadImageLink().stream().map(imgLinkDigest -> new AlbumPicture(imgLinkDigest.getUrl(), imgLinkDigest.getAnchor(), imgLinkDigest.getRanking())).collect(Collectors.toList());
        if (null != aps && !aps.isEmpty()) { //重新创建像册|重新设置话题关联的像册
            //是否已经存在像册了
            Optional<Album> topicAlbum = albumDao.findOneByTopic(id); //ST3
            if (!topicAlbum.isPresent()) {
                Posts oneFloorPosts = new Posts();
                oneFloorPosts.setVolumesId(topic.getVolumesId());
                oneFloorPosts.setBoardId(topic.getBoardId());
                oneFloorPosts.setMemberId(topic.getMemberId());
                oneFloorPosts.setMemberNickname(topic.getMemberNickname());
                album = Album.createAlbum(aps, oneFloorPosts, id, encodeTitle).orElse(null);
            } else {
                Album existTopicAlbum = topicAlbum.get();
                existTopicAlbum.getPictures().addAll(aps);
                album = existTopicAlbum;
            }
        }
        
        //标签编辑
        Set<TopicTag> tags = buildTopicTag(id, title, content, keywords);//ST4
        return modifyTopic(topic.getId(), encodeTitle, modifyTopicContent, album, tags, culpritor.getMemberId(), culpritor.getMemberNickname());
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_MOVE)
    @CacheEvict(key="#id")
    @Override
    public Optional<Boolean> move(long id, long boardId, long targetBoardId, ActionEventCulpritor culpritor)throws ReplicableException,IllegalArgumentException,IllegalStateException { //ASP-T11
        logger.info("[MTV][1]话题: " + id + ", 现属版块: " + boardId + ", 移动至版块: " + targetBoardId);
        TopicReplica topic = get(id, boardId).orElseThrow(()->new IllegalArgumentException("话题不存在或暂时无法访问"));
        //
        Board targetBoardObj = boardDao.findOne(targetBoardId).orElseThrow(()->new IllegalArgumentException("目标版块不存在或暂时无法访问"));
        logger.info("[MTV][3]话题开始移动");
        
        int affect = topicDao.moveTopic(id, targetBoardObj.getId(), targetBoardObj.getVolumesId());
        if (affect >= 1) {
            //统计
            logger.info("[MTV][11.1]话题移动成功,开始平衡统计");
            //原版块话题-1,新版块话题+1
            //原版块回复-d.successValue,新版块回复+d.successValue
            boardStatsDao.balanceTopicPosts(targetBoardId, boardId, affect); //1话题的内容
            topicMoveProducer.create(new TopicMoveEvent(topic, topic.getBoard(), targetBoardObj, culpritor.getMemberNickname())).submit();
            return Optional.of(true);
        } 
        throw new IllegalStateException("话题移动操作失败");
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_CONFIG_EDIT)
    @CacheEvict(key="'topic_config_'+#id")
    @Override
    public Optional<Boolean> editTopicConfig(long id, TopicConfig updateConfig, long configId, ActionEventCulpritor culpritor)throws IllegalArgumentException { //ASP-T12 
        TopicConfig tc = topicConfigDao.findOne(configId).orElseThrow(()->new IllegalArgumentException("话题配置文件不存在"));
        tc.setPrivacy(updateConfig.isPrivacy());
        tc.setReply(updateConfig.isReply());
        tc.setNotify(updateConfig.isNotify());
        tc.setReadMinScore(updateConfig.getReadMinScore());
        tc.setReadLowMemberGroup(updateConfig.getReadLowMemberGroup());
        tc.setReadLowMemberRole(updateConfig.getReadLowMemberRole());
        tc.setReadLowMemberLevel(updateConfig.getReadLowMemberLevel());
        tc.setWriteMinScore(updateConfig.getWriteMinScore());
        tc.setWriteLowMemberGroup(updateConfig.getWriteLowMemberGroup());
        tc.setWriteLowMemberRole(updateConfig.getWriteLowMemberRole());
        tc.setWriteLowMemberLevel(updateConfig.getWriteLowMemberLevel());
        tc.setAtomPoster(updateConfig.isAtomPoster());
        return topicConfigDao.edit(tc);
    }
    
    @Override
    public Stream<TopicReplica> getRecentForMember(long memberId, int size) {
        Stream<Topic> data = topicDao.findAllByMember(memberId, size);
        return associatedBoardAsync(data.collect(Collectors.toList()));
    }
    
    @Override
    public Stream<TopicReplica> getAllForMemberReply(long memberId, int size) {
        Stream<Topic> data = topicDao.findAllByMemberReply(memberId, size);
        return associatedBoardAsync(data.collect(Collectors.toList()));
    }
    
    @Override
    public Stream<TopicReplica> getAllForMemberPopular(long memberId, int size) {
        Stream<Topic> data = topicDao.findAllByMemberPopular(memberId, size);
        return associatedBoardAndStatsAsync(data.collect(Collectors.toList()));
    }
    
    @Override
    public List<TopicReplica> getRecentRelateContent(int size) {
        Stream<Topic> data = topicDao.findAllOfRecent(size).filter(t -> t.isNormal());
        return associatedBoardAndStatsAsync(data.collect(Collectors.toList())).collect(Collectors.toList());
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_PUBLISH, isBool=false)
    @Override
    public long create(int volumesId, long boardId, String title, String content, ImageIOMeta imageIO, ActionEventCulpritor culpritor) { //ASP-T13
        return create(volumesId, boardId, 0, title, content, imageIO, culpritor);
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_PUBLISH, isBool=false)
    @Override
    public long create(int volumesId, long boardId, int categoryId, String title, String content, ImageIOMeta imageIO, ActionEventCulpritor culpritor) { //ASP-T14
        // 编码回复内容
        final ForumEncoder postsEncode = new ForumEncoder(content).encodeUploadImage(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
        //-------------------------------------------------------------------------
        // 计算TopicCategory
        TopicCategory tc = TopicCategory.empty();
        if (categoryId > 0) {
            tc = topicCategoryDao.findOne(categoryId).orElse(TopicCategory.empty());
        }
        // 无HTML tag,Emoji安全
        final String encodeTitle = new ForumEncoder(title).noneHtmlTag().parseEmoji().getContent();
        Topic topic = new Topic(encodeTitle, culpritor.getMemberId(), culpritor.getMemberNickname(), volumesId, boardId, culpritor.getIpAddr());
        topic.setTopicCategoryName(tc.getNames());
        topic.setTopicCategoryValue(tc.getValue());
        if(culpritor instanceof AnonyActionEventCulpritor){
            topic.setAnony(((AnonyActionEventCulpritor)culpritor).isAnony());
        }
        topic.setSummary(postsEncode.noneHtmlTag().parseEmoji().getContent(200));
        // 标签(原始数据：title,content)
        Set<TopicTag> tages = buildTopicTag(-2L, title, content); //需要回填topic.id
        // 话题内容
        Posts oneFloorPosts = new Posts(postsEncode.relaxedHtmlTag().parseEmoji().getContent(), culpritor.getMemberId(), culpritor.getMemberNickname(), 0L, volumesId, boardId, culpritor.getIpAddr(), false); //需要回填topic.id
        // 话题像册
        Album album=null;
        List<AlbumPicture> aps = postsEncode.extractUploadImageLink().stream().map(imgLinkDigest -> new AlbumPicture(imgLinkDigest.getUrl(), imgLinkDigest.getAnchor(), imgLinkDigest.getRanking())).collect(Collectors.toList());
        if (null != aps && !aps.isEmpty()) {
            album = Album.createAlbum(aps, oneFloorPosts, -1L, encodeTitle).orElse(null); //需要回填topic.id
        }
        // 最后一步
        Optional<TopicReplica> tr = storeReplicaTopic(topic, oneFloorPosts, album, tages, null, culpritor.getUserAgent());
        tr.ifPresent(this::generalTopicEvent);
        return tr.map(TopicReplica::getId).get();
    }
    
    @Override
    public long create(int volumesId, long boardId, String categoryName, String categoryValue, String title, String content, String[] keywords, ImageIOMeta imageIO, ActionEventCulpritor culpritor){
        int boardGroupId = (volumesId > 0) ? volumesId : 0;
        // 编码回复内容
        final ForumEncoder postsEncode = new ForumEncoder(content).encodeUploadImage(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
        // 无HTML tag,Emoji安全
        final String encodeTitle = new ForumEncoder(title).noneHtmlTag().parseEmoji().getContent();
        Topic topic = new Topic(encodeTitle, culpritor.getMemberId(), culpritor.getMemberNickname(), boardGroupId, boardId, culpritor.getIpAddr());
        topic.setTopicCategoryName(categoryName);
        topic.setTopicCategoryValue(categoryValue);
        topic.setSummary(postsEncode.noneHtmlTag().parseEmoji().getContent(200));
        // 标签
        Set<TopicTag> tages = buildTopicTag(-2L, title, content, keywords); //需要回填topic.id
        // 话题内容
        Posts oneFloorPosts = new Posts(postsEncode.relaxedHtmlTag().parseEmoji().getContent(), culpritor.getMemberId(), culpritor.getMemberNickname(), 0L, volumesId, boardId, culpritor.getIpAddr(), false); //需要回填topic.id
        // 话题像册
        Album album = null;
        List<AlbumPicture> aps = postsEncode.extractUploadImageLink().stream().map(imgLinkDigest -> new AlbumPicture(imgLinkDigest.getUrl(), imgLinkDigest.getAnchor(), imgLinkDigest.getRanking())).collect(Collectors.toList());
        if (null != aps && !aps.isEmpty()) {
            album = Album.createAlbum(aps, oneFloorPosts, -1L, encodeTitle).orElse(null); //需要回填topic.id
        }
        // 最后一步
        Optional<TopicReplica> tr = storeReplicaTopic(topic, oneFloorPosts, album, tages, null, culpritor.getUserAgent());
        tr.ifPresent(this::generalTopicEvent);
        return tr.map(TopicReplica::getId).get();
    }
    
    @Override
    public long createTermArticle(int sectionId, long termId, String title, String content, ImageIOMeta imageIO, ActionEventCulpritor culpritor) {
        // 编码回复内容
        final ForumEncoder postsEncode = new ForumEncoder(content).encodeUploadImage(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
        //-------------------------------------------------------------------------
        // 无HTML tag,Emoji安全
        final String encodeTitle = new ForumEncoder(title).noneHtmlTag().parseEmoji().getContent();
        Topic article = Topic.buildArticle(encodeTitle, culpritor.getMemberId(), culpritor.getMemberNickname(), sectionId, termId, culpritor.getIpAddr());
        article.setSummary(postsEncode.noneHtmlTag().parseEmoji().getContent(200));
        // 文章内容
        Posts oneFloorPosts = new Posts(postsEncode.relaxedHtmlTag().parseEmoji().getContent(), culpritor.getMemberId(), culpritor.getMemberNickname(), 0L, sectionId, termId, culpritor.getIpAddr(), false); //需要回填topic.id
        // 文章像册
        Album album = null;
        List<AlbumPicture> aps = postsEncode.extractUploadImageLink().stream().map(imgLinkDigest -> new AlbumPicture(imgLinkDigest.getUrl(), imgLinkDigest.getAnchor(), imgLinkDigest.getRanking())).collect(Collectors.toList());
        if (null != aps && !aps.isEmpty()) {
            album = Album.createAlbum(aps, oneFloorPosts, -1L, encodeTitle).orElse(null); //需要回填topic.id
        }
        // 文章配置
        TopicConfig config = TopicConfig.originConfig(-1L); //需要回填topic.id
        // 最后一步
        Optional<TopicReplica> tr = storeReplicaTopic(article, oneFloorPosts, album, null, config, culpritor.getUserAgent());
        tr.ifPresent(this::generalTopicEvent);
        return tr.map(TopicReplica::getId).get();
    }
    
    @Override
    public Optional<TopicReplica> plug(AbstractPlugTopic topicBuildPlug) {
        final ActionEventCulpritor culpritor = topicBuildPlug.getCulpritor();
        String topicAuthor = (culpritor.getMemberId() > 0)?culpritor.getMemberNickname():String.format("Guest#%d", Commons.ipHashcode(culpritor.getIpAddr()));
        long topicAuthorId = (culpritor.getMemberId() > 0)?culpritor.getMemberId():0L;
        Topic pt = new Topic(topicBuildPlug.getTitle(),
                topicAuthorId,
                topicAuthor,
                topicBuildPlug.getVolumes(),
                topicBuildPlug.getBoard(),
                culpritor.getIpAddr(),
                topicBuildPlug.getCategory().getNames(),
                topicBuildPlug.getCategory().getValue());
        pt.setSuggest(topicBuildPlug.isSuggest());
        // 话题内容 
        // 需要回填topic.id
        Posts oneFloorPosts = new Posts(topicBuildPlug.getContent(),
                culpritor.getMemberId(),
                topicAuthor,
                0L,
                pt.getVolumesId(),
                pt.getBoardId(),
                culpritor.getIpAddr(), false);
        // 话题配置
        TopicConfig config = topicBuildPlug.getConfig(); //需要回填topic.id
        // 最后一步
        Optional<TopicReplica> tr = storeReplicaTopic(pt, oneFloorPosts, null, null, config, culpritor.getUserAgent());
        // 发布事件
        final ForumActionEnum action = topicBuildPlug.getAction(); 
        tr.ifPresent((TopicReplica tr1) -> {
            Topic t = tr1.toEventPayload();
            plugTopicPublishProducer.create(new PlugTopicPublishEvent(t, tr1.getConfigure(), tr1.getContent().getId(), action, culpritor));
        });
        return tr;
    }

    @Override
    public Optional<TopicReplica> plug(PlugTopic topicBuildPlug, final ForumActionEnum action, final ActionEventCulpritor culpritor) {
        String topicAuthor = (culpritor.getMemberId() > 0)?culpritor.getMemberNickname():String.format("Guest#%d", Commons.ipHashcode(culpritor.getIpAddr()));
        long topicAuthorId = (culpritor.getMemberId() > 0)?culpritor.getMemberId():0L;
        Topic pt = Topic.buildArticle(topicBuildPlug.getTitle(), topicAuthorId, topicAuthor, topicBuildPlug.getVolumes(), topicBuildPlug.getBoard(), culpritor.getIpAddr());
        final ForumEncoder postsEncode = new ForumEncoder(topicBuildPlug.getContent());
        pt.setSummary(postsEncode.noneHtmlTag().parseEmoji().getContent(200));
        pt.setSuggest(topicBuildPlug.isSuggest());
        pt.setTopicCategoryName(topicBuildPlug.getCategory().getNames());
        pt.setTopicCategoryValue(topicBuildPlug.getCategory().getValue());
        // 话题内容 
        // 需要回填topic.id
        Posts oneFloorPosts = new Posts(postsEncode.relaxedHtmlTag().parseEmoji().getContent(),
                culpritor.getMemberId(),
                topicAuthor,
                0L,
                pt.getVolumesId(),
                pt.getBoardId(),
                culpritor.getIpAddr(), false);
        // 话题配置
        TopicConfig config = topicBuildPlug.getConfig(); //需要回填topic.id
        // 最后一步
        Optional<TopicReplica> tr = storeReplicaTopic(pt, oneFloorPosts, null, null, config, culpritor.getUserAgent());
        // 发布事件
        tr.ifPresent((TopicReplica tr1) -> {
            Topic t = tr1.toEventPayload();
            plugTopicPublishProducer.create(new PlugTopicPublishEvent(t, tr1.getConfigure(), tr1.getContent().getId(), action, culpritor)).submit();
        });
        return tr;
    }
    
    @Override
    public Optional<Boolean> editTermArticle(long articleId, String title, String content, ImageIOMeta imageIO, ActionEventCulpritor culpritor)throws IllegalArgumentException,IllegalStateException {
        Topic topic = get(articleId).orElseThrow(()->new IllegalArgumentException("指定的文章不存在或暂时无法访问"));
        //
        //编辑标题: 无HTML tag,Emoji安全
        final String encodeTitle = new ForumEncoder(title).noneHtmlTag().parseEmoji().getContent(); //ST1
        final ForumEncoder postsEncode = new ForumEncoder(content).encodeUploadImage(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
        //编辑话题内容
        String modifyTopicContent = postsEncode.parseEmoji().getContent(); //ST2
        //回复像册
        Album album = null;
        List<AlbumPicture> aps = postsEncode.extractUploadImageLink().stream().map(imgLinkDigest -> new AlbumPicture(imgLinkDigest.getUrl(), imgLinkDigest.getAnchor(), imgLinkDigest.getRanking())).collect(Collectors.toList());
        if (null != aps && !aps.isEmpty()) { //重新创建像册|重新设置话题关联的像册
            //是否忆经存在像册了
            Optional<Album> topicAlbum = albumDao.findOneByTopic(articleId); //ST3
            if (!topicAlbum.isPresent()) {
                Posts oneFloorPosts = new Posts();
                oneFloorPosts.setVolumesId(topic.getVolumesId());
                oneFloorPosts.setBoardId(topic.getBoardId());
                oneFloorPosts.setMemberId(topic.getMemberId());
                oneFloorPosts.setMemberNickname(topic.getMemberNickname());
                album = Album.createAlbum(aps, oneFloorPosts, articleId, encodeTitle).orElse(null);
            } else {
                Album existTopicAlbum = topicAlbum.get();
                existTopicAlbum.getPictures().addAll(aps);
                album = existTopicAlbum; //ST5
            }
        }
        return modifyTopic(topic.getId(), encodeTitle, modifyTopicContent, album, null, culpritor.getMemberId(), culpritor.getMemberNickname());//("文章编辑失败");
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_LIKED)
    @Override
    public Optional<Boolean> like(long id, ActionEventCulpritor culpritor)throws IllegalStateException { //ASP-T3 | 20200709
        //不能重复点赞
        if (!isLiked(id, culpritor.getMemberId())) {
            throw new IllegalStateException("已经点过赞了");
        }
        return topicStatsDao.plusLikes(id, culpritor.getMemberId());
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_LIKED_CANCEL)
    @Override
    public Optional<Boolean> removeLike(long id, ActionEventCulpritor culpritor)throws IllegalStateException { //ASP-T4 | 20200709
        //不能重复取消赞
        long data = topicActionCollectionDao.countMemberAction(culpritor.getMemberId(), id, ForumActionEnum.TOPIC_LIKED_CANCEL);
        if (data > 0) {
            throw new IllegalStateException("点赞已经取消");
        }
        return topicStatsDao.negateLikes(id, culpritor.getMemberId());
    }
    
    @Override
    public Optional<Boolean> removeTermArticle(long id, long termId, ActionEventCulpritor culpritor) {
        return topicDao.removeTermArticle(id, termId);
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_FAVORITE)
    @Override
    public Optional<Boolean> favorite(long id, ActionEventCulpritor culpritor)throws IllegalStateException{ //ASP-T8 | 20200709
        //更新话题的收藏统计|重复收藏
        if (!isFavorited(id, culpritor.getMemberId())) {
            throw new IllegalStateException("收藏记录已经存在");
        }
        return topicStatsDao.plusFavorites(id, culpritor.getMemberId());
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_FAVORITE_CANCEL)
    @Override
    public Optional<Boolean> removeFavorite(long id, ActionEventCulpritor culpritor)throws IllegalStateException { //ASP-T9 | 20200709
        //更新话题的收藏统计|重复收藏
        long data = topicActionCollectionDao.countMemberAction(culpritor.getMemberId(), id, ForumActionEnum.TOPIC_FAVORITE_CANCEL);
        if (data > 0) {
            throw new IllegalStateException("已经取消收藏");
        }
        return topicStatsDao.negateFavorites(id, culpritor.getMemberId());
    }
    
    @Override
    public boolean isLiked(long id, long memberId) {
        return topicActionCollectionDao.isLiked(id, memberId).orElse(false);
    }
    
    @Override
    public boolean isFavorited(long id, long memberId) {
        return topicActionCollectionDao.isFavorited(id, memberId).orElse(false);
    }
    
    @ActionDescriptor(action=ForumActionEnum.TOPIC_BROWSE)
    @Override
    public Optional<Boolean> browse(long id, ActionEventCulpritor culpritor) { //ASP-T10
        //更新话题的收藏统计
        //是否达到火贴的标准了
        return topicStatsDao.updateDisplaies(id);
    }
    
    @Override
    public Page<TopicReplica> getAll(long boardId, Pageable pageable) {
        Page<Topic> data = topicDao.findAllByBoard(boardId, pageable);
        //
        final long total = data.getTotalElements();
        final Stream<TopicReplica> result = associationTopicStatsAsync(data.getResult().collect(Collectors.toList()));
        //
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return total;
            }
            
            @Override
            public Stream<TopicReplica> getResult() {
                return result;
            }
        };
    }
    
    @Override
    public Page<TopicReplica> getAll(long boardId, String categoryValue, Pageable pageable) { 
        if (!Commons.isNotBlank(categoryValue)) {
            return getAll(boardId, pageable);
        }
        Page<Topic> data = topicDao.findAllByBoard(boardId, categoryValue, pageable);
        //
        final long total = data.getTotalElements();
        final Stream<TopicReplica> result = associationTopicStatsAsync(data.getResult().collect(Collectors.toList()));
        //
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return total;
            }
            
            @Override
            public Stream<TopicReplica> getResult() {
                return result;
            }
        };
    }
    
    @Override
    public Page<TopicReplica> getAllForMember(long memberId, Pageable pageable) {
        Page<Topic> rs = topicDao.findAllByMember(memberId, pageable);
        final Stream<TopicReplica> data = associationTopicStatsAsync(rs.getResult().collect(Collectors.toList()));
        
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return rs.getTotalElements();
            }
            
            @Override
            public Stream<TopicReplica> getResult() {
                return data;
            }
        };
    }
    
    @Override
    public long countAllForMember(long memberId) {
        return topicDao.findAllByMemberCount(memberId);
    }
    
    @Override
    public Page<TopicReplica> getAllForMemberReply(long memberId, Pageable pageable) {
        Page<Topic> rs = topicDao.findAllByMemberReply(memberId, pageable);
        final Stream<TopicReplica> data = associationTopicStatsAsync(rs.getResult().collect(Collectors.toList()));
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return rs.getTotalElements();
            }
            
            @Override
            public Stream<TopicReplica> getResult() {
                return data;
            }
        };
    }
    
    @Override
    public long countAllForMemberReply(long memberId) {
        return topicDao.findAllByMemberReplyCount(memberId);
    }
    
    @Override
    public Page<Topic> getTermArticle(long termId, Pageable pageable) {
        return topicDao.findAllTermArticle(termId, pageable);
    }
    
    @Override
    public Page<Topic> getAllForTag(List<String> tagNames, Pageable pageable) {
        return topicDao.findAllByTag(tagNames, pageable); //DAO会进行检查
    }
    
    @Override
    public Page<TopicReplica> getAllRelateContent(LocalDateTime start, LocalDateTime finish, Pageable pageable) {
        Page<Topic> rs = topicDao.findAllByDate(start, finish, pageable);
        
        final Stream<TopicReplica> trs = associatedBoardAndStatsAsync(rs.getResult().collect(Collectors.toList()));
        return new Page<TopicReplica>() {
            @Override
            public long getTotalElements() {
                return rs.getTotalElements();
            }
            
            @Override
            public Stream<TopicReplica> getResult() {
                return trs;
            }
        };
    }
    
    @Override
    public Stream<Topic> getRecentForBoard(long boardId, int size) {
        return topicDao.findAllByBoard(boardId, size);
    }
    
    @Override
    public Stream<Topic> getRecentForBoardIgnoreStatus(long boardId, int size) {
        return topicDao.findAllByBoardIgnoreStatus(boardId, size);
    }
    
    @Override
    public Stream<TopicReplica> getTopForBoard(long boardId) {
        return associationTopicStatsAsync(topicDao.findAllByBoardOfTop(boardId).collect(Collectors.toList()));
    }
    
    @Override
    public Stream<Topic> getRecent(int size) {
        return topicDao.findAllOfRecent(size);
    }
    
    @Override
    public Stream<Topic> getRecentIgnoreCondition(int size) {
        return topicDao.findAllOfRecentIgnoreCondition(size);
    }
    
    @Override
    public Stream<TopicReplica> getRecent(int boardGroupId, int size) {
        return associationTopicStatsAsync(topicDao.findAllOfRecent(boardGroupId, size).collect(Collectors.toList()));
    }
    
    @Override
    public Stream<Topic> getGoodsForBoardGroup(int boardGroupId, int size) {
        return topicDao.findAllByBoardGroupOfGoods(boardGroupId, size);
    }
    
    @Override
    public Stream<Topic> getMaxReplyForBoardGroup(int boardGroupId, int size) {
        return topicDao.findAllByBoardGroupByMaxReply(boardGroupId, size).stream();
    }
    
    @Override
    public Stream<Topic> getHot(int size) {
        return topicDao.findAllByMaxReply(size).stream();
    }
    
    @Override
    public Stream<Topic> getRecentReply(int size) {
        return topicDao.findAllByReplyDate(size);
    }
    
    @Override
    public Stream<Topic> getAllByBoard(long boardId, ForumEntityStatusEnum status) {
        return topicDao.findAllByBoard(boardId, status);
    }
    
    @Override
    public Stream<Topic> getRecentTermArticle(int size) {
        return topicDao.findAllOfRecentTermArticle(size);
    }
    
    @Override
    public List<TopicReplica> getRecentByUnixStamp(long boardId, int prevUnixStamp) {
        logger.info(String.format("[TS]args board:%d, prev ux:%d", boardId, prevUnixStamp));
        LocalDateTime prevDate = DateTimeUtils.getDateTimeByUnixTimestamp(prevUnixStamp);
        logger.info(String.format("[TS]calc localdate:%s", DateTimeUtils.getRFC3339(prevDate)));
        return associationTopicStatsAsync(topicDao.findAllRecentByBoard(boardId, prevDate).collect(Collectors.toList())).collect(Collectors.toList());
    }

    @Override
    public long getRecentByUnixStampSize(long boardId, int prevUnixStamp) {
        LocalDateTime prevDate = DateTimeUtils.getDateTimeByUnixTimestamp(prevUnixStamp);
        return topicDao.findAllRecentByBoardSize(boardId, prevDate);
    }

    @Override
    public Optional<TopicReplica> getTopicContentAndStats(final long id, final ImageIOMeta imageIO){
        CompletableFuture<PostsReplica> oneFloor = CompletableFuture.supplyAsync(()->postsDao.findOneByTopicId(id).map(PostsReplica.copyPosts).orElse(null))
                .thenCompose(postsReplica->
                    CompletableFuture.supplyAsync(()->{
                        ForumPostsDecorator pd = ForumPostsDecorator.init(postsReplica).decorateUploadImageEmptyScale(true).apply(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
                        postsReplica.setContent(pd.getContent());
                        return postsReplica;
                }).completeOnTimeout(null, 1, TimeUnit.SECONDS));
        return getTopicStatsAsync(id).thenCombine(oneFloor, (topicReplica, postsReplica)->{
            topicReplica.ifPresent(trObj->trObj.setContent(postsReplica));
            return topicReplica;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> getTopicContentAndStatsForRSS(final long id, final ImageIOMeta imageIO){
        CompletableFuture<PostsReplica> oneFloor = CompletableFuture.supplyAsync(()->postsDao.findOneByTopicId(id).map(PostsReplica.copyPosts).orElse(null))
                .thenCompose(postsReplica->
                    CompletableFuture.supplyAsync(()->{
                        String oneFloorBody = ForumPostsDecorator.init(postsReplica)
                                                .block("作者被禁止发言或内容自动屏蔽")
                                                .decorateUploadImageEmptyScale(false)
                                                .apply(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName())
                                                .modify("回复最近由管理人员进行过编辑")
                                                .getContent();
                        postsReplica.setContent(oneFloorBody);
                        return postsReplica;
                    }).completeOnTimeout(null, 1, TimeUnit.SECONDS));
        return getTopicStatsAsync(id).thenCombine(oneFloor, (topicReplica, postsReplica)->{
            topicReplica.ifPresent(trObj->trObj.setContent(postsReplica));
            return topicReplica;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> getTermArticleContent(final long id, final ImageIOMeta imageIO){
        CompletableFuture<PostsReplica> oneFloor = CompletableFuture.supplyAsync(()->postsDao.findOneByTopicId(id).map(PostsReplica.copyPosts).orElse(null))
                .thenCompose(postsReplica->
                    CompletableFuture.supplyAsync(()->{
                        String oneFloorBody = ForumPostsDecorator.init(postsReplica)
                                                .block("内容自动屏蔽")
                                                .decorateUploadImageEmptyScale(true)
                                                .apply(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName())
                                                .getContent();
                        postsReplica.setContent(oneFloorBody);
                        return postsReplica;
                    }).completeOnTimeout(null, 1, TimeUnit.SECONDS));
        return getTopicAsync(id).thenCombine(oneFloor, (topic, postsReplica)->{
            Optional<TopicReplica> tr = Optional.ofNullable(topic).map(TopicReplica.copyTopic);
            tr.ifPresent(trObj->trObj.setContent(postsReplica));
            return tr;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> getFirstArticleForTerm(long termId, ImageIOMeta imageIO) throws ReplicableException,IllegalArgumentException { //20200709
        Optional<Topic> topic = topicDao.findOneArticleForTerm(termId);
        if(!topic.isPresent()){
            return Optional.empty();
        }
        TopicReplica tr = topic.map(TopicReplica.copyTopic).orElseThrow(()->new ReplicableException("复制话题失败,操作无法继续"));
        //
        PostsReplica oneFloor = postsDao.findOneByTopicId(topic.get().getId()).map(PostsReplica.copyPosts).orElseThrow(()->new IllegalArgumentException("内容不存在或暂时无法访问"));
        //-------------------------------------------------------------------------解码图片
        ForumPostsDecorator pd = ForumPostsDecorator.init(oneFloor).decorateUploadImageEmptyScale(true).apply(imageIO.getImageBucketDomain(), imageIO.getUploadImageDirectName());
        oneFloor.setContent(pd.getContent());
        //-------------------------------------------------------------------------
        tr.setContent(oneFloor);
        return Optional.of(tr);
    }
    
    @Override
    public Optional<TopicReplica> getTopicStats(final long id){
        return getTopicStatsAsync(id).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> get(final long id, final long boardId, final int boardGroupId){
        CompletableFuture<BoardGroup> bg = CompletableFuture.supplyAsync(()->boardGroupDao.findOneById(boardGroupId)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getTopicBoardAsync(id, boardId).thenCombine(bg, (topicReplica, boardGroup)->{
            topicReplica.ifPresent(trObj->trObj.setVolumes(boardGroup));
            return topicReplica;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> get(final long id, final long boardId){
        return getTopicBoardAsync(id, boardId).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> getTopicConfig(final long id){
        CompletableFuture<TopicConfig> tc = CompletableFuture.supplyAsync(()->topicConfigDao.findOneByTopic(id).orElse(null)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getTopicAsync(id).thenCombineAsync(tc, (topic, topicConfig)->{
            Optional<TopicReplica> tr = Optional.ofNullable(topic).map(TopicReplica.copyTopic);
            tr.ifPresent(trObj->trObj.setConfigure(topicConfig));
            return tr;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> getTopicContentAndStats(final long id){
        CompletableFuture<TopicStats> ts = CompletableFuture.supplyAsync(()->topicStatsDao.findOneByTopic(id).orElse(null)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getTopicContentAsync(id).thenCombine(ts, (topicReplica, topicStats)->{
            topicReplica.ifPresent(trObj->trObj.setStats(topicStats));
            return topicReplica;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Optional<TopicReplica> getTopicContent(final long id){
        return getTopicContentAsync(id).orTimeout(1, TimeUnit.SECONDS).join();
    }
    
    @Override
    public Map<Long, Long> statsBoardTopicesForToday() {
        LocalDateTime start = DateTimeUtils.getTodayEarlyMorning();
        return statsBoardTopicesForDate(start, LocalDateTime.now());
    }
    
    @Override
    public Map<Long, Long> statsBoardTopicesForDate(LocalDateTime start, LocalDateTime finish) {
        return topicDao.statsBoardTopicSize(start, finish);
    }
    
    @Override
    public TreeMap<String, Long> groupTopicesForDate(LocalDateTime start, LocalDateTime finish) {
        return topicDao.groupTopicSize(start, finish);
    }
    
    @Override
    public Map<ForumEntityStatusEnum, Long> groupTopicesForStatus() {
        return topicDao.groupTopicesForStatus();
    }
    
    @Override
    public Map<String, Long> groupTopicesForCategory() {
        return topicDao.groupTopicesForCategory();
    }
    
    @Override
    public Map<String, Topic> getPrevNextTermArticle(long termId, long articleId) {
        return getPrevNextTermArticleAsync(termId, articleId);
    }
    
    @Override
    public Map<Board, Long> groupTopicesForBoard() {
        return CompletableFuture.supplyAsync(()->topicDao.groupTopicesForBoard())
                .thenComposeAsync((Map<Long, Long> rs) -> 
                        CompletableFuture.supplyAsync(() -> 
                                boardDao.findAllById(rs.keySet()).collect(Collectors.toMap(board->board, board->rs.get(board.getId()))))
                        .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS))
                .orTimeout(1, TimeUnit.SECONDS)
                .join();
    }
    
    @Override
    public Stream<TagRelateTopic> getRelateTopic(long id, int size) { //20200709
        //ST1找到需要计算的集合
        logger.info("[QR]Step 1 get Topic tages");
        List<TopicTag> tages = topicTagDao.findAllRelateTopic(id, size * 100).collect(Collectors.toList());
        if (tages.isEmpty()) {
            logger.info("[QR]Step 1.1 Topic tages is empty");
            return Stream.empty();
        }
        Map<Boolean, List<TopicTag>> rs = tages.stream().collect(Collectors.partitioningBy(tt -> tt.getTopicId() == id));//分区:true为话题的标签,false为计算的源
        if (rs.get(false).isEmpty()) {
            logger.info("[QR]Step 2.1 Tag Relate source is empty");
            return Stream.empty();
        }
        //ST2计算距离.以决定哪个在前哪个在后
        logger.info("[QR]Step 3 Relate Tag Executor");
        List<RelateWordStats> sourceRS = rs.get(false).parallelStream().map(tt -> new RelateWordStats(tt.getTopicId(), tt.getNames(), tt.getRates())).collect(Collectors.toList());
        AbstractTagRelateQueryExecutor trqe = new TagRelateDistance(rs.get(true).stream().collect(Collectors.toMap(TopicTag::getNames, TopicTag::getRates))).load(sourceRS);
        //ST3排序返回结果
        logger.info("[QR]Step 4 Relate Topice expect size: " + size);
        Map<Long, TagRelateResult> relateTopices = trqe.getResult(size).collect(Collectors.toMap(TagRelateResult::getTopic, Function.identity()));
        if (relateTopices.isEmpty()) {
            logger.info("[QR]Step 4.1 Relate Topice is empty");
            return Stream.empty();
        }
        BiFunction<Topic, TagRelateResult, TagRelateTopic> bi = (t, trr)->{
            return new TagRelateTopic(t, trr.getRanking(), trr.getSimilarity());
        };
        return topicDao.findAllById(relateTopices.keySet()).stream().map(topic->bi.apply(topic, relateTopices.get(topic.getId())));
    }
    private Optional<Boolean> modifyTopic(long topicId, String title, String content, Album album, Set<TopicTag> tags, long modifyMember, String modifyMemberNickname){
        try{
            topicDao.modifyTopic(topicId, title, content, album, tags, modifyMember, modifyMemberNickname);
            return Optional.of(true);
        }catch(Exception e){
            if(logger.isDebugEnabled()){
                logger.debug("编辑话题失败", e);
            }
        }
        return Optional.empty();
    }
    // 话题发布事件方法
    private void generalTopicEvent(TopicReplica tr){
        Topic t = tr.toEventPayload();
        topicPublishProducer.create(new TopicPublishEvent(t, tr.getConfigure(), tr.getStats(), tr.getTages(), tr.getUserAgent())).submit();
    }
    //从标题和内容中汲取标签[20200109]
    private Set<TopicTag> buildTopicTag(final long topicId, String title, String content, String... keywords) {
        TagExtraction tagExtraction = new TagNLPExtraction(10, WordNatureEnum.Noun, WordNatureEnum.Name, WordNatureEnum.Region, WordNatureEnum.Organization, WordNatureEnum.EFB);
        //构造关建词
        Map<String, Integer> tages = tagExtraction.extract(title, content, keywords);
        if (null == tages || tages.isEmpty()) {
            return Collections.emptySet();
        }
        return tages.entrySet().stream().map(entry->new TopicTag(entry.getKey(), entry.getValue(), topicId)).collect(Collectors.toSet());
    }
    private Optional<TopicReplica> storeReplicaTopic(Topic topic, Posts content, Album album, Set<TopicTag> tages, TopicConfig config, String userAgent){
        try{
            TopicReplica tr = topicDao.pushTopic(topic, content, album).orElseGet(TopicReplica::new);
            if(tr.getId()>0){
                final long topicId = tr.getId();
                if(null != tages && !tages.isEmpty()){
                    Stream<TopicTag> tts = tages.stream().peek(tt->tt.setTopicId(topicId));
                    tr.setTages(tts);
                }
                if(null != config){
                    config.setTopicId(topicId);
                }
                tr.setConfigure(config);
                tr.setUserAgent(userAgent);
                return Optional.of(tr);
            }
        }catch(Exception e){
            if(logger.isDebugEnabled()){
                logger.debug("发布话题失败", e);
            }
        }
        return Optional.empty();
    }
    //[异步]级联话题统计
    private Stream<TopicReplica> associationTopicStatsAsync(final List<Topic> topices) {
        if (null == topices || topices.isEmpty()) {
            return Stream.empty();
        }
        BiFunction<Topic, TopicStats, TopicReplica> bi = (t, ts)->{
            TopicReplica tr = Optional.ofNullable(t).map(TopicReplica.copyTopic).orElseGet(TopicReplica::new);
            if(null != ts){
                tr.setStats(ts);
            }else{
                tr.setStats(TopicStats.empty(t.getId(), t.getMemberId(), t.getMemberNickname()));
            }
            return tr;
        };
        Map<Long, TopicStats> topicStatsMap = CompletableFuture.supplyAsync(()->topices.stream().map(Topic::getId).collect(Collectors.toSet()))
                                .thenCompose(topicIdSet-> CompletableFuture.supplyAsync(()->topicStatsDao.findAllByTopicId(topicIdSet).collect(Collectors.toMap(TopicStats::getTopicId, Function.identity())))
                                                        .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS))
                                .orTimeout(1, TimeUnit.SECONDS)
                                .join();
        Comparator<Topic> comparator = (topic, otherTopic) -> topic.getRankingDateTime().compareTo(otherTopic.getRankingDateTime());
        return topices.parallelStream().sorted(comparator.reversed()).map(topic->bi.apply(topic, topicStatsMap.get(topic.getId())));
    }
    //[异步]级联话题统计 && 所属的版块
    private Stream<TopicReplica> associatedBoardAndStatsAsync(final List<Topic> topices){
        //20200710
        //Key=话题的Id,Value=版块的Id
        final Map<Long, Long> allQueryParam = Commons.collectMap(topices.stream(), Topic::getId, Topic::getBoardId);
        if(null == allQueryParam || allQueryParam.isEmpty()){
            return Stream.empty();
        }
        final TriFunction<Topic, TopicStats, Board, TopicReplica> action = (t, ts, b) ->{
            TopicReplica tr = Optional.ofNullable(t).map(TopicReplica.copyTopic).orElseGet(TopicReplica::new);
            if(null != b){
                tr.copyBoard(b);
            }else{
                tr.copyBoard(Board.empty(t.getBoardId()));
            }
            if(null != ts){
                tr.setStats(ts);
            }else{
                tr.setStats(TopicStats.empty(t.getId(), t.getMemberId(), t.getMemberNickname()));
            }
            return tr;
        };
        CompletableFuture<Map<Long, TopicStats>> topicStats = CompletableFuture.supplyAsync(()->topicStatsDao.findAllByTopicId(allQueryParam.keySet()))
                                                .thenCompose((Stream<TopicStats> ts)->
                                                         CompletableFuture.supplyAsync(()->
                                                                ts.collect(Collectors.toMap(TopicStats::getTopicId, Function.identity()))))
                                                .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS);
        CompletableFuture<Map<Long, Board>> boards = CompletableFuture.supplyAsync(()->boardDao.findAllById(allQueryParam.values()))
                                                .thenCompose((Stream<Board> bs)->
                                                        CompletableFuture.supplyAsync(()->
                                                                bs.collect(Collectors.toMap(Board::getId, Function.identity()))))
                                                .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS);
        return topicStats.thenCombine(boards, (topicStatsMap, boardsMap)->{
            return topices.parallelStream().map(topic->action.apply(topic, topicStatsMap.get(topic.getId()), boardsMap.get(topic.getBoardId())));
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    //[异步]级联话题所属的版块
    private Stream<TopicReplica> associatedBoardAsync(final List<Topic> topices){
        if (null == topices || topices.isEmpty()) {
            return Stream.empty();
        }
        BiFunction<Topic, Board, TopicReplica> bi = (t, b)->{
            TopicReplica tr = Optional.ofNullable(t).map(TopicReplica.copyTopic).orElseGet(TopicReplica::new);
            if(null != b){
                tr.copyBoard(b);
            }else{
                tr.copyBoard(Board.empty(t.getBoardId()));
            }
            return tr;
        };
        Map<Long, Board> boards = CompletableFuture.supplyAsync(()->topices.stream().map(Topic::getBoardId).collect(Collectors.toSet()))
                                .thenCompose(boardIdSet-> CompletableFuture.supplyAsync(()->boardDao.findAllById(boardIdSet).collect(Collectors.toMap(Board::getId, Function.identity())))
                                                        .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS))
                                .orTimeout(1, TimeUnit.SECONDS)
                                .join();
        return topices.parallelStream().map(topic->bi.apply(topic, boards.get(topic.getBoardId())));
    }
    private Map<String, Topic> getPrevNextTermArticleAsync(long termId, long articleId) {
        CompletableFuture<Optional<Topic>> prev = CompletableFuture.supplyAsync(()->topicDao.findPrevTermArticle(termId, articleId)).completeOnTimeout(Optional.empty(), 1, TimeUnit.SECONDS);
        CompletableFuture<Optional<Topic>> next = CompletableFuture.supplyAsync(()->topicDao.findNextTermArticle(termId, articleId)).completeOnTimeout(Optional.empty(), 1, TimeUnit.SECONDS);
        return prev.thenCombineAsync(next, (prevTopic, nextTopic)->{
            Map<String, Topic> data = new HashMap<>();
            if (prevTopic.isPresent()) {
                data.put("PREV", prevTopic.get());
            }
            if (nextTopic.isPresent()) {
                data.put("NEXT", nextTopic.get());
            }
            return data;
        }).orTimeout(1, TimeUnit.SECONDS).join();
    }
    private CompletableFuture<Optional<TopicReplica>> getTopicStatsAsync(final long id)throws ReplicableException,IllegalArgumentException {
        CompletableFuture<TopicStats> ts = CompletableFuture.supplyAsync(()->topicStatsDao.findOneByTopic(id).orElse(null)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getTopicAsync(id).thenCombineAsync(ts, (topic, topicStats)->{
            Optional<TopicReplica> tr = Optional.ofNullable(topic).map(TopicReplica.copyTopic);
            tr.ifPresent(trObj->trObj.setStats(topicStats));
            return tr;
        });
    }
    private CompletableFuture<Optional<TopicReplica>> getTopicBoardAsync(final long id, final long boardId)throws ReplicableException,IllegalArgumentException {
        CompletableFuture<Board> b = CompletableFuture.supplyAsync(()->boardDao.findOneById(boardId)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getTopicAsync(id).thenCombineAsync(b, (topic, board)->{
            Optional<TopicReplica> tr = Optional.ofNullable(topic).map(TopicReplica.copyTopic);
            tr.ifPresent(trObj->trObj.copyBoard(board));
            return tr;
        });
    }
    private CompletableFuture<Optional<TopicReplica>> getTopicContentAsync(final long id){ 
        CompletableFuture<PostsReplica> pr = CompletableFuture.supplyAsync(()->postsDao.findOneByTopicId(id).map(PostsReplica.copyPosts).orElseGet(PostsReplica::new)).completeOnTimeout(null, 1, TimeUnit.SECONDS);
        return getTopicAsync(id).thenCombineAsync(pr, (topic, postsReplica)->{
            Optional<TopicReplica> tr = Optional.ofNullable(topic).map(TopicReplica.copyTopic);
            tr.ifPresent(trObj->{
                trObj.setContent(postsReplica);
            });
            return tr;
        });
    }
    private CompletableFuture<Topic> getTopicAsync(final long id){
        return CompletableFuture.supplyAsync(()->topicDao.findOneById(id));
    }
}