# CbAdjServiceImpl.java

    package cloud.wconnect.finance.service.chargeback.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.*;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.exception.ForbiddenException;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.UserInternalService;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.finance.cons.S3BucketFunctionCons;
    import cloud.wconnect.finance.cons.chargeback.CaseActionEnum;
    import cloud.wconnect.finance.cons.chargeback.CaseErrorCons;
    import cloud.wconnect.finance.cons.chargeback.CaseStatusEnum;
    import cloud.wconnect.finance.cons.chargeback.StageCons;
    import cloud.wconnect.finance.entity.chargeback.*;
    import cloud.wconnect.finance.mapper.chargeback.CbAdjMapper;
    import cloud.wconnect.finance.pojo.dto.chargeback.CbAdjCommentDTO;
    import cloud.wconnect.finance.pojo.dto.chargeback.CbAdjDTO;
    import cloud.wconnect.finance.pojo.dto.chargeback.SaveAdjDTO;
    import cloud.wconnect.finance.pojo.dto.chargeback.UpdateAdjDTO;
    import cloud.wconnect.finance.service.chargeback.*;
    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.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Sets;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.checkerframework.checker.nullness.qual.Nullable;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.text.MessageFormat;
    import java.time.format.DateTimeFormatter;
    import java.util.*;
    import java.util.concurrent.Executor;
    import java.util.concurrent.atomic.AtomicBoolean;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * 服务实现类
     * </p>
     *
     * @author yyt
     * @since 2023-08-22
     */
    @Service
    @DS("finance")
    public class CbAdjServiceImpl extends ServiceImpl<CbAdjMapper, CbAdj> implements ICbAdjService {

        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private ICbCaseActionService cbCaseActionService;
        @Lazy
        @Autowired
        private ICbCaseService cbCaseService;
        @Autowired
        private ICbAdjCommentService cbAdjCommentService;
        @Autowired
        private UserInternalService userInternalService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private Executor executor;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;
        @Autowired
        private ICbAdjSenderService cbAdjSenderService;
        @Value("${aws-s3.region}")
        private String region;
        @Value("#{${aws-s3.function-bucket}}")
        private Map<String, Map<String, Object>> bucketMap;

        /**
         * 新增 adj
         *
         * @param cs                  case
         * @param saveAdjDTOS         adj 信息集合
         * @param cbCaseIwcAdRequests 绑定的IWC Adjustment数据
         * @param loginUserId         登录用户id
         * @author yyt
         * @date 2023/9/1
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void save(CbCase cs, List<SaveAdjDTO> saveAdjDTOS, List<CbCaseIwcAdRequest> cbCaseIwcAdRequests, int loginUserId) {
            // 阶段必须是 hotel operation / patch，审批状态是 待审批、拒绝
            Integer stage = cs.getStage();
            if (!Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.PATCH).contains(stage)
                    || !Lists.newArrayList(ApproveStatusEnum.PENDING.getStatus(), ApproveStatusEnum.REJECTED.getStatus())
                    .contains(cs.getApproveStatus())) {
                throw new BadRequestException(CaseErrorCons.OPERATION_ADJ_FAIL_STAGE_APPROVE,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The stage must be hotel operation or patch, and the approval status must be pending or rejected before operation can be carried out.");
            }

            CbCaseAction caseAction = cbCaseActionService.getCaseAction(cs.getId(), stage);
            // hotel operation 阶段，action 必须选择 accept
            if (stage == StageCons.HOTEL_OPERATION) {
                if (Objects.isNull(caseAction)) {
                    throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the caseAction is not found");
                }
                if (caseAction.getAction() != CaseActionEnum.ACCEPT.getCode()) {
                    throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                            i18nMessageUtils.getErrorParamMapNotNeedTranslate("action", CaseActionEnum.ACCEPT.getCode()),
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the caseAction is not accept");
                }
            }

            String guestName = ConcatenateCustomerNameUtils.concatenateCustomerName(cs.getGuestFirstName(), cs.getGuestLastName());
            // 生成 adj.时，将通知酒店的财务账号记为 adj.中的 requested by
            int patchBy = cs.getPatchBy();
            int requestedBy = patchBy == 0 ? cs.getHotelOperationBy() : patchBy;

            // 绑定的adjNumber集合
            List<String> bindAdjNumbers = cbCaseIwcAdRequests.stream()
                    .map(CbCaseIwcAdRequest::getAdjNumber).toList();
            // 绑定的IWC AdReqest数据
            Map<String, CbCaseIwcAdRequest> adjNumber2CbCaseIwcAdRequest = cbCaseIwcAdRequests.stream()
                    .collect(Collectors.toMap(CbCaseIwcAdRequest::getAdjNumber, Function.identity()));
            // 转换成新增的Cb Adj集合
            List<CbAdj> cbAdjs = convertToInsertCbAdj(saveAdjDTOS, cs, adjNumber2CbCaseIwcAdRequest,
                    guestName, requestedBy, loginUserId, DateTimeUtils.nowTimestamp());
            // 批量新增
            saveBatch(cbAdjs);

            if (stage == StageCons.PATCH) {
                // patch 阶段补一个 action 操作
                cbCaseActionService.save(new CbCaseAction().setPropertyId(cs.getPropertyId()).setCaseId(cs.getId())
                        .setStage(stage).setAction(CaseActionEnum.ACCEPT.getCode())
                        .setCreatedBy(loginUserId).setCreatedTime(DateTimeUtils.nowTimestamp()));

                CbCase updateCase = new CbCase();
                updateCase.setId(cs.getId());
                updateCase.setStatus(CaseStatusEnum.FILL_OUT_ADJ.getCode());
                updateCase.setStatusSetBy(loginUserId);
                updateCase.setStatusSetTime(DateTimeUtils.nowTimestamp());
                cbCaseService.updateById(updateCase);
            }

            TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                // 绑定的adj数据
                List<CbAdj> bindAdjs = cbAdjs.stream().filter(cbAdj -> bindAdjNumbers.contains(cbAdj.getAdjNumber())).collect(Collectors.toList());
                cbAdjSenderService.handleSentBindAdjEvent(cs, bindAdjs);

                // 新增的adj数据
                List<CbAdj> addAdjs = cbAdjs.stream().filter(cbAdj -> !bindAdjNumbers.contains(cbAdj.getAdjNumber())).collect(Collectors.toList());
                cbAdjSenderService.handleSentSaveAdjEvent(cs, addAdjs);
            });
        }

        /**
         * 转换成新增的Cb Adj数据
         *
         * @param saveAdjDTOS                  saveAdjDTO集合
         * @param cs                           case
         * @param adjNumber2CbCaseIwcAdRequest Adjustment功能同步的调账数据
         * @param nowTimestamp                 当前时间
         * @return java.util.List<cloud.wconnect.finance.entity.chargeback.CbAdj>
         * @author lichen
         * @date 2026/2/11
         */
        private List<CbAdj> convertToInsertCbAdj(List<SaveAdjDTO> saveAdjDTOS, CbCase cs,
                                                 Map<String, CbCaseIwcAdRequest> adjNumber2CbCaseIwcAdRequest,
                                                 String guestName, int requestedBy, int loginUserId, long nowTimestamp) {
            if (CollectionUtils.isEmpty(saveAdjDTOS)) return Collections.emptyList();

            List<CbAdj> cbAdjs = beanMapper.mapList(saveAdjDTOS, CbAdj.class, (dto, entity) -> {
                entity.setPropertyId(cs.getPropertyId());
                entity.setCaseId(cs.getId());
                entity.setCheckInDate(cs.getCheckInDate());
                entity.setCheckOutDate(cs.getCheckOutDate());
                entity.setRequestedBy(requestedBy);

                if (dto.getType() == CbAdj.TYPE_BIND) {
                    CbCaseIwcAdRequest cbCaseIwcAdRequest = adjNumber2CbCaseIwcAdRequest.get(dto.getAdjNumber());

                    entity.setCreateBy(cbCaseIwcAdRequest.getCreateBy());
                    entity.setCreateTime(cbCaseIwcAdRequest.getCreateTime());
                    entity.setUpdateBy(cbCaseIwcAdRequest.getUpdateBy());
                    entity.setUpdateTime(cbCaseIwcAdRequest.getUpdateTime());

                    if (cbCaseIwcAdRequest.getBusinessDate() > 0) {
                        // 调账数据已配对时，guestName、roomNumber、confNumber不可更改
                        entity.setGuestName(cbCaseIwcAdRequest.getGuestName());
                        entity.setRoomNumber(cbCaseIwcAdRequest.getRoomNumber());
                        entity.setConfNumber(cbCaseIwcAdRequest.getConfNumber());
                    } else {
                        entity.setGuestName(guestName);
                        entity.setRoomNumber(cs.getRoomNumber());
                        entity.setConfNumber(cs.getFolioNumber());
                    }
                } else {
                    // 手动创建
                    entity.setGuestName(guestName);
                    entity.setRoomNumber(cs.getRoomNumber());
                    entity.setConfNumber(cs.getFolioNumber());
                    entity.setCreateBy(loginUserId);
                    entity.setCreateTime(nowTimestamp);
                    entity.setUpdateBy(0);
                    entity.setUpdateTime(0L);
                }
            });

            // 设置adjNumber
            setAdjNumber(cs.getPropertyId(), cbAdjs);
            return cbAdjs;
        }

        /**
         * 设置 adj number
         *
         * @param propertyId 资产id
         * @param cbAdjs     cb adj集合
         * @author yyt
         * @date 2023/9/2
         */
        private void setAdjNumber(int propertyId, List<CbAdj> cbAdjs) {
            List<CbAdj> needGenerateAdjNumbers = cbAdjs.stream().filter(cbAdj -> cbAdj.getType() == CbAdj.TYPE_MANUAL).toList();
            if (CollectionUtils.isEmpty(needGenerateAdjNumbers)) {
                return;
            }

            PropertyDTO property = propertyI18nRedisService.get(propertyId);
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYMMDDHH);
            String dateStr = DateTimeUtils.dateTimeNow(property.getTimezone()).format(dateTimeFormatter);
            String prefix = StringUtils.upperCase(property.getAbbr()) + dateStr;

            // 查询手动创建的最大adjNumber
            String maxAdjNumber = baseMapper.selectMaxAdjNumForUpdate(propertyId, prefix + "____");

            AtomicInteger maxNumber = new AtomicInteger();
            if (maxAdjNumber != null) {
                maxNumber.set(Integer.parseInt(StringUtils.removeStart(maxAdjNumber, prefix)) + 1);
            } else {
                maxNumber.set(1);
            }

            // 设置adj number
            for (CbAdj cbAdj : cbAdjs) {
                if (StringUtils.isEmpty(cbAdj.getAdjNumber())) {
                    cbAdj.setAdjNumber(prefix + StringUtils.leftPad(String.valueOf(maxNumber.get()), 4, '0'));
                    maxNumber.getAndIncrement();
                }
            }
        }

        /**
         * 校验存在
         *
         * @param id adj id
         * @return cb adj
         * @author yyt
         * @date 2023/9/1
         */
        @Override
        public CbAdj checkExist(long id) {
            CbAdj cbAdj = getById(id);
            if (Objects.isNull(cbAdj)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the cbAdj is not found;id:" + id);
            }
            return cbAdj;
        }

        /**
         * 校验存在
         *
         * @param ids adj id 集合
         * @return cb adj集合
         * @author yyt
         * @date 2026/2/10
         */
        @Override
        public List<CbAdj> checkExist(List<Long> ids) {
            if (CollectionUtils.isEmpty(ids)) return Collections.emptyList();
            // 获取已存在的数据
            List<CbAdj> existCbAdjList = listByIds(ids);
            if (existCbAdjList.size() != ids.size()) {
                List<Long> existCbAdjIds = existCbAdjList.stream().map(CbAdj::getId).toList();
                List<Long> notExistIds = ids.stream().filter(item -> !existCbAdjIds.contains(item)).toList();
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "CbAdj not found, notExistIds = " + notExistIds);
            }
            return existCbAdjList;
        }

        /**
         * 修改 adj
         *
         * @param cs                  case
         * @param updateAdjDTO        修改后DTO
         * @param oriCbAdjs           修改前或删除前的Cb Adj
         * @param cbCaseIwcAdRequests 绑定的IWC Adjustment数据
         * @param loginUserId         登录用户id
         * @author yyt
         * @date 2023/9/1
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void update(CbCase cs, UpdateAdjDTO updateAdjDTO, List<CbAdj> oriCbAdjs, List<CbCaseIwcAdRequest> cbCaseIwcAdRequests, int loginUserId) {
            // 阶段必须是 hotel operation / patch，审批状态是 待审批、拒绝
            if (!Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.PATCH).contains(cs.getStage())
                    || !Lists.newArrayList(ApproveStatusEnum.PENDING.getStatus(), ApproveStatusEnum.REJECTED.getStatus())
                    .contains(cs.getApproveStatus())) {
                throw new BadRequestException(CaseErrorCons.OPERATION_ADJ_FAIL_STAGE_APPROVE,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The stage must be hotel operation or patch, and the approval status must be pending or rejected before operation can be carried out.");
            }
            // 修改Case
            CbCase updateCase = new CbCase();
            updateCase.setApproveStatus(ApproveStatusEnum.PENDING.getStatus());
            updateCase.setId(cs.getId());
            cbCaseService.updateById(updateCase);

            String guestName = ConcatenateCustomerNameUtils.concatenateCustomerName(cs.getGuestFirstName(), cs.getGuestLastName());
            // 生成 adj.时，将通知酒店的财务账号记为 adj.中的 requested by
            int patchBy = cs.getPatchBy();
            int requestedBy = patchBy == 0 ? cs.getHotelOperationBy() : patchBy;
            long nowTimestamp = DateTimeUtils.nowTimestamp();

            // 获取Case修改前的所有Cb Adj
            List<CbAdj> updateBeforeAllCbAdjs = listByCaseId(cs.getId());

            List<SaveAdjDTO> saveCbAdjs = updateAdjDTO.getSaveCbAdjs();
            ArrayList<CbAdj> updateAfterAllCbAdjs = Lists.newArrayListWithCapacity(saveCbAdjs.size());

            Map<String, CbCaseIwcAdRequest> adjNumber2CbCaseIwcAdRequest = cbCaseIwcAdRequests.stream().collect(Collectors.toMap(CbCaseIwcAdRequest::getAdjNumber, Function.identity()));
            Map<Long, CbAdj> oriCbAdjId2Entity = oriCbAdjs.stream().collect(Collectors.toMap(CbAdj::getId, Function.identity()));

            // 筛选出修改的Cb Adj集合
            List<SaveAdjDTO> updateCbAdjs = saveCbAdjs.stream().filter(saveAdjDTO -> saveAdjDTO.getCbAdjId() > 0).collect(Collectors.toList());
            ArrayList<CbAdj> updateBatch = Lists.newArrayListWithCapacity(updateCbAdjs.size());
            for (SaveAdjDTO updateCbAdjDTO : updateCbAdjs) {
                // 修改前的Cb Adj
                CbAdj updateBeforeAdj = oriCbAdjId2Entity.get(updateCbAdjDTO.getCbAdjId());

                updateBeforeAdj.setMistakeType(updateCbAdjDTO.getMistakeType());
                updateBeforeAdj.setMistakeBy(updateCbAdjDTO.getMistakeBy());
                updateBeforeAdj.setRevenueLost(updateCbAdjDTO.getRevenueLost());
                updateBeforeAdj.setComment(updateCbAdjDTO.getComment());
                updateBeforeAdj.setAttachments(updateCbAdjDTO.getAttachments());
                updateBeforeAdj.setUpdateBy(loginUserId);
                updateBeforeAdj.setUpdateTime(nowTimestamp);
                if (updateBeforeAdj.getType() == CbAdj.TYPE_BIND) {
                    CbCaseIwcAdRequest cbCaseIwcAdRequest = adjNumber2CbCaseIwcAdRequest.get(updateBeforeAdj.getAdjNumber());
                    if (cbCaseIwcAdRequest.getBusinessDate() > 0) {
                        // 调账数据已配对时，guestName、roomNumber、confNumber不可更改
                        updateBeforeAdj.setGuestName(cbCaseIwcAdRequest.getGuestName());
                        updateBeforeAdj.setRoomNumber(cbCaseIwcAdRequest.getRoomNumber());
                        updateBeforeAdj.setConfNumber(cbCaseIwcAdRequest.getConfNumber());
                    } else {
                        // 调账数据未配对时，guestName、roomNumber、confNumber使用Case数据覆盖
                        updateBeforeAdj.setGuestName(guestName);
                        updateBeforeAdj.setRoomNumber(cs.getRoomNumber());
                        updateBeforeAdj.setConfNumber(cs.getFolioNumber());
                    }
                }
                updateBatch.add(updateBeforeAdj);
            }
            if (CollectionUtils.isNotEmpty(updateBatch)) {
                // 批量修改
                updateBatchById(updateBatch);
                updateAfterAllCbAdjs.addAll(updateBatch);
            }

            // 新增的 adj集合
            List<SaveAdjDTO> insertSaveAdjDTOS = saveCbAdjs.stream().filter(saveAdjDTO -> saveAdjDTO.getCbAdjId() == 0).collect(Collectors.toList());
            // 转换成新增的Cb Adj集合
            List<CbAdj> insertBatch = convertToInsertCbAdj(insertSaveAdjDTOS, cs, adjNumber2CbCaseIwcAdRequest,
                    guestName, requestedBy, loginUserId, DateTimeUtils.nowTimestamp());
            if (CollectionUtils.isNotEmpty(insertBatch)) {
                // 批量新增
                saveBatch(insertBatch);
                updateAfterAllCbAdjs.addAll(insertBatch);
            }

            // 删除Cb Adj
            List<Long> delCbAdjIds = updateAdjDTO.getDelCbAdjIds();
            removeBatchByIds(delCbAdjIds);
            cbAdjCommentService.removeByCbAdjIds(delCbAdjIds);

            TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                //修改前的s3Key集合
                List<String> updateBeforeS3Keys = updateBeforeAllCbAdjs.stream().map(item -> AttachmentUtil.getKeysByAbbrAttachmentArrayJson(item.getAttachments(), objectMapper))
                        .flatMap(Collection::stream).collect(Collectors.toList());
                //修改后的s3Key集合
                List<String> updateAfterS3Keys = saveCbAdjs.stream().map(saveAdjDTO -> AttachmentUtil.getKeysByAbbrAttachmentArrayJson(saveAdjDTO.getAttachments(), objectMapper))
                        .flatMap(Collection::stream).collect(Collectors.toList());
                //删除的附件 = 修改前的附件集合 - 修改后的附件集合
                List<String> delS3Keys = Lists.newArrayList(updateBeforeS3Keys);
                delS3Keys.removeAll(updateAfterS3Keys);
                S3Utils.batchDeleteFile(region, bucketMap.get(S3BucketFunctionCons.CHARGEBACK), delS3Keys);

                // 发送删除adj.事件
                List<CbAdj> delCbAdjs = updateBeforeAllCbAdjs.stream().filter(item -> delCbAdjIds.contains(item.getId())).collect(Collectors.toList());
                cbAdjSenderService.handleSentDeleteAdjEvent(cs, delCbAdjs);

                Map<Boolean, List<CbAdj>> isBind2CbAdjs = updateAfterAllCbAdjs.stream().collect(Collectors.partitioningBy(item -> item.getType() == CbAdj.TYPE_BIND));
                // 绑定IWC Adjustment数据
                List<CbAdj> bindCbAdjs = isBind2CbAdjs.getOrDefault(true, Collections.emptyList());
                cbAdjSenderService.handleSentBindAdjEvent(cs, bindCbAdjs);
                // 手动创建IWC Adjustment数据
                List<CbAdj> manualCreateCbAdjs = isBind2CbAdjs.getOrDefault(false, Collections.emptyList());
                cbAdjSenderService.handleSentSaveAdjEvent(cs, manualCreateCbAdjs);
            });
        }

        /**
         * 批量处理审批 adj
         *
         * @param cases case
         * @author yyt
         * @date 2023/11/17
         */
        @Override
        public void handleApproveAdjBatch(List<CbCase> cases) {
            List<Long> caseIds = cases.stream().map(CbCase::getId).collect(Collectors.toList());
            Map<Long, List<CbAdj>> caseId2CbAdjs = listByCaseIds(caseIds).stream().collect(Collectors.groupingBy(CbAdj::getCaseId));
            for (CbCase cbCase : cases) {
                List<CbAdj> cbAdjs = caseId2CbAdjs.get(cbCase.getId());
                cbAdjSenderService.handleSentApprovedAdjEvent(cbCase, cbAdjs);
            }
        }

        /**
         * 批量处理删除 adj
         *
         * @param propertyId 资产id
         * @param cases      case
         * @author yyt
         * @date 2023/9/11
         */
        @Override
        public void handleDeleteAdjBatch(int propertyId, List<CbCase> cases) {
            List<Long> caseIds = cases.stream().map(CbCase::getId).collect(Collectors.toList());
            Map<Long, List<CbAdj>> caseId2CbAdjs = listByCaseIds(caseIds).stream().collect(Collectors.groupingBy(CbAdj::getCaseId));
            for (CbCase cbCase : cases) {
                List<CbAdj> cbAdjs = caseId2CbAdjs.get(cbCase.getId());
                cbAdjSenderService.handleSentDeleteAdjEvent(cbCase, cbAdjs);
            }
        }

        /**
         * 查看 adj 详情
         *
         * @param propertyId 资产id
         * @param caseId     case id
         * @return adj 详情
         * @author yyt
         * @date 2023/9/2
         */
        @Override
        public List<CbAdjDTO> getDetail(int propertyId, Long caseId) {
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            // 查询adj集合
            List<CbAdj> cbAdjs = listByCaseId(caseId);
            if (CollectionUtils.isEmpty(cbAdjs)) return Collections.emptyList();

            // 查询备注
            List<Long> cbAdjIds = cbAdjs.stream().map(CbAdj::getId).collect(Collectors.toList());
            List<CbAdjComment> cbAdjComments = cbAdjCommentService.listByCbAdjIds(cbAdjIds);
            Map<Long, List<CbAdjComment>> cbAdjId2Comments = cbAdjComments.stream().collect(Collectors.groupingBy(CbAdjComment::getCbAdjId));

            HashSet<Integer> userIds = Sets.newHashSet();
            if (CollectionUtils.isNotEmpty(cbAdjComments)) {
                userIds.addAll(cbAdjComments.stream().map(CbAdjComment::getCreatedBy).toList());
            }
            userIds.addAll(cbAdjs.stream().map(CbAdj::getRequestedBy).collect(Collectors.toList()));
            Map<Integer, String> userNameById = userInternalService.getNameMapByIds(propertyDTO.getTenantId(), userIds);

            return beanMapper.mapList(cbAdjs, CbAdjDTO.class, (entity, dto) -> {
                dto.setCbAdjId(entity.getId());
                dto.setRequestedBy(userNameById.get(entity.getRequestedBy()));

                List<CbAdjComment> commentsByCbAdjId = cbAdjId2Comments.getOrDefault(entity.getId(), Collections.emptyList());
                dto.setAdjComments(beanMapper.mapList(commentsByCbAdjId, CbAdjCommentDTO.class,
                        item -> item.setCreatedName(userNameById.get(item.getCreatedBy()))));
            });
        }

        /**
         * 删除 adj
         *
         * @param cs case
         * @author yyt
         * @date 2023/9/4
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void deleteAdj(CbCase cs) {
            // 阶段必须是 hotel operation / patch，审批状态是 待审批、拒绝
            if (!Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.PATCH).contains(cs.getStage())
                    || !Lists.newArrayList(ApproveStatusEnum.PENDING.getStatus(), ApproveStatusEnum.REJECTED.getStatus())
                    .contains(cs.getApproveStatus())) {
                throw new BadRequestException(CaseErrorCons.OPERATION_ADJ_FAIL_STAGE_APPROVE,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The stage must be hotel operation or patch, and the approval status must be pending or rejected before operation can be carried out.");
            }

            if (cs.getStage() != StageCons.HOTEL_OPERATION) {
                cbCaseActionService.remove(new QueryWrapper<CbCaseAction>().lambda().eq(CbCaseAction::getCaseId, cs.getId())
                        .eq(CbCaseAction::getStage, StageCons.PATCH));
            }
            CbCase updateCase = new CbCase();
            updateCase.setApproveStatus(ApproveStatusEnum.PENDING.getStatus());
            updateCase.setId(cs.getId());
            cbCaseService.updateById(updateCase);

            List<CbAdj> cbAdjs = listByCaseId(cs.getId());

            // 批量删除
            List<Long> cbAdjIds = cbAdjs.stream().map(CbAdj::getId).toList();
            removeBatchByIds(cbAdjIds);
            cbAdjCommentService.removeByCbAdjIds(cbAdjIds);

            //删除附件
            TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                List<String> delS3Keys = cbAdjs.stream()
                        .map(cbAdj -> AttachmentUtil.getKeysByAbbrAttachmentArrayJson(cbAdj.getAttachments(), objectMapper))
                        .flatMap(Collection::stream).collect(Collectors.toList());
                S3Utils.batchDeleteFile(region, bucketMap.get(S3BucketFunctionCons.CHARGEBACK), delS3Keys);

                // 删除adj发送事件
                cbAdjSenderService.handleSentDeleteAdjEvent(cs, cbAdjs);
            });
        }

        /**
         * 处理删除 adj
         *
         * @param cs case
         * @author yyt
         * @date 2023/9/6
         */
        @Override
        public void handleDeleteAdj(CbCase cs) {
            Map<Integer, CbCaseAction> caseActionByStage =
                    cbCaseActionService.list(Lists.newArrayList(cs.getId()), Lists.newArrayList(StageCons.HOTEL_OPERATION,
                            StageCons.PATCH)).stream().collect(Collectors.toMap(CbCaseAction::getStage, v -> v));
            CbCaseAction hotelOperationAction = caseActionByStage.get(StageCons.HOTEL_OPERATION);
            if (Objects.nonNull(hotelOperationAction)) {
                if (hotelOperationAction.getAction() == CaseActionEnum.ACCEPT.getCode()) {
                    deleteCbAdj(cs);
                }
            }
            CbCaseAction patchAction = caseActionByStage.get(StageCons.PATCH);
            if (Objects.nonNull(patchAction)) {
                deleteCbAdj(cs);
            }
        }

        /**
         * 处理删除 patch 阶段 adj
         *
         * @param cs case
         * @author yyt
         * @date 2023/9/7
         */
        @Override
        public void handleDeletePatchStageAdj(CbCase cs) {
            CbCaseAction patchAction = cbCaseActionService.getCaseAction(cs.getId(), StageCons.PATCH);
            if (Objects.nonNull(patchAction)) {
                deleteCbAdj(cs);
            }
        }

        /**
         * 根据 case id 获取 adj
         *
         * @param caseId case id
         * @return java.util.List<cloud.wconnect.finance.entity.chargeback.CbAdj>
         * @author lichen
         * @date 2026/2/10
         */
        @Override
        public List<CbAdj> listByCaseId(Long caseId) {
            return list(new LambdaQueryWrapper<CbAdj>().eq(CbAdj::getCaseId, caseId));
        }

        /**
         * 根据 case id集合获取 adj
         *
         * @param caseIds case id集合
         * @return java.util.List<cloud.wconnect.finance.entity.chargeback.CbAdj>
         * @author lichen
         * @date 2026/2/10
         */
        @Override
        public List<CbAdj> listByCaseIds(List<Long> caseIds) {
            if (CollectionUtils.isEmpty(caseIds)) return Collections.emptyList();
            return list(new LambdaQueryWrapper<CbAdj>().in(CbAdj::getCaseId, caseIds));
        }

        /**
         * 校验 adj 是否属于该资产
         *
         * @param propertyId 资产id
         * @param cbAdjs     adj集合
         * @author lichen
         * @date 2026/2/10
         */
        @Override
        public void checkBelong(Integer propertyId, List<CbAdj> cbAdjs) {
            if (cbAdjs.stream().anyMatch(item -> !item.getPropertyId().equals(propertyId))) {
                List<String> adjNumbers = cbAdjs.stream().map(CbAdj::getAdjNumber).toList();
                throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                        BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(),
                        MessageFormat.format("ChargeBack update cbAdj not belong currentPropertyId, adjNumbers:{0},currentPropertyId:{1}",
                                adjNumbers, propertyId));
            }
        }

        /**
         * 校验 adj 是否属于该case
         *
         * @param caseId case id
         * @param cbAdjs adj集合
         * @author lichen
         * @date 2026/2/10
         */
        @Override
        public void checkBelongCase(Long caseId, List<CbAdj> cbAdjs) {
            if (cbAdjs.stream().anyMatch(item -> !item.getCaseId().equals(caseId))) {
                List<String> adjNumbers = cbAdjs.stream().map(CbAdj::getAdjNumber).toList();
                throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                        BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(),
                        MessageFormat.format("ChargeBack update cbAdj not belong currentCase, adjNumbers:{0},currentCaseId:{1}",
                                adjNumbers, caseId));
            }
        }

        /**
         * 根据 case id 获取 adj 数量
         *
         * @param caseId case id
         * @return java.lang.Long
         * @author lichen
         * @date 2026/2/12
         */
        @Override
        public Long countByCaseId(Long caseId) {
            return count(new LambdaQueryWrapper<CbAdj>().eq(CbAdj::getCaseId, caseId));
        }

        /**
         * 根据 adj number 获取 cb adj
         *
         * @param adjNumbers adj number集合
         * @return java.util.List<cloud.wconnect.finance.entity.chargeback.CbAdj>
         * @author lichen
         * @date 2026/2/24
         */
        @Override
        public List<CbAdj> listByAdjNumbers(List<String> adjNumbers) {
            if (CollectionUtils.isEmpty(adjNumbers)) return Collections.emptyList();
            return list(new LambdaQueryWrapper<CbAdj>().in(CbAdj::getAdjNumber, adjNumbers));
        }

        /**
         * 删除adj
         *
         * @param cs case 信息
         * @author lichen
         * @date 2026/2/10
         */
        private void deleteCbAdj(CbCase cs) {
            List<CbAdj> cbAdjs = listByCaseId(cs.getId());
            if (CollectionUtils.isNotEmpty(cbAdjs)) {
                List<Long> cbAdjIds = cbAdjs.stream().map(CbAdj::getId).collect(Collectors.toList());
                // 物理删除CbAdj
                removeBatchByIds(cbAdjIds);
                // 删除CbAdjComment
                cbAdjCommentService.removeByCbAdjIds(cbAdjIds);

                TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                    //删除s3附件
                    List<String> delS3Keys = cbAdjs.stream().map(cbAdj -> AttachmentUtil.getKeysByAbbrAttachmentArrayJson(cbAdj.getAttachments(), objectMapper))
                            .flatMap(Collection::stream).collect(Collectors.toList());
                    S3Utils.batchDeleteFile(region, bucketMap.get(S3BucketFunctionCons.CHARGEBACK), delS3Keys);

                    // 发送删除消息
                    cbAdjSenderService.handleSentDeleteAdjEvent(cs, cbAdjs);
                });
            }
        }

    }