# ItticketServiceImpl.java

    package cloud.wconnect.it.service.itrequest.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.cons.CommonErrorI18nConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.UserDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.RoleInternalService;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.it.cons.S3BucketFunctionCons;
    import cloud.wconnect.it.cons.itrequest.ItRequestConstant;
    import cloud.wconnect.it.cons.itrequest.ItRequestNotifyEnum;
    import cloud.wconnect.it.cons.itrequest.ItRequestStatusEnum;
    import cloud.wconnect.it.entity.itrequest.Itticket;
    import cloud.wconnect.it.entity.itrequest.ItticketComment;
    import cloud.wconnect.it.entity.itrequest.ItticketOperationLog;
    import cloud.wconnect.it.entity.itrequest.ItticketPriority;
    import cloud.wconnect.it.mapper.itrequest.ItticketMapper;
    import cloud.wconnect.it.pojo.dto.itrequest.*;
    import cloud.wconnect.it.pojo.query.itrequest.ItticketQuery;
    import cloud.wconnect.it.service.itrequest.IItticketCommentService;
    import cloud.wconnect.it.service.itrequest.IItticketOperationLogService;
    import cloud.wconnect.it.service.itrequest.IItticketPriorityService;
    import cloud.wconnect.it.service.itrequest.IItticketService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.concurrent.Executor;
    import java.util.stream.Collectors;

    /**
     * <p>
     *  服务实现类
     * </p>
     *
     * @author yyt
     * @since 2023-03-25
     */
    @DS("it_manage")
    @Service
    public class ItticketServiceImpl extends ServiceImpl<ItticketMapper, Itticket> implements IItticketService {

        @Autowired
        private IItticketCommentService itticketCommentService;
        @Autowired
        private IItticketOperationLogService itticketOperationLogService;
        @Autowired
        private IItticketPriorityService itticketPriorityService;
        @Autowired
        private RoleInternalService roleInternalService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private Executor executor;

        @Value("#{${aws-s3.function-bucket}}")
        private Map<String, Map<String, Object>> bucketMap;
        @Value("${aws-s3.region}")
        private String region;
        @Value("${default.timezone}")
        private String defaultTimezone;

        /**
         * 添加 itticket
         *
         * @param propertyId        资产 id
         * @param updateItticketDTO itticket 信息
         * @param userId            操作人
         * @return itticket
         * @author yyt
         * @date 2023/3/25
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public Itticket saveItticket(int propertyId, UpdateItticketDTO updateItticketDTO, int userId) {
            Itticket itticket = beanMapper.map(updateItticketDTO, Itticket.class);
            itticket.setPropertyId(propertyId);
            itticket.setUserId(userId);
            itticket.setTicketCreateTime(DateTimeUtils.nowTimestamp());
            save(itticket);

            itticketCommentService.saveItticketAttachment(propertyId, itticket.getId(), updateItticketDTO.getAttachments(), userId);

            // 保存 log
            itticketOperationLogService.save(propertyId, userId, itticket.getId(), ItRequestNotifyEnum.OPERATION_CREATED.getCode(), null);

            return itticket;
        }

        /**
         * 校验存在
         *
         * @param id itticket id
         * @return itticket
         * @author yyt
         * @date 2023/3/25
         */
        @Override
        public Itticket checkExist(int id) {
            Itticket itticket = getById(id);
            if (Objects.isNull(itticket)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the itticket is not found;id:" + id);
            }
            return itticket;
        }

        /**
         * 修改 itticket
         *
         * @param updateBeforeItticket 修改之前的 itticket
         * @param updateItticketDTO    itticket 信息
         * @param userId               操作人
         * @return itticket
         * @author yyt
         * @date 2023/3/25
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public Itticket updateItticket(Itticket updateBeforeItticket, UpdateItticketDTO updateItticketDTO, int userId) {
            Integer itticketId = updateBeforeItticket.getId();
            Itticket updateContent = beanMapper.map(updateItticketDTO, Itticket.class);
            updateContent.setId(itticketId);
            updateById(updateContent);

            //查询修改前的附件
            List<String> updateBeforeContentList = itticketCommentService.list(new QueryWrapper<ItticketComment>().lambda()
                            .eq(ItticketComment::getItticketId, itticketId)
                            .eq(ItticketComment::getCommentType, ItticketComment.TYPE_ITTICKET_ATTACHMENT))
                    .stream().map(ItticketComment::getContent).toList();

            itticketCommentService.clearItticketAttachment(itticketId);

            Integer propertyId = updateBeforeItticket.getPropertyId();
            itticketCommentService.saveItticketAttachment(propertyId, itticketId, updateItticketDTO.getAttachments(), userId);

            // 记录操作日志
            itticketOperationLogService.save(propertyId, userId, itticketId, ItRequestNotifyEnum.OPERATION_UPDATED.getCode(), null);

            updateContent.setPropertyId(propertyId).setUserId(updateBeforeItticket.getUserId());

            TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                //修改前的s3Keys
                List<String> updateBeforeS3Keys = AttachmentUtil.getKeysByAttachments(updateBeforeContentList, objectMapper);
                //修改后的s3Keys
                List<String> updateAfterS3Keys = AttachmentUtil.getKeysByAttachments(updateItticketDTO.getAttachments(), objectMapper);
                //删除的附件 = 修改前的附件集合 - 修改后的附件集合
                List<String> delS3Keys = Lists.newArrayList(updateBeforeS3Keys);
                delS3Keys.removeAll(updateAfterS3Keys);
                S3Utils.batchDeleteFile(region,bucketMap.get(S3BucketFunctionCons.IT_REQUEST),delS3Keys);
            });
            return updateContent;
        }

        /**
         * 未解决
         *
         * @param itticket           it
         * @param createdByHandleDTO 处理信息
         * @param loginUserId        操作人
         * @author yyt
         * @date 2024/4/15
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void unResolved(Itticket itticket, CreatedByHandleDTO createdByHandleDTO, int loginUserId) {
            Integer itticketId = itticket.getId();
            update(new UpdateWrapper<Itticket>().lambda().set(Itticket::getTicketStatus, ItRequestStatusEnum.IN_PROGRESS.getCode())
                    .set(Itticket::getResolveTime, null).eq(Itticket::getId, itticketId));

            Integer propertyId = itticket.getPropertyId();
            itticketOperationLogService.save(propertyId, loginUserId, itticketId,
                    ItRequestNotifyEnum.OPERATION_STATUS_UN_RESOLVE.getCode(),
                    JsonUtils.convertObjToString(objectMapper, new ExpansionBasicDTO(null, createdByHandleDTO.getComment())));

            itticketCommentService.save(new ItticketComment().setPropertyId(propertyId).setItticketId(itticketId)
                    .setCreateId(loginUserId).setCreateTime(DateTimeUtils.nowTimestamp())
                    .setContent(createdByHandleDTO.getComment()).setCommentType(ItticketComment.TYPE_UN_RESOLVE));
        }

        /**
         * 关闭
         *
         * @param itticket           it
         * @param createdByHandleDTO 处理信息
         * @param loginUserId        操作人
         * @author yyt
         * @date 2024/4/15
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void closed(Itticket itticket, CreatedByHandleDTO createdByHandleDTO, int loginUserId) {
            Integer itticketId = itticket.getId();
            updateById(new Itticket().setTicketStatus(ItRequestStatusEnum.CLOSED.getCode())
                    .setClosedBy(loginUserId).setClosedTime(DateTimeUtils.nowTimestamp())
                    .setId(itticketId));

            itticketOperationLogService.save(itticket.getPropertyId(), loginUserId, itticketId,
                    ItRequestNotifyEnum.OPERATION_STATUS_CLOSED.getCode(), null);
        }

        /**
         * 接受
         *
         * @param itticket    it
         * @param loginUserId 操作人
         * @author yyt
         * @date 2024/4/15
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void accept(Itticket itticket, int loginUserId) {
            if (itticket.getTicketStatus() != ItRequestStatusEnum.OPEN.getCode()) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_UPDATE_FAILED,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "failed to update the data that maybe removed or updated by others;id:" + itticket.getId());
            }

            Integer itticketId = itticket.getId();
            updateById(new Itticket().setTicketStatus(ItRequestStatusEnum.IN_PROGRESS.getCode())
                    .setDutyUser(loginUserId).setAcceptTime(DateTimeUtils.nowTimestamp())
                    .setId(itticketId));

            itticketOperationLogService.save(itticket.getPropertyId(), loginUserId, itticketId,
                    ItRequestNotifyEnum.OPERATION_STATUS_ACCEPT.getCode(), null);
        }

        /**
         * 分配
         *
         * @param propertyDTO  资产
         * @param itticket    it
         * @param itHandleDTO 处理信息
         * @param loginUserId 操作人
         * @author yyt
         * @date 2024/4/15
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void distribution(PropertyDTO propertyDTO, Itticket itticket, ItHandleDTO itHandleDTO, int loginUserId) {
            if (!Lists.newArrayList(ItRequestStatusEnum.OPEN.getCode(), ItRequestStatusEnum.IN_PROGRESS.getCode())
                    .contains(itticket.getTicketStatus())) {
                throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("status", Lists.newArrayList(ItRequestStatusEnum.OPEN.getCode(), ItRequestStatusEnum.IN_PROGRESS.getCode())),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "the status must in [0: Open，2:In Progress];id:" + itticket.getId());
            }

            Integer itticketId = itticket.getId();
            AcceptDTO accept = itHandleDTO.getAccept();
            Integer acceptBy = accept.getAcceptBy();
            List<UserDTO> itUsers = roleInternalService.getUsers(propertyDTO.getTenantId(), propertyDTO.getId(), BaseConstant.IT_ROLE);
            if (itUsers.stream().noneMatch(item -> Objects.equals(item.getId(), acceptBy))) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_PERMISSION_INVALID,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "select a user who is not in the it role");
            }
            updateById(new Itticket().setTicketStatus(ItRequestStatusEnum.IN_PROGRESS.getCode())
                    .setDutyUser(acceptBy).setAcceptTime(DateTimeUtils.nowTimestamp())
                    .setId(itticketId));

            String comment = accept.getComment();
            Integer propertyId = itticket.getPropertyId();
            itticketOperationLogService.save(propertyId, loginUserId, itticketId,
                    ItRequestNotifyEnum.OPERATION_STATUS_DISTRIBUTION.getCode(),
                    JsonUtils.convertObjToString(objectMapper, new ExpansionBasicDTO(acceptBy, comment)));

            itticketCommentService.save(new ItticketComment().setPropertyId(propertyId).setItticketId(itticketId)
                    .setCreateId(loginUserId).setCreateTime(DateTimeUtils.nowTimestamp())
                    .setContent(StringUtils.isEmpty(comment) ? StringUtils.EMPTY : comment).setCommentType(ItticketComment.TYPE_DISTRIBUTION)
                    .setExpansionJson(JsonUtils.convertObjToString(objectMapper, new ExpansionBasicDTO(acceptBy, null))));
        }

        /**
         * 解决
         *
         * @param itticket    it
         * @param loginUserId 操作人
         * @author yyt
         * @date 2024/4/15
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void resolve(Itticket itticket, int loginUserId) {
            if (itticket.getTicketStatus() != ItRequestStatusEnum.IN_PROGRESS.getCode()) {
                throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("status", Lists.newArrayList(ItRequestStatusEnum.IN_PROGRESS.getCode())),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "the status must in [2:In Progress];id:" + itticket.getId());
            }

            Integer itticketId = itticket.getId();
            updateById(new Itticket().setTicketStatus(ItRequestStatusEnum.WAIT_CONFIRM.getCode())
                    .setResolveTime(DateTimeUtils.nowTimestamp())
                    .setId(itticketId));

            itticketOperationLogService.save(itticket.getPropertyId(), loginUserId, itticketId,
                    ItRequestNotifyEnum.OPERATION_STATUS_RESOLVE.getCode(), null);
        }

        /**
         * 统计等待我处理
         *
         * @param propertyId  资产 id
         * @param loginUserId 登录用户
         * @return 统计等待我处理
         * @author yyt
         * @date 2024/4/15
         **/
        @Override
        public long statisticsWaitProcessing(Integer propertyId, int loginUserId) {
            LambdaQueryWrapper<Itticket> queryWrapper = packageSearchWaitProcessingSql(propertyId, loginUserId);
            return count(queryWrapper);
        }

        /**
         * 检索统计数据
         *
         * @param propertyId  资产 id
         * @param query       查询条件
         * @param loginUserId 登录用户
         * @return it
         * @author yyt
         * @date 2024/4/15
         **/
        @Override
        public Page<Itticket> listStatistics(Integer propertyId, ItticketQuery query, int loginUserId) {
            LambdaQueryWrapper<Itticket> queryWrapper = packageSearchWaitProcessingSql(propertyId, loginUserId);
            queryWrapper.orderByDesc(Itticket::getTicketCreateTime);
            return page(new Page<>(query.getPage(), query.getSize()), queryWrapper);
        }

        /**
         * 获取检索等待我处理 sql
         *
         * @param propertyId  资产 id
         * @param loginUserId 登录用户
         * @return 等待我处理 sql
         * @author yyt
         * @date 2024/4/15
         **/
        private LambdaQueryWrapper<Itticket> packageSearchWaitProcessingSql(Integer propertyId, int loginUserId) {
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            List<UserDTO> itUsers = roleInternalService.getUsers(propertyDTO.getTenantId(), propertyId, BaseConstant.IT_ROLE);
            if (itUsers.stream().anyMatch(item -> item.getId() == loginUserId)) {
                // 当前用户是 IT 部门，则统计状态是待处理 或 状态是进行中且责任人是当前用户的数据
                return new QueryWrapper<Itticket>().lambda().eq(Itticket::getPropertyId, propertyId)
                        .nested(sql ->
                                sql.eq(Itticket::getTicketStatus, ItRequestStatusEnum.OPEN.getCode())
                                        .or()
                                        .eq(Itticket::getTicketStatus, ItRequestStatusEnum.IN_PROGRESS.getCode())
                                        .eq(Itticket::getDutyUser, loginUserId));
            } else {
                // 当前用户是普通用户：则统计是当前人创建且状态是待确认的数据
                return new QueryWrapper<Itticket>().lambda().eq(Itticket::getPropertyId, propertyId)
                        .eq(Itticket::getUserId, loginUserId)
                        .eq(Itticket::getTicketStatus, ItRequestStatusEnum.WAIT_CONFIRM.getCode());
            }
        }

        /**
         * 自动关闭 >= 7 天未关闭的数据
         *
         * @author yyt
         * @date 2024/9/28
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void autoClose() {
            // 条件：状态是待确认，解决时间是6天前（不包含6）
            long targetDateTime = DateTimeUtils.dateNow(defaultTimezone).minusDays(6).toEpochSecond();
            List<Itticket> notCloseIts = list(new QueryWrapper<Itticket>().lambda()
                    .eq(Itticket::getTicketStatus, ItRequestStatusEnum.WAIT_CONFIRM.getCode())
                    .lt(Itticket::getResolveTime, targetDateTime).select(Itticket::getId, Itticket::getPropertyId));
            if (CollectionUtils.isEmpty(notCloseIts)) {
                return;
            }

            long now = DateTimeUtils.nowTimestamp();
            updateBatchById(notCloseIts.stream().map(item -> new Itticket().setId(item.getId()).setTicketStatus(ItRequestStatusEnum.CLOSED.getCode())
                    .setClosedBy(BaseConstant.ZERO).setClosedTime(now)).collect(Collectors.toList()));

            // 添加 close 日志
            itticketOperationLogService.saveBatch(notCloseIts.stream().map(item ->
                    new ItticketOperationLog().setPropertyId(item.getPropertyId())
                            .setItId(item.getId())
                            .setAction(ItRequestNotifyEnum.OPERATION_STATUS_CLOSED.getCode())
                            .setExpansionJson(null)
                            .setCreatedBy(BaseConstant.ZERO)
                            .setCreatedTime(now)).collect(Collectors.toList()));
        }

        /**
         * 分页查询数据
         *
         * @param propertyId 资产 id
         * @param query      查询条件
         * @return it request 数据
         * @author yyt
         * @date 2024/11/14
         **/
        @Override
        public Page<Itticket> listPage(Integer propertyId, ItticketQuery query) {
            LambdaQueryWrapper<Itticket> sql = new QueryWrapper<Itticket>()
                    .lambda().eq(Itticket::getPropertyId, propertyId)
                    .eq(Objects.nonNull(query.getCategory()), Itticket::getCategory, query.getCategory())
                    .eq(Objects.nonNull(query.getPriority()), Itticket::getPriority, query.getPriority())
                    .in(CollectionUtils.isNotEmpty(query.getStatus()), Itticket::getTicketStatus, query.getStatus());
            if (Objects.isNull(query.getSortWay())) {
                sql.orderByDesc(Itticket::getTicketCreateTime);
            } else {
                if (query.getSort() == ItticketQuery.ASC) {
                    sql.orderByAsc(Itticket::getResolveTime);
                } else {
                    sql.orderByDesc(Itticket::getResolveTime);
                }
            }
            return page(new Page<>(query.getPage(), query.getSize()), sql);
        }

        /**
         * 查询 ticket 数据
         *
         * @param propertyId 资产 id
         * @param query      查询条件
         * @return ticket 数据
         * @author yyt
         * @date 2026/1/16
         **/
        @Override
        public ListPagedDTO<ItticketDTO> listTicketDTO(int propertyId, ItticketQuery query) {
            Page<Itticket> itticketPage = listPage(propertyId, query);

            return packageItDTO(itticketPage);
        }

        /**
         * 封装 IT DTO
         *
         * @param itticketPage it 分页信息
         * @return IT DTO
         * @author yyt
         * @date 2024/4/15
         **/
        private ListPagedDTO<ItticketDTO> packageItDTO(Page<Itticket> itticketPage) {
            List<Itticket> records = itticketPage.getRecords();
            Map<Integer, ItticketPriority> priorityById;
            if (CollectionUtils.isNotEmpty(records)) {
                priorityById = itticketPriorityService.listByIds(records.stream().map(Itticket::getPriority).distinct()
                        .collect(Collectors.toList())).stream().collect(Collectors.toMap(ItticketPriority::getId, v -> v));
            } else {
                priorityById = Collections.emptyMap();
            }
            return new ListPagedDTO<>(itticketPage.getTotal(), beanMapper.mapList(records, ItticketDTO.class,
                    item -> {
                        ItticketPriority priority = priorityById.get(item.getPriority());
                        item.setPriorityName(priority.getName());
                        item.setShortDescription(priority.getShortDescription());
                        item.setLongDescription(priority.getLongDescription());
                    }));
        }

        /**
         * 统计 ticket 数据
         *
         * @param propertyId 资产 id
         * @param query      查询条件
         * @param userId     操作人
         * @return ticket 数据
         * @author yyt
         * @date 2026/1/16
         **/
        @Override
        public ListPagedDTO<ItticketDTO> listStatisticsDTO(int propertyId, ItticketQuery query, int userId) {
            Page<Itticket> itticketPage = listStatistics(propertyId, query, userId);
            return packageItDTO(itticketPage);
        }

        /**
         * 获取 ticket 详情
         *
         * @param ticket ticket
         * @return ticket 详情
         * @author yyt
         * @date 2026/1/19
         **/
        @Override
        public ItticketDTO getDetailDTO(Itticket ticket) {
            Integer ticketId = ticket.getId();
            ItticketDTO itticketDTO = beanMapper.map(ticket, ItticketDTO.class);
            List<ItticketComment> attachments = itticketCommentService.getItticketAttachment(ticketId);
            itticketDTO.setAttachments(attachments.stream().map(ItticketComment::getContent).collect(Collectors.toList()));
            ItticketPriority priority = itticketPriorityService.getById(ticket.getPriority());
            itticketDTO.setPriorityName(priority.getName());
            itticketDTO.setShortDescription(priority.getShortDescription());
            itticketDTO.setLongDescription(priority.getLongDescription());

            List<ItticketOperationLog> operationLogs = itticketOperationLogService.list(new QueryWrapper<ItticketOperationLog>().lambda()
                    .eq(ItticketOperationLog::getItId, ticketId).orderByDesc(ItticketOperationLog::getId));
            itticketDTO.setOperationLog(beanMapper.mapList(operationLogs, ItticketOperationLogDTO.class, (entity, item) -> {
                if (StringUtils.isNotEmpty(entity.getExpansionJson())) {
                    item.setExpansion(JsonUtils.convertStringToObject(objectMapper, entity.getExpansionJson(), ExpansionDTO.class));
                }
            }));

            return itticketDTO;
        }

        /**
         * 校验 priority 是否被使用
         *
         * @param id priority id
         * @author yyt
         * @date 2026/1/27
         **/
        @Override
        public void checkPriorityUsed(int id) {
            long count = count(new QueryWrapper<Itticket>().lambda().eq(Itticket::getPriority, id));
            if (count > 0) {
                throw new BadRequestException(ItRequestConstant.PRIORITY_USED_BY_ITREQUEST,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "Sorry, the priority is used by it request.");
            }
        }

    }