Java全栈项目--校园快递管理与配送系统(3)

源代码续

package com.campus.express.service.impl;

import com.campus.express.dto.NotificationCreateRequest;
import com.campus.express.dto.NotificationDTO;
import com.campus.express.dto.NotificationSendRequest;
import com.campus.express.dto.NotificationUpdateRequest;
import com.campus.express.exception.BusinessException;
import com.campus.express.exception.ResourceNotFoundException;
import com.campus.express.model.Notification;
import com.campus.express.repository.NotificationRepository;
import com.campus.express.service.NotificationService;
import com.campus.express.service.NotificationTemplateService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

/**
 * 通知服务实现类
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class NotificationServiceImpl implements NotificationService {

    private final NotificationRepository notificationRepository;
    private final NotificationTemplateService notificationTemplateService;

    /**
     * 创建通知
     *
     * @param request 创建通知请求
     * @return 通知DTO
     */
    @Override
    @Transactional
    public NotificationDTO createNotification(NotificationCreateRequest request) {
        log.info("Creating notification with title: {}", request.getTitle());
        
        Notification notification = new Notification();
        BeanUtils.copyProperties(request, notification);
        
        // 设置默认值
        notification.setReadStatus(0); // 默认未读
        notification.setSendStatus(0); // 默认未发送
        notification.setRetryCount(0); // 默认重试次数为0
        notification.setCreatedTime(LocalDateTime.now());
        notification.setUpdatedTime(LocalDateTime.now());
        
        Notification savedNotification = notificationRepository.save(notification);
        log.info("Notification created with ID: {}", savedNotification.getId());
        
        return convertToDTO(savedNotification);
    }

    /**
     * 根据ID查询通知
     *
     * @param id 通知ID
     * @return 通知DTO
     */
    @Override
    public NotificationDTO getNotificationById(Long id) {
        log.info("Getting notification by ID: {}", id);
        
        Notification notification = notificationRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Notification not found with id: " + id));
        
        return convertToDTO(notification);
    }

    /**
     * 更新通知
     *
     * @param id      通知ID
     * @param request 更新通知请求
     * @return 更新后的通知DTO
     */
    @Override
    @Transactional
    public NotificationDTO updateNotification(Long id, NotificationUpdateRequest request) {
        log.info("Updating notification with ID: {}", id);
        
        Notification notification = notificationRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Notification not found with id: " + id));
        
        // 只更新非空字段
        if (request.getTitle() != null) {
            notification.setTitle(request.getTitle());
        }
        if (request.getContent() != null) {
            notification.setContent(request.getContent());
        }
        if (request.getType() != null) {
            notification.setType(request.getType());
        }
        if (request.getChannel() != null) {
            notification.setChannel(request.getChannel());
        }
        if (request.getReadStatus() != null) {
            notification.setReadStatus(request.getReadStatus());
        }
        if (request.getSendStatus() != null) {
            notification.setSendStatus(request.getSendStatus());
        }
        if (request.getFailReason() != null) {
            notification.setFailReason(request.getFailReason());
        }
        if (request.getRetryCount() != null) {
            notification.setRetryCount(request.getRetryCount());
        }
        if (request.getRemark() != null) {
            notification.setRemark(request.getRemark());
        }
        
        notification.setUpdatedTime(LocalDateTime.now());
        
        Notification updatedNotification = notificationRepository.save(notification);
        log.info("Notification updated with ID: {}", updatedNotification.getId());
        
        return convertToDTO(updatedNotification);
    }

    /**
     * 删除通知
     *
     * @param id 通知ID
     */
    @Override
    @Transactional
    public void deleteNotification(Long id) {
        log.info("Deleting notification with ID: {}", id);
        
        if (!notificationRepository.existsById(id)) {
            throw new ResourceNotFoundException("Notification not found with id: " + id);
        }
        
        notificationRepository.deleteById(id);
        log.info("Notification deleted with ID: {}", id);
    }

    /**
     * 分页查询通知列表
     *
     * @param pageable 分页参数
     * @return 通知DTO分页列表
     */
    @Override
    public Page<NotificationDTO> getNotifications(Pageable pageable) {
        log.info("Getting notifications with pagination: {}", pageable);
        
        Page<Notification> notificationPage = notificationRepository.findAll(pageable);
        List<NotificationDTO> notificationDTOs = notificationPage.getContent().stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
        
        return new PageImpl<>(notificationDTOs, pageable, notificationPage.getTotalElements());
    }

    /**
     * 根据接收者ID和接收者类型分页查询通知列表
     *
     * @param receiverId   接收者ID
     * @param receiverType 接收者类型
     * @param pageable     分页参数
     * @return 通知DTO分页列表
     */
    @Override
    public Page<NotificationDTO> getNotificationsByReceiver(Long receiverId, Integer receiverType, Pageable pageable) {
        log.info("Getting notifications by receiver ID: {} and receiver type: {} with pagination: {}", 
                receiverId, receiverType, pageable);
        
        Page<Notification> notificationPage = notificationRepository.findByReceiverIdAndReceiverType(
                receiverId, receiverType, pageable);
        List<NotificationDTO> notificationDTOs = notificationPage.getContent().stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
        
        return new PageImpl<>(notificationDTOs, pageable, notificationPage.getTotalElements());
    }

    /**
     * 根据接收者ID、接收者类型和已读状态分页查询通知列表
     *
     * @param receiverId   接收者ID
     * @param receiverType 接收者类型
     * @param readStatus   已读状态
     * @param pageable     分页参数
     * @return 通知DTO分页列表
     */
    @Override
    public Page<NotificationDTO> getNotificationsByReceiverAndReadStatus(
            Long receiverId, Integer receiverType, Integer readStatus, Pageable pageable) {
        log.info("Getting notifications by receiver ID: {}, receiver type: {} and read status: {} with pagination: {}", 
                receiverId, receiverType, readStatus, pageable);
        
        Page<Notification> notificationPage = notificationRepository.findByReceiverIdAndReceiverTypeAndReadStatus(
                receiverId, receiverType, readStatus, pageable);
        List<NotificationDTO> notificationDTOs = notificationPage.getContent().stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
        
        return new PageImpl<>(notificationDTOs, pageable, notificationPage.getTotalElements());
    }

    /**
     * 根据通知类型分页查询通知列表
     *
     * @param type     通知类型
     * @param pageable 分页参数
     * @return 通知DTO分页列表
     */
    @Override
    public Page<NotificationDTO> getNotificationsByType(Integer type, Pageable pageable) {
        log.info("Getting notifications by type: {} with pagination: {}", type, pageable);
        
        Page<Notification> notificationPage = notificationRepository.findByType(type, pageable);
        List<NotificationDTO> notificationDTOs = notificationPage.getContent().stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
        
        return new PageImpl<>(notificationDTOs, pageable, notificationPage.getTotalElements());
    }

    /**
     * 根据通知渠道分页查询通知列表
     *
     * @param channel  通知渠道
     * @param pageable 分页参数
     * @return 通知DTO分页列表
     */
    @Override
    public Page<NotificationDTO> getNotificationsByChannel(Integer channel, Pageable pageable) {
        log.info("Getting notifications by channel: {} with pagination: {}", channel, pageable);
        
        Page<Notification> notificationPage = notificationRepository.findByChannel(channel, pageable);
        List<NotificationDTO> notificationDTOs = notificationPage.getContent().stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
        
        return new PageImpl<>(notificationDTOs, pageable, notificationPage.getTotalElements());
    }

    /**
     * 根据发送状态分页查询通知列表
     *
     * @param sendStatus 发送状态
     * @param pageable   分页参数
     * @return 通知DTO分页列表
     */
    @Override
    public Page<NotificationDTO> getNotificationsBySendStatus(Integer sendStatus, Pageable pageable) {
        log.info("Getting notifications by send status: {} with pagination: {}", sendStatus, pageable);
        
        Page<Notification> notificationPage = notificationRepository.findBySendStatus(sendStatus, pageable);
        List<NotificationDTO> notificationDTOs = notificationPage.getContent().stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
        
        return new PageImpl<>(notificationDTOs, pageable, notificationPage.getTotalElements());
    }

    /**
     * 根据创建时间范围分页查询通知列表
     *
     * @param startTime 开始时间
     * @param endTime   结束时间
     * @param pageable  分页参数
     * @return 通知DTO分页列表
     */
    @Override
    public Page<NotificationDTO> getNotificationsByCreatedTimeBetween(
            LocalDateTime startTime, LocalDateTime endTime, Pageable pageable) {
        log.info("Getting notifications by created time between: {} and {} with pagination: {}", 
                startTime, endTime, pageable);
        
        Page<Notification> notificationPage = notificationRepository.findByCreatedTimeBetween(
                startTime, endTime, pageable);
        List<NotificationDTO> notificationDTOs = notificationPage.getContent().stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
        
        return new PageImpl<>(notificationDTOs, pageable, notificationPage.getTotalElements());
    }

    /**
     * 根据关联业务ID和关联业务类型查询通知列表
     *
     * @param businessId   关联业务ID
     * @param businessType 关联业务类型
     * @return 通知DTO列表
     */
    @Override
    public List<NotificationDTO> getNotificationsByBusiness(Long businessId, Integer businessType) {
        log.info("Getting notifications by business ID: {} and business type: {}", businessId, businessType);
        
        List<Notification> notifications = notificationRepository.findByBusinessIdAndBusinessType(
                businessId, businessType);
        
        return notifications.stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }

    /**
     * 发送通知
     *
     * @param request 发送通知请求
     * @return 通知DTO列表
     */
    @Override
    @Transactional
    public List<NotificationDTO> sendNotification(NotificationSendRequest request) {
        log.info("Sending notification to {} receivers", request.getReceiverIds().size());
        
        List<Notification> notifications = new ArrayList<>();
        
        // 如果使用模板,则渲染模板内容
        String title = request.getTitle();
        String content = request.getContent();
        
        if (request.getTemplateCode() != null && !request.getTemplateCode().isEmpty()) {
            Map<String, String> renderedContent = notificationTemplateService.renderTemplate(
                    request.getTemplateCode(), request.getTemplateParams());
            
            title = renderedContent.get("title");
            content = renderedContent.get("content");
            
            if (title == null || content == null) {
                throw new BusinessException("Failed to render template: " + request.getTemplateCode());
            }
        } else if (title == null || content == null) {
            throw new BusinessException("Title and content are required when not using template");
        }
        
        // 创建通知
        for (Long receiverId : request.getReceiverIds()) {
            Notification notification = new Notification();
            notification.setTitle(title);
            notification.setContent(content);
            notification.setType(request.getType());
            notification.setChannel(request.getChannel());
            notification.setReceiverId(receiverId);
            notification.setReceiverType(request.getReceiverType());
            notification.setSenderId(request.getSenderId());
            notification.setSenderType(request.getSenderType() != null ? request.getSenderType() : 1);
            notification.setBusinessId(request.getBusinessId());
            notification.setBusinessType(request.getBusinessType());
            notification.setReadStatus(0); // 默认未读
            notification.setSendStatus(0); // 默认未发送
            notification.setRetryCount(0);
            notification.setRemark(request.getRemark());
            notification.setCreatedTime(LocalDateTime.now());
            notification.setUpdatedTime(LocalDateTime.now());
            
            notifications.add(notification);
        }
        
        // 保存通知
        List<Notification> savedNotifications = notificationRepository.saveAll(notifications);
        
        // 如果需要立即发送,则发送通知
        if (request.getSendImmediately()) {
            // 实际发送逻辑,这里模拟发送
            for (Notification notification : savedNotifications) {
                try {
                    // 模拟发送通知
                    boolean success = sendNotificationByChannel(notification);
                    
                    if (success) {
                        notification.setSendStatus(1); // 发送成功
                    } else {
                        notification.setSendStatus(0); // 发送失败
                        notification.setFailReason("Failed to send notification");
                        notification.setRetryCount(notification.getRetryCount() + 1);
                        notification.setNextRetryTime(LocalDateTime.now().plusMinutes(5)); // 5分钟后重试
                    }
                } catch (Exception e) {
                    log.error("Failed to send notification: {}", e.getMessage(), e);
                    notification.setSendStatus(0); // 发送失败
                    notification.setFailReason(e.getMessage());
                    notification.setRetryCount(notification.getRetryCount() + 1);
                    notification.setNextRetryTime(LocalDateTime.now().plusMinutes(5)); // 5分钟后重试
                }
                
                notification.setUpdatedTime(LocalDateTime.now());
            }
            
            // 更新通知状态
            savedNotifications = notificationRepository.saveAll(savedNotifications);
        }
        
        return savedNotifications.stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }

    /**
     * 标记通知为已读
     *
     * @param id 通知ID
     * @return 更新后的通知DTO
     */
    @Override
    @Transactional
    public NotificationDTO markAsRead(Long id) {
        log.info("Marking notification as read with ID: {}", id);
        
        Notification notification = notificationRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Notification not found with id: " + id));
        
        if (notification.getReadStatus() == 1) {
            return convertToDTO(notification); // 已经是已读状态,无需更新
        }
        
        int updated = notificationRepository.updateReadStatus(id, 1, LocalDateTime.now());
        
        if (updated > 0) {
            notification.setReadStatus(1);
            notification.setUpdatedTime(LocalDateTime.now());
        }
        
        return convertToDTO(notification);
    }

    /**
     * 批量标记通知为已读
     *
     * @param ids 通知ID列表
     * @return 更新行数
     */
    @Override
    @Transactional
    public int batchMarkAsRead(List<Long> ids) {
        log.info("Batch marking notifications as read with IDs: {}", ids);
        
        if (ids == null || ids.isEmpty()) {
            return 0;
        }
        
        return notificationRepository.batchUpdateReadStatus(ids, 1, LocalDateTime.now());
    }

    /**
     * 标记接收者的所有通知为已读
     *
     * @param receiverId   接收者ID
     * @param receiverType 接收者类型
     * @return 更新行数
     */
    @Override
    @Transactional
    public int markAllAsRead(Long receiverId, Integer receiverType) {
        log.info("Marking all notifications as read for receiver ID: {} and receiver type: {}", 
                receiverId, receiverType);
        
        // 查询所有未读通知
        Page<Notification> unreadNotifications = notificationRepository.findByReceiverIdAndReceiverTypeAndReadStatus(
                receiverId, receiverType, 0, Pageable.unpaged());
        
        List<Long> unreadIds = unreadNotifications.getContent().stream()
                .map(Notification::getId)
                .collect(Collectors.toList());
        
        if (unreadIds.isEmpty()) {
            return 0;
        }
        
        return notificationRepository.batchUpdateReadStatus(unreadIds, 1, LocalDateTime.now());
    }

    /**
     * 重试发送失败的通知
     *
     * @param id 通知ID
     * @return 更新后的通知DTO
     */
    @Override
    @Transactional
    public NotificationDTO retrySendNotification(Long id) {
        log.info("Retrying to send notification with ID: {}", id);
        
        Notification notification = notificationRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Notification not found with id: " + id));
        
        if (notification.getSendStatus() == 1) {
            throw new BusinessException("Notification has already been sent successfully");
        }
        
        try {
            // 模拟发送通知
            boolean success = sendNotificationByChannel(notification);
            
            if (success) {
                notification.setSendStatus(1); // 发送成功
                notification.setFailReason(null);
            } else {
                notification.setSendStatus(0); // 发送失败
                notification.setFailReason("Failed to send notification");
                notification.setRetryCount(notification.getRetryCount() + 1);
                notification.setNextRetryTime(LocalDateTime.now().plusMinutes(5)); // 5分钟后重试
            }
        } catch (Exception e) {
            log.error("Failed to retry sending notification: {}", e.getMessage(), e);
            notification.setSendStatus(0); // 发送失败
            notification.setFailReason(e.getMessage());
            notification.setRetryCount(notification.getRetryCount() + 1);
            notification.setNextRetryTime(LocalDateTime.now().plusMinutes(5)); // 5分钟后重试
        }
        
        notification.setUpdatedTime(LocalDateTime.now());
        Notification updatedNotification = notificationRepository.save(notification);
        
        return convertToDTO(updatedNotification);
    }

    /**
     * 统计接收者未读通知数量
     *
     * @param receiverId   接收者ID
     * @param receiverType 接收者类型
     * @return 未读通知数量
     */
    @Override
    public long countUnreadNotifications(Long receiverId, Integer receiverType) {
        log.info("Counting unread notifications for receiver ID: {} and receiver type: {}", 
                receiverId, receiverType);
        
        return notificationRepository.countByReceiverIdAndReceiverTypeAndReadStatus(
                receiverId, receiverType, 0);
    }

    /**
     * 统计通知数量
     *
     * @param params 查询参数
     * @return 通知数量
     */
    @Override
    public Map<String, Long> countNotifications(Map<String, Object> params) {
        log.info("Counting notifications with params: {}", params);
        
        Map<String, Long> result = new HashMap<>();
        result.put("total", notificationRepository.count());
        
        // 统计各类型通知数量
        result.put("system", notificationRepository.countByType(1)); // 系统通知
        result.put("express", notificationRepository.countByType(2)); // 快递通知
        result.put("delivery", notificationRepository.countByType(3)); // 配送通知
        result.put("activity", notificationRepository.countByType(4)); // 活动通知
        
        // 统计各渠道通知数量
        result.put("inApp", notificationRepository.countByChannel(1)); // 站内信
        result.put("sms", notificationRepository.countByChannel(2)); // 短信
        result.put("email", notificationRepository.countByChannel(3)); // 邮件
        result.put("push", notificationRepository.countByChannel(4)); // 推送
        
        // 统计发送状态
        result.put("sent", notificationRepository.countBySendStatus(1)); // 已发送
        result.put("unsent", notificationRepository.countBySendStatus(0)); // 未发送
        
        return result;
    }

    /**
     * 根据通知渠道发送通知
     *
     * @param notification 通知对象
     * @return 是否发送成功
     */
    private boolean sendNotificationByChannel(Notification notification) {
        // 模拟发送通知,根据渠道不同,调用不同的发送方法
        switch (notification.getChannel()) {
            case 1: // 站内信
                return sendInAppNotification(notification);
            case 2: // 短信
                return sendSmsNotification(notification);
            case 3: // 邮件
                return sendEmailNotification(notification);
            case 4: // 推送
                return sendPushNotification(notification);
            default:
                log.warn("Unsupported notification channel: {}", notification.getChannel());
                return false;
        }
    }

    /**
     * 发送站内信
     *
     * @param notification 通知对象
     * @return 是否发送成功
     */
    private boolean sendInAppNotification(Notification notification) {
        // 模拟发送站内信,实际项目中应该调用相应的服务
        log.info("Sending in-app notification: {}", notification.getTitle());
        return true; // 模拟发送成功
    }

    /**
     * 发送短信
     *
     * @param notification 通知对象
     * @return 是否发送成功
     */
    private boolean sendSmsNotification(Notification notification) {
        // 模拟发送短信,实际项目中应该调用短信服务商的API
        log.info("Sending SMS notification: {}", notification.getTitle());
        
        // 模拟发送成功率为90%
        return ThreadLocalRandom.current().nextInt(100) < 90;
    }

    /**
     * 发送邮件
     *
     * @param notification 通知对象
     * @return 是否发送成功
     */
    private boolean sendEmailNotification(Notification notification) {
        // 模拟发送邮件,实际项目中应该调用邮件服务
        log.info("Sending email notification: {}", notification.getTitle());
        
        // 模拟发送成功率为95%
        return ThreadLocalRandom.current().nextInt(100) < 95;
    }

    /**
     * 发送推送
     *
     * @param notification 通知对象
     * @return 是否发送成功
     */
    private boolean sendPushNotification(Notification notification) {
        // 模拟发送推送,实际项目中应该调用推送服务
        log.info("Sending push notification: {}", notification.getTitle());
        
        // 模拟发送成功率为85%
        return ThreadLocalRandom.current().nextInt(100) < 85;
    }

    /**
     * 将实体对象转换为DTO对象
     *
     * @param notification 通知实体
     * @return 通知DTO
     */
    private NotificationDTO convertToDTO(Notification notification) {
        NotificationDTO dto = new NotificationDTO();
        BeanUtils.copyProperties(notification, dto);
        
        // 设置类型描述
        if (notification.getType() != null) {
            switch (notification.getType()) {
                case 1:
                    dto.setTypeDesc("系统通知");
                    break;
                case 2:
                    dto.setTypeDesc("快递通知");
                    break;
                case 3:
                    dto.setTypeDesc("配送通知");
                    break;
                case 4:
                    dto.setTypeDesc("活动通知");
                    break;
                default:
                    dto.setTypeDesc("未知类型");
            }
        }
        
        // 设置渠道描述
        if (notification.getChannel() != null) {
            switch (notification.getChannel()) {
                case 1:
                    dto.setChannelDesc("站内信");
                    break;
                case 2:
                    dto.setChannelDesc("短信");
                    break;
                case 3:
                    dto.setChannelDesc("邮件");
                    break;
                case 4:
                    dto.setChannelDesc("推送");
                    break;
                default:
                    dto.setChannelDesc("未知渠道");
            }
        }
        
        // 设置接收者类型描述
        if (notification.getReceiverType() != null) {
            switch (notification.getReceiverType()) {
                case 1:
                    dto.setReceiverTypeDesc("用户");
                    break;
                case 2:
                    dto.setReceiverTypeDesc("配送员");
                    break;
                case 3:
                    dto.setReceiverTypeDesc("管理员");
                    break;
                default:
                    dto.setReceiverTypeDesc("未知类型");
            }
        }
        
        // 设置发送者类型描述
        if (notification.getSenderType() != null) {
            switch (notification.getSenderType()) {
                case 1:
                    dto.setSenderTypeDesc("系统");
                    break;
                case 2:
                    dto.setSenderTypeDesc("用户");
                    break;
                case 3:
                    dto.setSenderTypeDesc("配送员");
                    break;
                case 4:
                    dto.setSenderTypeDesc("管理员");
                    break;
                default:
                    dto.setSenderTypeDesc("未知类型");
            }
        }
        
        // 设置业务类型描述
        if (notification.getBusinessType() != null) {
            switch (notification.getBusinessType()) {
                case 1:
                    dto.setBusinessTypeDesc("快递");
                    break;
                case 2:
                    dto.setBusinessTypeDesc("配送");
                    break;
                case 3:
                    dto.setBusinessTypeDesc("活动");
                    break;
                default:
                    dto.setBusinessTypeDesc("未知类型");
            }
        }
        
        // 设置已读状态描述
        if (notification.getReadStatus() != null) {
            switch (notification.getReadStatus()) {
                case 0:
                    dto.setReadStatusDesc("未读");
                    break;
                case 1:
                    dto.setReadStatusDesc("已读");
                    break;
                default:
                    dto.setReadStatusDesc("未知状态");
            }
        }
        
        // 设置发送状态描述
        if (notification.getSendStatus() != null) {
            switch (notification.getSendStatus()) {
                case 0:
                    dto.setSendStatusDesc("未发送");
                    break;
                case 1:
                    dto.setSendStatusDesc("已发送");
                    break;
                default:
                    dto.setSendStatusDesc("未知状态");
            }
        }
        
        return dto;
    }
}

