# NotifyPendingAllocationTask.java

    package cloud.wconnect.it.businesstask.servicerequestcenter;

    import cloud.wconnect.commonlib.cons.*;
    import cloud.wconnect.commonlib.pojo.dto.*;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationDateTimeDTO;
    import cloud.wconnect.commonlib.pojo.query.OpenFunctionPropertyQuery;
    import cloud.wconnect.commonlib.pojo.query.UserQuery;
    import cloud.wconnect.commonlib.service.*;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.it.cons.FunctionPointEnum;
    import cloud.wconnect.it.cons.servicerequestcenter.*;
    import cloud.wconnect.it.entity.servicerequestcenter.ServiceRequestCenter;
    import cloud.wconnect.it.entity.servicerequestcenter.ServiceRequestCenterProperty;
    import cloud.wconnect.it.pojo.vo.servicerequestcenter.ServiceRequestCenterEmailVO;
    import cloud.wconnect.it.service.servicerequestcenter.IServiceRequestCenterNotifyConfigService;
    import cloud.wconnect.it.service.servicerequestcenter.IServiceRequestCenterPropertyService;
    import cloud.wconnect.it.service.servicerequestcenter.IServiceRequestCenterService;
    import com.alibaba.fastjson.JSONObject;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Maps;
    import com.google.common.util.concurrent.RateLimiter;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    import org.thymeleaf.TemplateEngine;
    import org.thymeleaf.context.Context;

    import java.util.*;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.Collectors;

    import static java.util.stream.Collectors.toList;

    /**
     * @author: yyt
     * 2025-12-19
     */
    @Slf4j
    @Component
    public class NotifyPendingAllocationTask {

        @Autowired
        private PropertyInternalService propertyInternalService;
        @Autowired
        private UserInternalService userInternalService;
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        @Autowired
        private MailInternalService mailInternalService;
        @Autowired
        private NotifyInternalService notifyInternalService;
        @Autowired
        private IServiceRequestCenterService serviceRequestCenterService;
        @Autowired
        private IServiceRequestCenterNotifyConfigService serviceRequestCenterNotifyConfigService;
        @Autowired
        private IServiceRequestCenterPropertyService serviceRequestCenterPropertyService;
        @Autowired
        private TenantInternalService tenantInternalService;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;
        @Autowired
        private LineMessageUtil lineMessageUtil;
        @Autowired
        private TemplateEngine templateEngine;

        @Value("${default.timezone}")
        private String defaultTimezone;

        private static final String PREFIX_KEY = "wconnect:task:service-request-center-notify-pending-allocation:";

        /**
         * 限制速率（每秒5次）
         */
        private final RateLimiter rateLimiter = RateLimiter.create(5.0);

        /**
         * 周一至周五，每天下午1点，检查过去24小时内未分配的Ticket，统计并根据Ticket类型邮件通知Manager
         *
         * @author yyt
         * @date 2025/12/19
         **/
        @Scheduled(cron = "${schedule.cron.notify.service-request-center-notify-pending-allocation}", zone = "${default.timezone}")
        public void notifyPendingAllocation() {
            String currentDateStr = DateTimeUtils.dateNowStr(defaultTimezone, BaseConstant.DATE_FORMAT_YYMMDDHH);
            if (!preventConcurrency(PREFIX_KEY + currentDateStr)) {
                return;
            }

            log.info("NotifyPendingAllocationTask... start");
            String alertTitle = "[Warning] - Exceptions About Notify Pending Allocation";
            StringBuilder warningEmail = new StringBuilder("<p>There is something wrong when notify pending allocation.</p>");

            try {
                // 查询开通此功能的未关闭的资产
                List<PropertyDTO> openModuleProperty = propertyInternalService.getOpenFunctionProperty(OpenFunctionPropertyQuery.builder()
                        .openFunctionIds(Lists.newArrayList(FunctionIdConstant.DASHBOARD)).build())
                        .stream().filter(PropertyDTO::getStatus).toList();
                if (CollectionUtils.isEmpty(openModuleProperty)) {
                    log.info("NotifyPendingAllocationTask... there is no open module property");
                    log.info("NotifyPendingAllocationTask... end");
                    return;
                }

                Map<Integer, List<PropertyDTO>> propertyDTOsByTenantId = openModuleProperty.stream().collect(Collectors.groupingBy(PropertyDTO::getTenantId));
                propertyDTOsByTenantId.forEach((tenantId, propertyDTOs) -> {
                    List<Integer> propertyIds = propertyDTOs.stream().map(PropertyDTO::getId).collect(toList());
                    List<String> abbrs = propertyDTOs.stream().map(PropertyDTO::getAbbr).collect(toList());
                    // 查询资产下的部门管理员（过滤掉离职的），若没，则不处理后续逻辑
                    UserQuery query = new UserQuery();
                    query.setTenantId(tenantId);
                    query.setPropertyIds(propertyIds);
                    Map<String, List<String>> positionsByDepartment = Maps.newHashMap();
                    for (ServiceRequestCenterDepartmentEnum department : ServiceRequestCenterDepartmentEnum.values()) {
                        positionsByDepartment.put(department.getDepartment(), department.getManagerPositions());
                    }
                    query.setPositionsByDepartment(positionsByDepartment);
                    List<UserDTO> userDTOS = userInternalService.listUsers(query).stream().filter(UserDTO::getStatus).collect(Collectors.toList());
                    if (CollectionUtils.isEmpty(userDTOS)) {
                        log.info("NotifyPendingAllocationTask... there is no manager, property: {}", abbrs);
                        return;
                    }

                    // 查询当前资产过去24小时内未分配的 request
                    List<ServiceRequestCenter> pendingAllocationRequests = serviceRequestCenterService.listPendingAllocationTicket(tenantId, propertyDTOs);
                    if (CollectionUtils.isEmpty(pendingAllocationRequests)) {
                        log.info("NotifyPendingAllocationTask... there is no pending allocation request, property: {}", abbrs);
                        return;
                    }

                    // 然后按照部门通知各个部门下的 manager
                    notify(tenantInternalService.getTenantById(tenantId), propertyDTOs, userDTOS, pendingAllocationRequests);
                });
            } catch (Exception e) {
                warningEmail.append("<p>Exception: ").append(e.getMessage()).append("</p>");
                mailInternalService.sendAlertEmail(alertTitle, warningEmail.toString());
                log.error("NotifyPendingAllocationTask... error", e);
            }

            log.info("NotifyPendingAllocationTask... end");
        }

        /**
         * 通知
         *
         * @param tenant                    租户
         * @param propertyDTOs              资产
         * @param userDTOS                  部门管理员
         * @param pendingAllocationRequests 未分配的 request
         * @author yyt
         * @date 2025/12/19
         */
        private void notify(TenantDTO tenant, List<PropertyDTO> propertyDTOs, List<UserDTO> userDTOS, List<ServiceRequestCenter> pendingAllocationRequests) {
            Map<Boolean, List<ServiceRequestCenter>> requestByIsAccount = pendingAllocationRequests.stream()
                    .collect(Collectors.partitioningBy(item-> item.getType() == ServiceRequestCenterTypeEnum.ACCOUNT.getCode()));

            // 查询 request 绑定资产
            Map<Integer, List<Integer>> propertyIdsByRequestId = serviceRequestCenterPropertyService.listPropertyIds(pendingAllocationRequests.stream().map(ServiceRequestCenter::getId).toList())
                    .stream().collect(Collectors.groupingBy(ServiceRequestCenterProperty::getRequestId, Collectors.mapping(ServiceRequestCenterProperty::getPropertyId, toList())));

            List<String> accountDptNames = ServiceRequestCenterDepartmentEnum.listDepartments(ServiceRequestCenterTypeEnum.ACCOUNT.getCode())
                    .stream().map(ServiceRequestCenterDepartmentEnum::getDepartment).toList();
            Map<Boolean, List<UserDTO>> usersIsAccount = userDTOS.stream().collect(Collectors.partitioningBy(user -> accountDptNames.contains(user.getDepartment())));

            // 通知财务部门
            handleNotifyAccount(tenant, propertyDTOs, requestByIsAccount.getOrDefault(true, Collections.emptyList()), usersIsAccount.getOrDefault(true, Collections.emptyList()));
            // 通知其他部门
            handleNotifyOtherDepartment(tenant.getId(), propertyDTOs, requestByIsAccount.getOrDefault(false, Collections.emptyList()),
                    propertyIdsByRequestId, usersIsAccount.getOrDefault(false, Collections.emptyList()));
        }

        /**
         * 通知财务部门管理员
         *
         * @param tenant             租户
         * @param propertyDTOs        资产
         * @param needNotifyRequests 待分配的 request
         * @param users              务部门管理员
         * @author yyt
         * @date 2026/4/2
         **/
        private void handleNotifyAccount(TenantDTO tenant, List<PropertyDTO> propertyDTOs, List<ServiceRequestCenter> needNotifyRequests, List<UserDTO> users) {
            if (CollectionUtils.isEmpty(needNotifyRequests) || CollectionUtils.isEmpty(users)) return;

            String ticket = "Ticket";
            if (needNotifyRequests.size() > 1) {
                ticket = "Tickets";
            }

            // 默认英文
            String language = LanguageEnum.ENGLISH.getLanguage();

            String functionName = i18nMessageUtils.message(ServiceRequestCenterCons.SERVICE_REQUEST_CENTER, language);

            // 封装邮件内容，发送通知
            NotifyDTO notifyDTO = new NotifyDTO();
            notifyDTO.setFunctionPoint(FunctionPointEnum.SERVICE_REQUEST_CENTER_OPERATION.getFunctionPoint());
            // Reminder: 【pendingTickets.length】Ticket(数量大于1需要加上s) Awaiting Assignment
            String subject = i18nMessageUtils.message(ServiceRequestCenterNotifyEnum.TO_BE_ASSIGNED.getI18nCode(), language, needNotifyRequests.size(), ticket);
            notifyDTO.setSubject(subject);

            Map<Integer, PropertyDTO> propertyById = propertyDTOs.stream().collect(Collectors.toMap(PropertyDTO::getId, v -> v));

            for (UserDTO user : users) {
                // 获取令牌后执行后续逻辑
                rateLimiter.acquire();

                Integer propertyId = user.getProperties().get(0).getId();
                // 邮件跳转链接
                String hereLink = serviceRequestCenterNotifyConfigService.getHereLink(propertyById.get(propertyId), ServiceRequestCenterActiveTypeEnum.ASSIGNED_TO_ME.getType());
                // 邮件内容
                Context context = generateEmailContentForAccount(tenant, functionName, needNotifyRequests, hereLink);
                notifyDTO.setPropertyId(propertyId);
                context.setVariable("receiverName", user.getName());
                String emailContent = EmailUtil.getFullEmailContent(subject, templateEngine.process("servicerequestcenter/to-be-assigned_" + language, context), language);
                notifyDTO.setEmailContent(emailContent);

                notifyDTO.setIdentityList(Lists.newArrayList(new NotifyIdentityDTO().setTargetType(EmailConfigCons.TARGET_TYPE_TO)
                        .setSource(EmailConfigCons.SOURCE_USER).setIdentityId(user.getId())));
                notifyDTO.setLineContent(generateLineMsgByContent(needNotifyRequests));

                notifyInternalService.sendNotify(notifyDTO);
            }
        }

        /**
         * 通知非财务部门管理员
         *
         * @param tenantId                租户 id
         * @param propertyDTOs            资产
         * @param request                 待分配的 request
         * @param propertyIdsByRequestId  request 绑定的资产
         * @param usersForOtherDepartment 非财务部门管理员
         * @author yyt
         * @date 2026/4/2
         **/
        private void handleNotifyOtherDepartment(int tenantId, List<PropertyDTO> propertyDTOs, List<ServiceRequestCenter> request,
                                                 Map<Integer, List<Integer>> propertyIdsByRequestId, List<UserDTO> usersForOtherDepartment) {
            if (CollectionUtils.isEmpty(request) || CollectionUtils.isEmpty(usersForOtherDepartment)) return;

            String language = LanguageEnum.ENGLISH.getLanguage();
            String functionName = i18nMessageUtils.message(ServiceRequestCenterCons.SERVICE_REQUEST_CENTER, language);
            Map<Integer, PropertyDTO> propertyById = propertyDTOs.stream().collect(Collectors.toMap(PropertyDTO::getId, v -> v));
            Map<Integer, List<ServiceRequestCenter>> requestByDepartment = request.stream().collect(Collectors.groupingBy(ServiceRequestCenter::getType));

            requestByDepartment.forEach((type, requests) -> {
                // 获取当前部门下的 manager，如果没有，则跳过
                List<ServiceRequestCenterDepartmentEnum> departments = ServiceRequestCenterDepartmentEnum.listDepartments(type);
                if (CollectionUtils.isEmpty(departments)) return;
                List<String> departmentNames = departments.stream().map(ServiceRequestCenterDepartmentEnum::getDepartment).toList();
                List<UserDTO> users = usersForOtherDepartment.stream().filter(user -> departmentNames.contains(user.getDepartment())).toList();
                if (CollectionUtils.isEmpty(users)) return;

                for (UserDTO user : users) {
                    // 获取令牌后执行后续逻辑
                    rateLimiter.acquire();

                    List<Integer> propertyIds = user.getProperties().stream().map(PropertyBasicInfoDTO::getId).toList();
                    if (CollectionUtils.isEmpty(propertyIds)) continue;
                    // 过滤当前用户有权限的单据
                    List<ServiceRequestCenter> needNotifyRequests = requests.stream()
                            .filter(item -> propertyIdsByRequestId.getOrDefault(item.getId(), Collections.emptyList())
                                    .stream().anyMatch(propertyIds::contains))
                            .toList();
                    if (CollectionUtils.isEmpty(needNotifyRequests)) continue;

                    String ticket = "Ticket";
                    if (needNotifyRequests.size() > 1) {
                        ticket = "Tickets";
                    }

                    Integer propertyId = propertyIdsByRequestId.get(needNotifyRequests.get(0).getId()).stream().filter(propertyIds::contains).findFirst().get();
                    PropertyDTO propertyDTO = propertyById.get(propertyId);

                    // 封装邮件内容，发送通知
                    NotifyDTO notifyDTO = new NotifyDTO();
                    notifyDTO.setPropertyId(propertyId);
                    notifyDTO.setFunctionPoint(FunctionPointEnum.SERVICE_REQUEST_CENTER_OPERATION.getFunctionPoint());

                    // Reminder: 【pendingTickets.length】Ticket(数量大于1需要加上s) Awaiting Assignment
                    String subject = i18nMessageUtils.message(ServiceRequestCenterNotifyEnum.TO_BE_ASSIGNED.getI18nCode(), language, needNotifyRequests.size(), ticket);
                    notifyDTO.setSubject(subject);

                    // 邮件跳转链接
                    String hereLink = serviceRequestCenterNotifyConfigService.getHereLink(propertyDTO, ServiceRequestCenterActiveTypeEnum.ASSIGNED_TO_ME.getType());

                    // 邮件内容
                    List<Integer> propertyIdsForRequest = needNotifyRequests.stream()
                            .flatMap(item-> propertyIdsByRequestId.getOrDefault(item.getId(), Collections.emptyList()).stream())
                            .distinct().toList();
                    List<PropertyDTO> propertyForRequest = propertyDTOs.stream().filter(item -> propertyIdsForRequest.contains(item.getId())).toList();
                    Context context = generateEmailContentForOtherDepartment(tenantId, propertyForRequest, functionName, needNotifyRequests, hereLink);
                    context.setVariable("receiverName", user.getName());
                    String emailContent = EmailUtil.getFullEmailContent(subject, templateEngine.process("servicerequestcenter/to-be-assigned_" + language, context), language);
                    notifyDTO.setEmailContent(emailContent);

                    notifyDTO.setIdentityList(Lists.newArrayList(new NotifyIdentityDTO().setTargetType(EmailConfigCons.TARGET_TYPE_TO)
                            .setSource(EmailConfigCons.SOURCE_USER).setIdentityId(user.getId())));

                    notifyDTO.setLineContent(generateLineMsgByContent(needNotifyRequests));

                    notifyInternalService.sendNotify(notifyDTO);
                }
            });
        }

        /**
         * 生成 line 消息
         *
         * @param needNotifyRequests 待分配的 request
         * @return line 消息
         * @author yyt
         * @date 2026/1/9
         **/
        private List<JSONObject> generateLineMsgByContent(List<ServiceRequestCenter> needNotifyRequests) {
            // 有几条待分配的 ticket
            String lineContent = "There are " + needNotifyRequests.size() + " tickets to be assigned";
            return Lists.newArrayList(lineMessageUtil.generateTextMsgByContent(lineContent));
        }

        /**
         * 邮件内容
         *
         * @param tenantDTO    租户
         * @param functionName 功能名称
         * @param requests     未分配的 request
         * @param hereLink     链接
         * @return 邮件内容
         * @author yyt
         * @date 2025/12/19
         */
        private Context generateEmailContentForAccount(TenantDTO tenantDTO, String functionName, List<ServiceRequestCenter> requests, String hereLink) {
            int limit = 5;
            Context context = new Context();
            context.setVariable("propertyName", tenantDTO.getName());
            context.setVariable("functionName", functionName);
            context.setVariable("hereLink", hereLink);
            context.setVariable("requests", packRequestVO(tenantDTO.getId(), requests.stream().sorted(Comparator.comparing(ServiceRequestCenter::getId).reversed())
                    .limit(limit).collect(toList())));
            context.setVariable("requestCount", requests.size());
            context.setVariable("toBeRatedExceedLimit", requests.size() > limit);
            return context;
        }

        /**
         * 邮件内容
         *
         * @param tenantId     租户 id
         * @param propertyDTOS request 的资产
         * @param functionName 功能名称
         * @param requests     未分配的 request
         * @param hereLink     链接
         * @return 邮件内容
         * @author yyt
         * @date 2025/12/19
         */
        private Context generateEmailContentForOtherDepartment(int tenantId, List<PropertyDTO> propertyDTOS, String functionName,
                                                               List<ServiceRequestCenter> requests, String hereLink) {
            int limit = 5;
            Context context = new Context();
            context.setVariable("propertyName", serviceRequestCenterNotifyConfigService.getPropertyNameForEmail(propertyDTOS));
            context.setVariable("functionName", functionName);
            context.setVariable("hereLink", hereLink);
            context.setVariable("requests", packRequestVO(tenantId, requests.stream().sorted(Comparator.comparing(ServiceRequestCenter::getId).reversed())
                    .limit(limit).collect(toList())));
            context.setVariable("requestCount", requests.size());
            context.setVariable("toBeRatedExceedLimit", requests.size() > limit);
            return context;
        }

        /**
         * 封装邮件内容
         *
         * @param tenantId 租户 id
         * @param requests request
         * @return 邮件内容
         * @author yyt
         * @date 2025/12/19
         */
        private List<ServiceRequestCenterEmailVO> packRequestVO(int tenantId, List<ServiceRequestCenter> requests) {
            if (CollectionUtils.isEmpty(requests)) return Collections.emptyList();

            String language = LanguageEnum.ENGLISH.getLanguage();
            String longDate = InternationalizationDateTimeDTO.LONG_DATE_2;

            Map<Integer, String> userNameById = userInternalService.getNameMapByIds(tenantId, requests.stream().map(ServiceRequestCenter::getCreatedBy).collect(Collectors.toSet()));

            return requests.stream().map(request -> {
                ServiceRequestCenterEmailVO emailVO = new ServiceRequestCenterEmailVO();
                emailVO.setServiceNumber(request.getServiceNumber());
                emailVO.setTitle(request.getTitle());
                emailVO.setTypeStr(i18nMessageUtils.message(ServiceRequestCenterTypeEnum.getI18nCode(request.getType()), language));
                emailVO.setStatusStr(i18nMessageUtils.message(ServiceRequestCenterStatusEnum.getI18nCode(request.getStatus()), language));
                emailVO.setPriorityStr(i18nMessageUtils.message(ServiceRequestCenterPriorityEnum.getI18nCode(request.getPriority()), language));
                emailVO.setCreateName(userNameById.get(request.getCreatedBy()));
                emailVO.setCreateDate(InternationalizationUtils.formatDateTime(request.getCreatedTime(), longDate, null, defaultTimezone, language));
                return emailVO;
            }).collect(Collectors.toList());
        }

        /**
         * 防止并发
         *
         * @param key 唯一标识
         * @return 锁成功
         * @author yyt
         * @date 2025/12/19
         */
        private Boolean preventConcurrency(String key) {
            return redisTemplate.opsForValue().setIfAbsent(key, 0, 1, TimeUnit.DAYS);
        }

    }