package com.hz.pm.api.meeting.manage;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hz.pm.api.common.helper.RegionCacheHelper;
import com.hz.pm.api.expert.entity.ExpertUserFullInfo;
import com.hz.pm.api.expert.service.IExpertReviewService;
import com.hz.pm.api.expert.service.IExpertUserFullInfoService;
import com.hz.pm.api.gov.service.IBelongOrgService;
import com.hz.pm.api.meeting.builder.ExpertInviteBuilder;
import com.hz.pm.api.meeting.builder.MeetingReviewProjectBuilder;
import com.hz.pm.api.meeting.builder.ProjectReviewResultBuilder;
import com.hz.pm.api.meeting.entity.domain.*;
import com.hz.pm.api.meeting.entity.dto.*;
import com.hz.pm.api.meeting.entity.enumeration.ExpertAttendStatusEnum;
import com.hz.pm.api.meeting.entity.enumeration.ExpertInviteTypeEnum;
import com.hz.pm.api.meeting.entity.enumeration.MeetingReviewTypeEnum;
import com.hz.pm.api.meeting.entity.enumeration.MeetingStatusEnum;
import com.hz.pm.api.meeting.entity.req.*;
import com.hz.pm.api.meeting.entity.vo.*;
import com.hz.pm.api.meeting.helper.ExpertInviteHelper;
import com.hz.pm.api.meeting.helper.MeetingCallOrMsgHelper;
import com.hz.pm.api.meeting.helper.MeetingManageHelper;
import com.hz.pm.api.meeting.service.*;
import com.hz.pm.api.meeting.task.ExpertRandomInviteTask;
import com.hz.pm.api.meta.helper.DictionaryCache;
import com.hz.pm.api.meta.helper.TagCache;
import com.hz.pm.api.organization.service.IDingOrganizationService;
import com.hz.pm.api.projectlib.model.enumeration.ProjectStatusEnum;
import com.hz.pm.api.projectlib.model.entity.Project;
import com.hz.pm.api.projectlib.service.IProjectService;
import com.hz.pm.api.sys.model.dto.RegionDTO;
import com.hz.pm.api.user.security.model.UserInfoDetails;
import com.hz.pm.api.user.util.LoginUserUtil;
import com.ningdatech.basic.exception.BizException;
import com.ningdatech.basic.model.IdVo;
import com.ningdatech.basic.model.PageVo;
import com.ningdatech.basic.util.CollUtils;
import com.ningdatech.basic.util.ValidUtil;
import com.ningdatech.cache.lock.DistributedLock;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import java.time.LocalDateTime;
import java.util.*;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

import static com.hz.pm.api.meeting.entity.enumeration.ExpertAttendStatusEnum.*;
import static com.hz.pm.api.meeting.entity.enumeration.ExpertInviteTypeEnum.APPOINT;
import static com.hz.pm.api.meeting.helper.ExpertInviteHelper.getExpertInviteRule;

/**
 * <p>
 * MeetingManage
 * </p>
 *
 * @author WendyYang
 * @since 13:35 2022/7/26
 */
@Component
@RequiredArgsConstructor
public class MeetingManage {

    private final RegionCacheHelper regionCache;
    private final IMeetingService meetingService;
    private final IExpertInviteAvoidRuleService inviteAvoidRuleService;
    private final IExpertInviteRuleService inviteRuleService;
    private final IExpertUserFullInfoService expertUserFullInfoService;
    private final TagCache tagCache;
    private final DictionaryCache dictionaryCache;
    private final IMeetingExpertService meetingExpertService;
    private final ExpertInviteManage expertInviteManage;
    private final ExpertRandomInviteTask expertRandomInviteTask;
    private final MeetingManageHelper meetingManageHelper;
    private final DistributedLock distributedLock;
    private final IProjectService projectService;
    private final IMeetingInnerProjectService meetingInnerProjectService;
    private final IBelongOrgService belongOrgService;
    private final IDingOrganizationService dingOrganizationService;
    private final ExpertInviteHelper expertInviteHelper;
    private final MeetingCallOrMsgHelper meetingCallOrMsgHelper;
    private final IMeetingExpertJudgeService expertJudgeService;
    private final IMeetingExpertJudgeService meetingExpertJudgeService;

    private static final String INVITED_RULE_CREATE = "INVITED_RULE_CREATE:";

    private static final String MEETING_CREATE_KEY = "MEETING_CREATE:";

    private String getDictName(String dictCode) {
        return dictionaryCache.getByCode(dictCode)
                .flatMap(w -> Optional.ofNullable(w.getName()))
                .orElse(StrUtil.EMPTY);
    }

    /**
     * 锁定时间10 * 60秒
     */
    public static final int RETRY_TIMES = 3;