express-service\src\main\java\com\campus\express\service\impl\NotificationTemplateServiceImpl.java

package com.campus.express.service.impl;

import com.campus.express.dto.NotificationTemplateCreateRequest;
import com.campus.express.dto.NotificationTemplateDTO;
import com.campus.express.dto.NotificationTemplateUpdateRequest;
import com.campus.express.exception.BusinessException;
import com.campus.express.exception.ResourceNotFoundException;
import com.campus.express.model.NotificationTemplate;
import com.campus.express.repository.NotificationTemplateRepository;
import com.campus.express.service.NotificationTemplateService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 通知模板服务实现类
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class NotificationTemplateServiceImpl implements NotificationTemplateService {

    private final NotificationTemplateRepository notificationTemplateRepository;

    /**
     * 创建通知模板
     *
     * @param request 创建通知模板请求
     * @return 通知模板DTO
     */
    @Override
    @Transactional
    public NotificationTemplateDTO createNotificationTemplate(NotificationTemplateCreateRequest request) {
        log.info("Creating notification template with code: {}", request.getCode());
        
        // 检查模板编码是否已存在
        if (notificationTemplateRepository.existsByCode(request.getCode())) {
            throw new BusinessException("Template code already exists: " + request.getCode());
        }
        
        NotificationTemplate template = new NotificationTemplate();
        BeanUtils.copyProperties(request, template);
        
        // 设置默认值
        template.setStatus(1); // 默认启用
        template.setCreatedTime(LocalDateTime.now());
        template.setUpdatedTime(LocalDateTime.now());
        
        NotificationTemplate savedTemplate = notificationTemplateRepository.save(template);
        log.info("Notification template created with ID: {}", savedTemplate.getId());
        
        return convertToDTO(savedTemplate);
    }

    /**
     * 根据ID查询通知模板
     *
     * @param id 通知模板ID
     * @return 通知模板DTO
     */
    @Override
    public NotificationTemplateDTO getNotificationTemplateById(Long id) {
        log.info("Getting notification template by ID: {}", id);
        
        NotificationTemplate template = notificationTemplateRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Notification template not found with id: " + id));
        
        return convertToDTO(template);
    }

    /**
     * 根据模板编码查询通知模板
     *
     * @param code 模板编码
     * @return 通知模板DTO
     */
    @Override
    public NotificationTemplateDTO getNotificationTemplateByCode(String code) {
        log.info("Getting notification template by code: {}", code);
        
        NotificationTemplate template = notificationTemplateRepository.findByCode(code)
                .orElseThrow(() -> new ResourceNotFoundException("Notification template not found with code: " + code));
        
        return convertToDTO(template);
    }

    /**
     * 更新通知模板
     *
     * @param id      通知模板ID
     * @param request 更新通知模板请求
     * @return 更新后的通知模板DTO
     */
    @Override
    @Transactional
    public NotificationTemplateDTO updateNotificationTemplate(Long id, NotificationTemplateUpdateRequest request) {
        log.info("Updating notification template with ID: {}", id);
        
        NotificationTemplate template = notificationTemplateRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Notification template not found with id: " + id));
        
        // 只更新非空字段
        if (request.getName() != null) {
            template.setName(request.getName());
        }
        if (request.getTitle() != null) {
            template.setTitle(request.getTitle());
        }
        if (request.getContent() != null) {
            template.setContent(request.getContent());
        }
        if (request.getType() != null) {
            template.setType(request.getType());
        }
        if (request.getChannel() != null) {
            template.setChannel(request.getChannel());
        }
        if (request.getStatus() != null) {
            template.setStatus(request.getStatus());
        }
        if (request.getRemark() != null) {
            template.setRemark(request.getRemark());
        }
        
        template.setUpdatedTime(LocalDateTime.now());
        
        NotificationTemplate updatedTemplate = notificationTemplateRepository.save(template);
        log.info("Notification template updated with ID: {}", updatedTemplate.getId());
        
        return convertToDTO(updatedTemplate);
    }

    /**
     * 删除通知模板
     *
     * @param id 通知模板ID
     */
    @Override
    @Transactional
    public void deleteNotificationTemplate(Long id) {
        log.info("Deleting notification template with ID: {}", id);
        
        if (!notificationTemplateRepository.existsById(id)) {
            throw new ResourceNotFoundException("Notification template not found with id: " + id);
        }
        
        notificationTemplateRepository.deleteById(id);
        log.info("Notification template deleted with ID: {}", id);
    }

    /**
     * 分页查询通知模板列表
     *
     * @param pageable 分页参数
     * @return 通知模板DTO分页列表
     */
    @Override
    public Page<NotificationTemplateDTO> getNotificationTemplates(Pageable pageable) {
        log.info("Getting notification templates with pagination: {}", pageable);
        
        Page<NotificationTemplate> templatePage = notificationTemplateRepository.findAll(pageable);
        List<NotificationTemplateDTO> templateDTOs = templatePage.getContent().stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
        
        return new PageImpl<>(templateDTOs, pageable, templatePage.getTotalElements());
    }

    /**
     * 根据模板名称分页查询通知模板列表
     *
     * @param name     模板名称
     * @param pageable 分页参数
     * @return 通知模板DTO分页列表
     */
    @Override
    public Page<NotificationTemplateDTO> getNotificationTemplatesByName(String name, Pageable pageable) {
        log.info("Getting notification templates by name: {} with pagination: {}", name, pageable);
        
        Page<NotificationTemplate> templatePage = notificationTemplateRepository.findByNameContaining(name, pageable);
        List<NotificationTemplateDTO> templateDTOs = templatePage.getContent().stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
        
        return new PageImpl<>(templateDTOs, pageable, templatePage.getTotalElements());
    }

    /**
     * 根据模板类型分页查询通知模板列表
     *
     * @param type     模板类型
     * @param pageable 分页参数
     * @return 通知模板DTO分页列表
     */
    @Override
    public Page<NotificationTemplateDTO> getNotificationTemplatesByType(Integer type, Pageable pageable) {
        log.info("Getting notification templates by type: {} with pagination: {}", type, pageable);
        
        Page<NotificationTemplate> templatePage = notificationTemplateRepository.findByType(type, pageable);
        List<NotificationTemplateDTO> templateDTOs = templatePage.getContent().stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
        
        return new PageImpl<>(templateDTOs, pageable, templatePage.getTotalElements());
    }

    /**
     * 根据适用渠道分页查询通知模板列表
     *
     * @param channel  适用渠道
     * @param pageable 分页参数
     * @return 通知模板DTO分页列表
     */
    @Override
    public Page<NotificationTemplateDTO> getNotificationTemplatesByChannel(Integer channel, Pageable pageable) {
        log.info("Getting notification templates by channel: {} with pagination: {}", channel, pageable);
        
        Page<NotificationTemplate> templatePage = notificationTemplateRepository.findByChannel(channel, pageable);
        List<NotificationTemplateDTO> templateDTOs = templatePage.getContent().stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
        
        return new PageImpl<>(templateDTOs, pageable, templatePage.getTotalElements());
    }

    /**
     * 根据状态分页查询通知模板列表
     *
     * @param status   状态
     * @param pageable 分页参数
     * @return 通知模板DTO分页列表
     */
    @Override
    public Page<NotificationTemplateDTO> getNotificationTemplatesByStatus(Integer status, Pageable pageable) {
        log.info("Getting notification templates by status: {} with pagination: {}", status, pageable);
        
        Page<NotificationTemplate> templatePage = notificationTemplateRepository.findByStatus(status, pageable);
        List<NotificationTemplateDTO> templateDTOs = templatePage.getContent().stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
        
        return new PageImpl<>(templateDTOs, pageable, templatePage.getTotalElements());
    }

    /**
     * 根据模板类型和适用渠道查询通知模板列表
     *
     * @param type    模板类型
     * @param channel 适用渠道
     * @return 通知模板DTO列表
     */
    @Override
    public List<NotificationTemplateDTO> getNotificationTemplatesByTypeAndChannel(Integer type, Integer channel) {
        log.info("Getting notification templates by type: {} and channel: {}", type, channel);
        
        List<NotificationTemplate> templates = notificationTemplateRepository.findByTypeAndChannel(type, channel);
        
        return templates.stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }

    /**
     * 根据模板类型和状态查询通知模板列表
     *
     * @param type   模板类型
     * @param status 状态
     * @return 通知模板DTO列表
     */
    @Override
    public List<NotificationTemplateDTO> getNotificationTemplatesByTypeAndStatus(Integer type, Integer status) {
        log.info("Getting notification templates by type: {} and status: {}", type, status);
        
        List<NotificationTemplate> templates = notificationTemplateRepository.findByTypeAndStatus(type, status);
        
        return templates.stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }

    /**
     * 根据适用渠道和状态查询通知模板列表
     *
     * @param channel 适用渠道
     * @param status  状态
     * @return 通知模板DTO列表
     */
    @Override
    public List<NotificationTemplateDTO> getNotificationTemplatesByChannelAndStatus(Integer channel, Integer status) {
        log.info("Getting notification templates by channel: {} and status: {}", channel, status);
        
        List<NotificationTemplate> templates = notificationTemplateRepository.findByChannelAndStatus(channel, status);
        
        return templates.stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }

    /**
     * 更新通知模板状态
     *
     * @param id     通知模板ID
     * @param status 状态
     * @return 更新后的通知模板DTO
     */
    @Override
    @Transactional
    public NotificationTemplateDTO updateTemplateStatus(Long id, Integer status) {
        log.info("Updating notification template status with ID: {} to status: {}", id, status);
        
        NotificationTemplate template = notificationTemplateRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Notification template not found with id: " + id));
        
        int updated = notificationTemplateRepository.updateStatus(id, status, LocalDateTime.now());
        
        if (updated > 0) {
            template.setStatus(status);
            template.setUpdatedTime(LocalDateTime.now());
        }
        
        return convertToDTO(template);
    }

    /**
     * 检查模板编码是否存在
     *
     * @param code 模板编码
     * @return 是否存在
     */
    @Override
    public boolean isTemplateCodeExists(String code) {
        log.info("Checking if template code exists: {}", code);
        
        return notificationTemplateRepository.existsByCode(code);
    }

    /**
     * 渲染模板内容
     *
     * @param templateCode 模板编码
     * @param params       模板参数
     * @return 渲染后的内容
     */
    @Override
    public Map<String, String> renderTemplate(String templateCode, Map<String, Object> params) {
        log.info("Rendering template with code: {} and params: {}", templateCode, params);
        
        NotificationTemplate template = notificationTemplateRepository.findByCode(templateCode)
                .orElseThrow(() -> new ResourceNotFoundException("Notification template not found with code: " + templateCode));
        
        // 检查模板状态
        if (template.getStatus() != 1) {
            throw new BusinessException("Notification template is disabled: " + templateCode);
        }
        
        String title = template.getTitle();
        String content = template.getContent();
        
        // 渲染标题
        title = renderContent(title, params);
        
        // 渲染内容
        content = renderContent(content, params);
        
        Map<String, String> result = new HashMap<>();
        result.put("title", title);
        result.put("content", content);
        
        return result;
    }

    /**
     * 统计通知模板数量
     *
     * @param params 查询参数
     * @return 通知模板数量
     */
    @Override
    public Map<String, Long> countNotificationTemplates(Map<String, Object> params) {
        log.info("Counting notification templates with params: {}", params);
        
        Map<String, Long> result = new HashMap<>();
        result.put("total", notificationTemplateRepository.count());
        
        // 统计各类型模板数量
        result.put("system", notificationTemplateRepository.countByType(1)); // 系统通知
        result.put("express", notificationTemplateRepository.countByType(2)); // 快递通知
        result.put("delivery", notificationTemplateRepository.countByType(3)); // 配送通知
        result.put("activity", notificationTemplateRepository.countByType(4)); // 活动通知
        
        // 统计各渠道模板数量
        result.put("inApp", notificationTemplateRepository.countByChannel(1)); // 站内信
        result.put("sms", notificationTemplateRepository.countByChannel(2)); // 短信
        result.put("email", notificationTemplateRepository.countByChannel(3)); // 邮件
        result.put("push", notificationTemplateRepository.countByChannel(4)); // 推送
        
        // 统计状态
        result.put("enabled", notificationTemplateRepository.countByStatus(1)); // 启用
        result.put("disabled", notificationTemplateRepository.countByStatus(0)); // 停用
        
        return result;
    }

    /**
     * 渲染内容
     *
     * @param content 模板内容
     * @param params  模板参数
     * @return 渲染后的内容
     */
    private String renderContent(String content, Map<String, Object> params) {
        if (content == null || content.isEmpty() || params == null || params.isEmpty()) {
            return content;
        }
        
        // 使用正则表达式匹配 ${key} 格式的占位符
        Pattern pattern = Pattern.compile("\\$\\{([^}]*)\\}");
        Matcher matcher = pattern.matcher(content);
        
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            String key = matcher.group(1);
            Object value = params.get(key);
            
            // 如果参数中没有对应的值,保留原占位符
            String replacement = (value != null) ? value.toString() : matcher.group();
            
            // 替换特殊字符
            replacement = replacement.replace("\\", "\\\\").replace("$", "\\$");
            
            matcher.appendReplacement(sb, replacement);
        }
        matcher.appendTail(sb);
        
        return sb.toString();
    }

    /**
     * 将实体对象转换为DTO对象
     *
     * @param template 通知模板实体
     * @return 通知模板DTO
     */
    private NotificationTemplateDTO convertToDTO(NotificationTemplate template) {
        NotificationTemplateDTO dto = new NotificationTemplateDTO();
        BeanUtils.copyProperties(template, dto);
        
        // 设置类型描述
        if (template.getType() != null) {
            switch (template.getType()) {
                case 1:
                    dto.setTypeDesc("系统通知");
                    break;
                case 2:
                    dto.setTypeDesc("快递通知");
                    break;
                case 3:
                    dto.setTypeDesc("配送通知");
                    break;
                case 4:
                    dto.setTypeDesc("活动通知");
                    break;
                default:
                    dto.setTypeDesc("未知类型");
            }
        }
        
        // 设置渠道描述
        if (template.getChannel() != null) {
            switch (template.getChannel()) {
                case 1:
                    dto.setChannelDesc("站内信");
                    break;
                case 2:
                    dto.setChannelDesc("短信");
                    break;
                case 3:
                    dto.setChannelDesc("邮件");
                    break;
                case 4:
                    dto.setChannelDesc("推送");
                    break;
                default:
                    dto.setChannelDesc("未知渠道");
            }
        }
        
        // 设置状态描述
        if (template.getStatus() != null) {
            switch (template.getStatus()) {
                case 0:
                    dto.setStatusDesc("停用");
                    break;
                case 1:
                    dto.setStatusDesc("启用");
                    break;
                default:
                    dto.setStatusDesc("未知状态");
            }
        }
        
        return dto;
    }
}

