package com.tanpu.community.manager; import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.collect.Sets; import com.tanpu.biz.common.enums.RelTypeEnum; import com.tanpu.biz.common.enums.community.CollectionTypeEnum; import com.tanpu.biz.common.enums.community.ReportTypeEnum; import com.tanpu.common.api.CommonResp; import com.tanpu.common.constant.ErrorCodeConstant; import com.tanpu.common.exception.BizException; import com.tanpu.common.util.JsonUtil; import com.tanpu.community.api.beans.qo.ESThemeQo; import com.tanpu.community.api.beans.qo.FormerThemeQo; import com.tanpu.community.api.beans.qo.ThemeContentQo; import com.tanpu.community.api.beans.qo.ThemeQo; import com.tanpu.community.api.beans.req.homepage.QueryRecordThemeReq; import com.tanpu.community.api.beans.req.theme.*; import com.tanpu.community.api.beans.resp.CreateThemeResp; import com.tanpu.community.api.beans.resp.ThemeFullSearchResp; import com.tanpu.community.api.beans.resp.ThemeListResp; import com.tanpu.community.api.beans.vo.feign.fatools.UserInfoResp; import com.tanpu.community.api.enums.BlockTypeEnum; import com.tanpu.community.api.enums.OperationTypeEnum; import com.tanpu.community.api.enums.ThemeListTypeEnum; import com.tanpu.community.api.enums.ThemeTypeEnum; import com.tanpu.community.cache.RedisCache; import com.tanpu.community.dao.entity.community.*; import com.tanpu.community.feign.fatools.FeignClientForFatools; import com.tanpu.community.service.*; import com.tanpu.community.service.base.ESService; import com.tanpu.community.util.BizUtils; import com.tanpu.community.util.ConvertUtil; import com.tanpu.community.util.RankUtils; import com.tanpu.community.util.TencentcloudUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.*; import java.util.stream.Collectors; import static com.tanpu.community.api.constants.RedisKeyConstant.*; @Slf4j @Service public class ThemeManager { @Resource private ThemeService themeService; @Autowired private CollectionService collectionService; @Autowired private CommentService commentService; @Autowired private FollowRelService followRelService; @Autowired private BlackListService blackListService; @Autowired private ThemeAttachmentService themeAttachmentService; @Resource private BatchFeignCallService batchFeignCallService; @Autowired private VisitLogService visitLogService; @Autowired private ReportLogService reportLogService; @Autowired private RankService rankService; @Autowired private ESService esService; @Autowired private FeignClientForFatools feignClientForFatools; @Autowired private RedisCache redisCache; @Autowired private RecommendService recommendService; public ThemeFullSearchResp themeFullSearch(String keyword, Integer pageNo, Integer pageSize, String ident, String userId) { List<String> excludeIds; if (pageNo > 1) { String l = redisCache.get("themeFullSearch_" + ident); excludeIds = StringUtils.isBlank(l) ? new ArrayList<>() : JsonUtil.toBean(l, new TypeReference<List<String>>() { }); } else { excludeIds = new ArrayList<>(); } Integer from = (pageNo - 1) * pageSize; ThemeFullSearchResp resp = new ThemeFullSearchResp(); String[] keywords = StringUtils.split(keyword, " "); // 按时间倒叙查询 List<ESThemeQo> esIds = esService.queryThemeIdByContentAndTitle(keywords, from, pageSize * 5); if (esIds.isEmpty()) { return resp; } // 排除已经展示过的id List<String> filterEsIds = esIds.stream().map(ESThemeQo::getThemeId).filter(tId -> { return !excludeIds.contains(tId); }).limit(pageSize).collect(Collectors.toList()); resp.themes = convertEntityToQo(themeService.queryByThemeIds(filterEsIds), userId); resp.themes.sort(new Comparator<ThemeQo>() { @Override public int compare(ThemeQo o1, ThemeQo o2) { return o2.createTime.compareTo(o1.createTime); } }); // 截取关键词出现的那一部分段落 for (ThemeQo theme : resp.themes) { theme.briefContent4FullSearch = BizUtils.getThemeContent(keywords, theme); } excludeIds.addAll(resp.themes.stream().map(ThemeQo::getThemeId).collect(Collectors.toList())); redisCache.put("themeFullSearch_" + ident, excludeIds, 60 * 60 * 6); return resp; } /** * 发表主题(修改) */ @Transactional public CreateThemeResp publishTheme(CreateThemeReq req, String userId) { // 保存主题表 ThemeEntity themeEntity = new ThemeEntity(); BeanUtils.copyProperties(req, themeEntity); themeEntity.setAuthorId(userId); // 腾讯云敏感词校验 checkContent(req); themeEntity.setContent(JsonUtil.toJson(req.getContent())); //附件校验 checkAttachment(req.getContent()); if (StringUtils.isEmpty(req.getEditThemeId())) { // 新建 themeService.insertTheme(themeEntity); } else { // 修改 themeService.update(themeEntity, req.getEditThemeId()); themeEntity.setThemeId(req.getEditThemeId()); } // 保存附件表 List<ThemeAttachmentEntity> themeAttachments = ConvertUtil.themeReqToAttachmentList(req, themeEntity.getThemeId()); if (StringUtils.isNotEmpty(req.getEditThemeId())) { // 修改需要刪除 themeAttachmentService.deleteByThemeId(req.getEditThemeId()); } themeAttachmentService.insertList(themeAttachments); try { esService.insertOrUpdateTheme(ConvertUtil.convert(themeEntity)); } catch (Exception e) { log.error("error in save theme to ES. themeId:{}, error:{}", themeEntity.getThemeId(), ExceptionUtils.getStackTrace(e)); } redisCache.evict(StringUtils.joinWith("_", CACHE_THEME_ID, themeEntity.getThemeId())); return CreateThemeResp.builder().themeId(themeEntity.getThemeId()).build(); } /** * 参数校验 * * @param themeAttachments */ private void checkAttachment(List<ThemeContentReq> themeAttachments) { for (ThemeContentReq content : themeAttachments) { if (content.getType() == null) { throw new IllegalArgumentException("主题内容ThemeContentReq缺少类型"); } if (content.getType().equals(RelTypeEnum.FUND.type)) { if (content.getProductType() == null) { throw new IllegalArgumentException("附件产品FUND缺少类型"); } } } } // 转发主题 public CreateThemeResp forward(ForwardThemeReq req, String userId) { ThemeEntity targetTheme = themeService.queryByThemeId(req.getFormerThemeId()); ThemeEntity themeEntity = ThemeEntity.builder() .content(JsonUtil.toJson(req.getContent())) .topicId(req.getTopicId()) .formerThemeId(req.getFormerThemeId()) .authorId(userId) .themeType(ThemeTypeEnum.FORWARD.getCode()) .build(); if (StringUtils.isEmpty(req.getEditThemeId()) || req.getEditThemeId().equals(req.getFormerThemeId())) { // 新建 themeService.insertTheme(themeEntity); } else { // 修改 themeService.update(themeEntity, req.getEditThemeId()); themeEntity.setThemeId(req.getEditThemeId()); } try { esService.insertOrUpdateTheme(ConvertUtil.convert(themeEntity)); } catch (Exception e) { log.error("error in save theme to ES. themeId:{}, error:{}", themeEntity.getThemeId(), ExceptionUtils.getStackTrace(e)); } return CreateThemeResp.builder().themeId(themeEntity.getThemeId()).build(); } /** * 推荐:由最热,最新和python推荐三个部分组成,比例为6,3,1 */ // 查询主题列表:推荐/关注/热门/最新 public ThemeListResp queryList(ThemeListReq req, String userId) { List<String> excludeIds; if (req.page.pageNumber > 1) { String l = redisCache.get("queryThemes_" + req.ident); excludeIds = StringUtils.isBlank(l) ? new ArrayList<>() : JsonUtil.toBean(l, new TypeReference<List<String>>() { }); } else { excludeIds = new ArrayList<>(); } Integer pageStart = (req.page.pageNumber - 1) * req.page.pageSize; Integer pageSize = req.page.pageSize; List<ThemeEntity> themes = new ArrayList<>(); if (ThemeListTypeEnum.RECOMMEND.getCode().equals(req.getType())) { // 推荐 // 需要筛掉用户访问过详情的 & 最近出现在列表页过的. List<String> visitedIds = visitLogService.queryUserRecentVisited(userId); List<String> excludes = ListUtils.union(excludeIds, visitedIds); List<String> recmdIds = recommendService.getRecommendThemes(pageStart, pageSize, userId, excludes); themes = themeService.queryByThemeIds(recmdIds); themes = RankUtils.sortThemeEntityByIds(themes, recmdIds).stream().limit(pageSize).collect(Collectors.toList()); } else if (ThemeListTypeEnum.FOLLOW.getCode().equals(req.getType())) { // 根据关注列表查询,按时间倒序 List<String> fansList = followRelService.queryIdolsByFollowerId(userId); themes = themeService.queryByUserIdsCreateDesc(fansList, pageStart, pageSize); if (CollectionUtils.isEmpty(excludeIds) && !themes.isEmpty()) { // 说明是从头开始刷,则直接把最新的lastId放到redis中,保留一个月 Long lastId = themes.stream().map(ThemeEntity::getId).max(Long::compareTo).get(); redisCache.put(CACHE_IDOL_THEME_LAST_ID + userId, lastId, 60 * 60 * 24 * 7 * 4); // visitLogService.addPageView(userId, userId, VisitTypeEnum.FOLLOW_THEME_VIEW); } } else if (ThemeListTypeEnum.TOPIC_HOT.getCode().equals(req.getType())) { // 根据话题查询热门 if (StringUtils.isEmpty(req.getTopicId())) { throw new BizException("TopicId为空"); } List<String> rankThemeIds = rankService.getRankThemeListByTopic(req.getTopicId(), excludeIds); rankThemeIds = BizUtils.subList(rankThemeIds, pageStart, pageSize); themes = themeService.queryByThemeIds(rankThemeIds); themes = RankUtils.sortThemeEntityByIds(themes, rankThemeIds); } else if (ThemeListTypeEnum.TOPIC_LATEST.getCode().equals(req.getType())) { //根据话题查询最新 if (StringUtils.isEmpty(req.getTopicId())) { throw new BizException("TopicId为空"); } themes = themeService.queryNewestByTopic(req.topicId, pageStart, pageSize, excludeIds); } ThemeListResp resp = new ThemeListResp(); resp.themes = convertEntityToQo(themes, userId); excludeIds.addAll(resp.themes.stream().map(ThemeQo::getThemeId).collect(Collectors.toList())); redisCache.put("queryThemes_" + req.ident, excludeIds, 60 * 60 * 6); //组装详情 return resp; } // 主题Entity转QO,组装所有信息 private List<ThemeQo> convertEntityToQo(List<ThemeEntity> themeEntities, String userId) { //Entity转Qo List<ThemeQo> themeQos = ConvertUtil.themeEntitiesToDTOs(themeEntities); // 批量查询附件detail batchFeignCallService.getAttachDetailByBatch(themeQos); //其他信息 for (ThemeQo themeQO : themeQos) { // 通用信息 buildThemeQoExtraInfo(themeQO); } // 和用户相关信息 buildThemeExtraInfoByUser(userId, themeQos); return themeQos; } // 转发对象、点赞、收藏、转发数 private void buildThemeQoExtraInfo(ThemeQo themeQo) { String themeId = themeQo.getThemeId(); // 封装转发对象 FormerThemeQo former = redisCache.getObject(StringUtils.joinWith("_", CACHE_FORMER_THEME_ID, themeId), 60, () -> this.getFormerTheme(themeQo.getFormerThemeId()), FormerThemeQo.class); themeQo.setFormerTheme(former); // 点赞,收藏,转发 Integer likeCount = collectionService.getCountByTypeAndId(themeId, CollectionTypeEnum.LIKE_THEME); Integer commentCount = commentService.getCommentCountByThemeId(themeId); Integer forwardCount = themeService.getForwardCountById(themeId); themeQo.setCommentCount(commentCount); themeQo.setLikeCount(likeCount); themeQo.setForwardCount(forwardCount); } // 组装和当前用户相关信息(单个查询) private void buildThemeExtraInfoByUser(String userId, ThemeQo themeQo) { String themeId = themeQo.getThemeId(); // 是否关注作者 themeQo.setFollow(followRelService.checkFollow(themeQo.getAuthorId(), userId)); // 是否点赞 CollectionEntity likeEntity = collectionService.getTarget(themeId, userId, CollectionTypeEnum.LIKE_THEME); themeQo.setHasLiked(likeEntity != null); // 是否转发 themeQo.setHasForward(themeService.judgeForwardByUser(themeId, userId)); // 是否收藏 CollectionEntity collectionEntity = collectionService.getTarget(themeId, userId, CollectionTypeEnum.COLLECT_THEME); themeQo.setHasCollect(collectionEntity != null); } // 组装和当前用户相关信息(批量查询) private void buildThemeExtraInfoByUser(String userId, List<ThemeQo> themeQos) { // 批量查询 List<String> themeIds = themeQos.stream().map(ThemeQo::getThemeId).collect(Collectors.toList()); Set<String> idolSet = new HashSet<>(followRelService.queryIdolsByFollowerId(userId)); Set<String> likeSet = collectionService.getTargets(themeIds, userId, CollectionTypeEnum.LIKE_THEME); Set<String> bookSet = collectionService.getTargets(themeIds, userId, CollectionTypeEnum.COLLECT_THEME); Set<String> forwardUsers = themeService.getForwardUsers(themeIds); // 从set中查找 for (ThemeQo themeQo : themeQos) { themeQo.setFollow(idolSet.contains(themeQo.getAuthorId())); themeQo.setHasLiked(likeSet.contains(themeQo.getThemeId())); themeQo.setHasCollect(bookSet.contains(themeQo.getThemeId())); themeQo.setHasForward(forwardUsers.contains(userId)); } } // 返回用户发布、回复、点赞、收藏的主题列表 public List<ThemeQo> queryThemesByUser(QueryRecordThemeReq req, String userId) { List<ThemeEntity> themeEntities = Collections.emptyList(); switch (req.getRecordType()) { case 1://发布 themeEntities = themeService.queryThemesByUserId(req.getUserId(), req.getLastId(), req.getPageSize()); break; case 2://回复 List<ThemeQo> commentThemeList = getCommentThemeQos(req, userId); return commentThemeList; case 3://点赞 List<String> likeThemeIds = collectionService.getListByUser(req.getUserId(), CollectionTypeEnum.LIKE_THEME); themeEntities = themeService.queryByThemeIds(likeThemeIds, req.getLastId(), req.getPageSize()); themeEntities = RankUtils.sortThemeEntityByIds(themeEntities, likeThemeIds); break; case 4://收藏 List<String> collectThemeIds = collectionService.getListByUser(req.getUserId(), CollectionTypeEnum.COLLECT_THEME); themeEntities = themeService.queryByThemeIds(collectThemeIds, req.getLastId(), req.getPageSize()); themeEntities = RankUtils.sortThemeEntityByIds(themeEntities, collectThemeIds); break; } List<ThemeQo> themeQos = convertEntityToQo(themeEntities, userId); if (userId.equals(req.getUserId())) { //如果用户是查询自己的帖子,需要实时查询用户自己的个人信息,防止数据不一致 CommonResp<UserInfoResp> userInfoNewCommonResp = feignClientForFatools.queryUsersListNew(userId); if (userInfoNewCommonResp.isNotSuccess()) { throw new BizException("内部接口调用失败"); } UserInfoResp user = userInfoNewCommonResp.getData(); themeQos.stream().forEach(o->reBuildAuthorInfo(o,user)); redisCache.put(StringUtils.joinWith("_", CACHE_FEIGN_USER_INFO, userId), user, 60); } return themeQos; } // 查询正文 public ThemeQo getThemeDetail(String themeId, String userId) { // 查询详情 ThemeQo themeQo = redisCache.getObject(StringUtils.joinWith("_", CACHE_THEME_ID, themeId), 60, () -> this.getDetailCommon(themeId), ThemeQo.class); // 添加用户相关信息 buildThemeExtraInfoByUser(userId, themeQo); return themeQo; } // 正文通用信息,与用户无关,可使用缓存 private ThemeQo getDetailCommon(String themeId) { ThemeEntity themeEntity = themeService.queryByThemeId(themeId); if (themeEntity == null) { throw new BizException("找不到帖子id:" + themeId); } ThemeQo themeQo = ConvertUtil.themeEntityToQo(themeEntity); //附件 batchFeignCallService.getAttachDetail(themeQo); //转发、收藏、点赞 buildThemeQoExtraInfo(themeQo); return themeQo; } // 点赞/取消点赞 public void like(LikeThemeReq req, String userId) { if (OperationTypeEnum.CONFIRM.getCode().equals(req.getType())) { collectionService.saveOrUpdate(req.getThemeId(), userId, CollectionTypeEnum.LIKE_THEME); } else if (OperationTypeEnum.CANCEL.getCode().equals(req.getType())) { collectionService.delete(req.getThemeId(), userId, CollectionTypeEnum.LIKE_THEME); } } //收藏/取消收藏 public void collect(CollectThemeReq req, String userId) { if (OperationTypeEnum.CONFIRM.getCode().equals(req.getType())) { collectionService.saveOrUpdate(req.getThemeId(), userId, CollectionTypeEnum.COLLECT_THEME); } else if (OperationTypeEnum.CANCEL.getCode().equals(req.getType())) { collectionService.delete(req.getThemeId(), userId, CollectionTypeEnum.COLLECT_THEME); } } //举报主题 @Transactional public void report(ReportThemeReq req, String userId) { //更改举报状态 themeService.updateReportStatus(req.getThemeId()); //写入举报记录表 ThemeEntity themeEntity = themeService.queryByThemeId(req.getThemeId()); reportLogService.insert(ReportTypeEnum.THEME, userId, req.getThemeId(), themeEntity.getAuthorId(), req.getReason()); } //关注用户是否有更新 public Integer getFollowUpdateCount(String userId) { String lastIdStr = redisCache.get(CACHE_IDOL_THEME_LAST_ID + userId); Long lastId = StringUtils.isBlank(lastIdStr) ? 0L : Long.parseLong(lastIdStr); List<String> fansList = followRelService.queryIdolsByFollowerId(userId); return themeService.queryCountFromLastId(fansList, lastId); } // 屏蔽(用户) public void blockUser(String blockUser, String userId) { BlackListEntity selectOne = blackListService.selectOne(blockUser, userId, BlockTypeEnum.USER.getCode()); if (selectOne == null) { blackListService.addBlock(blockUser, userId, BlockTypeEnum.USER); } } //返回被转发主题 private FormerThemeQo getFormerTheme(String formerThemeId) { if (StringUtils.isNotEmpty(formerThemeId)) { ThemeQo formerTheme = ConvertUtil.themeEntityToQo(themeService.queryByThemeId(formerThemeId)); if (formerTheme != null) { batchFeignCallService.getAttachDetail(formerTheme); FormerThemeQo f = ConvertUtil.themeQo2FormerThemeQo(formerTheme); return f; } } return null; } // 逻辑删除主题,校验用户 public void delete(String themeId, String userId) { themeService.deleteById(themeId, userId); } /** * 腾讯云-内容检测 * * @param req */ private void checkContent(CreateThemeReq req) { StringBuilder sb = new StringBuilder(); for (ThemeContentReq themeContentReq : req.getContent()) { if (RelTypeEnum.TEXT.type.equals(themeContentReq.getType())) { sb.append(themeContentReq.getValue()); } } String content = sb.toString(); // 腾讯云接口最多支持5000文字校验 // 检查内容是否涉黄违法 boolean b; while (content.length()>5000){ b = TencentcloudUtils.textModeration(content.substring(0, 5000)); if (!b) { throw new BizException(ErrorCodeConstant.CONTENT_ILLEGAL); } content=content.substring(5000); } b = TencentcloudUtils.textModeration(content); if (!b) { throw new BizException(ErrorCodeConstant.CONTENT_ILLEGAL); } } /** * 直播类型做转播检查 * * @param userId * @param contents */ private void liveRelayCheck(String userId, List<ThemeContentReq> contents) { for (ThemeContentReq content : contents) { if (content != null && content.getType().equals(RelTypeEnum.LIVE.type)) { CommonResp<Set<String>> notRelayResp = feignClientForFatools.getNotRelaySet(userId, Sets.newHashSet(content.getValue())); if (!notRelayResp.isSuccess()) { throw new BizException("转播失败"); } if (CollectionUtils.isEmpty(notRelayResp.getData())) { throw new BizException("9999", "很抱歉!您需要购买或报名成功后才可以添加这个直播哦~"); } } } } /** * 查询用户评论过的主题,并封装成转发主题结构 * * @param req * @param userId 当前用户 * @return */ private List<ThemeQo> getCommentThemeQos(QueryRecordThemeReq req, String userId) { List<ThemeQo> commentThemeList = new ArrayList<>(); List<ThemeEntity> themeEntities; // 评论列表 List<CommentEntity> commentEntities = commentService.queryCommentsByUserId(req.getUserId(), req.getLastId(), req.getPageSize()); // 当前用户信息 UserInfoResp userInfo = redisCache.getObject(StringUtils.joinWith("_", CACHE_FEIGN_USER_INFO, req.getUserId()), 60, () -> this.getUserInfo(req.getUserId()), UserInfoResp.class); Set<String> replyThemeIds = commentEntities.stream().map(CommentEntity::getThemeId).collect(Collectors.toSet()); if (CollectionUtils.isEmpty(replyThemeIds)) { return commentThemeList; } themeEntities = themeService.queryByThemeIds(new ArrayList<>(replyThemeIds)); List<ThemeQo> themeQos = convertEntityToQo(themeEntities, userId); // 组装附件 batchFeignCallService.getAttachDetailByBatch(themeQos); // 主题列表 Map<String, ThemeQo> themeMap = themeQos.stream() .collect(Collectors.toMap(ThemeQo::getThemeId, o -> o)); // 主题+评论封装转发对象 for (CommentEntity commentEntity : commentEntities) { String themeId = commentEntity.getThemeId(); //评论内容包装到ThemeContentQo里 ThemeContentQo commentContent = ThemeContentQo.builder() .type(RelTypeEnum.TEXT.type) .value(commentEntity.getContent()) .build(); ThemeQo commentThemeQo = ThemeQo.builder() .authorId(userInfo.getUserId()) .nickName(userInfo.getNickName()) .userImg(userInfo.getHeadImageUrl()) .userType(userInfo.getUserType()) .levelGrade(userInfo.getLevelGrade()) .userInvestorType(userInfo.getUserInvestorType()) .belongUserOrgId(userInfo.getBelongUserOrgId()) .belongUserOrgName(userInfo.getBelongUserOrgName()) .content(Collections.singletonList(commentContent)) .commentId(commentEntity.getCommentId()) .themeType(ThemeTypeEnum.RES_COMMENT.getCode()) .follow(followRelService.checkFollow(userId, userId)) .build(); //原主题包装到formerThemeQo中 ThemeQo themeQo = themeMap.get(themeId); if (themeQo != null) { FormerThemeQo f = ConvertUtil.themeQo2FormerThemeQo(themeQo); commentThemeQo.setFormerTheme(f); } commentThemeList.add(commentThemeQo); } return commentThemeList; } private UserInfoResp getUserInfo(String authorId) { CommonResp<UserInfoResp> userInfoNewCommonResp = feignClientForFatools.queryUsersListNew(authorId); if (userInfoNewCommonResp.isNotSuccess()) { throw new BizException("内部接口调用失败"); } return userInfoNewCommonResp.getData(); } private void reBuildAuthorInfo(ThemeQo themeQo,UserInfoResp userInfo){ themeQo.setNickName(userInfo.getNickName()); themeQo.setUserImg(userInfo.getHeadImageUrl()); themeQo.setUserIntroduction(userInfo.getIntroduction()); //认证标签相关 themeQo.setUserType(userInfo.getUserType()); themeQo.setLevelGrade(userInfo.getLevelGrade()); themeQo.setUserInvestorType(userInfo.getUserInvestorType()); themeQo.setBelongUserOrgId(userInfo.getBelongUserOrgId()); themeQo.setBelongUserOrgName(userInfo.getBelongUserOrgName()); //工作室相关 themeQo.setWorkshopName(userInfo.getWorkshopName()); themeQo.setWorkshopStatus(userInfo.getWorkshopStatus()); themeQo.setWorkshopIntroduction(userInfo.getWorkshopIntroduction()); } }