    @Transactional(rollbackFor = Exception.class)
    public IdVo<Long> meetingCreateAndInviteExpert(MeetingCreateReq req) {
        MeetingBasicDTO meetingBasic = req.getMeetingBasicInfo();
        // 校验关联项目信息
        meetingManageHelper.checkReviewProject(meetingBasic);
        String meetingMd5 = SecureUtil.md5(JSONUtil.toJsonStr(req));
        String key = MEETING_CREATE_KEY + meetingMd5;
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("会议正在创建中");
        }
        try {
            UserInfoDetails userDetail = LoginUserUtil.loginUserDetail();
            ExpertInviteReq inviteRule = req.getExpertInviteRule();
            // 保存会议基本信息
            Meeting meeting = BeanUtil.copyProperties(meetingBasic, Meeting.class);
            meeting.setStatus(MeetingStatusEnum.NORMAL.getCode());
            meeting.setHoldOrg(String.valueOf(userDetail.getMhUnitId()));
            meeting.setHoldOrgCode(userDetail.getMhUnitName());
            meeting.setCreator(userDetail.getUsername());
            meeting.setInviteStatus(Boolean.FALSE);
            meeting.setConfirmedRoster(Boolean.FALSE);
            meeting.setInviteType(inviteRule.getInviteType());
            meetingService.save(meeting);
            if (Boolean.TRUE.equals(meetingBasic.getIsInnerProject())) {
                List<MeetingInnerProject> projects = meetingBasic.getInnerProjects().stream().map(w -> {
                    MeetingInnerProject project = BeanUtil.copyProperties(w, MeetingInnerProject.class);
                    project.setMeetingId(meeting.getId());
                    return project;
                }).collect(Collectors.toList());
                meetingInnerProjectService.saveBatch(projects);
            }
            // 抽取专家
            inviteRule.setMeetingId(meeting.getId());
            expertInviteByCreate(inviteRule);
            return IdVo.of(meeting.getId());
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void continueInvite(Long meetingId) {
        String key = "CONTINUE_INVITE:" + meetingId;
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("已进行续抽，请勿重复点击");
        }
        try {
            Meeting meeting = meetingService.getById(meetingId);
            if (!meeting.getInviteStatus()) {
                throw BizException.wrap("该会议正在抽取专家，暂无法续抽");
            }
            if (meeting.getStartTime().isBefore(LocalDateTime.now())) {
                throw BizException.wrap("会议已开始，不允许续抽");
            }
            if (!MeetingStatusEnum.NORMAL.eq(meeting.getStatus())) {
                throw BizException.wrap("续抽失败，请刷新后重试");
            }
            boolean invitedContinue = meetingManageHelper.checkCouldBeInvitedContinue(meetingId);
            if (!invitedContinue) {
                throw BizException.wrap("抽取人员数量已满足抽取规则");
            }
            expertRandomInviteTask.notifyInviteTask(meetingId);
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void convertToAppoint(Long meetingId) {
        String key = "CONVERT_TO_APPOINT:" + meetingId;
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("已进行转换，请勿重复点击");
        }
        try {
            Meeting meeting = meetingService.getById(meetingId);
            if (!MeetingStatusEnum.NORMAL.eq(meeting.getStatus())) {
                throw BizException.wrap("转换失败，请刷新后重试");
            }
            expertRandomInviteTask.cancelByMeetingIdAndKillTask(meetingId);
            LambdaUpdateWrapper<Meeting> meetingUpdate = Wrappers.lambdaUpdate(Meeting.class)
                    .set(Meeting::getInviteType, APPOINT.getCode())
                    .eq(Meeting::getId, meetingId);
            meetingService.update(meetingUpdate);
            saveAppointRuleByConvertFromRandomRule(meetingId);
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    private void saveAppointRuleByConvertFromRandomRule(Long meetingId) {
        AppointInviteRuleDTO rule = new AppointInviteRuleDTO();
        rule.setInviteDesc("转为指定抽取");
        rule.setExpertIdList(Collections.emptyList());
        rule.setInviteType(APPOINT.getCode());
        rule.setCount(0);
        ExpertInviteRule inviteRule = new ExpertInviteRule();
        inviteRule.setMeetingId(meetingId);
        inviteRule.setInviteType(rule.getInviteType());
        inviteRule.setInviteCount(0);
        inviteRule.setInviteRule(JSONUtil.toJsonStr(rule));
        inviteRuleService.save(inviteRule);
    }

    @Transactional(rollbackFor = Exception.class)
    public void expertInviteByCreate(ExpertInviteReq req) {
        String key = INVITED_RULE_CREATE + req.getMeetingId();
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("不可重复进行专家抽取");
        }
        try {
            Meeting meeting = meetingService.getById(req.getMeetingId());
            if (ExpertInviteTypeEnum.RANDOM.eq(req.getInviteType())) {
                List<RandomInviteRuleDTO> randomRules = req.getRandomRules();
                Assert.notEmpty(randomRules, "随机抽取规则不能为空");
                AvoidRuleDTO avoidInfo = req.getAvoidRule();
                Assert.notNull(avoidInfo, "回避信息不能为空");
                // 随机抽取的话则需进行抽取数量校验
                LocalDateTime now = LocalDateTime.now();
                expertInviteManage.expertInviteByMeetingCreate(meeting, randomRules, avoidInfo);
                expertRandomInviteTask.addInviteTaskByMeetingCreate(meeting.getId(), now);
                LambdaUpdateWrapper<Meeting> mUpdate = Wrappers.lambdaUpdate(Meeting.class)
                        .set(Meeting::getInviteStatus, false)
                        .eq(Meeting::getId, meeting.getId());
                meetingService.update(mUpdate);
                // 回避规则
                ExpertInviteAvoidRule avoidRule = new ExpertInviteAvoidRule();
                avoidRule.setMeetingId(meeting.getId());
                // 未传值时设置为0 表示不限制周参与次数
                avoidRule.setWeekInviteCount(ObjectUtil.defaultIfNull(avoidInfo.getWeekInviteCount(), 0));
                avoidRule.setAvoidOrgIds(CollUtils.joinByComma(avoidInfo.getAvoidOrgIdList()));
                avoidRule.setAvoidUnitIds(CollUtils.joinByComma(avoidInfo.getAvoidUnitIdList()));
                avoidRule.setAvoidExpertIds(CollUtils.joinByComma(avoidInfo.getExpertIds()));
                avoidRule.setAvoidType(avoidInfo.getAvoidType());
                inviteAvoidRuleService.save(avoidRule);
            } else {
                // 指定邀请
                AppointInviteRuleDTO appointRule = req.getAppointRule();
                Assert.notNull(appointRule, "指定邀请规则不能为空");
                Map<Long, ExpertUserFullInfo> usersMap = expertInviteHelper.checkAppointExpert(appointRule);
                // 处理指定抽取规则
                ExpertInviteRule appoint = getExpertInviteRule(appointRule, meeting.getId());
                inviteRuleService.save(appoint);
                Long ruleId = appoint.getId();
                List<MeetingExpert> experts = appointRule.getExpertIdList().stream().map(w -> {
                    ExpertUserFullInfo info = usersMap.get(w);
                    MeetingExpert expert = ExpertInviteBuilder.getExpertByAppoint(meeting.getId(), info, ruleId);
                    expert.setStatus(NOTICING.getCode());
                    return expert;
                }).collect(Collectors.toList());
                meetingExpertService.saveBatch(experts);
            }
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    /**
     * 抽取数量校验
     *
     * @param req 抽取参数
     * @return {@link ExpertCountOnChangeVO}
     * @author WendyYang
     **/
    public ExpertCountOnChangeVO expertCountOnChange(ExpertInviteReq req) {
        // 未传递指定邀请专家直接忽略
        expertInviteHelper.checkAppointExpert(req.getAppointRule());
        Meeting meeting = meetingService.getById(req.getMeetingId());
        ExpertCountOnChangeVO resultCount = new ExpertCountOnChangeVO();
        resultCount.setStatus(true);
        resultCount.setCountList(new ArrayList<>());
        // 参数校验
        String errMsg = ValidUtil.validFast(req, AbstractInviteRule.CountCheck.class);
        if (errMsg != null) {
            resultCount.setStatus(false);
            resultCount.setMessage(errMsg);
            return resultCount;
        }
        List<Long> expertIdsChoose = new ArrayList<>();
        for (int i = 0; i < req.getRandomRules().size(); i++) {
            RandomInviteRuleDTO randomRule = req.getRandomRules().get(i);
            ExpertChooseDTO chooseExpert = expertInviteManage.expertInviteByRandomRule(req.getAvoidRule(),
                    randomRule, expertIdsChoose, meeting.getStartTime(), meeting.getEndTime(), meeting.getCreateOn());
            if (i < req.getRandomRules().size() - 1 && chooseExpert.getTotal() > 0) {
                List<Long> tempUserIds = CollUtils.fieldList(chooseExpert.getExperts(), ExpertUserFullInfo::getUserId);
                expertIdsChoose.addAll(tempUserIds);
            }
            resultCount.addCountList(chooseExpert.getTotal());
        }
        return resultCount;
    }

    /**
     * 专家会议列表
     *
     * @param req 查询参数
     * @return 会议列表
     * @author WendyYang
     **/
    public PageVo<MeetingByManagerVO> meetingListForExpert(MeetingListReq req) {
        Long expertId = req.getExpertId() != null ? req.getExpertId() : LoginUserUtil.getUserId();
        List<ExpertInvitedRecordDTO> meetings = meetingExpertService.listByExpertIdAndStatus(expertId, null, null);
        if (meetings.isEmpty()) {
            return PageVo.empty();
        }
        Map<Long, ExpertInvitedRecordDTO> mapByMeetingId = new HashMap<>(16);
        List<Long> meetingExpertIds = new ArrayList<>();
        meetings.forEach(w -> {
            mapByMeetingId.put(w.getMeetingId(), w);
            meetingExpertIds.add(w.getMeetingExpertId());
        });
        LambdaQueryWrapper<Meeting> query = new LambdaQueryWrapper<Meeting>()
                .orderByDesc(Meeting::getCreateOn)
                .in(Meeting::getId, mapByMeetingId.keySet())
                .eq(Meeting::getConfirmedRoster, Boolean.TRUE)
                .ne(Meeting::getStatus, MeetingStatusEnum.CANCELED.getCode());
        if (req.getExpertId() == null) {
            meetingManageHelper.buildMeetingQuery(query, req);
        }
        Page<Meeting> page = meetingService.page(req.page(), query);
        if (page.getTotal() == 0) {
            return PageVo.empty();
        }
        List<MeetingExpertJudge> expertJudges = expertJudgeService.listByMeetingExpertIds(meetingExpertIds);
        Map<Long, MeetingExpertJudge> expertJudgeMap = CollUtils.listToMap(expertJudges, MeetingExpertJudge::getMeetingExpertId);
        PageVo<MeetingByManagerVO> result = new PageVo<>(new ArrayList<>(), page.getTotal());
        page.getRecords().forEach(meeting -> {
            MeetingByManagerVO item = meetingManageHelper.buildByMeeting(meeting);
            ExpertInvitedRecordDTO info = mapByMeetingId.get(meeting.getId());
            item.setMeetingExpertId(info.getMeetingExpertId());
            item.setExpertStatus(info.getStatus());
            MeetingExpertJudge judge = expertJudgeMap.get(info.getMeetingExpertId());
            item.setHasJudge(judge != null);
            if (judge != null) {
                item.setJudgeScore(judge.getScore());
            }
            result.getRecords().add(item);
        });
        return result;
    }

    public PageVo<MeetingByManagerVO> meetingListForLeave(MeetingLeaveListReq req) {
        Long expertId = ObjectUtil.defaultIfNull(req.getExpertId(), LoginUserUtil.getUserId());
        LambdaQueryWrapper<MeetingExpert> meQuery = Wrappers.lambdaQuery(MeetingExpert.class)
                .eq(MeetingExpert::getExpertId, expertId)
                .in(MeetingExpert::getStatus, AGREED.getCode(), ON_LEAVE.getCode());
        List<MeetingExpert> meetings = meetingExpertService.list(meQuery);
        if (meetings.isEmpty()) {
            return PageVo.empty();
        }
        Map<Long, MeetingExpert> meetingMap = new HashMap<>(16);
        meetings.forEach(w -> meetingMap.put(w.getMeetingId(), w));
        LambdaQueryWrapper<Meeting> query = new LambdaQueryWrapper<Meeting>()
                .orderByAsc(Meeting::getStartTime)
                .in(Meeting::getId, meetingMap.keySet())
                .eq(Meeting::getConfirmedRoster, Boolean.TRUE)
                .ne(Meeting::getStatus, MeetingStatusEnum.CANCELED.getCode());
        Page<Meeting> page = meetingService.page(req.page(), query);
        if (page.getTotal() == 0) {
            return PageVo.empty();
        }
        PageVo<MeetingByManagerVO> result = new PageVo<>(new ArrayList<>(), page.getTotal());
        page.getRecords().forEach(meeting -> {
            MeetingByManagerVO item = meetingManageHelper.buildByMeeting(meeting);
            MeetingExpert info = meetingMap.get(meeting.getId());
            item.setExpertStatus(info.getStatus());
            result.getRecords().add(item);
        });
        return result;
    }


    /**
     * 管理员会议列表
     *
     * @param req 查询参数
     * @return 会议列表
     * @author WendyYang
     */
    public PageVo<MeetingByManagerVO> meetingListForManager(MeetingListReq req) {
        LambdaQueryWrapper<Meeting> query = new LambdaQueryWrapper<Meeting>()
                .eq(req.getStatus() != null, Meeting::getStatus, req.getStatus())
                .orderByDesc(Meeting::getCreateOn);
        meetingManageHelper.buildMeetingQuery(query, req);
        Page<Meeting> page = meetingService.page(req.page(), query);
        if (page.getTotal() == 0) {
            return PageVo.empty();
        }
        PageVo<MeetingByManagerVO> result = new PageVo<>(new ArrayList<>(), page.getTotal());
        LocalDateTime now = LocalDateTime.now();
        List<Long> meetingIds = page.getRecords().stream()
                .filter(w -> {
                    if (!MeetingStatusEnum.NORMAL.eq(w.getStatus())) {
                        return Boolean.FALSE;
                    }
                    if (!Boolean.TRUE.equals(w.getIsInnerProject())) {
                        return Boolean.FALSE;
                    }
                    return w.getEndTime().isBefore(now);
                }).map(Meeting::getId)
                .collect(Collectors.toList());
        Map<Long, Boolean> projectReviewProgress;
        if (!meetingIds.isEmpty()) {
            projectReviewProgress = getMeetingProjectReviewProgress(meetingIds);
        } else {
            projectReviewProgress = Collections.emptyMap();
        }
        page.getRecords().forEach(meeting -> {
            MeetingByManagerVO item = meetingManageHelper.buildByMeeting(meeting);
            item.setAllProjectReviewed(projectReviewProgress.get(meeting.getId()));
            result.getRecords().add(item);
        });
        return result;
    }

    private Map<Long, Boolean> getMeetingProjectReviewProgress(Collection<Long> meetingIds) {
        Wrapper<MeetingInnerProject> query = Wrappers.lambdaQuery(MeetingInnerProject.class)
                .select(MeetingInnerProject::getReviewResult, MeetingInnerProject::getMeetingId)
                .in(MeetingInnerProject::getMeetingId, meetingIds);
        List<MeetingInnerProject> projects = meetingInnerProjectService.list(query);
        return projects.stream().collect(Collectors.groupingBy(MeetingInnerProject::getMeetingId,
                Collectors.collectingAndThen(Collectors.toList(),
                        w -> w.stream().noneMatch(x -> x.getReviewResult() == null))));
    }

    public MeetingDetailBasicVO getMeetingDetail(Long meetingId) {
        Meeting meeting = meetingService.getById(meetingId);
        Assert.notNull(meeting, "会议不存在");
        MeetingDetailBasicVO detail = MeetingDetailBasicVO.builder()
                .meetingId(meeting.getId())
                .meetingName(meeting.getName())
                .meetingType(meeting.getType())
                .meetingAddress(meeting.getMeetingAddress())
                .typeName(getDictName(meeting.getType()))
                .startTime(meeting.getStartTime())
                .endTime(meeting.getEndTime())
                .contact(meeting.getContact())
                .connecter(meeting.getConnecter())
                .status(meeting.getStatus())
                .holdOrg(meeting.getHoldOrg())
                .createOn(meeting.getCreateOn())
                .creator(meeting.getCreator())
                .createBy(meeting.getCreator())
                .inviteType(meeting.getInviteType())
                .confirmedRoster(meeting.getConfirmedRoster())
                .invitedStopped(meeting.getInviteStatus())
                .attachFiles(meeting.getAttachFiles())
                .description(meeting.getDescription())
                .resultDescription(meeting.getResultDescription())
                .resultAttachFiles(meeting.getResultAttachFiles())
                .remark(meeting.getRemark())
                .build();
        if (Boolean.TRUE.equals(meeting.getIsInnerProject())) {
            List<MeetingInnerProject> innerProjects = meetingInnerProjectService.listByMeetingId(meetingId);
            Map<Long, MeetingInnerProject> innerProjectMap = CollUtils.listToMap(innerProjects, MeetingInnerProject::getProjectId);
            List<Project> projects = projectService.listByIds(innerProjectMap.keySet());
            List<MeetingReviewProjectDTO> convert = CollUtils.convert(projects, w -> {
                MeetingReviewProjectDTO mrp = MeetingReviewProjectBuilder.convert(w);
                MeetingInnerProject mip = innerProjectMap.get(mrp.getProjectId());
                mrp.setMeetingProjectId(mip.getId());
                mrp.setReviewResultDetail(ProjectReviewResultBuilder.convert(mip));
                return mrp;
            });
            detail.setProjects(convert);
        }
        detail.setInviteRule(inviteRuleDetail(meetingId));
        return detail;
    }

    public ExpertInviteDetailVO inviteExpertList(Long meetingId) {
        Meeting meeting = meetingService.getById(meetingId);
        if (meeting == null) {
            throw BizException.wrap("该会议信息不存在");
        }
        ExpertInviteDetailVO result = new ExpertInviteDetailVO();
        result.setInvitedStopped(meeting.getInviteStatus());
        result.setConfirmedRoster(meeting.getConfirmedRoster());
        List<MeetingExpert> experts = meetingExpertService.listByMeetingId(meetingId);
        if (experts.isEmpty()) {
            return result;
        }
        List<Long> expertIds = CollUtils.fieldList(experts, MeetingExpert::getExpertId);
        Map<Long, ExpertBasicInfoVO> expertMap = meetingManageHelper.getExpertBasicInfo(expertIds);
        BiFunction<MeetingExpert, Map<Long, ExpertInviteRule>, InviteExpertListItemVO> mapping = (me, ruleMap) -> {
            ExpertBasicInfoVO expert = expertMap.get(me.getExpertId());
            InviteExpertListItemVO item = BeanUtil.copyProperties(expert, InviteExpertListItemVO.class);
            item.setExpertMeetingId(me.getId());
            item.setMeetingId(me.getMeetingId());
            item.setStatus(me.getStatus());
            item.setStatusName(ExpertAttendStatusEnum.getValByCode(me.getStatus()));
            item.setMobile(me.getMobile());
            item.setNoticeTime(me.getCreateOn());
            item.setRuleId(me.getRuleId());
            item.setIsHeadman(me.getIsHeadman());
            ExpertInviteRule rule = ruleMap.get(me.getRuleId());
            item.setInviteType(rule == null ? APPOINT.getCode() : rule.getInviteType());
            item.setNoticeStatus(NOTICING.eq(me.getStatus()) ? "通知中" : "已通知");
            return item;
        };
        List<ExpertInviteRule> inviteRules = inviteRuleService.listByMeetingId(meetingId);
        Map<Long, ExpertInviteRule> ruleMap = CollUtils.listToMap(inviteRules, ExpertInviteRule::getId);
        boolean isRandom = ExpertInviteTypeEnum.RANDOM.eq(meeting.getInviteType());
        if (isRandom) {
            result.setInviteStatistics(new ArrayList<>());
            Map<Long, List<MeetingExpert>> groupByRule = CollUtils.group(experts, MeetingExpert::getRuleId);
            ruleMap.forEach((k, v) -> {
                InviteStatisticsByRuleVO statistics = InviteStatisticsByRuleVO.init(k);
                statistics.setInviteCnt(v.getInviteCount());
                List<MeetingExpert> expertList = groupByRule.get(k);
                if (expertList != null) {
                    expertList.forEach(expert -> {
                        if (AGREED.eq(expert.getStatus())) {
                            statistics.incrAgreeCnt();
                        }
                        statistics.incrNoticedCnt();
                    });
                }
                result.getInviteStatistics().add(statistics);
            });
        }
        List<InviteExpertListItemVO> converts = CollUtils.convert(experts, me -> mapping.apply(me, ruleMap));
        result.setInviteExpertList(converts);
        // 确定参加列表
        return result;
    }

    /**
     * 会议基本信息修改
     *
     * @param po 修改参数
     **/
    public void meetingBasicInfoModify(MeetingBasicInfoModifyReq po) {
        Meeting meeting = new Meeting();
        BeanUtil.copyProperties(po, meeting);
        LocalDateTime now = LocalDateTime.now();
        Meeting old = meetingService.getById(po.getId());
        List<MeetingExpert> meList = meetingExpertService.listAgreedExperts(Collections.singletonList(po.getId()));
        if (!meList.isEmpty() && old.getStartTime().isAfter(now)) {
            // TODO
            /*String meetingType = dictionaryCache.getByCode(old.getType()).getName();
              List<SendSmsContext> contexts = YxtSmsContextBuilder.smsToExpertByMeetingChange(old, meeting, meList, meetingType);
              yxtCallOrSmsHelper.sendSms(contexts);*/
        }
        meetingService.updateById(meeting);
    }

    /**
     * 抽取规则详情
     *
     * @param meetingId 会议ID
     * @return {@link InviteRuleDetailVO}
     * @author WendyYang
     **/
    public InviteRuleDetailVO inviteRuleDetail(Long meetingId) {
        InviteRuleDetailVO result = new InviteRuleDetailVO();
        List<ExpertInviteRule> inviteRules = inviteRuleService.listByMeetingId(meetingId);
        result.setRandomRules(new ArrayList<>());
        Map<ExpertInviteTypeEnum, List<ExpertInviteRule>> groupByType = CollUtils.group(inviteRules, w -> ExpertInviteTypeEnum.getByCode(w.getInviteType()));
        List<ExpertInviteRule> randoms = groupByType.get(ExpertInviteTypeEnum.RANDOM);
        if (CollUtil.isNotEmpty(randoms)) {
            randoms.forEach(random -> {
                RandomInviteRuleVO randomRule = JSON.parseObject(random.getInviteRule(), RandomInviteRuleVO.class);
                randomRule.setId(random.getId());
                if (randomRule.getExpertTags() != null) {
                    randomRule.getExpertTags().forEach(w -> {
                        List<String> tagNames = CollUtils.convert(w.getTagCodes(), tagCode -> tagCache.getByTagCode(tagCode).getTagName());
                        w.setTagCodes(tagNames);
                    });
                }
                if (randomRule.getExpertDicts() != null) {
                    randomRule.getExpertDicts().forEach(w -> {
                        List<String> dictNames = CollUtils.convert(w.getDictCodes(), this::getDictName);
                        w.setDictCodes(dictNames);
                    });
                }
                if (StrUtil.isNotEmpty(randomRule.getIntentionRegionCode())) {
                    List<RegionDTO> intentionRegions = regionCache.listParents(randomRule.getIntentionRegionCode(), randomRule.getIntentionRegionLevel());
                    randomRule.setIntentionRegions(intentionRegions);
                }
                if (CollUtil.isNotEmpty(randomRule.getExpertRegionList())) {
                    List<RegionDTO> regions = CollUtils.convert(randomRule.getExpertRegionList(),
                            w -> regionCache.getByCodeAndLevel(w.getRegionCode(), w.getRegionLevel()));
                    randomRule.setExpertRegions(regions);
                }
                result.getRandomRules().add(randomRule);
            });
            AvoidRuleDTO avoidInfo = inviteAvoidRuleService.getAvoidInfo(meetingId);
            AvoidInfoVO vo = new AvoidInfoVO();
            vo.setAvoidType(avoidInfo.getAvoidType());
            vo.setWeekInviteCount(avoidInfo.getWeekInviteCount());
            vo.setAvoidOrgIds(avoidInfo.getAvoidOrgIdList());
            vo.setAvoidUnitIds(avoidInfo.getAvoidUnitIdList());
            if (CollUtil.isNotEmpty(vo.getAvoidOrgIds())) {
                vo.setAvoidOrgs(belongOrgService.listNameByCodes(avoidInfo.getAvoidOrgIdList()));
            }
            if (CollUtil.isNotEmpty(vo.getAvoidUnitIds())) {
                vo.setAvoidUnits(dingOrganizationService.listNameByCodes(avoidInfo.getAvoidUnitIdList()));
            }
            if (CollUtil.isNotEmpty(avoidInfo.getExpertIds())) {
                vo.setExperts(new ArrayList<>(meetingManageHelper.getExpertBasicInfo(avoidInfo.getExpertIds()).values()));
            }
            result.setAvoidInfo(vo);
        } else {
            List<ExpertInviteRule> appoints = groupByType.get(APPOINT);
            if (!CollUtil.isEmpty(appoints)) {
                ExpertInviteRule appoint = appoints.get(0);
                AppointInviteRuleDTO appointRule = JSON.parseObject(appoint.getInviteRule(), AppointInviteRuleDTO.class);
                appointRule.setId(appoint.getId());
                AppointRuleVO vo = new AppointRuleVO();
                vo.setInviteDesc(appointRule.getInviteDesc());
                vo.setExperts(new ArrayList<>(meetingManageHelper.getExpertBasicInfo(appointRule.getExpertIdList()).values()));
                result.setAppointRule(vo);
            }
        }
        return result;
    }

    public void batchAppointExperts(BatchAppointExpertsReq req) {
        Long meetingId = req.getMeetingId();
        String key = "BATCH_APPOINT_EXPERT:" + meetingId;
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("补充专家中，请勿重复点击");
        }
        try {
            Meeting meeting = meetingService.getById(meetingId);
            if (!APPOINT.eq(meeting.getInviteType())) {
                throw BizException.wrap("该会议不能指定邀请专家");
            }
            if (meeting.getConfirmedRoster()) {
                throw BizException.wrap("补充专家失败，已确认过专家名单");
            }
            if (!MeetingStatusEnum.NORMAL.eq(meeting.getStatus())) {
                throw BizException.wrap("补充专家失败");
            }
            AppointInviteRuleDTO rule = inviteRuleService.appointRuleByMeetingId(meetingId);
            List<ExpertUserFullInfo> userInfos = meetingManageHelper.appointExpertCheck(meetingId, meeting.getInviteType(), req.getExpertIdList());
            List<MeetingExpert> expertList = CollUtils.convert(userInfos, w -> {
                MeetingExpert me = ExpertInviteBuilder.getExpertByAppoint(meetingId, w, rule.getId());
                me.setStatus(NOTICING.getCode());
                return me;
            });
            meetingExpertService.saveBatch(expertList);
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    public void stopRandomInvite(Long meetingId) {
        expertRandomInviteTask.cancelByMeetingIdAndKillTask(meetingId);
    }

    @Transactional(rollbackFor = Exception.class)
    public void cancelMeeting(MeetingCancelReq req) {
        Long meetingId = req.getMeetingId();
        String key = "CANCEL_MEETING:" + meetingId;
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("正在取消会议，请刷新后重试");
        }
        try {
            Meeting meeting = meetingService.getById(meetingId);
            if (MeetingStatusEnum.CANCELED.eq(meeting.getStatus())) {
                throw BizException.wrap("会议已取消");
            }
            if (meeting.getStartTime().isBefore(LocalDateTime.now())) {
                throw BizException.wrap("会议已开始，暂时无法取消");
            }
            LambdaUpdateWrapper<Meeting> meetingUpdate = Wrappers.lambdaUpdate(Meeting.class)
                    .set(Meeting::getStatus, MeetingStatusEnum.CANCELED.getCode())
                    .eq(Meeting::getId, meetingId);
            meetingService.update(meetingUpdate);
            expertRandomInviteTask.cancelByMeetingIdAndKillTask(meetingId);
            // 发送通知给专家
            List<MeetingExpert> experts = meetingExpertService.listAgreedExperts(meetingId);
            if (!experts.isEmpty()) {
                meetingCallOrMsgHelper.sendCancelMeetingMsg(experts, meeting);
            }
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    public ExpertInvitationDetailVO expertInvitationDetail(Long meetingId, Long expertId) {
        Long userId = expertId == null ? LoginUserUtil.getUserId() : expertId;
        MeetingExpert me = meetingExpertService.getByMeetingIdAndExpertId(meetingId, userId);
        if (me == null) {
            throw BizException.wrap("未被邀请参加此会议");
        }
        if (AGREED.eq(me.getStatus())) {
            throw BizException.wrap("未确认参加此会议");
        }
        ExpertUserFullInfo userInfo = expertUserFullInfoService.getByUserId(userId);
        Meeting meeting = meetingService.getById(meetingId);
        return ExpertInvitationDetailVO.builder()
                .expertName(userInfo.getExpertName())
                .holdOrg(meeting.getHoldOrg())
                .meetingName(meeting.getName())
                .startTime(meeting.getStartTime())
                .endTime(meeting.getEndTime())
                .inviteTime(me.getCreateOn())
                .connecter(meeting.getConnecter())
                .contact(meeting.getContact())
                .build();
    }

    public void confirmAttendByManager(ExpertConfirmReq req) {
        String key = "CONFIRM_ATTEND:" + req.getExpertMeetingId();
        String msgPrefix = req.getAgreed() ? "确认参加" : "拒绝参加";
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("%s失败，请重试！", msgPrefix);
        }
        try {
            MeetingExpert me = meetingExpertService.getById(req.getExpertMeetingId());
            if (!NOTICING.eq(me.getStatus())) {
                throw BizException.wrap("%s失败，请重试！", msgPrefix);
            }
            LambdaUpdateWrapper<MeetingExpert> update = Wrappers.lambdaUpdate(MeetingExpert.class)
                    .set(MeetingExpert::getStatus, (req.getAgreed() ? AGREED : REFUSED).getCode())
                    .eq(MeetingExpert::getId, req.getExpertMeetingId());
            meetingExpertService.update(update);
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void expertRemove(ExpertRemoveReq req) {
        String key = "EXPERT_REMOVE:" + req.getExpertMeetingId();
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("删除专家失败，请重试！");
        }
        try {
            Meeting meeting = meetingService.getById(req.getMeetingId());
            if (MeetingStatusEnum.CANCELED.eq(meeting.getStatus())) {
                throw BizException.wrap("会议已取消！");
            }
            if (LocalDateTime.now().isAfter(meeting.getStartTime())) {
                throw BizException.wrap("会议已开始，不允许移除专家！");
            }
            MeetingExpert expert = meetingExpertService.getById(req.getExpertMeetingId());
            if (!APPOINT.eq(expert.getInviteType())) {
                throw BizException.wrap("随机抽取的专家不允许移除！");
            }
            if (!NOTICING.eq(expert.getStatus())) {
                throw BizException.wrap("已确认过的专家不允许移除！");
            }
            LambdaUpdateWrapper<Meeting> mUpdate = Wrappers.lambdaUpdate(Meeting.class)
                    .set(Meeting::getConfirmedRoster, false)
                    .eq(Meeting::getId, req.getMeetingId());
            meetingService.update(mUpdate);
            meetingExpertService.removeById(req.getExpertMeetingId());
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    public void releaseExperts(MeetingCancelReq req) {
        String key = "EXPERT_RELEASE:" + req.getMeetingId();
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("释放专家失败，请重试！");
        }
        try {
            Meeting meeting = meetingService.getById(req.getMeetingId());
            if (MeetingStatusEnum.CANCELED.eq(meeting.getStatus())) {
                throw BizException.wrap("会议已取消");
            }
            LambdaUpdateWrapper<MeetingExpert> update = Wrappers.lambdaUpdate(MeetingExpert.class)
                    .set(MeetingExpert::getStatus, RELEASED.getCode())
                    .eq(MeetingExpert::getMeetingId, req.getMeetingId());
            meetingExpertService.update(update);
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void setUpHeadman(ExpertConfirmReq req) {
        String key = "SETUP_HEADMAN:" + req.getMeetingId();
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("设置专家组长失败，请重试！");
        }
        try {
            Meeting meeting = meetingService.getById(req.getMeetingId());
            if (MeetingStatusEnum.CANCELED.eq(meeting.getStatus())) {
                throw BizException.wrap("会议已取消");
            }
            LambdaUpdateWrapper<MeetingExpert> cancel = Wrappers.lambdaUpdate(MeetingExpert.class)
                    .set(MeetingExpert::getIsHeadman, Boolean.FALSE)
                    .eq(MeetingExpert::getIsHeadman, Boolean.TRUE)
                    .eq(MeetingExpert::getMeetingId, req.getMeetingId());
            meetingExpertService.update(cancel);
            MeetingExpert headman = meetingExpertService.getById(req.getExpertMeetingId());
            if (!AGREED.eq(headman.getStatus())) {
                ExpertAttendStatusEnum status = getByCode(headman.getStatus());
                throw BizException.wrap("该专家处于：%s状态，不能被设置为专家组长！", status.getValue());
            }
            LambdaUpdateWrapper<MeetingExpert> setup = Wrappers.lambdaUpdate(MeetingExpert.class)
                    .set(MeetingExpert::getIsHeadman, Boolean.TRUE)
                    .eq(MeetingExpert::getId, req.getExpertMeetingId());
            meetingExpertService.update(setup);
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    public void confirmedRoster(ConfirmedRosterReq req) {
        Long meetingId = req.getMeetingId();
        String key = "MEETING_RESEND_SMS:" + meetingId;
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("请刷新后重试！");
        }
        try {
            Meeting meeting = meetingService.getById(meetingId);
            if (!MeetingStatusEnum.NORMAL.eq(meeting.getStatus())) {
                throw BizException.wrap("请刷新后重试！");
            }
            if (!meeting.getConfirmedRoster()) {
                // 首次确认的
                LambdaUpdateWrapper<Meeting> update = Wrappers.lambdaUpdate(Meeting.class);
                update.set(Meeting::getConfirmedRoster, Boolean.TRUE);
                update.eq(Meeting::getId, meetingId);
                meetingService.update(update);
            }
            List<MeetingExpert> experts = meetingExpertService.listAgreedExperts(meetingId);
            List<MeetingExpert> expertNoticing = experts.stream()
                    .filter(w -> meeting.getConfirmedRoster() || !w.getConfirmedRoster())
                    .collect(Collectors.toList());
            if (expertNoticing.isEmpty()) {
                return;
            }
            List<Long> currConfirmedMeIds = CollUtils.fieldList(expertNoticing, MeetingExpert::getId);
            LambdaUpdateWrapper<MeetingExpert> meUpdate = Wrappers.lambdaUpdate(MeetingExpert.class)
                    .in(MeetingExpert::getId, currConfirmedMeIds)
                    .set(MeetingExpert::getConfirmedRoster, Boolean.TRUE);
            meetingExpertService.update(meUpdate);
            meetingCallOrMsgHelper.sendConfirmedRosterMsg(expertNoticing, meeting);
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    public PageVo<ReviewProjectDTO> pageReviewProject(ReviewProjectListReq req) {
        Page<ReviewProjectDTO> page = meetingExpertService.pageReviewProjectList(req);
        return PageVo.of(page.getRecords(), page.getTotal());
    }

    public PageVo<MeetingReviewProjectVO> optionProject(MeetingOptionProjectReq req) {
        String meetingType = req.getMeetingType();
        LambdaQueryWrapper<Project> query = Wrappers.lambdaQuery(Project.class)
                .eq(Project::getNewest, Boolean.TRUE);
        switch (MeetingReviewTypeEnum.getByCode(meetingType)) {
            case PRELIMINARY_SCHEME_REVIEW:
                buildOptionProjectQuery(query, meetingType, ProjectStatusEnum.PRE_APPLYING);
                break;
            case CONSTRUCTION_SCHEME_REVIEW:
                buildOptionProjectQuery(query, meetingType, ProjectStatusEnum.SCHEME_UNDER_REVIEW);
                break;
            case ACCEPTANCE_SCHEME_REVIEW:
                buildOptionProjectQuery(query, meetingType, ProjectStatusEnum.FINAL_ACCEPTANCE_IS_UNDER_REVIEW);
                break;
            case DEPT_JOIN_REVIEW:
                buildOptionProjectQuery(query, meetingType, ProjectStatusEnum.DEPARTMENT_JOINT_REVIEW);
                break;
            case FAIRNESS_REVIEW:
                break;
            default:
                return PageVo.empty();
        }
        Page<Project> page = projectService.page(req.page(), query);
        PageVo<MeetingReviewProjectVO> result = PageVo.of(null, page.getTotal());
        if (result.getTotal() > 0) {
            result.setRecords(CollUtils.convert(page.getRecords(), MeetingReviewProjectBuilder::to));
        }
        return result;
    }

    public List<MeetingReviewProjectVO> projectsByMeetingId(Long meetingId) {
        Meeting meeting = meetingService.getById(meetingId);
        if (Boolean.TRUE.equals(meeting.getIsInnerProject())) {
            List<MeetingInnerProject> projects = meetingInnerProjectService.listByMeetingId(meetingId);
            Map<Long, MeetingInnerProject> projectMap = CollUtils.listToMap(projects, MeetingInnerProject::getProjectId);
            List<Project> projectList = projectService.listByIds(projectMap.keySet());
            return CollUtils.convert(projectList, w -> {
                MeetingReviewProjectVO vo = MeetingReviewProjectBuilder.to(w);
                MeetingInnerProject mip = projectMap.get(w.getId());
                vo.setMeetingProjectId(mip.getId());
                vo.setReviewResultDetail(ProjectReviewResultBuilder.convert(mip));
                return vo;
            });
        }
        return Collections.emptyList();
    }

    private void buildOptionProjectQuery(LambdaQueryWrapper<Project> query, String meetingType, ProjectStatusEnum status) {
        query.eq(Project::getStatus, status.getCode());
        String sql = String.format("select 1 from meeting m inner join meeting_inner_project mip on" +
                " m.is_inner_project = 1 and m.id = mip.meeting_id and nd_project.id = mip.project_id" +
                " and m.type = %s and m.status != 3", meetingType);
        String sql2 = String.format("1 = (select case when review_result in (2,3) then 1 else 0 end from " +
                " (select ner.review_result from meeting m inner join meeting_inner_project mip on" +
                " m.is_inner_project = 1 and m.id = mip.meeting_id and nd_project.id = mip.project_id" +
                " and m.type = %s and m.status != 3 left join nd_expert_review ner on ner.meeting_id = m.id " +
                " and ner.project_id = mip.project_id and ner.is_final = 1 order by m.create_on desc limit 1) t)", meetingType);
        query.and(q1 -> q1.notExists(sql).or(q2 -> q2.apply(sql2)));
    }

    public void uploadMeetingResult(MeetingResultUploadReq req) {
        String key = "meeting_result_upload:" + req.getMeetingId();
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("正在上传会议结果，请勿重复操作");
        }
        try {
            Meeting meeting = meetingService.getById(req.getMeetingId());
            if (meeting == null || MeetingStatusEnum.CANCELED.eq(meeting.getStatus())) {
                throw BizException.wrap("会议不存在或已取消");
            }
            LocalDateTime now = LocalDateTime.now();
            if (meeting.getEndTime().isAfter(now)) {
                throw BizException.wrap("会议未结束");
            }
            Wrapper<Meeting> update = Wrappers.lambdaUpdate(Meeting.class)
                    .set(Meeting::getResultDescription, req.getResultDescription())
                    .set(Meeting::getResultAttachFiles, req.getResultAttachFiles())
                    .set(Meeting::getUpdateOn, now)
                    .eq(Meeting::getId, req.getMeetingId());
            meetingService.update(update);
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    public MeetingResultUploadReq meetingResultDetail(Long meetingId) {
        Meeting meeting = meetingService.getById(meetingId);
        if (StrUtil.isBlank(meeting.getResultDescription())) {
            throw BizException.wrap("未上传会议结果");
        }
        MeetingResultUploadReq result = new MeetingResultUploadReq();
        result.setResultDescription(meeting.getResultDescription());
        result.setResultAttachFiles(meeting.getResultAttachFiles());
        return result;
    }

    public void uploadProjectReviewResult(ProjectReviewResultUploadReq req) {
        String key = "meeting_project_result_upload:" + req.getMeetingProjectId();
        if (!distributedLock.lock(key, RETRY_TIMES)) {
            throw BizException.wrap("正在上传会议结果，请勿重复操作");
        }
        try {
            Meeting meeting = meetingService.getById(req.getMeetingId());
            if (meeting == null || MeetingStatusEnum.CANCELED.eq(meeting.getStatus())) {
                throw BizException.wrap("会议不存在或已取消");
            }
            LocalDateTime now = LocalDateTime.now();
            if (meeting.getEndTime().isAfter(now)) {
                throw BizException.wrap("会议未结束");
            }
            if (!Boolean.TRUE.equals(meeting.getIsInnerProject())) {
                throw BizException.wrap("此会议未关联项目");
            }
            MeetingInnerProject mip = BeanUtil.copyProperties(req, MeetingInnerProject.class);
            mip.setId(req.getMeetingProjectId());
            meetingInnerProjectService.updateById(mip);
        } finally {
            distributedLock.releaseLock(key);
        }
    }

    public List<ExpertEvalListItemVO> listExpertJudge(Long meetingId) {
        List<MeetingExpert> experts = meetingExpertService.listAgreedExperts(meetingId);
        if (experts.isEmpty()) {
            return Collections.emptyList();
        }
        Map<Long, Long> expertJudgeMap = meetingExpertJudgeService.getExpertJudgeIdMap(meetingId);
        List<Long> expertIds = CollUtils.fieldList(experts, MeetingExpert::getExpertId);
        Map<Long, ExpertBasicInfoVO> expertMap = meetingManageHelper.getExpertBasicInfo(expertIds);
        return experts.stream().map(w -> {
            Long expertId = w.getExpertId();
            ExpertEvalListItemVO item = new ExpertEvalListItemVO();
            BeanUtil.copyProperties(expertMap.get(expertId), item);
            item.setExpertJudgeId(expertJudgeMap.get(expertId));
            return item;
        }).collect(Collectors.toList());
    }

}
