package com.tcm.training.admin.app.notice.service;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.tcm.training.admin.app.auth.service.AppAuthSessionService;
|
import com.tcm.training.admin.app.notice.entity.AppUserNoticeReadRecord;
|
import com.tcm.training.admin.app.notice.mapper.AppUserNoticeReadRecordMapper;
|
import com.tcm.training.admin.app.notice.vo.AppNoticeItemVO;
|
import com.tcm.training.admin.app.settings.entity.AppUserNotificationSetting;
|
import com.tcm.training.admin.app.settings.mapper.AppUserNotificationSettingMapper;
|
import com.tcm.training.admin.live.entity.CoursePurchaseOrder;
|
import com.tcm.training.admin.live.entity.HealthCourse;
|
import com.tcm.training.admin.live.entity.HealthCourseMemberJoinRecord;
|
import com.tcm.training.admin.live.entity.HealthCourseVideo;
|
import com.tcm.training.admin.live.entity.LiveCourse;
|
import com.tcm.training.admin.live.entity.LiveReserve;
|
import com.tcm.training.admin.live.entity.LiveSession;
|
import com.tcm.training.admin.live.mapper.CoursePurchaseOrderMapper;
|
import com.tcm.training.admin.live.mapper.HealthCourseMapper;
|
import com.tcm.training.admin.live.mapper.HealthCourseMemberJoinRecordMapper;
|
import com.tcm.training.admin.live.mapper.HealthCourseVideoMapper;
|
import com.tcm.training.admin.live.mapper.LiveCourseMapper;
|
import com.tcm.training.admin.live.mapper.LiveReserveMapper;
|
import com.tcm.training.admin.live.mapper.LiveSessionMapper;
|
import com.tcm.training.admin.member.entity.AppUser;
|
import com.tcm.training.admin.member.mapper.UserMapper;
|
import com.tcm.training.admin.member.entity.MemberPurchaseOrder;
|
import com.tcm.training.admin.member.mapper.MemberPurchaseOrderMapper;
|
import com.tcm.training.admin.points.entity.PointsExchangeOrder;
|
import com.tcm.training.admin.points.mapper.PointsExchangeOrderMapper;
|
import com.tcm.training.admin.system.entity.ActivityNotice;
|
import com.tcm.training.admin.system.entity.SystemSettings;
|
import com.tcm.training.admin.system.mapper.ActivityNoticeMapper;
|
import com.tcm.training.admin.system.mapper.SystemSettingsMapper;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.time.LocalDateTime;
|
import java.time.format.DateTimeFormatter;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Collections;
|
import java.util.HashMap;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
/**
|
* APP 通知列表服务。
|
*/
|
@Service
|
public class AppNoticeService {
|
|
private static final String NOTICE_TYPE_SERVICE = "SERVICE";
|
private static final String NOTICE_TYPE_MEMBER_EXPIRE = "MEMBER_EXPIRE";
|
private static final String NOTICE_TYPE_LIVE_RESERVE = "LIVE_RESERVE";
|
private static final String NOTICE_TYPE_ACTIVITY = "ACTIVITY";
|
private static final String NOTICE_TYPE_ORDER = "ORDER";
|
private static final String NOTICE_TYPE_COURSE_UPDATE = "COURSE_UPDATE";
|
private static final List<String> NOTICE_TYPES = Arrays.asList(
|
NOTICE_TYPE_SERVICE,
|
NOTICE_TYPE_MEMBER_EXPIRE,
|
NOTICE_TYPE_LIVE_RESERVE,
|
NOTICE_TYPE_ACTIVITY,
|
NOTICE_TYPE_ORDER,
|
NOTICE_TYPE_COURSE_UPDATE
|
);
|
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm");
|
private static final int USER_STATUS_ENABLED = 1;
|
private static final int USER_DELETED_NO = 0;
|
private static final int COURSE_STATUS_ENABLED = 1;
|
private static final int LIVE_COURSE_STATUS_ENABLED = 1;
|
private static final int LIVE_STATUS_PENDING = 0;
|
private static final String ORDER_STATUS_PAID = "PAID";
|
|
private final AppAuthSessionService appAuthSessionService;
|
private final UserMapper userMapper;
|
private final SystemSettingsMapper systemSettingsMapper;
|
private final ActivityNoticeMapper activityNoticeMapper;
|
private final LiveReserveMapper liveReserveMapper;
|
private final LiveSessionMapper liveSessionMapper;
|
private final LiveCourseMapper liveCourseMapper;
|
private final CoursePurchaseOrderMapper coursePurchaseOrderMapper;
|
private final MemberPurchaseOrderMapper memberPurchaseOrderMapper;
|
private final PointsExchangeOrderMapper pointsExchangeOrderMapper;
|
private final HealthCourseMapper healthCourseMapper;
|
private final HealthCourseVideoMapper healthCourseVideoMapper;
|
private final HealthCourseMemberJoinRecordMapper healthCourseMemberJoinRecordMapper;
|
private final AppUserNotificationSettingMapper appUserNotificationSettingMapper;
|
private final AppUserNoticeReadRecordMapper appUserNoticeReadRecordMapper;
|
|
/**
|
* 注入 APP 通知列表所需依赖。
|
*/
|
public AppNoticeService(AppAuthSessionService appAuthSessionService,
|
UserMapper userMapper,
|
SystemSettingsMapper systemSettingsMapper,
|
ActivityNoticeMapper activityNoticeMapper,
|
LiveReserveMapper liveReserveMapper,
|
LiveSessionMapper liveSessionMapper,
|
LiveCourseMapper liveCourseMapper,
|
CoursePurchaseOrderMapper coursePurchaseOrderMapper,
|
MemberPurchaseOrderMapper memberPurchaseOrderMapper,
|
PointsExchangeOrderMapper pointsExchangeOrderMapper,
|
HealthCourseMapper healthCourseMapper,
|
HealthCourseVideoMapper healthCourseVideoMapper,
|
HealthCourseMemberJoinRecordMapper healthCourseMemberJoinRecordMapper,
|
AppUserNotificationSettingMapper appUserNotificationSettingMapper,
|
AppUserNoticeReadRecordMapper appUserNoticeReadRecordMapper) {
|
this.appAuthSessionService = appAuthSessionService;
|
this.userMapper = userMapper;
|
this.systemSettingsMapper = systemSettingsMapper;
|
this.activityNoticeMapper = activityNoticeMapper;
|
this.liveReserveMapper = liveReserveMapper;
|
this.liveSessionMapper = liveSessionMapper;
|
this.liveCourseMapper = liveCourseMapper;
|
this.coursePurchaseOrderMapper = coursePurchaseOrderMapper;
|
this.memberPurchaseOrderMapper = memberPurchaseOrderMapper;
|
this.pointsExchangeOrderMapper = pointsExchangeOrderMapper;
|
this.healthCourseMapper = healthCourseMapper;
|
this.healthCourseVideoMapper = healthCourseVideoMapper;
|
this.healthCourseMemberJoinRecordMapper = healthCourseMemberJoinRecordMapper;
|
this.appUserNotificationSettingMapper = appUserNotificationSettingMapper;
|
this.appUserNoticeReadRecordMapper = appUserNoticeReadRecordMapper;
|
}
|
|
/**
|
* 获取当前登录用户的通知列表。
|
*/
|
public List<AppNoticeItemVO> list() {
|
AppUser user = requireCurrentEnabledUser();
|
AppUserNotificationSetting setting = getNotificationSetting(user.getId());
|
Map<String, LocalDateTime> readTimeMap = getReadTimeMap(user.getId());
|
List<AppNoticeItemVO> list = new ArrayList<AppNoticeItemVO>();
|
list.add(buildServiceNotice(user, readTimeMap.get(NOTICE_TYPE_SERVICE)));
|
list.add(buildMemberExpireNotice(user, readTimeMap.get(NOTICE_TYPE_MEMBER_EXPIRE)));
|
list.add(buildLiveReserveNotice(user.getId(), setting, readTimeMap.get(NOTICE_TYPE_LIVE_RESERVE)));
|
list.add(buildActivityNotice(setting, readTimeMap.get(NOTICE_TYPE_ACTIVITY)));
|
list.add(buildOrderNotice(user.getId(), setting, readTimeMap.get(NOTICE_TYPE_ORDER)));
|
list.add(buildCourseUpdateNotice(user.getId(), setting, readTimeMap.get(NOTICE_TYPE_COURSE_UPDATE)));
|
return list;
|
}
|
|
/**
|
* 将当前登录用户的全部通知标记为已读。
|
*/
|
@Transactional
|
public void markAllRead() {
|
Long userId = requireCurrentEnabledUser().getId();
|
LocalDateTime now = LocalDateTime.now();
|
for (String noticeType : NOTICE_TYPES) {
|
saveReadRecord(userId, noticeType, now);
|
}
|
}
|
|
/**
|
* 将当前登录用户指定类型通知标记为已读。
|
*/
|
@Transactional
|
public void markRead(String noticeType) {
|
Long userId = requireCurrentEnabledUser().getId();
|
String normalizedNoticeType = normalizeNoticeType(noticeType);
|
saveReadRecord(userId, normalizedNoticeType, LocalDateTime.now());
|
}
|
|
/**
|
* 构建系统客服通知。
|
*/
|
private AppNoticeItemVO buildServiceNotice(AppUser user, LocalDateTime lastReadAt) {
|
SystemSettings settings = systemSettingsMapper.selectOne(new LambdaQueryWrapper<SystemSettings>()
|
.eq(SystemSettings::getDeleted, 0)
|
.orderByDesc(SystemSettings::getId)
|
.last("limit 1"));
|
String phone = trimToEmpty(settings == null ? null : settings.getServicePhone());
|
String content = phone.isEmpty() ? "如需帮助,请联系系统客服" : "客服电话:" + phone;
|
LocalDateTime noticeTime = settings == null ? null : settings.getUpdatedAt();
|
return buildNoticeItem(
|
NOTICE_TYPE_SERVICE,
|
"系统客服",
|
content,
|
noticeTime,
|
"/static/image/notice/service.png",
|
calculateUnreadBadge(noticeTime, lastReadAt)
|
);
|
}
|
|
/**
|
* 构建会员到期提醒通知。
|
*/
|
private AppNoticeItemVO buildMemberExpireNotice(AppUser user, LocalDateTime lastReadAt) {
|
LocalDateTime expireAt = user.getMemberExpireAt();
|
String content = "暂无会员到期提醒";
|
LocalDateTime noticeTime = null;
|
int badge = 0;
|
if (expireAt != null) {
|
int remindDays = resolveMemberExpireRemindDays();
|
LocalDateTime remindTime = expireAt.minusDays(remindDays);
|
if (!LocalDateTime.now().isBefore(remindTime)) {
|
content = "您的会员有效期不足" + remindDays + "天,为避免影响你的学习体验,请及时续费";
|
noticeTime = expireAt;
|
badge = calculateUnreadBadge(noticeTime, lastReadAt);
|
}
|
}
|
return buildNoticeItem(
|
NOTICE_TYPE_MEMBER_EXPIRE,
|
"会员到期提醒",
|
content,
|
noticeTime,
|
"/static/image/notice/lingdang.png",
|
badge
|
);
|
}
|
|
/**
|
* 构建直播提醒通知。
|
*/
|
private AppNoticeItemVO buildLiveReserveNotice(Long userId,
|
AppUserNotificationSetting setting,
|
LocalDateTime lastReadAt) {
|
if (!isEnabled(setting == null ? null : setting.getLiveReservationEnabled())) {
|
return buildNoticeItem(
|
NOTICE_TYPE_LIVE_RESERVE,
|
"直播提醒",
|
"直播预约通知已关闭",
|
null,
|
"/static/image/notice/live.png",
|
0
|
);
|
}
|
List<LiveReserve> reserves = liveReserveMapper.selectList(new LambdaQueryWrapper<LiveReserve>()
|
.eq(LiveReserve::getUserId, userId)
|
.eq(LiveReserve::getIsCancel, 0)
|
.orderByDesc(LiveReserve::getReserveTime)
|
.orderByDesc(LiveReserve::getId));
|
if (reserves == null || reserves.isEmpty()) {
|
return buildNoticeItem(
|
NOTICE_TYPE_LIVE_RESERVE,
|
"直播提醒",
|
"暂无直播预约提醒",
|
null,
|
"/static/image/notice/live.png",
|
0
|
);
|
}
|
List<Long> sessionIds = new ArrayList<Long>();
|
for (LiveReserve reserve : reserves) {
|
if (reserve.getSessionId() != null) {
|
sessionIds.add(reserve.getSessionId());
|
}
|
}
|
if (sessionIds.isEmpty()) {
|
return buildNoticeItem(
|
NOTICE_TYPE_LIVE_RESERVE,
|
"直播提醒",
|
"暂无直播预约提醒",
|
null,
|
"/static/image/notice/live.png",
|
0
|
);
|
}
|
List<LiveSession> sessions = liveSessionMapper.selectList(new LambdaQueryWrapper<LiveSession>()
|
.in(LiveSession::getId, sessionIds)
|
.eq(LiveSession::getDeleted, 0));
|
if (sessions == null || sessions.isEmpty()) {
|
return buildNoticeItem(
|
NOTICE_TYPE_LIVE_RESERVE,
|
"直播提醒",
|
"暂无直播预约提醒",
|
null,
|
"/static/image/notice/live.png",
|
0
|
);
|
}
|
Map<Long, LiveSession> sessionMap = new HashMap<Long, LiveSession>();
|
for (LiveSession session : sessions) {
|
sessionMap.put(session.getId(), session);
|
}
|
List<Long> courseIds = new ArrayList<Long>();
|
for (LiveSession session : sessions) {
|
if (session.getLiveCourseId() != null) {
|
courseIds.add(session.getLiveCourseId());
|
}
|
}
|
Map<Long, LiveCourse> courseMap = getLiveCourseMap(courseIds);
|
NotificationSnapshot snapshot = null;
|
int unreadCount = 0;
|
LocalDateTime now = LocalDateTime.now();
|
Integer advanceMinutes = setting == null ? null : setting.getAdvanceNoticeMinutes();
|
int normalizedAdvanceMinutes = advanceMinutes == null || advanceMinutes.intValue() <= 0 ? 60 : advanceMinutes.intValue();
|
for (LiveReserve reserve : reserves) {
|
LiveSession session = sessionMap.get(reserve.getSessionId());
|
if (session == null || session.getStartTime() == null || session.getLiveStatus() == null) {
|
continue;
|
}
|
if (session.getLiveStatus().intValue() != LIVE_STATUS_PENDING) {
|
continue;
|
}
|
LocalDateTime remindTime = session.getStartTime().minusMinutes(normalizedAdvanceMinutes);
|
if (now.isBefore(remindTime)) {
|
continue;
|
}
|
LiveCourse liveCourse = courseMap.get(session.getLiveCourseId());
|
String title = trimToEmpty(liveCourse == null ? null : liveCourse.getTitle());
|
String content = title.isEmpty() ? "您预约的直播即将开播。" : "您预约的【" + title + "】即将开播。";
|
NotificationSnapshot current = new NotificationSnapshot(content, session.getStartTime());
|
if (snapshot == null || compareTime(current.getNoticeTime(), snapshot.getNoticeTime()) > 0) {
|
snapshot = current;
|
}
|
if (isUnread(session.getStartTime(), lastReadAt)) {
|
unreadCount++;
|
}
|
}
|
return buildNoticeItem(
|
NOTICE_TYPE_LIVE_RESERVE,
|
"直播提醒",
|
snapshot == null ? "暂无直播预约提醒" : snapshot.getContent(),
|
snapshot == null ? null : snapshot.getNoticeTime(),
|
"/static/image/notice/live.png",
|
unreadCount
|
);
|
}
|
|
/**
|
* 构建活动通知。
|
*/
|
private AppNoticeItemVO buildActivityNotice(AppUserNotificationSetting setting, LocalDateTime lastReadAt) {
|
if (!isEnabled(setting == null ? null : setting.getActivityNoticeEnabled())) {
|
return buildNoticeItem(
|
NOTICE_TYPE_ACTIVITY,
|
"活动通知",
|
"活动通知已关闭",
|
null,
|
"/static/image/notice/activity.png",
|
0
|
);
|
}
|
List<ActivityNotice> notices = activityNoticeMapper.selectList(new LambdaQueryWrapper<ActivityNotice>()
|
.eq(ActivityNotice::getDeleted, 0)
|
.orderByDesc(ActivityNotice::getCreatedAt)
|
.orderByDesc(ActivityNotice::getId));
|
if (notices == null || notices.isEmpty()) {
|
return buildNoticeItem(
|
NOTICE_TYPE_ACTIVITY,
|
"活动通知",
|
"暂无活动通知",
|
null,
|
"/static/image/notice/activity.png",
|
0
|
);
|
}
|
ActivityNotice latest = notices.get(0);
|
int unreadCount = 0;
|
for (ActivityNotice notice : notices) {
|
if (isUnread(notice.getCreatedAt(), lastReadAt)) {
|
unreadCount++;
|
}
|
}
|
return buildNoticeItem(
|
NOTICE_TYPE_ACTIVITY,
|
"活动通知",
|
trimToEmpty(latest.getTitle()),
|
latest.getCreatedAt(),
|
"/static/image/notice/activity.png",
|
unreadCount
|
);
|
}
|
|
/**
|
* 构建订单状态更新通知。
|
*/
|
private AppNoticeItemVO buildOrderNotice(Long userId,
|
AppUserNotificationSetting setting,
|
LocalDateTime lastReadAt) {
|
if (!isEnabled(setting == null ? null : setting.getOrderStatusUpdateEnabled())) {
|
return buildNoticeItem(
|
NOTICE_TYPE_ORDER,
|
"订单状态更新",
|
"订单状态更新通知已关闭",
|
null,
|
"/static/image/notice/order.png",
|
0
|
);
|
}
|
NotificationSnapshot latestSnapshot = null;
|
int unreadCount = 0;
|
|
List<PointsExchangeOrder> exchangeOrders = pointsExchangeOrderMapper.selectList(new LambdaQueryWrapper<PointsExchangeOrder>()
|
.eq(PointsExchangeOrder::getClinicAppUserId, userId)
|
.eq(PointsExchangeOrder::getDeleted, 0)
|
.orderByDesc(PointsExchangeOrder::getUpdatedAt)
|
.orderByDesc(PointsExchangeOrder::getId));
|
for (PointsExchangeOrder order : exchangeOrders) {
|
LocalDateTime noticeTime = chooseTime(order.getShippedAt(), order.getUpdatedAt(), order.getCreatedAt());
|
String content = buildExchangeOrderContent(order);
|
NotificationSnapshot snapshot = new NotificationSnapshot(content, noticeTime);
|
if (latestSnapshot == null || compareTime(snapshot.getNoticeTime(), latestSnapshot.getNoticeTime()) > 0) {
|
latestSnapshot = snapshot;
|
}
|
if (isUnread(noticeTime, lastReadAt)) {
|
unreadCount++;
|
}
|
}
|
|
List<MemberPurchaseOrder> memberOrders = memberPurchaseOrderMapper.selectList(new LambdaQueryWrapper<MemberPurchaseOrder>()
|
.eq(MemberPurchaseOrder::getAppUserId, userId)
|
.eq(MemberPurchaseOrder::getDeleted, 0)
|
.orderByDesc(MemberPurchaseOrder::getUpdatedAt)
|
.orderByDesc(MemberPurchaseOrder::getId));
|
for (MemberPurchaseOrder order : memberOrders) {
|
LocalDateTime noticeTime = chooseTime(order.getPaidAt(), order.getUpdatedAt(), order.getCreatedAt());
|
String content = "您的会员订单状态已更新";
|
NotificationSnapshot snapshot = new NotificationSnapshot(content, noticeTime);
|
if (latestSnapshot == null || compareTime(snapshot.getNoticeTime(), latestSnapshot.getNoticeTime()) > 0) {
|
latestSnapshot = snapshot;
|
}
|
if (isUnread(noticeTime, lastReadAt)) {
|
unreadCount++;
|
}
|
}
|
|
List<CoursePurchaseOrder> courseOrders = coursePurchaseOrderMapper.selectList(new LambdaQueryWrapper<CoursePurchaseOrder>()
|
.eq(CoursePurchaseOrder::getAppUserId, userId)
|
.eq(CoursePurchaseOrder::getDeleted, 0)
|
.orderByDesc(CoursePurchaseOrder::getUpdatedAt)
|
.orderByDesc(CoursePurchaseOrder::getId));
|
for (CoursePurchaseOrder order : courseOrders) {
|
LocalDateTime noticeTime = chooseTime(order.getPaidAt(), order.getUpdatedAt(), order.getCreatedAt());
|
String title = trimToEmpty(order.getProductNameSnapshot());
|
String content = title.isEmpty() ? "您的课程订单状态已更新" : "您的【" + title + "】订单状态已更新";
|
NotificationSnapshot snapshot = new NotificationSnapshot(content, noticeTime);
|
if (latestSnapshot == null || compareTime(snapshot.getNoticeTime(), latestSnapshot.getNoticeTime()) > 0) {
|
latestSnapshot = snapshot;
|
}
|
if (isUnread(noticeTime, lastReadAt)) {
|
unreadCount++;
|
}
|
}
|
|
return buildNoticeItem(
|
NOTICE_TYPE_ORDER,
|
"订单状态更新",
|
latestSnapshot == null ? "暂无订单状态更新" : latestSnapshot.getContent(),
|
latestSnapshot == null ? null : latestSnapshot.getNoticeTime(),
|
"/static/image/notice/order.png",
|
unreadCount
|
);
|
}
|
|
/**
|
* 构建课程更新通知。
|
*/
|
private AppNoticeItemVO buildCourseUpdateNotice(Long userId,
|
AppUserNotificationSetting setting,
|
LocalDateTime lastReadAt) {
|
if (!isEnabled(setting == null ? null : setting.getCourseUpdateEnabled())) {
|
return buildNoticeItem(
|
NOTICE_TYPE_COURSE_UPDATE,
|
"课程更新通知",
|
"课程更新通知已关闭",
|
null,
|
"/static/image/notice/book.png",
|
0
|
);
|
}
|
List<Long> ownedCourseIds = resolveOwnedCourseIds(userId);
|
if (ownedCourseIds.isEmpty()) {
|
return buildNoticeItem(
|
NOTICE_TYPE_COURSE_UPDATE,
|
"课程更新通知",
|
"暂无课程更新通知",
|
null,
|
"/static/image/notice/book.png",
|
0
|
);
|
}
|
List<HealthCourse> courses = healthCourseMapper.selectList(new LambdaQueryWrapper<HealthCourse>()
|
.in(HealthCourse::getId, ownedCourseIds)
|
.eq(HealthCourse::getDeleted, 0)
|
.eq(HealthCourse::getStatus, COURSE_STATUS_ENABLED));
|
if (courses == null || courses.isEmpty()) {
|
return buildNoticeItem(
|
NOTICE_TYPE_COURSE_UPDATE,
|
"课程更新通知",
|
"暂无课程更新通知",
|
null,
|
"/static/image/notice/book.png",
|
0
|
);
|
}
|
List<HealthCourseVideo> videos = healthCourseVideoMapper.selectList(new LambdaQueryWrapper<HealthCourseVideo>()
|
.in(HealthCourseVideo::getCourseId, ownedCourseIds)
|
.eq(HealthCourseVideo::getDeleted, 0)
|
.orderByDesc(HealthCourseVideo::getUpdatedAt)
|
.orderByDesc(HealthCourseVideo::getId));
|
Map<Long, HealthCourse> courseMap = new HashMap<Long, HealthCourse>();
|
for (HealthCourse course : courses) {
|
courseMap.put(course.getId(), course);
|
}
|
NotificationSnapshot latestSnapshot = null;
|
int unreadCount = 0;
|
for (HealthCourseVideo video : videos) {
|
HealthCourse course = courseMap.get(video.getCourseId());
|
if (course == null) {
|
continue;
|
}
|
LocalDateTime noticeTime = chooseTime(video.getUpdatedAt(), course.getUpdatedAt(), course.getCreatedAt());
|
String title = trimToEmpty(course.getTitle());
|
String content = title.isEmpty() ? "您的课程有更新" : "你的【" + title + "】有更新";
|
NotificationSnapshot snapshot = new NotificationSnapshot(content, noticeTime);
|
if (latestSnapshot == null || compareTime(snapshot.getNoticeTime(), latestSnapshot.getNoticeTime()) > 0) {
|
latestSnapshot = snapshot;
|
}
|
if (isUnread(noticeTime, lastReadAt)) {
|
unreadCount++;
|
}
|
}
|
return buildNoticeItem(
|
NOTICE_TYPE_COURSE_UPDATE,
|
"课程更新通知",
|
latestSnapshot == null ? "暂无课程更新通知" : latestSnapshot.getContent(),
|
latestSnapshot == null ? null : latestSnapshot.getNoticeTime(),
|
"/static/image/notice/book.png",
|
unreadCount
|
);
|
}
|
|
/**
|
* 组装统一通知列表项。
|
*/
|
private AppNoticeItemVO buildNoticeItem(String type,
|
String title,
|
String desc,
|
LocalDateTime noticeTime,
|
String icon,
|
int unreadCount) {
|
AppNoticeItemVO itemVO = new AppNoticeItemVO();
|
itemVO.setType(type);
|
itemVO.setTitle(title);
|
itemVO.setDesc(trimToEmpty(desc));
|
itemVO.setTime(formatTime(noticeTime));
|
itemVO.setIcon(icon);
|
itemVO.setBadge(Integer.valueOf(Math.max(unreadCount, 0)));
|
itemVO.setHasRedDot(Boolean.valueOf(unreadCount > 0));
|
return itemVO;
|
}
|
|
/**
|
* 读取当前登录用户。
|
*/
|
private AppUser requireCurrentEnabledUser() {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
AppUser user = userMapper.selectById(userId);
|
if (user == null || !Integer.valueOf(USER_DELETED_NO).equals(user.getDeleted())) {
|
throw new IllegalArgumentException("用户不存在");
|
}
|
if (!Integer.valueOf(USER_STATUS_ENABLED).equals(user.getStatus())) {
|
throw new IllegalArgumentException("用户已禁用");
|
}
|
return user;
|
}
|
|
/**
|
* 获取用户通知设置,没有记录时返回默认开关。
|
*/
|
private AppUserNotificationSetting getNotificationSetting(Long userId) {
|
AppUserNotificationSetting setting = appUserNotificationSettingMapper.selectOne(new LambdaQueryWrapper<AppUserNotificationSetting>()
|
.eq(AppUserNotificationSetting::getAppUserId, userId)
|
.eq(AppUserNotificationSetting::getDeleted, 0)
|
.last("limit 1"));
|
if (setting != null) {
|
return setting;
|
}
|
AppUserNotificationSetting defaultSetting = new AppUserNotificationSetting();
|
defaultSetting.setAppUserId(userId);
|
defaultSetting.setOrderStatusUpdateEnabled(1);
|
defaultSetting.setActivityNoticeEnabled(1);
|
defaultSetting.setCourseUpdateEnabled(1);
|
defaultSetting.setLiveReservationEnabled(1);
|
defaultSetting.setAdvanceNoticeMinutes(60);
|
return defaultSetting;
|
}
|
|
/**
|
* 读取用户各通知类型的已读时间。
|
*/
|
private Map<String, LocalDateTime> getReadTimeMap(Long userId) {
|
Map<String, LocalDateTime> result = new LinkedHashMap<String, LocalDateTime>();
|
List<AppUserNoticeReadRecord> records = appUserNoticeReadRecordMapper.selectList(new LambdaQueryWrapper<AppUserNoticeReadRecord>()
|
.eq(AppUserNoticeReadRecord::getAppUserId, userId)
|
.eq(AppUserNoticeReadRecord::getDeleted, 0));
|
if (records == null) {
|
return result;
|
}
|
for (AppUserNoticeReadRecord record : records) {
|
result.put(trimToEmpty(record.getNoticeType()), record.getLastReadAt());
|
}
|
return result;
|
}
|
|
/**
|
* 保存指定类型通知的已读时间。
|
*/
|
private void saveReadRecord(Long userId, String noticeType, LocalDateTime readAt) {
|
AppUserNoticeReadRecord record = appUserNoticeReadRecordMapper.selectOne(new LambdaQueryWrapper<AppUserNoticeReadRecord>()
|
.eq(AppUserNoticeReadRecord::getAppUserId, userId)
|
.eq(AppUserNoticeReadRecord::getNoticeType, noticeType)
|
.eq(AppUserNoticeReadRecord::getDeleted, 0)
|
.last("limit 1"));
|
if (record == null) {
|
AppUserNoticeReadRecord created = new AppUserNoticeReadRecord();
|
created.setAppUserId(userId);
|
created.setNoticeType(noticeType);
|
created.setLastReadAt(readAt);
|
created.setDeleted(0);
|
appUserNoticeReadRecordMapper.insert(created);
|
return;
|
}
|
record.setLastReadAt(readAt);
|
appUserNoticeReadRecordMapper.updateById(record);
|
}
|
|
/**
|
* 校验并规范通知类型。
|
*/
|
private String normalizeNoticeType(String noticeType) {
|
String normalized = trimToEmpty(noticeType).toUpperCase();
|
if (!NOTICE_TYPES.contains(normalized)) {
|
throw new IllegalArgumentException("通知类型不合法");
|
}
|
return normalized;
|
}
|
|
/**
|
* 解析会员到期提醒天数。
|
*/
|
private int resolveMemberExpireRemindDays() {
|
SystemSettings settings = systemSettingsMapper.selectOne(new LambdaQueryWrapper<SystemSettings>()
|
.eq(SystemSettings::getDeleted, 0)
|
.orderByDesc(SystemSettings::getId)
|
.last("limit 1"));
|
Integer remindDays = settings == null ? null : settings.getMemberExpireRemindDays();
|
if (remindDays == null || remindDays.intValue() <= 0) {
|
return 3;
|
}
|
return remindDays.intValue();
|
}
|
|
/**
|
* 读取直播课程映射,避免循环查库。
|
*/
|
private Map<Long, LiveCourse> getLiveCourseMap(List<Long> courseIds) {
|
if (courseIds == null || courseIds.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
List<LiveCourse> liveCourses = liveCourseMapper.selectList(new LambdaQueryWrapper<LiveCourse>()
|
.in(LiveCourse::getId, courseIds)
|
.eq(LiveCourse::getDeleted, 0)
|
.eq(LiveCourse::getStatus, LIVE_COURSE_STATUS_ENABLED));
|
Map<Long, LiveCourse> result = new HashMap<Long, LiveCourse>();
|
for (LiveCourse liveCourse : liveCourses) {
|
result.put(liveCourse.getId(), liveCourse);
|
}
|
return result;
|
}
|
|
/**
|
* 解析用户已拥有的课程主键集合。
|
*/
|
private List<Long> resolveOwnedCourseIds(Long userId) {
|
Map<Long, Long> courseIdMap = new LinkedHashMap<Long, Long>();
|
List<CoursePurchaseOrder> paidCourseOrders = coursePurchaseOrderMapper.selectList(new LambdaQueryWrapper<CoursePurchaseOrder>()
|
.eq(CoursePurchaseOrder::getAppUserId, userId)
|
.eq(CoursePurchaseOrder::getDeleted, 0)
|
.eq(CoursePurchaseOrder::getStatus, ORDER_STATUS_PAID)
|
.isNotNull(CoursePurchaseOrder::getCourseId));
|
for (CoursePurchaseOrder order : paidCourseOrders) {
|
courseIdMap.put(order.getCourseId(), order.getCourseId());
|
}
|
List<HealthCourseMemberJoinRecord> joinedRecords = healthCourseMemberJoinRecordMapper.selectList(new LambdaQueryWrapper<HealthCourseMemberJoinRecord>()
|
.eq(HealthCourseMemberJoinRecord::getAppUserId, userId)
|
.eq(HealthCourseMemberJoinRecord::getDeleted, 0));
|
for (HealthCourseMemberJoinRecord record : joinedRecords) {
|
courseIdMap.put(record.getCourseId(), record.getCourseId());
|
}
|
return new ArrayList<Long>(courseIdMap.values());
|
}
|
|
/**
|
* 构建积分兑换订单通知文案。
|
*/
|
private String buildExchangeOrderContent(PointsExchangeOrder order) {
|
String giftName = trimToEmpty(order.getGiftName());
|
Integer status = order.getStatus();
|
if (status != null && status.intValue() == 1) {
|
return giftName.isEmpty() ? "您的礼品已邮寄发货" : "您的【" + giftName + "】已邮寄发货";
|
}
|
if (status != null && status.intValue() == 2) {
|
return giftName.isEmpty() ? "您的礼品已确认收货" : "您的【" + giftName + "】已确认收货";
|
}
|
return giftName.isEmpty() ? "您的兑换订单状态已更新" : "您的【" + giftName + "】兑换订单状态已更新";
|
}
|
|
/**
|
* 判断通知开关是否开启。
|
*/
|
private boolean isEnabled(Integer value) {
|
return value == null || value.intValue() == 1;
|
}
|
|
/**
|
* 计算通知是否未读。
|
*/
|
private boolean isUnread(LocalDateTime noticeTime, LocalDateTime lastReadAt) {
|
if (noticeTime == null) {
|
return false;
|
}
|
if (lastReadAt == null) {
|
return true;
|
}
|
return noticeTime.isAfter(lastReadAt);
|
}
|
|
/**
|
* 按单条通知计算未读数量。
|
*/
|
private int calculateUnreadBadge(LocalDateTime noticeTime, LocalDateTime lastReadAt) {
|
return isUnread(noticeTime, lastReadAt) ? 1 : 0;
|
}
|
|
/**
|
* 从多个时间中选择最合适的通知时间。
|
*/
|
private LocalDateTime chooseTime(LocalDateTime primary,
|
LocalDateTime secondary,
|
LocalDateTime fallback) {
|
if (primary != null) {
|
return primary;
|
}
|
if (secondary != null) {
|
return secondary;
|
}
|
return fallback;
|
}
|
|
/**
|
* 比较两个时间先后。
|
*/
|
private int compareTime(LocalDateTime left, LocalDateTime right) {
|
if (left == null && right == null) {
|
return 0;
|
}
|
if (left == null) {
|
return -1;
|
}
|
if (right == null) {
|
return 1;
|
}
|
return left.compareTo(right);
|
}
|
|
/**
|
* 将时间格式化为页面所需字符串。
|
*/
|
private String formatTime(LocalDateTime time) {
|
if (time == null) {
|
return "";
|
}
|
return TIME_FORMATTER.format(time);
|
}
|
|
/**
|
* 将可选文本归一化为空字符串。
|
*/
|
private String trimToEmpty(String value) {
|
return value == null ? "" : value.trim();
|
}
|
|
/**
|
* 聚合通知快照结构。
|
*/
|
private static final class NotificationSnapshot {
|
|
private final String content;
|
private final LocalDateTime noticeTime;
|
|
/**
|
* 记录聚合通知的摘要与时间。
|
*/
|
private NotificationSnapshot(String content, LocalDateTime noticeTime) {
|
this.content = content;
|
this.noticeTime = noticeTime;
|
}
|
|
/**
|
* 获取通知摘要。
|
*/
|
private String getContent() {
|
return content;
|
}
|
|
/**
|
* 获取通知时间。
|
*/
|
private LocalDateTime getNoticeTime() {
|
return noticeTime;
|
}
|
}
|
}
|