express-service\src\main\java\com\campus\express\util\NotificationUtils.java

package com.campus.express.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 通知工具类
 */
@Component
@Slf4j
public class NotificationUtils {

    /**
     * 通知类型常量
     */
    public static final class NotificationType {
        public static final int SYSTEM = 1; // 系统通知
        public static final int EXPRESS = 2; // 快递通知
        public static final int DELIVERY = 3; // 配送通知
        public static final int ACTIVITY = 4; // 活动通知
    }

    /**
     * 通知渠道常量
     */
    public static final class NotificationChannel {
        public static final int IN_APP = 1; // 站内信
        public static final int SMS = 2; // 短信
        public static final int EMAIL = 3; // 邮件
        public static final int PUSH = 4; // 推送
    }

    /**
     * 通知读取状态常量
     */
    public static final class NotificationReadStatus {
        public static final int UNREAD = 0; // 未读
        public static final int READ = 1; // 已读
    }

    /**
     * 通知发送状态常量
     */
    public static final class NotificationSendStatus {
        public static final int UNSENT = 0; // 未发送
        public static final int SENT = 1; // 已发送
        public static final int FAILED = 2; // 发送失败
    }

    /**
     * 接收者类型常量
     */
    public static final class ReceiverType {
        public static final int USER = 1; // 用户
        public static final int DELIVERYMAN = 2; // 配送员
        public static final int ADMIN = 3; // 管理员
    }

    /**
     * 发送者类型常量
     */
    public static final class SenderType {
        public static final int SYSTEM = 1; // 系统
        public static final int USER = 2; // 用户
        public static final int DELIVERYMAN = 3; // 配送员
        public static final int ADMIN = 4; // 管理员
    }

    /**
     * 业务类型常量
     */
    public static final class BusinessType {
        public static final int EXPRESS = 1; // 快递
        public static final int DELIVERY = 2; // 配送
        public static final int ACTIVITY = 3; // 活动
    }

    /**
     * 模板状态常量
     */
    public static final class TemplateStatus {
        public static final int DISABLED = 0; // 停用
        public static final int ENABLED = 1; // 启用
    }

    /**
     * 获取通知类型描述
     *
     * @param type 通知类型
     * @return 通知类型描述
     */
    public static String getNotificationTypeDesc(Integer type) {
        if (type == null) {
            return "未知类型";
        }
        
        switch (type) {
            case NotificationType.SYSTEM:
                return "系统通知";
            case NotificationType.EXPRESS:
                return "快递通知";
            case NotificationType.DELIVERY:
                return "配送通知";
            case NotificationType.ACTIVITY:
                return "活动通知";
            default:
                return "未知类型";
        }
    }

    /**
     * 获取通知渠道描述
     *
     * @param channel 通知渠道
     * @return 通知渠道描述
     */
    public static String getNotificationChannelDesc(Integer channel) {
        if (channel == null) {
            return "未知渠道";
        }
        
        switch (channel) {
            case NotificationChannel.IN_APP:
                return "站内信";
            case NotificationChannel.SMS:
                return "短信";
            case NotificationChannel.EMAIL:
                return "邮件";
            case NotificationChannel.PUSH:
                return "推送";
            default:
                return "未知渠道";
        }
    }

    /**
     * 获取接收者类型描述
     *
     * @param receiverType 接收者类型
     * @return 接收者类型描述
     */
    public static String getReceiverTypeDesc(Integer receiverType) {
        if (receiverType == null) {
            return "未知类型";
        }
        
        switch (receiverType) {
            case ReceiverType.USER:
                return "用户";
            case ReceiverType.DELIVERYMAN:
                return "配送员";
            case ReceiverType.ADMIN:
                return "管理员";
            default:
                return "未知类型";
        }
    }

    /**
     * 获取发送者类型描述
     *
     * @param senderType 发送者类型
     * @return 发送者类型描述
     */
    public static String getSenderTypeDesc(Integer senderType) {
        if (senderType == null) {
            return "未知类型";
        }
        
        switch (senderType) {
            case SenderType.SYSTEM:
                return "系统";
            case SenderType.USER:
                return "用户";
            case SenderType.DELIVERYMAN:
                return "配送员";
            case SenderType.ADMIN:
                return "管理员";
            default:
                return "未知类型";
        }
    }

    /**
     * 获取业务类型描述
     *
     * @param businessType 业务类型
     * @return 业务类型描述
     */
    public static String getBusinessTypeDesc(Integer businessType) {
        if (businessType == null) {
            return "未知类型";
        }
        
        switch (businessType) {
            case BusinessType.EXPRESS:
                return "快递";
            case BusinessType.DELIVERY:
                return "配送";
            case BusinessType.ACTIVITY:
                return "活动";
            default:
                return "未知类型";
        }
    }

    /**
     * 获取通知读取状态描述
     *
     * @param readStatus 通知读取状态
     * @return 通知读取状态描述
     */
    public static String getReadStatusDesc(Integer readStatus) {
        if (readStatus == null) {
            return "未知状态";
        }
        
        switch (readStatus) {
            case NotificationReadStatus.UNREAD:
                return "未读";
            case NotificationReadStatus.READ:
                return "已读";
            default:
                return "未知状态";
        }
    }

    /**
     * 获取通知发送状态描述
     *
     * @param sendStatus 通知发送状态
     * @return 通知发送状态描述
     */
    public static String getSendStatusDesc(Integer sendStatus) {
        if (sendStatus == null) {
            return "未知状态";
        }
        
        switch (sendStatus) {
            case NotificationSendStatus.UNSENT:
                return "未发送";
            case NotificationSendStatus.SENT:
                return "已发送";
            case NotificationSendStatus.FAILED:
                return "发送失败";
            default:
                return "未知状态";
        }
    }

    /**
     * 获取模板状态描述
     *
     * @param status 模板状态
     * @return 模板状态描述
     */
    public static String getTemplateStatusDesc(Integer status) {
        if (status == null) {
            return "未知状态";
        }
        
        switch (status) {
            case TemplateStatus.DISABLED:
                return "停用";
            case TemplateStatus.ENABLED:
                return "启用";
            default:
                return "未知状态";
        }
    }

    /**
     * 渲染模板内容
     *
     * @param content 模板内容
     * @param params  模板参数
     * @return 渲染后的内容
     */
    public static String renderTemplate(String content, Map<String, Object> params) {
        if (content == null || content.isEmpty() || params == null || params.isEmpty()) {
            return content;
        }
        
        // 使用正则表达式匹配 ${key} 格式的占位符
        Pattern pattern = Pattern.compile("\\$\\{([^}]*)\\}");
        Matcher matcher = pattern.matcher(content);
        
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            String key = matcher.group(1);
            Object value = params.get(key);
            
            // 如果参数中没有对应的值,保留原占位符
            String replacement = (value != null) ? value.toString() : matcher.group();
            
            // 替换特殊字符
            replacement = replacement.replace("\\", "\\\\").replace("$", "\\$");
            
            matcher.appendReplacement(sb, replacement);
        }
        matcher.appendTail(sb);
        
        return sb.toString();
    }

    /**
     * 获取短信内容(去除HTML标签)
     *
     * @param content HTML内容
     * @return 纯文本内容
     */
    public static String getSmsContent(String content) {
        if (content == null || content.isEmpty()) {
            return content;
        }
        
        // 去除HTML标签
        return content.replaceAll("<[^>]*>", "");
    }

    /**
     * 截断内容(适用于短信等有长度限制的渠道)
     *
     * @param content 内容
     * @param maxLength 最大长度
     * @return 截断后的内容
     */
    public static String truncateContent(String content, int maxLength) {
        if (content == null || content.isEmpty() || content.length() <= maxLength) {
            return content;
        }
        
        return content.substring(0, maxLength - 3) + "...";
    }

    /**
     * 生成通知摘要
     *
     * @param content 通知内容
     * @param maxLength 最大长度
     * @return 通知摘要
     */
    public static String generateSummary(String content, int maxLength) {
        if (content == null || content.isEmpty()) {
            return "";
        }
        
        // 去除HTML标签
        String plainText = content.replaceAll("<[^>]*>", "");
        
        // 截断内容
        return truncateContent(plainText, maxLength);
    }

    /**
     * 验证邮箱格式
     *
     * @param email 邮箱地址
     * @return 是否有效
     */
    public static boolean isValidEmail(String email) {
        if (email == null || email.isEmpty()) {
            return false;
        }
        
        // 简单的邮箱格式验证
        String regex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$";
        return email.matches(regex);
    }

    /**
     * 验证手机号格式
     *
     * @param mobile 手机号
     * @return 是否有效
     */
    public static boolean isValidMobile(String mobile) {
        if (mobile == null || mobile.isEmpty()) {
            return false;
        }
        
        // 简单的手机号格式验证(中国大陆手机号)
        String regex = "^1[3-9]\\d{9}$";
        return mobile.matches(regex);
    }

    /**
     * 获取通知统计信息
     *
     * @return 统计信息
     */
    public static Map<String, String> getNotificationStatLabels() {
        Map<String, String> labels = new HashMap<>();
        labels.put("total", "总数");
        labels.put("system", "系统通知");
        labels.put("express", "快递通知");
        labels.put("delivery", "配送通知");
        labels.put("activity", "活动通知");
        labels.put("inApp", "站内信");
        labels.put("sms", "短信");
        labels.put("email", "邮件");
        labels.put("push", "推送");
        labels.put("sent", "已发送");
        labels.put("unsent", "未发送");
        labels.put("failed", "发送失败");
        labels.put("read", "已读");
        labels.put("unread", "未读");
        labels.put("enabled", "启用");
        labels.put("disabled", "停用");
        return labels;
    }
}

express-service\src\main\resources\application.yml

server:
  port: 8082
  servlet:
    context-path: /api/express

spring:
  application:
    name: express-service
  datasource:
    url: jdbc:mysql://localhost:3306/campus_express_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
        format_sql: true

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

# JWT配置
jwt:
  header: Authorization
  prefix: Bearer
  secret: campusExpressSecretKey123456789012345678901234567890
  expiration: 86400000  # 24小时,单位:毫秒

# Feign配置
feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
  hystrix:
    enabled: true

# 日志配置
logging:
  level:
    com.campus.express: INFO
    org.springframework.web: INFO
    org.hibernate: ERROR

express-service\src\main\resources\db\data.sql

-- 校园快递管理与配送系统初始数据

-- 初始化角色数据
INSERT INTO `role` (`id`, `role_name`, `role_key`, `role_sort`, `status`, `created_time`, `updated_time`, `remark`) VALUES
(1, '超级管理员', 'admin', 1, 1, NOW(), NOW(), '超级管理员'),
(2, '配送员', 'deliveryman', 2, 1, NOW(), NOW(), '配送员'),
(3, '普通用户', 'user', 3, 1, NOW(), NOW(), '普通用户');

-- 初始化用户数据(密码统一为123456的MD5加密)
INSERT INTO `user` (`id`, `username`, `password`, `salt`, `real_name`, `mobile`, `email`, `avatar`, `gender`, `student_id`, `status`, `type`, `created_time`, `updated_time`, `remark`) VALUES
(1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 'salt', '系统管理员', '13800138000', 'admin@example.com', NULL, 1, NULL, 1, 3, NOW(), NOW(), '系统管理员'),
(2, 'deliveryman1', 'e10adc3949ba59abbe56e057f20f883e', 'salt', '张三', '13800138001', 'deliveryman1@example.com', NULL, 1, NULL, 1, 2, NOW(), NOW(), '配送员'),
(3, 'deliveryman2', 'e10adc3949ba59abbe56e057f20f883e', 'salt', '李四', '13800138002', 'deliveryman2@example.com', NULL, 1, NULL, 1, 2, NOW(), NOW(), '配送员'),
(4, 'user1', 'e10adc3949ba59abbe56e057f20f883e', 'salt', '王五', '13800138003', 'user1@example.com', NULL, 1, '2021001', 1, 1, NOW(), NOW(), '普通用户'),
(5, 'user2', 'e10adc3949ba59abbe56e057f20f883e', 'salt', '赵六', '13800138004', 'user2@example.com', NULL, 2, '2021002', 1, 1, NOW(), NOW(), '普通用户');

-- 初始化用户角色关联数据
INSERT INTO `user_role` (`user_id`, `role_id`) VALUES
(1, 1),
(2, 2),
(3, 2),
(4, 3),
(5, 3);

-- 初始化配送员数据
INSERT INTO `deliveryman` (`id`, `user_id`, `status`, `work_status`, `score`, `delivery_count`, `created_time`, `updated_time`, `remark`) VALUES
(1, 2, 1, 1, 4.8, 120, NOW(), NOW(), '资深配送员'),
(2, 3, 1, 0, 4.5, 80, NOW(), NOW(), '配送员');

-- 初始化配送区域数据
INSERT INTO `delivery_area` (`id`, `name`, `description`, `status`, `created_time`, `updated_time`, `remark`) VALUES
(1, '东校区', '东校区包括1-5号宿舍楼', 1, NOW(), NOW(), '东校区配送范围'),
(2, '西校区', '西校区包括6-10号宿舍楼', 1, NOW(), NOW(), '西校区配送范围'),
(3, '南校区', '南校区包括11-15号宿舍楼', 1, NOW(), NOW(), '南校区配送范围'),
(4, '北校区', '北校区包括16-20号宿舍楼', 1, NOW(), NOW(), '北校区配送范围');

-- 初始化配送路线数据
INSERT INTO `delivery_route` (`id`, `name`, `area_id`, `description`, `status`, `created_time`, `updated_time`, `remark`) VALUES
(1, '东校区1号线', 1, '东校区1-3号宿舍楼配送路线', 1, NOW(), NOW(), '东校区1号配送路线'),
(2, '东校区2号线', 1, '东校区4-5号宿舍楼配送路线', 1, NOW(), NOW(), '东校区2号配送路线'),
(3, '西校区1号线', 2, '西校区6-8号宿舍楼配送路线', 1, NOW(), NOW(), '西校区1号配送路线'),
(4, '西校区2号线', 2, '西校区9-10号宿舍楼配送路线', 1, NOW(), NOW(), '西校区2号配送路线'),
(5, '南校区1号线', 3, '南校区11-15号宿舍楼配送路线', 1, NOW(), NOW(), '南校区1号配送路线'),
(6, '北校区1号线', 4, '北校区16-20号宿舍楼配送路线', 1, NOW(), NOW(), '北校区1号配送路线');

