源代码续
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;
@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);
notification.setCreatedTime(LocalDateTime.now());
notification.setUpdatedTime(LocalDateTime.now());
Notification savedNotification = notificationRepository.save(notification);
log.info("Notification created with ID: {}", savedNotification.getId());
return convertToDTO(savedNotification);
}
@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);
}
@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);
}
@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);
}
@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());
}
@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());
}
@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());
}
@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());
}
@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());
}
@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());
}
@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());
}
@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());
}
@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));
}
} 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));
}
notification.setUpdatedTime(LocalDateTime.now());
}
savedNotifications = notificationRepository.saveAll(savedNotifications);
}
return savedNotifications.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
@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);
}
@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());
}
@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());
}
@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));
}
} 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));
}
notification.setUpdatedTime(LocalDateTime.now());
Notification updatedNotification = notificationRepository.save(notification);
return convertToDTO(updatedNotification);
}
@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);
}
@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;
}
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;
}
}
private boolean sendInAppNotification(Notification notification) {
log.info("Sending in-app notification: {}", notification.getTitle());
return true;
}
private boolean sendSmsNotification(Notification notification) {
log.info("Sending SMS notification: {}", notification.getTitle());
return ThreadLocalRandom.current().nextInt(100) < 90;
}
private boolean sendEmailNotification(Notification notification) {
log.info("Sending email notification: {}", notification.getTitle());
return ThreadLocalRandom.current().nextInt(100) < 95;
}
private boolean sendPushNotification(Notification notification) {
log.info("Sending push notification: {}", notification.getTitle());
return ThreadLocalRandom.current().nextInt(100) < 85;
}
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;
@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);
}
@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);
}
@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);
}
@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);
}
@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);
}
@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());
}
@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());
}
@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());
}
@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());
}
@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());
}
@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());
}
@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());
}
@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());
}
@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);
}
@Override
public boolean isTemplateCodeExists(String code) {
log.info("Checking if template code exists: {}", code);
return notificationTemplateRepository.existsByCode(code);
}
@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;
}
@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;
}
private String renderContent(String content, Map<String, Object> params) {
if (content == null || content.isEmpty() || params == null || params.isEmpty()) {
return content;
}
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();
}
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;
}
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 "未知类型";
}
}
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 "未知渠道";
}
}
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 "未知类型";
}
}
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 "未知类型";
}
}
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 "未知类型";
}
}
public static String getReadStatusDesc(Integer readStatus) {
if (readStatus == null) {
return "未知状态";
}
switch (readStatus) {
case NotificationReadStatus.UNREAD:
return "未读";
case NotificationReadStatus.READ:
return "已读";
default:
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 "未知状态";
}
}
public static String getTemplateStatusDesc(Integer status) {
if (status == null) {
return "未知状态";
}
switch (status) {
case TemplateStatus.DISABLED:
return "停用";
case TemplateStatus.ENABLED:
return "启用";
default:
return "未知状态";
}
}
public static String renderTemplate(String content, Map<String, Object> params) {
if (content == null || content.isEmpty() || params == null || params.isEmpty()) {
return content;
}
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();
}
public static String getSmsContent(String content) {
if (content == null || content.isEmpty()) {
return content;
}
return content.replaceAll("<[^>]*>", "");
}
public static String truncateContent(String content, int maxLength) {
if (content == null || content.isEmpty() || content.length() <= maxLength) {
return content;
}
return content.substring(0, maxLength - 3) + "...";
}
public static String generateSummary(String content, int maxLength) {
if (content == null || content.isEmpty()) {
return "";
}
String plainText = content.replaceAll("<[^>]*>", "");
return truncateContent(plainText, maxLength);
}
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);
}
public static boolean isValidMobile(String mobile) {
if (mobile == null || mobile.isEmpty()) {
return false;
}
String regex = "^1[3-9]\\d{9}$";
return mobile.matches(regex);
}
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:
header: Authorization
prefix: Bearer
secret: campusExpressSecretKey123456789012345678901234567890
expiration: 86400000
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(), '普通用户');
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