-- 初始化快递数据
INSERT INTO `express` (`id`, `express_no`, `express_company`, `sender_name`, `sender_mobile`, `sender_address`, `receiver_name`, `receiver_mobile`, `receiver_address`, `user_id`, `weight`, `express_type`, `status`, `arrival_time`, `pickup_code`, `created_time`, `updated_time`, `remark`) VALUES
(1, 'SF1001001', '顺丰速运', '张三', '13900001111', '北京市海淀区中关村', '王五', '13800138003', '东校区3号宿舍楼303', 4, 1.5, 1, 5, DATE_SUB(NOW(), INTERVAL 1 DAY), '123456', DATE_SUB(NOW(), INTERVAL 3 DAY), NOW(), '书籍'),
(2, 'YT1001002', '圆通速递', '李四', '13900002222', '上海市浦东新区', '王五', '13800138003', '东校区3号宿舍楼303', 4, 2.0, 1, 6, DATE_SUB(NOW(), INTERVAL 2 DAY), '234567', DATE_SUB(NOW(), INTERVAL 4 DAY), NOW(), '衣物'),
(3, 'ZT1001003', '中通快递', '王五', '13900003333', '广州市天河区', '赵六', '13800138004', '西校区8号宿舍楼808', 5, 0.5, 1, 7, DATE_SUB(NOW(), INTERVAL 3 DAY), '345678', DATE_SUB(NOW(), INTERVAL 5 DAY), NOW(), '文件'),
(4, 'YD1001004', '韵达快递', '赵六', '13900004444', '深圳市南山区', '赵六', '13800138004', '西校区8号宿舍楼808', 5, 3.0, 2, 4, NOW(), '456789', DATE_SUB(NOW(), INTERVAL 1 DAY), NOW(), '电子产品'),
(5, 'JD1001005', '京东物流', '电商平台', '4008888888', '北京市朝阳区', '王五', '13800138003', '东校区3号宿舍楼303', 4, 1.0, 1, 5, DATE_SUB(NOW(), INTERVAL 1 DAY), '567890', DATE_SUB(NOW(), INTERVAL 2 DAY), NOW(), '日用品');

-- 初始化配送任务数据
INSERT INTO `delivery_task` (`id`, `express_id`, `deliveryman_id`, `route_id`, `status`, `expected_delivery_time`, `actual_delivery_time`, `created_time`, `updated_time`, `remark`) VALUES
(1, 1, 1, 1, 3, DATE_ADD(NOW(), INTERVAL 2 HOUR), NULL, NOW(), NOW(), '东校区配送任务'),
(2, 2, 1, 1, 4, DATE_ADD(NOW(), INTERVAL 1 HOUR), NULL, NOW(), NOW(), '东校区配送任务'),
(3, 3, 2, 3, 5, DATE_SUB(NOW(), INTERVAL 1 HOUR), DATE_SUB(NOW(), INTERVAL 30 MINUTE), NOW(), NOW(), '西校区配送任务'),
(4, 4, NULL, 3, 1, NULL, NULL, NOW(), NOW(), '西校区待分配配送任务'),
(5, 5, 1, 1, 2, DATE_ADD(NOW(), INTERVAL 3 HOUR), NULL, NOW(), NOW(), '东校区配送任务');

-- 初始化快递跟踪记录数据
INSERT INTO `express_tracking` (`id`, `express_id`, `status`, `operator_id`, `operator_type`, `operation_time`, `location`, `remark`) VALUES
(1, 1, 1, NULL, 1, DATE_SUB(NOW(), INTERVAL 3 DAY), '北京市海淀区中关村', '快件已揽收'),
(2, 1, 2, NULL, 1, DATE_SUB(NOW(), INTERVAL 2 DAY, 12 HOUR), '北京市海淀区中关村', '快件在运输中'),
(3, 1, 3, NULL, 1, DATE_SUB(NOW(), INTERVAL 2 DAY), '北京市海淀区中关村', '快件运输中'),
(4, 1, 4, NULL, 1, DATE_SUB(NOW(), INTERVAL 1 DAY), '校园快递驿站', '快件已到达'),
(5, 1, 5, 1, 4, DATE_SUB(NOW(), INTERVAL 12 HOUR), '校园快递驿站', '快件待配送'),
(6, 2, 1, NULL, 1, DATE_SUB(NOW(), INTERVAL 4 DAY), '上海市浦东新区', '快件已揽收'),
(7, 2, 2, NULL, 1, DATE_SUB(NOW(), INTERVAL 3 DAY), '上海市浦东新区', '快件在运输中'),
(8, 2, 3, NULL, 1, DATE_SUB(NOW(), INTERVAL 2 DAY, 12 HOUR), '上海市浦东新区', '快件运输中'),
(9, 2, 4, NULL, 1, DATE_SUB(NOW(), INTERVAL 2 DAY), '校园快递驿站', '快件已到达'),
(10, 2, 5, 1, 4, DATE_SUB(NOW(), INTERVAL 1 DAY), '校园快递驿站', '快件待配送'),
(11, 2, 6, 2, 3, DATE_SUB(NOW(), INTERVAL 2 HOUR), '校园快递驿站', '快件配送中');

-- 初始化通知模板数据
INSERT INTO `notification_template` (`id`, `code`, `name`, `title`, `content`, `type`, `channel`, `status`, `created_time`, `updated_time`, `remark`) VALUES
(1, 'EXPRESS_ARRIVAL', '快递到达通知', '您的快递已到达', '尊敬的${receiverName},您的快递(${expressNo})已到达校园驿站,取件码:${pickupCode},请及时取件。', 2, 1, 1, NOW(), NOW(), '快递到达站内信通知'),
(2, 'EXPRESS_ARRIVAL_SMS', '快递到达短信', '【校园快递】您的快递已到达', '【校园快递】尊敬的${receiverName},您的快递(${expressNo})已到达校园驿站,取件码:${pickupCode},请及时取件。', 2, 2, 1, NOW(), NOW(), '快递到达短信通知'),
(3, 'EXPRESS_DELIVERY', '快递配送通知', '您的快递正在配送中', '尊敬的${receiverName},您的快递(${expressNo})正在配送中,预计送达时间:${expectedTime},配送员:${deliverymanName},联系电话:${deliverymanMobile}。', 2, 1, 1, NOW(), NOW(), '快递配送站内信通知'),
(4, 'EXPRESS_DELIVERY_SMS', '快递配送短信', '【校园快递】您的快递正在配送中', '【校园快递】尊敬的${receiverName},您的快递(${expressNo})正在配送中,预计送达时间:${expectedTime},配送员:${deliverymanName},联系电话:${deliverymanMobile}。', 2, 2, 1, NOW(), NOW(), '快递配送短信通知'),
(5, 'EXPRESS_SIGNED', '快递签收通知', '您的快递已签收', '尊敬的${receiverName},您的快递(${expressNo})已签收,感谢您使用校园快递服务。', 2, 1, 1, NOW(), NOW(), '快递签收站内信通知'),
(6, 'EXPRESS_SIGNED_SMS', '快递签收短信', '【校园快递】您的快递已签收', '【校园快递】尊敬的${receiverName},您的快递(${expressNo})已签收,感谢您使用校园快递服务。', 2, 2, 1, NOW(), NOW(), '快递签收短信通知'),
(7, 'DELIVERY_TASK_ASSIGN', '配送任务分配通知', '您有新的配送任务', '尊敬的${deliverymanName},您有新的配送任务(${taskId}),请及时查看并接单。', 3, 1, 1, NOW(), NOW(), '配送任务分配站内信通知'),
(8, 'DELIVERY_TASK_ASSIGN_SMS', '配送任务分配短信', '【校园快递】您有新的配送任务', '【校园快递】尊敬的${deliverymanName},您有新的配送任务(${taskId}),请及时查看并接单。', 3, 2, 1, NOW(), NOW(), '配送任务分配短信通知'),
(9, 'SYSTEM_NOTICE', '系统通知', '系统通知:${title}', '${content}', 1, 1, 1, NOW(), NOW(), '系统站内信通知'),
(10, 'SYSTEM_NOTICE_SMS', '系统通知短信', '【校园快递】系统通知', '【校园快递】${content}', 1, 2, 1, NOW(), NOW(), '系统短信通知');

-- 初始化通知数据
INSERT INTO `notification` (`id`, `title`, `content`, `type`, `channel`, `receiver_id`, `receiver_type`, `sender_id`, `sender_type`, `business_id`, `business_type`, `read_status`, `send_status`, `created_time`, `updated_time`, `remark`) VALUES
(1, '您的快递已到达', '尊敬的王五,您的快递(SF1001001)已到达校园驿站,取件码:123456,请及时取件。', 2, 1, 4, 1, NULL, 1, 1, 1, 1, 1, DATE_SUB(NOW(), INTERVAL 1 DAY), NOW(), '快递到达通知'),
(2, '您的快递正在配送中', '尊敬的王五,您的快递(YT1001002)正在配送中,预计送达时间:2小时后,配送员:张三,联系电话:13800138001。', 2, 1, 4, 1, NULL, 1, 2, 1, 0, 1, NOW(), NOW(), '快递配送通知'),
(3, '您的快递已签收', '尊敬的赵六,您的快递(ZT1001003)已签收,感谢您使用校园快递服务。', 2, 1, 5, 1, NULL, 1, 3, 1, 0, 1, DATE_SUB(NOW(), INTERVAL 3 HOUR), NOW(), '快递签收通知'),
(4, '您有新的配送任务', '尊敬的张三,您有新的配送任务(5),请及时查看并接单。', 3, 1, 2, 2, NULL, 1, 5, 2, 1, 1, NOW(), NOW(), '配送任务分配通知'),
(5, '系统通知:系统维护', '尊敬的用户,系统将于2025年4月10日凌晨2:00-4:00进行系统维护,期间服务可能暂时不可用,给您带来的不便敬请谅解。', 1, 1, 4, 1, NULL, 1, NULL, NULL, 0, 1, NOW(), NOW(), '系统维护通知'),
(6, '系统通知:系统维护', '尊敬的用户,系统将于2025年4月10日凌晨2:00-4:00进行系统维护,期间服务可能暂时不可用,给您带来的不便敬请谅解。', 1, 1, 5, 1, NULL, 1, NULL, NULL, 0, 1, NOW(), NOW(), '系统维护通知'),
(7, '系统通知:系统维护', '尊敬的配送员,系统将于2025年4月10日凌晨2:00-4:00进行系统维护,期间服务可能暂时不可用,给您带来的不便敬请谅解。', 1, 1, 2, 2, NULL, 1, NULL, NULL, 1, 1, NOW(), NOW(), '系统维护通知'),
(8, '系统通知:系统维护', '尊敬的配送员,系统将于2025年4月10日凌晨2:00-4:00进行系统维护,期间服务可能暂时不可用,给您带来的不便敬请谅解。', 1, 1, 3, 2, NULL, 1, NULL, NULL, 0, 1, NOW(), NOW(), '系统维护通知'),
(9, '系统通知:系统维护', '尊敬的管理员,系统将于2025年4月10日凌晨2:00-4:00进行系统维护,期间服务可能暂时不可用,请提前做好相关准备工作。', 1, 1, 1, 3, NULL, 1, NULL, NULL, 1, 1, NOW(), NOW(), '系统维护通知');

-- 初始化系统配置数据
INSERT INTO `system_config` (`id`, `config_key`, `config_value`, `config_type`, `description`, `status`, `created_time`, `updated_time`, `remark`) VALUES
(1, 'system_name', '校园快递管理与配送系统', 'system', '系统名称', 1, NOW(), NOW(), '系统名称配置'),
(2, 'system_logo', '/static/logo.png', 'system', '系统Logo', 1, NOW(), NOW(), '系统Logo配置'),
(3, 'admin_email', 'admin@example.com', 'system', '管理员邮箱', 1, NOW(), NOW(), '管理员邮箱配置'),
(4, 'sms_enabled', 'true', 'notification', '是否启用短信通知', 1, NOW(), NOW(), '短信通知开关'),
(5, 'email_enabled', 'true', 'notification', '是否启用邮件通知', 1, NOW(), NOW(), '邮件通知开关'),
(6, 'push_enabled', 'true', 'notification', '是否启用推送通知', 1, NOW(), NOW(), '推送通知开关'),
(7, 'express_pickup_expiration_days', '7', 'express', '快递取件过期天数', 1, NOW(), NOW(), '快递取件过期天数配置'),
(8, 'delivery_timeout_minutes', '120', 'delivery', '配送超时时间(分钟)', 1, NOW(), NOW(), '配送超时时间配置'),
(9, 'max_delivery_count_per_day', '30', 'delivery', '配送员每日最大配送量', 1, NOW(), NOW(), '配送员每日最大配送量配置'),
(10, 'system_announcement', '欢迎使用校园快递管理与配送系统!', 'system', '系统公告', 1, NOW(), NOW(), '系统公告配置');

-- 初始化菜单数据
INSERT INTO `menu` (`id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `created_time`, `updated_time`, `remark`) VALUES
(1, '系统管理', 0, 1, 'system', NULL, 0, 0, 'M', 1, 1, '', 'system', NOW(), NOW(), '系统管理目录'),
(2, '用户管理', 1, 1, 'user', 'system/user/index', 0, 0, 'C', 1, 1, 'system:user:list', 'user', NOW(), NOW(), '用户管理菜单'),
(3, '角色管理', 1, 2, 'role', 'system/role/index', 0, 0, 'C', 1, 1, 'system:role:list', 'role', NOW(), NOW(), '角色管理菜单'),
(4, '菜单管理', 1, 3, 'menu', 'system/menu/index', 0, 0, 'C', 1, 1, 'system:menu:list', 'menu', NOW(), NOW(), '菜单管理菜单'),
(5, '配送管理', 0, 2, 'delivery', NULL, 0, 0, 'M', 1, 1, '', 'delivery', NOW(), NOW(), '配送管理目录'),
(6, '配送员管理', 5, 1, 'deliveryman', 'delivery/deliveryman/index', 0, 0, 'C', 1, 1, 'delivery:deliveryman:list', 'deliveryman', NOW(), NOW(), '配送员管理菜单'),
(7, '配送区域管理', 5, 2, 'area', 'delivery/area/index', 0, 0, 'C', 1, 1, 'delivery:area:list', 'area', NOW(), NOW(), '配送区域管理菜单'),
(8, '配送路线管理', 5, 3, 'route', 'delivery/route/index', 0, 0, 'C', 1, 1, 'delivery:route:list', 'route', NOW(), NOW(), '配送路线管理菜单'),
(9, '配送任务管理', 5, 4, 'task', 'delivery/task/index', 0, 0, 'C', 1, 1, 'delivery:task:list', 'task', NOW(), NOW(), '配送任务管理菜单'),
(10, '快递管理', 0, 3, 'express', NULL, 0, 0, 'M', 1, 1, '', 'express', NOW(), NOW(), '快递管理目录'),
(11, '快递管理', 10, 1, 'express', 'express/express/index', 0, 0, 'C', 1, 1, 'express:express:list', 'express', NOW(), NOW(), '快递管理菜单'),
(12, '快递跟踪', 10, 2, 'tracking', 'express/tracking/index', 0, 0, 'C', 1, 1, 'express:tracking:list', 'tracking', NOW(), NOW(), '快递跟踪菜单'),
(13, '通知管理', 0, 4, 'notification', NULL, 0, 0, 'M', 1, 1, '', 'notification', NOW(), NOW(), '通知管理目录'),
(14, '通知管理', 13, 1, 'notification', 'notification/notification/index', 0, 0, 'C', 1, 1, 'notification:notification:list', 'notification', NOW(), NOW(), '通知管理菜单'),
(15, '通知模板管理', 13, 2, 'template', 'notification/template/index', 0, 0, 'C', 1, 1, 'notification:template:list', 'template', NOW(), NOW(), '通知模板管理菜单'),
(16, '系统监控', 0, 5, 'monitor', NULL, 0, 0, 'M', 1, 1, '', 'monitor', NOW(), NOW(), '系统监控目录'),
(17, '操作日志', 16, 1, 'operlog', 'monitor/operlog/index', 0, 0, 'C', 1, 1, 'monitor:operlog:list', 'log', NOW(), NOW(), '操作日志菜单'),
(18, '登录日志', 16, 2, 'logininfor', 'monitor/logininfor/index', 0, 0, 'C', 1, 1, 'monitor:logininfor:list', 'logininfor', NOW(), NOW(), '登录日志菜单'),
(19, '系统配置', 1, 4, 'config', 'system/config/index', 0, 0, 'C', 1, 1, 'system:config:list', 'config', NOW(), NOW(), '系统配置菜单'),
(20, '文件管理', 1, 5, 'file', 'system/file/index', 0, 0, 'C', 1, 1, 'system:file:list', 'file', NOW(), NOW(), '文件管理菜单');

-- 初始化角色菜单关联数据
INSERT INTO `role_menu` (`role_id`, `menu_id`) VALUES
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10), (1, 11), (1, 12), (1, 13), (1, 14), (1, 15), (1, 16), (1, 17), (1, 18), (1, 19), (1, 20),
(2, 5), (2, 9), (2, 10), (2, 11), (2, 12),
(3, 10), (3, 11), (3, 12);

express-service\src\main\resources\db\schema.sql

-- 校园快递管理与配送系统数据库表结构

-- 用户表
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '密码',
  `salt` varchar(50) NOT NULL COMMENT '密码盐',
  `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
  `mobile` varchar(20) DEFAULT NULL COMMENT '手机号',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `avatar` varchar(200) DEFAULT NULL COMMENT '头像URL',
  `gender` tinyint(1) DEFAULT '0' COMMENT '性别:0-未知,1-男,2-女',
  `student_id` varchar(50) DEFAULT NULL COMMENT '学号',
  `id_card` varchar(50) DEFAULT NULL COMMENT '身份证号',
  `address` varchar(200) DEFAULT NULL COMMENT '地址',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
  `type` tinyint(1) DEFAULT '1' COMMENT '用户类型:1-普通用户,2-配送员,3-管理员',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '更新时间',
  `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`),
  UNIQUE KEY `uk_mobile` (`mobile`),
  UNIQUE KEY `uk_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 配送员表
CREATE TABLE `deliveryman` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '配送员ID',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `id_card_front` varchar(200) DEFAULT NULL COMMENT '身份证正面照片URL',
  `id_card_back` varchar(200) DEFAULT NULL COMMENT '身份证背面照片URL',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
  `work_status` tinyint(1) DEFAULT '0' COMMENT '工作状态:0-休息中,1-工作中',
  `score` decimal(3,1) DEFAULT '5.0' COMMENT '评分',
  `delivery_count` int(11) DEFAULT '0' COMMENT '配送次数',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '更新时间',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配送员表';

-- 快递表
CREATE TABLE `express` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '快递ID',
  `express_no` varchar(50) NOT NULL COMMENT '快递单号',
  `express_company` varchar(50) NOT NULL COMMENT '快递公司',
  `sender_name` varchar(50) DEFAULT NULL COMMENT '寄件人姓名',
  `sender_mobile` varchar(20) DEFAULT NULL COMMENT '寄件人手机号',
  `sender_address` varchar(200) DEFAULT NULL COMMENT '寄件人地址',
  `receiver_name` varchar(50) NOT NULL COMMENT '收件人姓名',
  `receiver_mobile` varchar(20) NOT NULL COMMENT '收件人手机号',
  `receiver_address` varchar(200) NOT NULL COMMENT '收件人地址',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `weight` decimal(10,2) DEFAULT NULL COMMENT '重量(kg)',
  `express_type` tinyint(1) DEFAULT '1' COMMENT '快递类型:1-普通,2-易碎品,3-贵重物品',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:1-待揽收,2-已揽收,3-运输中,4-已到达,5-待配送,6-配送中,7-已签收,8-已取消',
  `arrival_time` datetime DEFAULT NULL COMMENT '到达时间',
  `pickup_code` varchar(10) DEFAULT NULL COMMENT '取件码',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_express_no` (`express_no`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_status` (`status`),
  KEY `idx_arrival_time` (`arrival_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='快递表';

-- 配送区域表
CREATE TABLE `delivery_area` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '配送区域ID',
  `name` varchar(50) NOT NULL COMMENT '区域名称',
  `description` varchar(200) DEFAULT NULL COMMENT '区域描述',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '更新时间',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配送区域表';

-- 配送路线表
CREATE TABLE `delivery_route` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '配送路线ID',
  `name` varchar(50) NOT NULL COMMENT '路线名称',
  `area_id` bigint(20) NOT NULL COMMENT '配送区域ID',
  `description` varchar(200) DEFAULT NULL COMMENT '路线描述',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '更新时间',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_area_id` (`area_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配送路线表';

-- 配送任务表
CREATE TABLE `delivery_task` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '配送任务ID',
  `express_id` bigint(20) NOT NULL COMMENT '快递ID',
  `deliveryman_id` bigint(20) DEFAULT NULL COMMENT '配送员ID',
  `route_id` bigint(20) DEFAULT NULL COMMENT '配送路线ID',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:1-待分配,2-待接单,3-已接单,4-配送中,5-已完成,6-已取消',
  `expected_delivery_time` datetime DEFAULT NULL COMMENT '预计配送时间',
  `actual_delivery_time` datetime DEFAULT NULL COMMENT '实际配送时间',
  `signature_image` varchar(200) DEFAULT NULL COMMENT '签收图片URL',
  `score` tinyint(1) DEFAULT NULL COMMENT '评分:1-5',
  `evaluation` varchar(500) DEFAULT NULL COMMENT '评价内容',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '更新时间',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_express_id` (`express_id`),
  KEY `idx_deliveryman_id` (`deliveryman_id`),
  KEY `idx_route_id` (`route_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配送任务表';

-- 快递跟踪记录表
CREATE TABLE `express_tracking` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '跟踪记录ID',
  `express_id` bigint(20) NOT NULL COMMENT '快递ID',
  `status` tinyint(1) NOT NULL COMMENT '状态:1-待揽收,2-已揽收,3-运输中,4-已到达,5-待配送,6-配送中,7-已签收,8-已取消',
  `operator_id` bigint(20) DEFAULT NULL COMMENT '操作人ID',
  `operator_type` tinyint(1) DEFAULT NULL COMMENT '操作人类型:1-系统,2-用户,3-配送员,4-管理员',
  `operation_time` datetime NOT NULL COMMENT '操作时间',
  `location` varchar(200) DEFAULT NULL COMMENT '位置信息',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_express_id` (`express_id`),
  KEY `idx_operation_time` (`operation_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='快递跟踪记录表';

-- 通知表
CREATE TABLE `notification` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '通知ID',
  `title` varchar(100) NOT NULL COMMENT '通知标题',
  `content` text NOT NULL COMMENT '通知内容',
  `type` tinyint(1) NOT NULL COMMENT '通知类型:1-系统通知,2-快递通知,3-配送通知,4-活动通知',
  `channel` tinyint(1) NOT NULL COMMENT '通知渠道:1-站内信,2-短信,3-邮件,4-推送',
  `receiver_id` bigint(20) NOT NULL COMMENT '接收者ID',
  `receiver_type` tinyint(1) NOT NULL COMMENT '接收者类型:1-用户,2-配送员,3-管理员',
  `sender_id` bigint(20) DEFAULT NULL COMMENT '发送者ID',
  `sender_type` tinyint(1) DEFAULT '1' COMMENT '发送者类型:1-系统,2-用户,3-配送员,4-管理员',
  `business_id` bigint(20) DEFAULT NULL COMMENT '关联业务ID',
  `business_type` tinyint(1) DEFAULT NULL COMMENT '关联业务类型:1-快递,2-配送,3-活动',
  `read_status` tinyint(1) DEFAULT '0' COMMENT '已读状态:0-未读,1-已读',
  `send_status` tinyint(1) DEFAULT '0' COMMENT '发送状态:0-未发送,1-已发送,2-发送失败',
  `fail_reason` varchar(200) DEFAULT NULL COMMENT '失败原因',
  `retry_count` int(11) DEFAULT '0' COMMENT '重试次数',
  `next_retry_time` datetime DEFAULT NULL COMMENT '下次重试时间',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '更新时间',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_receiver` (`receiver_id`,`receiver_type`),
  KEY `idx_business` (`business_id`,`business_type`),
  KEY `idx_created_time` (`created_time`),
  KEY `idx_read_status` (`read_status`),
  KEY `idx_send_status` (`send_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知表';

-- 通知模板表
CREATE TABLE `notification_template` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '模板ID',
  `code` varchar(50) NOT NULL COMMENT '模板编码',
  `name` varchar(100) NOT NULL COMMENT '模板名称',
  `title` varchar(100) NOT NULL COMMENT '模板标题',
  `content` text NOT NULL COMMENT '模板内容',
  `type` tinyint(1) NOT NULL COMMENT '模板类型:1-系统通知,2-快递通知,3-配送通知,4-活动通知',
  `channel` tinyint(1) NOT NULL COMMENT '适用渠道:1-站内信,2-短信,3-邮件,4-推送',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-停用,1-启用',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '更新时间',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_code` (`code`),
  KEY `idx_type` (`type`),
  KEY `idx_channel` (`channel`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知模板表';

-- 系统配置表
CREATE TABLE `system_config` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '配置ID',
  `config_key` varchar(50) NOT NULL COMMENT '配置键',
  `config_value` varchar(500) NOT NULL COMMENT '配置值',
  `config_type` varchar(50) DEFAULT NULL COMMENT '配置类型',
  `description` varchar(200) DEFAULT NULL COMMENT '配置描述',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '更新时间',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_config_key` (`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表';

-- 操作日志表
CREATE TABLE `operation_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `username` varchar(50) DEFAULT NULL COMMENT '用户名',
  `operation` varchar(50) NOT NULL COMMENT '操作',
  `method` varchar(200) DEFAULT NULL COMMENT '方法名',
  `params` text COMMENT '请求参数',
  `result` text COMMENT '返回结果',
  `ip` varchar(50) DEFAULT NULL COMMENT 'IP地址',
  `user_agent` varchar(500) DEFAULT NULL COMMENT '用户代理',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-失败,1-成功',
  `error_msg` text COMMENT '错误信息',
  `operation_time` datetime NOT NULL COMMENT '操作时间',
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_operation_time` (`operation_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';

-- 登录日志表
CREATE TABLE `login_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `username` varchar(50) DEFAULT NULL COMMENT '用户名',
  `ip` varchar(50) DEFAULT NULL COMMENT 'IP地址',
  `location` varchar(100) DEFAULT NULL COMMENT '登录地点',
  `browser` varchar(50) DEFAULT NULL COMMENT '浏览器',
  `os` varchar(50) DEFAULT NULL COMMENT '操作系统',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-失败,1-成功',
  `msg` varchar(200) DEFAULT NULL COMMENT '提示消息',
  `login_time` datetime NOT NULL COMMENT '登录时间',
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_login_time` (`login_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录日志表';

-- 文件上传记录表
CREATE TABLE `file_upload` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '文件ID',
  `original_name` varchar(200) NOT NULL COMMENT '原始文件名',
  `file_name` varchar(200) NOT NULL COMMENT '存储文件名',
  `file_path` varchar(500) NOT NULL COMMENT '文件路径',
  `file_url` varchar(500) DEFAULT NULL COMMENT '文件URL',
  `file_size` bigint(20) NOT NULL COMMENT '文件大小(字节)',
  `file_type` varchar(50) DEFAULT NULL COMMENT '文件类型',
  `file_ext` varchar(20) DEFAULT NULL COMMENT '文件扩展名',
  `user_id` bigint(20) DEFAULT NULL COMMENT '上传用户ID',
  `business_type` varchar(50) DEFAULT NULL COMMENT '业务类型',
  `business_id` bigint(20) DEFAULT NULL COMMENT '业务ID',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_business` (`business_type`,`business_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件上传记录表';

-- 角色表
CREATE TABLE `role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` varchar(50) NOT NULL COMMENT '角色名称',
  `role_key` varchar(50) NOT NULL COMMENT '角色权限字符串',
  `role_sort` int(11) DEFAULT NULL COMMENT '显示顺序',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '更新时间',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_role_key` (`role_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

-- 用户角色关联表
CREATE TABLE `user_role` (
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

-- 菜单权限表
CREATE TABLE `menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `menu_name` varchar(50) NOT NULL COMMENT '菜单名称',
  `parent_id` bigint(20) DEFAULT '0' COMMENT '父菜单ID',
  `order_num` int(11) DEFAULT '0' COMMENT '显示顺序',
  `path` varchar(200) DEFAULT '' COMMENT '路由地址',
  `component` varchar(200) DEFAULT NULL COMMENT '组件路径',
  `is_frame` tinyint(1) DEFAULT '0' COMMENT '是否为外链:0-否,1-是',
  `is_cache` tinyint(1) DEFAULT '0' COMMENT '是否缓存:0-否,1-是',
  `menu_type` char(1) DEFAULT '' COMMENT '菜单类型:M-目录,C-菜单,F-按钮',
  `visible` tinyint(1) DEFAULT '1' COMMENT '菜单状态:0-隐藏,1-显示',
  `status` tinyint(1) DEFAULT '1' COMMENT '菜单状态:0-禁用,1-启用',
  `perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '更新时间',
  `remark` varchar(200) DEFAULT '' COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单权限表';

-- 角色菜单关联表
CREATE TABLE `role_menu` (
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  `menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
  PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联表';

express-ui\package.json

{
  "name": "campus-express-ui",
  "version": "1.0.0",
  "private": true,
  "description": "校园快递管理与配送系统前端",
  "author": "Campus Express Team",
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "axios": "^0.21.1",
    "core-js": "^3.6.5",
    "element-ui": "^2.15.6",
    "js-cookie": "^3.0.1",
    "nprogress": "^0.2.0",
    "vue": "^2.6.11",
    "vue-router": "^3.2.0",
    "vuex": "^3.4.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/eslint-config-standard": "^5.1.2",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-import": "^2.20.2",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-promise": "^4.2.1",
    "eslint-plugin-standard": "^4.0.0",
    "eslint-plugin-vue": "^6.2.2",
    "sass": "^1.26.5",
    "sass-loader": "^8.0.2",
    "vue-template-compiler": "^2.6.11"
  }
}

express-ui\src\App.vue

<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
#app {
  font-family: 'Microsoft YaHei', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  height: 100%;
}

html, body {
  height: 100%;
  margin: 0;
  padding: 0;
}
</style>

express-ui\src\main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import './assets/styles/index.scss'
import './permission' // 权限控制
import NotificationPlugin from './plugins/notification' // 通知插件

Vue.use(ElementUI, { size: 'medium' })
Vue.use(NotificationPlugin) // 注册通知插件

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

express-ui\src\api\notification.js

import request from '@/utils/request'

// 通知列表
export function getNotificationList(query) {
  return request({
    url: '/notification/list',
    method: 'get',
    params: query
  })
}

// 获取通知详情
export function getNotification(id) {
  return request({
    url: `/notification/${id}`,
    method: 'get'
  })
}

// 添加通知
export function addNotification(data) {
  return request({
    url: '/notification',
    method: 'post',
    data
  })
}

// 更新通知
export function updateNotification(id, data) {
  return request({
    url: `/notification/${id}`,
    method: 'put',
    data
  })
}

// 删除通知
export function deleteNotification(id) {
  return request({
    url: `/notification/${id}`,
    method: 'delete'
  })
}

// 发送通知
export function sendNotification(data) {
  return request({
    url: '/notification/send',
    method: 'post',
    data
  })
}

// 模板列表
export function getTemplateList(query) {
  return request({
    url: '/notification/template/list',
    method: 'get',
    params: query
  })
}

// 获取模板详情
export function getTemplate(id) {
  return request({
    url: `/notification/template/${id}`,
    method: 'get'
  })
}

// 添加模板
export function addTemplate(data) {
  return request({
    url: '/notification/template',
    method: 'post',
    data
  })
}

// 更新模板
export function updateTemplate(id, data) {
  return request({
    url: `/notification/template/${id}`,
    method: 'put',
    data
  })
}

// 删除模板
export function deleteTemplate(id) {
  return request({
    url: `/notification/template/${id}`,
    method: 'delete'
  })
}

// 获取用户列表
export function getUserList(query) {
  return request({
    url: '/user/list',
    method: 'get',
    params: query
  })
}

// 获取用户组列表
export function getUserGroupList() {
  return request({
    url: '/user/group/list',
    method: 'get'
  })
}

// 获取通知发送记录
export function getSendRecords(notificationId) {
  return request({
    url: `/notification/send-records/${notificationId}`,
    method: 'get'
  })
}

// 获取通知阅读记录
export function getReadRecords(notificationId) {
  return request({
    url: `/notification/read-records/${notificationId}`,
    method: 'get'
  })
}

// 获取我的通知列表
export function getMyNotifications(query) {
  return request({
    url: '/notification/my-notifications',
    method: 'get',
    params: query
  })
}

// 标记通知为已读
export function markAsRead(id) {
  return request({
    url: `/notification/mark-read/${id}`,
    method: 'put'
  })
}

// 标记所有通知为已读
export function markAllAsRead() {
  return request({
    url: '/notification/mark-all-read',
    method: 'put'
  })
}

// 获取未读通知数量
export function getUnreadCount() {
  return request({
    url: '/notification/unread-count',
    method: 'get'
  })
}

// 获取通知设置
export function getNotificationSettings() {
  return request({
    url: '/notification/settings',
    method: 'get'
  })
}

// 更新通知设置
export function updateNotificationSettings(data) {
  return request({
    url: '/notification/settings',
    method: 'put',
    data
  })
}

// 发送测试通知
export function sendTestNotification() {
  return request({
    url: '/notification/send-test',
    method: 'post'
  })
}

// 获取通知统计数据
export function getNotificationStatistics(params) {
  return request({
    url: '/notification/statistics',
    method: 'get',
    params
  })
}

// 获取通知趋势数据
export function getNotificationTrend(params) {
  return request({
    url: '/notification/trend',
    method: 'get',
    params
  })
}

// 获取通知类型分布数据
export function getTypeDistribution(params) {
  return request({
    url: '/notification/type-distribution',
    method: 'get',
    params
  })
}

// 获取通知渠道分布数据
export function getChannelDistribution(params) {
  return request({
    url: '/notification/channel-distribution',
    method: 'get',
    params
  })
}

// 导出通知统计数据
export function exportNotificationStatistics(params) {
  return request({
    url: '/notification/export-statistics',
    method: 'get',
    params,
    responseType: 'blob'
  })
}

// 获取通知历史记录
export function getNotificationHistory(params) {
  return request({
    url: '/notification/history',
    method: 'get',
    params
  })
}

// 批量删除通知
export function batchDeleteNotifications(ids) {
  return request({
    url: '/notification/batch-delete',
    method: 'delete',
    data: { ids }
  })
}

// 回复通知
export function replyNotification(data) {
  return request({
    url: '/notification/reply',
    method: 'post',
    data
  })
}

// 下载通知附件
export function downloadAttachment(id) {
  return request({
    url: `/notification/attachment/${id}`,
    method: 'get',
    responseType: 'blob'
  })
}

// 测试模板
export function testTemplate(data) {
  return request({
    url: '/notification/template/test',
    method: 'post',
    data
  })
}

// 绑定微信
export function bindWechat() {
  return request({
    url: '/notification/bind-wechat',
    method: 'get'
  })
}

// 确认微信绑定
export function confirmWechatBind() {
  return request({
    url: '/notification/confirm-wechat-bind',
    method: 'post'
  })
}

// 解绑微信
export function unbindWechat() {
  return request({
    url: '/notification/unbind-wechat',
    method: 'delete'
  })
}

// 解绑APP设备
export function unbindApp() {
  return request({
    url: '/notification/unbind-app',
    method: 'delete'
  })
}

// 获取通知订阅列表
export function getSubscriptionList() {
  return request({
    url: '/notification/subscription/list',
    method: 'get'
  })
}

// 更新通知订阅
export function updateSubscription(data) {
  return request({
    url: '/notification/subscription',
    method: 'put',
    data
  })
}

express-ui\src\components\Notification\index.vue

<template>
  <div class="notification-container">
    <el-popover
      placement="bottom"
      width="400"
      trigger="click"
      @show="handlePopoverShow"
      popper-class="notification-popover">
      <el-badge :value="unreadCount" :max="99" class="notification-badge" slot="reference">
        <el-button type="text" class="notification-button">
          <i class="el-icon-bell"></i>
        </el-button>
      </el-badge>
      
      <div class="notification-header">
        <span class="notification-title">通知中心</span>
        <div class="notification-actions">
          <el-button type="text" size="mini" @click="markAllAsRead" :disabled="!hasUnread">全部已读</el-button>
          <el-button type="text" size="mini" @click="viewAllNotifications">查看全部</el-button>
        </div>
      </div>
      
      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
        <el-tab-pane label="全部" name="all">
          <notification-list :notifications="filteredNotifications" @view="viewNotification" @mark-read="handleMarkAsRead" />
        </el-tab-pane>
        <el-tab-pane label="未读" name="unread">
          <notification-list :notifications="filteredNotifications" @view="viewNotification" @mark-read="handleMarkAsRead" />
        </el-tab-pane>
        <el-tab-pane :label="getTypeName(1)" name="system">
          <notification-list :notifications="filteredNotifications" @view="viewNotification" @mark-read="handleMarkAsRead" />
        </el-tab-pane>
        <el-tab-pane :label="getTypeName(2)" name="express">
          <notification-list :notifications="filteredNotifications" @view="viewNotification" @mark-read="handleMarkAsRead" />
        </el-tab-pane>
      </el-tabs>
      
      <div class="notification-footer">
        <el-button type="primary" size="small" @click="viewSettings">通知设置</el-button>
      </div>
    </el-popover>
    
    <!-- 通知详情对话框 -->
    <el-dialog title="通知详情" :visible.sync="detailDialogVisible" width="500px" append-to-body>
      <div class="notification-detail" v-if="currentNotification">
        <div class="detail-header">
          <h3 class="detail-title">{{ currentNotification.title }}</h3>
          <div class="detail-meta">
            <span>
              <i class="el-icon-time"></i>
              {{ formatDate(currentNotification.sendTime) }}
            </span>
            <span>
              <i class="el-icon-user"></i>
              {{ currentNotification.sender }}
            </span>
            <span>
              <el-tag :type="getTypeTagType(currentNotification.type)" size="small">
                {{ getTypeName(currentNotification.type) }}
              </el-tag>
            </span>
          </div>
        </div>
        <div class="detail-content" v-html="formatContent(currentNotification.content)"></div>
        <div v-if="currentNotification.attachments && currentNotification.attachments.length > 0" class="detail-attachments">
          <h4>附件</h4>
          <ul class="attachment-list">
            <li v-for="(attachment, index) in currentNotification.attachments" :key="index" class="attachment-item">
              <i class="el-icon-document"></i>
              <span class="attachment-name">{{ attachment.name }}</span>
              <span class="attachment-size">{{ formatFileSize(attachment.size) }}</span>
              <el-button type="text" @click="downloadAttachment(attachment)">下载</el-button>
            </li>
          </ul>
        </div>
        <div class="detail-actions">
          <el-button v-if="!currentNotification.read" type="success" @click="markAsReadInDialog">标为已读</el-button>
          <el-button type="primary" @click="viewDetail(currentNotification)">查看详情</el-button>
        </div>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import { getMyNotifications, markAsRead, markAllAsRead, getUnreadCount, downloadAttachment } from '@/api/notification'
import { NotificationType, getTypeTagType, formatContent, formatFileSize } from '@/utils/notification'
import NotificationList from './NotificationList'

export default {
  name: 'HeaderNotification',
  components: {
    NotificationList
  },
  data() {
    return {
      activeTab: 'all',
      notifications: [],
      unreadCount: 0,
      loading: false,
      currentNotification: null,
      detailDialogVisible: false,
      // 轮询定时器
      pollingTimer: null
    }
  },
  computed: {
    // 根据当前选中的标签页过滤通知
    filteredNotifications() {
      if (this.activeTab === 'all') {
        return this.notifications
      } else if (this.activeTab === 'unread') {
        return this.notifications.filter(item => !item.read)
      } else if (this.activeTab === 'system') {
        return this.notifications.filter(item => item.type === NotificationType.SYSTEM)
      } else if (this.activeTab === 'express') {
        return this.notifications.filter(item => item.type === NotificationType.EXPRESS)
      }
      return this.notifications
    },
    // 是否有未读通知
    hasUnread() {
      return this.notifications.some(item => !item.read)
    }
  },
  created() {
    this.fetchUnreadCount()
    // 启动轮询,每分钟检查一次未读通知数量
    this.startPolling()
  },
  beforeDestroy() {
    // 清除轮询定时器
    this.stopPolling()
  },
  methods: {
    // 获取通知列表
    fetchNotifications() {
      this.loading = true
      
      getMyNotifications({ page: 1, size: 10 }).then(response => {
        // 实际项目中应该使用API返回的数据
        // this.notifications = response.data.records
        // this.unreadCount = response.data.unreadCount
        
        // 模拟数据
        this.notifications = this.generateMockData()
        this.unreadCount = this.notifications.filter(item => !item.read).length
        this.loading = false
      }).catch(() => {
        this.loading = false
      })
    },
    
    // 获取未读通知数量
    fetchUnreadCount() {
      getUnreadCount().then(response => {
        // 实际项目中应该使用API返回的数据
        // this.unreadCount = response.data
        
        // 模拟数据
        this.unreadCount = 5
      })
    },
    
    // 生成模拟数据
    generateMockData() {
      const result = []
      const types = [1, 2, 3]
      const titles = [
        '系统维护通知',
        '您的快递已到达',
        '校园活动邀请',
        '账号安全提醒',
        '新功能上线通知'
      ]
      const senders = ['系统管理员', '快递服务', '学生会', '安全中心', '技术部门']
      
      for (let i = 0; i < 10; i++) {
        const type = types[Math.floor(Math.random() * types.length)]
        const title = titles[Math.floor(Math.random() * titles.length)]
        const sender = senders[Math.floor(Math.random() * senders.length)]
        const read = i > 4 // 前5条未读,后5条已读
        
        // 生成随机日期(最近7天内)
        const date = new Date()
        date.setDate(date.getDate() - Math.floor(Math.random() * 7))
        
        // 生成随机内容
        let content = ''
        if (type === 1) {
          content = '尊敬的用户,系统将于2025年4月15日凌晨2:00-4:00进行例行维护,届时系统将暂停服务。给您带来的不便,敬请谅解。'
        } else if (type === 2) {
          content = '您好,您的快递(顺丰速运 - SF1234567890)已到达校园快递中心,请凭取件码 8888 及时领取。取件时间:9:00-18:00。'
        } else {
          content = '诚邀您参加"校园科技创新大赛",时间:2025年4月20日14:00,地点:图书馆报告厅。欢迎踊跃参与!'
        }
        
        result.push({
          id: i + 1,
          type,
          title,
          content,
          sender,
          read,
          sendTime: date.toISOString(),
          readTime: read ? new Date(date.getTime() + Math.floor(Math.random() * 86400000)).toISOString() : null
        })
      }
      
      return result
    },
    
    // 启动轮询
    startPolling() {
      this.pollingTimer = setInterval(() => {
        this.fetchUnreadCount()
      }, 60000) // 每分钟检查一次
    },
    
    // 停止轮询
    stopPolling() {
      if (this.pollingTimer) {
        clearInterval(this.pollingTimer)
        this.pollingTimer = null
      }
    },
    
    // 处理标签页点击
    handleTabClick() {
      // 切换标签页时不需要额外处理,computed 属性会自动过滤
    },
    
    // 处理弹出框显示
    handlePopoverShow() {
      this.fetchNotifications()
    },
    
    // 查看通知详情
    viewNotification(notification) {
      this.currentNotification = notification
      this.detailDialogVisible = true
      
      // 如果是未读通知,自动标记为已读
      if (!notification.read) {
        this.handleMarkAsRead(notification)
      }
    },
    
    // 处理标记为已读
    handleMarkAsRead(notification) {
      markAsRead(notification.id).then(() => {
        notification.read = true
        notification.readTime = new Date().toISOString()
        this.unreadCount = Math.max(0, this.unreadCount - 1)
      })
    },
    
    // 在对话框中标记为已读
    markAsReadInDialog() {
      if (this.currentNotification && !this.currentNotification.read) {
        this.handleMarkAsRead(this.currentNotification)
      }
    },
    
    // 标记所有为已读
    markAllAsRead() {
      markAllAsRead().then(() => {
        this.notifications.forEach(item => {
          item.read = true
          item.readTime = new Date().toISOString()
        })
        this.unreadCount = 0
        
        this.$message({
          message: '已将所有通知标记为已读',
          type: 'success'
        })
      })
    },
    
    // 查看所有通知
    viewAllNotifications() {
      this.$router.push('/notification/history')
    },
    
    // 查看通知设置
    viewSettings() {
      this.$router.push('/notification/settings')
    },
    
    // 查看完整通知详情
    viewDetail(notification) {
      this.detailDialogVisible = false
      this.$router.push(`/notification/detail/${notification.id}`)
    },
    
    // 下载附件
    downloadAttachment(attachment) {
      downloadAttachment(attachment.id).then(response => {
        // 实际项目中应该处理文件下载
        this.$message({
          message: '开始下载:' + attachment.name,
          type: 'success'
        })
      })
    },
    
    // 获取通知类型名称
    getTypeName(type) {
      switch (type) {
        case NotificationType.SYSTEM:
          return '系统'
        case NotificationType.EXPRESS:
          return '快递'
        case NotificationType.ACTIVITY:
          return '活动'
        default:
          return '其他'
      }
    },
    
    // 格式化日期
    formatDate(dateString) {
      if (!dateString) return ''
      
      const date = new Date(dateString)
      const now = new Date()
      const diff = now.getTime() - date.getTime()
      
      // 1分钟内
      if (diff < 60000) {
        return '刚刚'
      }
      
      // 1小时内
      if (diff < 3600000) {
        return Math.floor(diff / 60000) + '分钟前'
      }
      
      // 24小时内
      if (diff < 86400000) {
        return Math.floor(diff / 3600000) + '小时前'
      }
      
      // 30天内
      if (diff < 2592000000) {
        return Math.floor(diff / 86400000) + '天前'
      }
      
      // 超过30天
      const year = date.getFullYear()
      const month = String(date.getMonth() + 1).padStart(2, '0')
      const day = String(date.getDate()).padStart(2, '0')
      
      return `${year}-${month}-${day}`
    },
    
    // 获取通知类型对应的标签类型
    getTypeTagType,
    
    // 格式化内容
    formatContent,
    
    // 格式化文件大小
    formatFileSize
  }
}
</script>

<style lang="scss" scoped>
.notification-container {
  display: inline-block;
  margin-right: 15px;
}

.notification-button {
  padding: 0;
  font-size: 20px;
  color: #fff;
  
  &:hover {
    color: #f0f0f0;
  }
}

.notification-badge {
  line-height: normal;
}

.notification-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 10px 10px;
  border-bottom: 1px solid #ebeef5;
  
  .notification-title {
    font-size: 16px;
    font-weight: bold;
  }
  
  .notification-actions {
    .el-button {
      padding: 0 5px;
    }
  }
}

.notification-footer {
  padding: 10px;
  text-align: center;
  border-top: 1px solid #ebeef5;
}

.notification-detail {
  .detail-header {
    margin-bottom: 20px;
    
    .detail-title {
      margin: 0 0 10px 0;
      font-size: 18px;
    }
    
    .detail-meta {
      display: flex;
      flex-wrap: wrap;
      color: #909399;
      font-size: 14px;
      
      span {
        margin-right: 15px;
        margin-bottom: 5px;
        display: flex;
        align-items: center;
        
        i {
          margin-right: 5px;
        }
      }
    }
  }
  
  .detail-content {
    padding: 15px;
    background-color: #f5f7fa;
    border-radius: 4px;
    min-height: 100px;
    line-height: 1.6;
    white-space: pre-wrap;
    margin-bottom: 20px;
  }
  
  .detail-attachments {
    margin-bottom: 20px;
    
    h4 {
      margin: 0 0 10px 0;
      font-size: 16px;
    }
    
    .attachment-list {
      list-style: none;
      padding: 0;
      margin: 0;
      
      .attachment-item {
        display: flex;
        align-items: center;
        padding: 8px 0;
        border-bottom: 1px solid #ebeef5;
        
        &:last-child {
          border-bottom: none;
        }
        
        i {
          margin-right: 10px;
          color: #909399;
        }
        
        .attachment-name {
          flex: 1;
        }
        
        .attachment-size {
          color: #909399;
          margin: 0 10px;
        }
      }
    }
  }
  
  .detail-actions {
    text-align: right;
  }
}
</style>

<style>
.notification-popover {
  padding: 0;
  max-height: 500px;
  overflow-y: auto;
}
</style>

express-ui\src\components\Notification\NotificationList.vue

<template>
  <div class="notification-list">
    <div v-if="loading" class="loading-container">
      <el-skeleton :rows="3" animated />
      <el-skeleton :rows="3" animated />
    </div>
    
    <template v-else>
      <div v-if="notifications.length === 0" class="empty-container">
        <el-empty description="暂无通知" :image-size="80"></el-empty>
      </div>
      
      <div v-else class="notification-items">
        <div
          v-for="item in notifications"
          :key="item.id"
          class="notification-item"
          :class="{ 'unread': !item.read }"
          @click="handleClick(item)">
          <div class="notification-icon" :class="'type-' + item.type">
            <i :class="getIconByType(item.type)"></i>
          </div>
          <div class="notification-content">
            <div class="notification-title">
              <span class="title-text">{{ item.title }}</span>
              <span v-if="!item.read" class="unread-dot"></span>
            </div>
            <div class="notification-body">{{ truncateContent(item.content) }}</div>
            <div class="notification-meta">
              <span class="notification-time">{{ formatTime(item.sendTime) }}</span>
              <span class="notification-sender">{{ item.sender }}</span>
              <el-tag size="mini" :type="getTypeTagType(item.type)">{{ getTypeName(item.type) }}</el-tag>
            </div>
          </div>
          <div class="notification-actions">
            <el-button
              v-if="!item.read"
              type="text"
              size="mini"
              @click.stop="markAsRead(item)"
              class="mark-read-btn">
              标为已读
            </el-button>
          </div>
        </div>
      </div>
    </template>
  </div>
</template>

<script>
import { NotificationType, getTypeTagType } from '@/utils/notification'

export default {
  name: 'NotificationList',
  props: {
    notifications: {
      type: Array,
      default: () => []
    },
    loading: {
      type: Boolean,
      default: false
    }
  },
  methods: {
    // 处理点击通知
    handleClick(notification) {
      this.$emit('view', notification)
    },
    
    // 标记为已读
    markAsRead(notification) {
      this.$emit('mark-read', notification)
    },
    
    // 根据通知类型获取图标
    getIconByType(type) {
      switch (type) {
        case NotificationType.SYSTEM:
          return 'el-icon-s-platform'
        case NotificationType.EXPRESS:
          return 'el-icon-s-goods'
        case NotificationType.ACTIVITY:
          return 'el-icon-s-flag'
        default:
          return 'el-icon-message'
      }
    },
    
    // 获取通知类型名称
    getTypeName(type) {
      switch (type) {
        case NotificationType.SYSTEM:
          return '系统'
        case NotificationType.EXPRESS:
          return '快递'
        case NotificationType.ACTIVITY:
          return '活动'
        default:
          return '其他'
      }
    },
    
    // 截断内容显示
    truncateContent(content) {
      if (!content) return ''
      return content.length > 50 ? content.substring(0, 50) + '...' : content
    },
    
    // 格式化时间
    formatTime(dateString) {
      if (!dateString) return ''
      
      const date = new Date(dateString)
      const now = new Date()
      const diff = now.getTime() - date.getTime()
      
      // 1分钟内
      if (diff < 60000) {
        return '刚刚'
      }
      
      // 1小时内
      if (diff < 3600000) {
        return Math.floor(diff / 60000) + '分钟前'
      }
      
      // 24小时内
      if (diff < 86400000) {
        return Math.floor(diff / 3600000) + '小时前'
      }
      
      // 30天内
      if (diff < 2592000000) {
        return Math.floor(diff / 86400000) + '天前'
      }
      
      // 超过30天
      const year = date.getFullYear()
      const month = String(date.getMonth() + 1).padStart(2, '0')
      const day = String(date.getDate()).padStart(2, '0')
      
      return `${year}-${month}-${day}`
    },
    
    // 获取通知类型对应的标签类型
    getTypeTagType
  }
}
</script>

<style lang="scss" scoped>
.notification-list {
  padding: 10px 0;
  
  .loading-container {
    padding: 10px;
  }
  
  .empty-container {
    padding: 20px 0;
    display: flex;
    justify-content: center;
  }
  
  .notification-items {
    .notification-item {
      display: flex;
      padding: 12px 16px;
      border-bottom: 1px solid #ebeef5;
      cursor: pointer;
      transition: background-color 0.3s;
      
      &:last-child {
        border-bottom: none;
      }
      
      &:hover {
        background-color: #f5f7fa;
        
        .notification-actions {
          opacity: 1;
        }
      }
      
      &.unread {
        background-color: #f0f9eb;
        
        &:hover {
          background-color: #e6f7df;
        }
      }
      
      .notification-icon {
        width: 40px;
        height: 40px;
        border-radius: 50%;
        display: flex;
        justify-content: center;
        align-items: center;
        margin-right: 12px;
        flex-shrink: 0;
        
        i {
          font-size: 20px;
          color: #fff;
        }
        
        &.type-1 {
          background-color: #409eff;
        }
        
        &.type-2 {
          background-color: #67c23a;
        }
        
        &.type-3 {
          background-color: #e6a23c;
        }
        
        &.type-4 {
          background-color: #f56c6c;
        }
      }
      
      .notification-content {
        flex: 1;
        min-width: 0;
        
        .notification-title {
          display: flex;
          align-items: center;
          margin-bottom: 5px;
          
          .title-text {
            font-weight: bold;
            font-size: 14px;
            color: #303133;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            flex: 1;
          }
          
          .unread-dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background-color: #f56c6c;
            margin-left: 8px;
            flex-shrink: 0;
          }
        }
        
        .notification-body {
          color: #606266;
          font-size: 13px;
          margin-bottom: 5px;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
        }
        
        .notification-meta {
          display: flex;
          align-items: center;
          font-size: 12px;
          color: #909399;
          
          .notification-time {
            margin-right: 10px;
          }
          
          .notification-sender {
            margin-right: 10px;
          }
        }
      }
      
      .notification-actions {
        display: flex;
        align-items: center;
        opacity: 0;
        transition: opacity 0.3s;
        
        .mark-read-btn {
          color: #67c23a;
        }
      }
    }
  }
}
</style>

express-ui\src\components\Notification\NotificationManager.js

import Vue from 'vue'
import { getUnreadCount, markAsRead } from '@/api/notification'
import { NotificationType } from '@/utils/notification'

// 创建一个Vue实例作为事件总线
const NotificationBus = new Vue()

// 通知管理器
const NotificationManager = {
  // 事件总线
  bus: NotificationBus,
  
  // 初始化
  init() {
    // 设置轮询定时器,定期检查新通知
    this.startPolling()
    
    // 监听页面可见性变化,当用户切换回页面时刷新通知
    document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this))
    
    // 初始化获取一次未读通知数量
    this.fetchUnreadCount()
    
    console.log('NotificationManager initialized')
  },
  
  // 销毁
  destroy() {
    // 清除轮询定时器
    this.stopPolling()
    
    // 移除事件监听
    document.removeEventListener('visibilitychange', this.handleVisibilityChange.bind(this))
    
    console.log('NotificationManager destroyed')
  },
  
  // 开始轮询
  startPolling() {
    // 清除可能存在的旧定时器
    this.stopPolling()
    
    // 创建新的定时器,每分钟检查一次
    this._pollingTimer = setInterval(() => {
      this.fetchUnreadCount()
    }, 60000)
  },
  
  // 停止轮询
  stopPolling() {
    if (this._pollingTimer) {
      clearInterval(this._pollingTimer)
      this._pollingTimer = null
    }
  },
  
  // 处理页面可见性变化
  handleVisibilityChange() {
    if (document.visibilityState === 'visible') {
      // 页面变为可见时,立即刷新通知
      this.fetchUnreadCount()
    }
  },
  
  // 获取未读通知数量
  fetchUnreadCount() {
    getUnreadCount().then(response => {
      // 实际项目中应该使用API返回的数据
      // const count = response.data
      
      // 模拟数据
      const count = Math.floor(Math.random() * 10)
      
      // 触发未读数量更新事件
      this.bus.$emit('unread-count-updated', count)
      
      // 如果有新通知且浏览器支持通知API,显示桌面通知
      if (count > 0 && this.isDesktopNotificationEnabled()) {
        this.showDesktopNotification('您有' + count + '条未读通知', '点击查看详情')
      }
    }).catch(error => {
      console.error('Failed to fetch unread count:', error)
    })
  },
  
  // 标记通知为已读
  markAsRead(notificationId) {
    return markAsRead(notificationId).then(() => {
      // 触发通知已读事件
      this.bus.$emit('notification-read', notificationId)
      
      // 重新获取未读数量
      this.fetchUnreadCount()
      
      return Promise.resolve()
    })
  },
  
  // 显示新通知
  showNotification(notification) {
    // 触发新通知事件
    this.bus.$emit('new-notification', notification)
    
    // 如果启用了桌面通知,显示桌面通知
    if (this.isDesktopNotificationEnabled()) {
      this.showDesktopNotification(notification.title, notification.content)
    }
    
    // 如果启用了声音提醒,播放提示音
    if (this.isSoundEnabled()) {
      this.playNotificationSound()
    }
  },
  
  // 检查是否启用了桌面通知
  isDesktopNotificationEnabled() {
    // 从本地存储获取设置
    return localStorage.getItem('enableDesktopNotification') === 'true'
  },
  
  // 检查是否启用了声音提醒
  isSoundEnabled() {
    // 从本地存储获取设置
    return localStorage.getItem('enableNotificationSound') === 'true'
  },
  
  // 显示桌面通知
  showDesktopNotification(title, body) {
    // 检查浏览器是否支持通知API
    if (!('Notification' in window)) {
      console.warn('This browser does not support desktop notifications')
      return
    }
    
    // 检查通知权限
    if (Notification.permission === 'granted') {
      // 已获得权限,直接显示通知
      this._createNotification(title, body)
    } else if (Notification.permission !== 'denied') {
      // 未获得权限且未被拒绝,请求权限
      Notification.requestPermission().then(permission => {
        if (permission === 'granted') {
          this._createNotification(title, body)
        }
      })
    }
  },
  
  // 创建通知
  _createNotification(title, body) {
    const notification = new Notification(title, {
      body: body,
      icon: '/favicon.ico'
    })
    
    // 点击通知时的行为
    notification.onclick = () => {
      // 激活窗口
      window.focus()
      
      // 关闭通知
      notification.close()
      
      // 触发通知点击事件
      this.bus.$emit('desktop-notification-clicked')
    }
    
    // 5秒后自动关闭
    setTimeout(() => {
      notification.close()
    }, 5000)
  },
  
  // 播放通知提示音
  playNotificationSound() {
    try {
      // 创建音频元素
      const audio = new Audio('/static/sounds/notification.mp3')
      
      // 播放
      audio.play().catch(error => {
        console.warn('Failed to play notification sound:', error)
      })
    } catch (error) {
      console.error('Error playing notification sound:', error)
    }
  },
  
  // 获取通知类型名称
  getTypeName(type) {
    switch (type) {
      case NotificationType.SYSTEM:
        return '系统通知'
      case NotificationType.EXPRESS:
        return '快递通知'
      case NotificationType.ACTIVITY:
        return '活动通知'
      default:
        return '通知'
    }
  },
  
  // 生成测试通知(仅用于开发测试)
  generateTestNotification() {
    const types = [
      NotificationType.SYSTEM,
      NotificationType.EXPRESS,
      NotificationType.ACTIVITY
    ]
    
    const type = types[Math.floor(Math.random() * types.length)]
    const id = Date.now()
    
    let title, content
    
    switch (type) {
      case NotificationType.SYSTEM:
        title = '系统维护通知'
        content = '系统将于今晚22:00-24:00进行例行维护,请提前做好准备。'
        break
      case NotificationType.EXPRESS:
        title = '新快递到达通知'
        content = '您有一个新的快递已到达校园快递中心,请及时领取。'
        break
      case NotificationType.ACTIVITY:
        title = '校园活动邀请'
        content = '诚邀您参加本周六下午的校园文化节活动,地点:中央广场。'
        break
      default:
        title = '新通知'
        content = '您有一条新的通知,请查看。'
    }
    
    const notification = {
      id,
      type,
      title,
      content,
      sender: '系统',
      sendTime: new Date().toISOString(),
      read: false
    }
    
    this.showNotification(notification)
    
    return notification
  }
}

export default NotificationManager

express-ui\src\layout\index.vue

<template>
  <div class="app-wrapper">
    <!-- 侧边栏 -->
    <sidebar class="sidebar-container" />
    
    <div class="main-container">
      <!-- 顶部导航栏 -->
      <navbar />
      
      <!-- 标签栏 -->
      <tags-view />
      
      <!-- 主要内容区域 -->
      <app-main />
    </div>
  </div>
</template>

<script>
import Navbar from './components/Navbar'
import Sidebar from './components/Sidebar'
import TagsView from './components/TagsView'
import AppMain from './components/AppMain'

export default {
  name: 'Layout',
  components: {
    Navbar,
    Sidebar,
    TagsView,
    AppMain
  }
}
</script>

<style lang="scss" scoped>
.app-wrapper {
  position: relative;
  height: 100%;
  width: 100%;
  
  &:after {
    content: "";
    display: table;
    clear: both;
  }
}

.sidebar-container {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 210px;
  height: 100%;
  overflow: hidden;
  background-color: #304156;
  transition: width 0.28s;
  z-index: 1001;
}

.main-container {
  min-height: 100%;
  margin-left: 210px;
  position: relative;
  transition: margin-left 0.28s;
}
</style>

express-ui\src\layout\components\AppMain.vue

<template>
  <section class="app-main">
    <transition name="fade-transform" mode="out-in">
      <keep-alive :include="cachedViews">
        <router-view :key="key" />
      </keep-alive>
    </transition>
  </section>
</template>

<script>
export default {
  name: 'AppMain',
  computed: {
    cachedViews() {
      return this.$store.state.tagsView.cachedViews
    },
    key() {
      return this.$route.path
    }
  }
}
</script>

<style lang="scss" scoped>
.app-main {
  padding: 20px;
  height: calc(100vh - 84px);
  overflow: auto;
  position: relative;
}

.fade-transform-enter-active,
.fade-transform-leave-active {
  transition: all 0.5s;
}

.fade-transform-enter {
  opacity: 0;
  transform: translateX(-30px);
}

.fade-transform-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
</style>

express-ui\src\layout\components\Navbar.vue

<template>
  <div class="navbar">
    <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
    <breadcrumb class="breadcrumb-container" />
    <div class="right-menu">
      <!-- 添加通知组件 -->
      <header-notification class="right-menu-item hover-effect" />
      
      <el-dropdown class="avatar-container right-menu-item" trigger="click">
        <div class="avatar-wrapper">
          <img :src="avatar" class="user-avatar">
          <span class="user-name">{{ name }}</span>
          <i class="el-icon-caret-bottom" />
        </div>
        <el-dropdown-menu slot="dropdown">
          <router-link to="/user/profile">
            <el-dropdown-item>个人中心</el-dropdown-item>
          </router-link>
          <el-dropdown-item divided @click.native="logout">
            <span>退出登录</span>
          </el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'
import HeaderNotification from '@/components/Notification'

export default {
  name: 'Navbar',
  components: {
    Breadcrumb,
    Hamburger,
    HeaderNotification
  },
  computed: {
    ...mapGetters([
      'sidebar',
      'name',
      'avatar'
    ])
  },
  methods: {
    toggleSideBar() {
      this.$store.dispatch('app/toggleSideBar')
    },
    async logout() {
      await this.$store.dispatch('user/logout')
      this.$router.push(`/login?redirect=${this.$route.fullPath}`)
    }
  }
}
</script>

<style lang="scss" scoped>
.navbar {
  height: 50px;
  overflow: hidden;
  position: relative;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);

  .hamburger-container {
    line-height: 46px;
    height: 100%;
    float: left;
    padding: 0 15px;
    cursor: pointer;
    transition: background 0.3s;

    &:hover {
      background: rgba(0, 0, 0, 0.025);
    }
  }

  .breadcrumb-container {
    float: left;
    margin-left: 15px;
  }

  .right-menu {
    float: right;
    height: 100%;
    line-height: 50px;
    display: flex;
    align-items: center;

    &:focus {
      outline: none;
    }

    .right-menu-item {
      display: inline-block;
      padding: 0 8px;
      height: 100%;
      font-size: 18px;
      color: #5a5e66;
      vertical-align: middle;

      &.hover-effect {
        cursor: pointer;
        transition: background 0.3s;

        &:hover {
          background: rgba(0, 0, 0, 0.025);
        }
      }
    }

    .avatar-container {
      margin-right: 30px;

      .avatar-wrapper {
        position: relative;
        display: flex;
        align-items: center;

        .user-avatar {
          cursor: pointer;
          width: 30px;
          height: 30px;
          border-radius: 50%;
          margin-right: 8px;
        }

        .user-name {
          cursor: pointer;
          color: #333;
        }

        .el-icon-caret-bottom {
          cursor: pointer;
          font-size: 12px;
          margin-left: 5px;
        }
      }
    }
  }
}
</style>

express-ui\src\layout\components\Sidebar\index.vue

<template>
  <div :class="{'has-logo': showLogo}">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="variables.menuBg"
        :text-color="variables.menuText"
        :unique-opened="false"
        :active-text-color="variables.menuActiveText"
        :collapse-transition="false"
        mode="vertical"
      >
        <sidebar-item
          v-for="route in routes"
          :key="route.path"
          :item="route"
          :base-path="route.path"
        />
      </el-menu>
    </el-scrollbar>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/assets/styles/variables.scss'

export default {
  name: 'Sidebar',
  components: { SidebarItem, Logo },
  computed: {
    ...mapGetters([
      'sidebar',
      'routes'
    ]),
    activeMenu() {
      const route = this.$route
      const { meta, path } = route
      // 如果设置了高亮路径,则使用设置的路径
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
      return variables
    },
    isCollapse() {
      return !this.sidebar.opened
    }
  }
}
</script>

express-ui\src\layout\components\Sidebar\Item.vue

<template>
  <div>
    <svg-icon v-if="icon" :icon-class="icon" />
    <span v-if="title" slot="title">{{ title }}</span>
  </div>
</template>

<script>
export default {
  name: 'MenuItem',
  props: {
    icon: {
      type: String,
      default: ''
    },
    title: {
      type: String,
      default: ''
    }
  }
}
</script>

express-ui\src\layout\components\Sidebar\Link.vue

<template>
  <component :is="type" v-bind="linkProps(to)">
    <slot />
  </component>
</template>

<script>
import { isExternal } from '@/utils/validate'

export default {
  name: 'AppLink',
  props: {
    to: {
      type: String,
      required: true
    }
  },
  computed: {
    isExternal() {
      return isExternal(this.to)
    },
    type() {
      if (this.isExternal) {
        return 'a'
      }
      return 'router-link'
    }
  },
  methods: {
    linkProps(to) {
      if (this.isExternal) {
        return {
          href: to,
          target: '_blank',
          rel: 'noopener'
        }
      }
      return {
        to: to
      }
    }
  }
}
</script>

express-ui\src\layout\components\Sidebar\Logo.vue

<template>
  <div class="sidebar-logo-container" :class="{'collapse': collapse}">
    <transition name="sidebarLogoFade">
      <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
        <img v-if="logo" :src="logo" class="sidebar-logo">
        <h1 v-else class="sidebar-title">{{ title }}</h1>
      </router-link>
      <router-link v-else key="expand" class="sidebar-logo-link" to="/">
        <img v-if="logo" :src="logo" class="sidebar-logo">
        <h1 class="sidebar-title">{{ title }}</h1>
      </router-link>
    </transition>
  </div>
</template>

<script>
export default {
  name: 'SidebarLogo',
  props: {
    collapse: {
      type: Boolean,
      required: true
    }
  },
  data() {
    return {
      title: '校园快递管理系统',
      logo: require('@/assets/logo.png')
    }
  }
}
</script>

<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
  transition: opacity 1.5s;
}

.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
  opacity: 0;
}

.sidebar-logo-container {
  position: relative;
  width: 100%;
  height: 50px;
  line-height: 50px;
  background: #2b2f3a;
  text-align: center;
  overflow: hidden;

  & .sidebar-logo-link {
    height: 100%;
    width: 100%;

    & .sidebar-logo {
      width: 32px;
      height: 32px;
      vertical-align: middle;
      margin-right: 12px;
    }

    & .sidebar-title {
      display: inline-block;
      margin: 0;
      color: #fff;
      font-weight: 600;
      line-height: 50px;
      font-size: 14px;
      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
      vertical-align: middle;
    }
  }

  &.collapse {
    .sidebar-logo {
      margin-right: 0px;
    }
  }
}
</style>

express-ui\src\layout\components\Sidebar\SidebarItem.vue

<template>
  <div v-if="!item.hidden">
    <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown': !isNest}">
          <item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" />
        </el-menu-item>
      </app-link>
    </template>

    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
      <template slot="title">
        <item v-if="item.meta" :icon="item.meta.icon" :title="item.meta.title" />
      </template>
      <sidebar-item
        v-for="child in item.children"
        :key="child.path"
        :is-nest="true"
        :item="child"
        :base-path="resolvePath(child.path)"
        class="nest-menu"
      />
    </el-submenu>
  </div>
</template>

<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'

export default {
  name: 'SidebarItem',
  components: { Item, AppLink },
  props: {
    // route object
    item: {
      type: Object,
      required: true
    },
    isNest: {
      type: Boolean,
      default: false
    },
    basePath: {
      type: String,
      default: ''
    }
  },
  data() {
    this.onlyOneChild = null
    return {}
  },
  methods: {
    hasOneShowingChild(children = [], parent) {
      const showingChildren = children.filter(item => {
        if (item.hidden) {
          return false
        } else {
          // 临时设置(如果只有一个子路由,则将其设置为默认路由)
          this.onlyOneChild = item
          return true
        }
      })

      // 当只有一个子路由时,默认显示子路由
      if (showingChildren.length === 1) {
        return true
      }

      // 没有子路由则显示父路由
      if (showingChildren.length === 0) {
        this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
        return true
      }

      return false
    },
    resolvePath(routePath) {
      if (isExternal(routePath)) {
        return routePath
      }
      if (isExternal(this.basePath)) {
        return this.basePath
      }
      return path.resolve(this.basePath, routePath)
    }
  }
}
</script>

express-ui\src\layout\components\TagsView\index.vue

<template>
  <div id="tags-view-container" class="tags-view-container">
    <scroll-pane ref="scrollPane" class="tags-view-wrapper">
      <router-link
        v-for="tag in visitedViews"
        ref="tag"
        :key="tag.path"
        :class="isActive(tag) ? 'active' : ''"
        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
        tag="span"
        class="tags-view-item"
        @click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
        @contextmenu.prevent.native="openMenu(tag, $event)"
      >
        {{ tag.title }}
        <span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
      </router-link>
    </scroll-pane>
    <ul v-show="visible" :style="{left: left+'px', top: top+'px'}" class="contextmenu">
      <li @click="refreshSelectedTag(selectedTag)">刷新</li>
      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
      <li @click="closeOthersTags">关闭其他</li>
      <li @click="closeAllTags(selectedTag)">关闭所有</li>
    </ul>
  </div>
</template>

<script>
import ScrollPane from './ScrollPane'
import path from 'path'

export default {
  name: 'TagsView',
  components: { ScrollPane },
  data() {
    return {
      visible: false,
      top: 0,
      left: 0,
      selectedTag: {},
      affixTags: []
    }
  },
  computed: {
    visitedViews() {
      return this.$store.state.tagsView.visitedViews
    },
    routes() {
      return this.$store.state.permission.routes
    }
  },
  watch: {
    $route() {
      this.addTags()
      this.moveToCurrentTag()
    },
    visible(value) {
      if (value) {
        document.body.addEventListener('click', this.closeMenu)
      } else {
        document.body.removeEventListener('click', this.closeMenu)
      }
    }
  },
  mounted() {
    this.initTags()
    this.addTags()
  },
  methods: {
    isActive(route) {
      return route.path === this.$route.path
    },
    isAffix(tag) {
      return tag.meta && tag.meta.affix
    },
    filterAffixTags(routes, basePath = '/') {
      let tags = []
      routes.forEach(route => {
        if (route.meta && route.meta.affix) {
          const tagPath = path.resolve(basePath, route.path)
          tags.push({
            fullPath: tagPath,
            path: tagPath,
            name: route.name,
            meta: { ...route.meta }
          })
        }
        if (route.children) {
          const tempTags = this.filterAffixTags(route.children, route.path)
          if (tempTags.length >= 1) {
            tags = [...tags, ...tempTags]
          }
        }
      })
      return tags
    },
    initTags() {
      const affixTags = this.affixTags = this.filterAffixTags(this.routes)
      for (const tag of affixTags) {
        // Must have tag name
        if (tag.name) {
          this.$store.dispatch('tagsView/addVisitedView', tag)
        }
      }
    },
    addTags() {
      const { name } = this.$route
      if (name) {
        this.$store.dispatch('tagsView/addView', this.$route)
      }
      return false
    },
    moveToCurrentTag() {
      const tags = this.$refs.tag
      this.$nextTick(() => {
        for (const tag of tags) {
          if (tag.to.path === this.$route.path) {
            this.$refs.scrollPane.moveToTarget(tag)
            // when query is different then update
            if (tag.to.fullPath !== this.$route.fullPath) {
              this.$store.dispatch('tagsView/updateVisitedView', this.$route)
            }
            break
          }
        }
      })
    },
    refreshSelectedTag(view) {
      this.$store.dispatch('tagsView/delCachedView', view).then(() => {
        const { fullPath } = view
        this.$nextTick(() => {
          this.$router.replace({
            path: '/redirect' + fullPath
          })
        })
      })
    },
    closeSelectedTag(view) {
      this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
        if (this.isActive(view)) {
          this.toLastView(visitedViews, view)
        }
      })
    },
    closeOthersTags() {
      this.$router.push(this.selectedTag)
      this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
        this.moveToCurrentTag()
      })
    },
    closeAllTags(view) {
      this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
        if (this.affixTags.some(tag => tag.path === view.path)) {
          return
        }
        this.toLastView(visitedViews, view)
      })
    },
    toLastView(visitedViews, view) {
      const latestView = visitedViews.slice(-1)[0]
      if (latestView) {
        this.$router.push(latestView.fullPath)
      } else {
        // now the default is to redirect to the home page if there is no tags-view,
        // you can adjust it according to your needs.
        if (view.name === 'Dashboard') {
          // to reload home page
          this.$router.replace({ path: '/redirect' + view.fullPath })
        } else {
          this.$router.push('/')
        }
      }
    },
    openMenu(tag, e) {
      const menuMinWidth = 105
      const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
      const offsetWidth = this.$el.offsetWidth // container width
      const maxLeft = offsetWidth - menuMinWidth // left boundary
      const left = e.clientX - offsetLeft + 15 // 15: margin right

      if (left > maxLeft) {
        this.left = maxLeft
      } else {
        this.left = left
      }

      this.top = e.clientY
      this.visible = true
      this.selectedTag = tag
    },
    closeMenu() {
      this.visible = false
    }
  }
}
</script>

<style lang="scss" scoped>
.tags-view-container {
  height: 34px;
  width: 100%;
  background: #fff;
  border-bottom: 1px solid #d8dce5;
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
  .tags-view-wrapper {
    .tags-view-item {
      display: inline-block;
      position: relative;
      cursor: pointer;
      height: 26px;
      line-height: 26px;
      border: 1px solid #d8dce5;
      color: #495060;
      background: #fff;
      padding: 0 8px;
      font-size: 12px;
      margin-left: 5px;
      margin-top: 4px;
      &:first-of-type {
        margin-left: 15px;
      }
      &:last-of-type {
        margin-right: 15px;
      }
      &.active {
        background-color: #42b983;
        color: #fff;
        border-color: #42b983;
        &::before {
          content: '';
          background: #fff;
          display: inline-block;
          width: 8px;
          height: 8px;
          border-radius: 50%;
          position: relative;
          margin-right: 2px;
        }
      }
    }
  }
  .contextmenu {
    margin: 0;
    background: #fff;
    z-index: 3000;
    position: absolute;
    list-style-type: none;
    padding: 5px 0;
    border-radius: 4px;
    font-size: 12px;
    font-weight: 400;
    color: #333;
    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
    li {
      margin: 0;
      padding: 7px 16px;
      cursor: pointer;
      &:hover {
        background: #eee;
      }
    }
  }
}
</style>

express-ui\src\layout\components\TagsView\ScrollPane.vue

<template>
  <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
    <slot />
  </el-scrollbar>
</template>

<script>
const tagAndTagSpacing = 4 // tag之间的间距

export default {
  name: 'ScrollPane',
  data() {
    return {
      left: 0
    }
  },
  computed: {
    scrollWrapper() {
      return this.$refs.scrollContainer.$refs.wrap
    }
  },
  mounted() {
    this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
  },
  beforeDestroy() {
    this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
  },
  methods: {
    handleScroll(e) {
      const eventDelta = e.wheelDelta || -e.deltaY * 40
      const $scrollWrapper = this.scrollWrapper
      $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft - eventDelta / 4
    },
    emitScroll() {
      this.$emit('scroll')
    },
    moveToTarget(currentTag) {
      const $container = this.$refs.scrollContainer.$el
      const $containerWidth = $container.offsetWidth
      const $scrollWrapper = this.scrollWrapper
      const tagList = this.$parent.$refs.tag

      let firstTag = null
      let lastTag = null

      // 找到第一个和最后一个tag
      if (tagList.length > 0) {
        firstTag = tagList[0]
        lastTag = tagList[tagList.length - 1]
      }

      if (firstTag === currentTag) {
        $scrollWrapper.scrollLeft = 0
      } else if (lastTag === currentTag) {
        $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
      } else {
        // 找到当前tag的前一个和后一个元素
        const currentIndex = tagList.findIndex(item => item === currentTag)
        const prevTag = tagList[currentIndex - 1]
        const nextTag = tagList[currentIndex + 1]

        // 当前tag的位置
        const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
        const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing

        // 当前tag后一个元素在可视区域右侧不可见
        if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
          $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
        } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
          // 当前tag前一个元素在可视区域左侧不可见
          $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
        }
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.scroll-container {
  white-space: nowrap;
  position: relative;
  overflow: hidden;
  width: 100%;
  ::v-deep {
    .el-scrollbar__bar {
      bottom: 0px;
    }
    .el-scrollbar__wrap {
      height: 49px;
    }
  }
}
</style>

express-ui\src\plugins\notification.js

import NotificationManager from '@/components/Notification/NotificationManager'

// Vue插件:通知系统
const NotificationPlugin = {
  install(Vue) {
    // 初始化通知管理器
    NotificationManager.init()
    
    // 将通知管理器添加到Vue原型,使所有组件都能访问
    Vue.prototype.$notification = {
      // 获取事件总线
      get bus() {
        return NotificationManager.bus
      },
      
      // 显示通知
      show(notification) {
        NotificationManager.showNotification(notification)
      },
      
      // 标记为已读
      markAsRead(notificationId) {
        return NotificationManager.markAsRead(notificationId)
      },
      
      // 刷新未读数量
      refreshUnreadCount() {
        NotificationManager.fetchUnreadCount()
      },
      
      // 生成测试通知(仅用于开发测试)
      test() {
        return NotificationManager.generateTestNotification()
      },
      
      // 检查是否启用了桌面通知
      isDesktopNotificationEnabled() {
        return NotificationManager.isDesktopNotificationEnabled()
      },
      
      // 检查是否启用了声音提醒
      isSoundEnabled() {
        return NotificationManager.isSoundEnabled()
      },
      
      // 启用桌面通知
      enableDesktopNotification(enable = true) {
        localStorage.setItem('enableDesktopNotification', enable.toString())
        
        // 如果启用,请求通知权限
        if (enable && 'Notification' in window && Notification.permission !== 'granted' && Notification.permission !== 'denied') {
          Notification.requestPermission()
        }
      },
      
      // 启用声音提醒
      enableSound(enable = true) {
        localStorage.setItem('enableNotificationSound', enable.toString())
      }
    }
    
    // 在Vue实例销毁前清理通知管理器
    const destroyApp = Vue.prototype.$destroy
    Vue.prototype.$destroy = function() {
      NotificationManager.destroy()
      destroyApp.apply(this, arguments)
    }
  }
}

export default NotificationPlugin

express-ui\src\router\index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Layout from '@/layout'

Vue.use(VueRouter)

// 公共路由
export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  {
    path: '/404',
    component: () => import('@/views/error/404'),
    hidden: true
  },
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index'),
        name: 'Dashboard',
        meta: { title: '首页', icon: 'dashboard', affix: true }
      }
    ]
  },
  // 通知管理路由
  {
    path: '/notification',
    component: Layout,
    redirect: '/notification/list',
    name: 'Notification',
    meta: { title: '通知管理', icon: 'notification' },
    children: [
      {
        path: 'list',
        component: () => import('@/views/notification/list'),
        name: 'NotificationList',
        meta: { title: '通知列表', icon: 'list' }
      },
      {
        path: 'template',
        component: () => import('@/views/notification/template'),
        name: 'NotificationTemplate',
        meta: { title: '通知模板', icon: 'template' }
      },
      {
        path: 'send',
        component: () => import('@/views/notification/send'),
        name: 'NotificationSend',
        meta: { title: '发送通知', icon: 'send' }
      },
      {
        path: 'detail/:id',
        component: () => import('@/views/notification/detail'),
        name: 'NotificationDetail',
        meta: { title: '通知详情', icon: 'detail' },
        hidden: true
      }
    ]
  },
  // 快递管理路由
  {
    path: '/express',
    component: Layout,
    redirect: '/express/list',
    name: 'Express',
    meta: { title: '快递管理', icon: 'express' },
    children: [
      {
        path: 'list',
        component: () => import('@/views/express/list'),
        name: 'ExpressList',
        meta: { title: '快递列表', icon: 'list' }
      },
      {
        path: 'tracking',
        component: () => import('@/views/express/tracking'),
        name: 'ExpressTracking',
        meta: { title: '快递跟踪', icon: 'tracking' }
      },
      {
        path: 'detail/:id',
        component: () => import('@/views/express/detail'),
        name: 'ExpressDetail',
        meta: { title: '快递详情', icon: 'detail' },
        hidden: true
      }
    ]
  },
  // 配送管理路由
  {
    path: '/delivery',
    component: Layout,
    redirect: '/delivery/task',
    name: 'Delivery',
    meta: { title: '配送管理', icon: 'delivery' },
    children: [
      {
        path: 'task',
        component: () => import('@/views/delivery/task'),
        name: 'DeliveryTask',
        meta: { title: '配送任务', icon: 'task' }
      },
      {
        path: 'area',
        component: () => import('@/views/delivery/area'),
        name: 'DeliveryArea',
        meta: { title: '配送区域', icon: 'area' }
      },
      {
        path: 'route',
        component: () => import('@/views/delivery/route'),
        name: 'DeliveryRoute',
        meta: { title: '配送路线', icon: 'route' }
      },
      {
        path: 'man',
        component: () => import('@/views/delivery/man'),
        name: 'DeliveryMan',
        meta: { title: '配送员管理', icon: 'deliveryman' }
      },
      {
        path: 'detail/:id',
        component: () => import('@/views/delivery/detail'),
        name: 'DeliveryDetail',
        meta: { title: '配送详情', icon: 'detail' },
        hidden: true
      }
    ]
  },
  // 系统管理路由
  {
    path: '/system',
    component: Layout,
    redirect: '/system/user',
    name: 'System',
    meta: { title: '系统管理', icon: 'system' },
    children: [
      {
        path: 'user',
        component: () => import('@/views/system/user'),
        name: 'User',
        meta: { title: '用户管理', icon: 'user' }
      },
      {
        path: 'role',
        component: () => import('@/views/system/role'),
        name: 'Role',
        meta: { title: '角色管理', icon: 'role' }
      },
      {
        path: 'menu',
        component: () => import('@/views/system/menu'),
        name: 'Menu',
        meta: { title: '菜单管理', icon: 'menu' }
      },
      {
        path: 'config',
        component: () => import('@/views/system/config'),
        name: 'Config',
        meta: { title: '参数设置', icon: 'config' }
      },
      {
        path: 'log',
        component: () => import('@/views/system/log'),
        name: 'Log',
        meta: { title: '操作日志', icon: 'log' }
      }
    ]
  },
  // 404 页面必须放在末尾
  { path: '*', redirect: '/404', hidden: true }
]

const createRouter = () => new VueRouter({
  mode: 'history',
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})

const router = createRouter()

// 重置路由
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher
}

export default router

express-ui\src\utils\notification.js

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天进步2015

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值