From ed527bae283588de0a9848390348e93c3820fef9 Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期一, 20 四月 2026 17:07:49 +0800
Subject: [PATCH] feat:通知功能完善
---
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/notice/service/AppNoticeService.java | 473 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 463 insertions(+), 10 deletions(-)
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/notice/service/AppNoticeService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/notice/service/AppNoticeService.java
index 4327b54..51c90fc 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/notice/service/AppNoticeService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/notice/service/AppNoticeService.java
@@ -1,12 +1,22 @@
package com.tcm.training.admin.app.notice.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tcm.training.admin.app.auth.service.AppAuthSessionService;
+import com.tcm.training.admin.app.notice.entity.AppSystemNotice;
+import com.tcm.training.admin.app.notice.entity.AppUserActivityNotice;
import com.tcm.training.admin.app.notice.entity.AppUserNoticeReadRecord;
+import com.tcm.training.admin.app.notice.mapper.AppSystemNoticeMapper;
+import com.tcm.training.admin.app.notice.mapper.AppUserActivityNoticeMapper;
import com.tcm.training.admin.app.notice.mapper.AppUserNoticeReadRecordMapper;
+import com.tcm.training.admin.app.notice.vo.AppActivityNoticeDetailVO;
+import com.tcm.training.admin.app.notice.vo.AppActivityNoticeListItemVO;
+import com.tcm.training.admin.app.notice.vo.AppNoticeDetailVO;
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.common.PageResponse;
import com.tcm.training.admin.live.entity.CoursePurchaseOrder;
import com.tcm.training.admin.live.entity.HealthCourse;
import com.tcm.training.admin.live.entity.HealthCourseMemberJoinRecord;
@@ -67,6 +77,7 @@
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 RECORD_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;
@@ -87,6 +98,8 @@
private final HealthCourseMemberJoinRecordMapper healthCourseMemberJoinRecordMapper;
private final AppUserNotificationSettingMapper appUserNotificationSettingMapper;
private final AppUserNoticeReadRecordMapper appUserNoticeReadRecordMapper;
+ private final AppUserActivityNoticeMapper appUserActivityNoticeMapper;
+ private final AppSystemNoticeMapper appSystemNoticeMapper;
/**
* 注入 APP 通知列表所需依赖。
@@ -105,7 +118,9 @@
HealthCourseVideoMapper healthCourseVideoMapper,
HealthCourseMemberJoinRecordMapper healthCourseMemberJoinRecordMapper,
AppUserNotificationSettingMapper appUserNotificationSettingMapper,
- AppUserNoticeReadRecordMapper appUserNoticeReadRecordMapper) {
+ AppUserNoticeReadRecordMapper appUserNoticeReadRecordMapper,
+ AppUserActivityNoticeMapper appUserActivityNoticeMapper,
+ AppSystemNoticeMapper appSystemNoticeMapper) {
this.appAuthSessionService = appAuthSessionService;
this.userMapper = userMapper;
this.systemSettingsMapper = systemSettingsMapper;
@@ -121,6 +136,8 @@
this.healthCourseMemberJoinRecordMapper = healthCourseMemberJoinRecordMapper;
this.appUserNotificationSettingMapper = appUserNotificationSettingMapper;
this.appUserNoticeReadRecordMapper = appUserNoticeReadRecordMapper;
+ this.appUserActivityNoticeMapper = appUserActivityNoticeMapper;
+ this.appSystemNoticeMapper = appSystemNoticeMapper;
}
/**
@@ -150,6 +167,7 @@
for (String noticeType : NOTICE_TYPES) {
saveReadRecord(userId, noticeType, now);
}
+ markAllActivityRead(userId, now);
}
/**
@@ -160,6 +178,88 @@
Long userId = requireCurrentEnabledUser().getId();
String normalizedNoticeType = normalizeNoticeType(noticeType);
saveReadRecord(userId, normalizedNoticeType, LocalDateTime.now());
+ if (NOTICE_TYPE_ACTIVITY.equals(normalizedNoticeType)) {
+ markAllActivityRead(userId, LocalDateTime.now());
+ }
+ }
+
+ /**
+ * 获取当前登录用户指定类型通知详情。
+ */
+ public AppNoticeDetailVO getNoticeDetail(String noticeType) {
+ AppUser user = requireCurrentEnabledUser();
+ String normalizedNoticeType = normalizeNoticeType(noticeType);
+ if (NOTICE_TYPE_ACTIVITY.equals(normalizedNoticeType)) {
+ throw new IllegalArgumentException("活动通知请使用活动通知详情接口");
+ }
+ AppUserNotificationSetting setting = getNotificationSetting(user.getId());
+ Map<String, LocalDateTime> readTimeMap = getReadTimeMap(user.getId());
+ if (NOTICE_TYPE_SERVICE.equals(normalizedNoticeType)) {
+ return buildServiceNoticeDetail(user, readTimeMap.get(normalizedNoticeType));
+ }
+ if (NOTICE_TYPE_MEMBER_EXPIRE.equals(normalizedNoticeType)) {
+ return buildMemberExpireNoticeDetail(user, readTimeMap.get(normalizedNoticeType));
+ }
+ if (NOTICE_TYPE_LIVE_RESERVE.equals(normalizedNoticeType)) {
+ return buildLiveReserveNoticeDetail(user.getId(), setting, readTimeMap.get(normalizedNoticeType));
+ }
+ if (NOTICE_TYPE_ORDER.equals(normalizedNoticeType)) {
+ return buildOrderNoticeDetail(user.getId(), setting, readTimeMap.get(normalizedNoticeType));
+ }
+ if (NOTICE_TYPE_COURSE_UPDATE.equals(normalizedNoticeType)) {
+ return buildCourseUpdateNoticeDetail(user.getId(), setting, readTimeMap.get(normalizedNoticeType));
+ }
+ throw new IllegalArgumentException("通知类型不合法");
+ }
+
+ /**
+ * 获取当前登录用户的活动通知分页列表。
+ */
+ public PageResponse<AppActivityNoticeListItemVO> pageActivityNotices(int page, int pageSize) {
+ Long userId = requireCurrentEnabledUser().getId();
+ Page<AppSystemNotice> result = appSystemNoticeMapper.selectPage(
+ new Page<AppSystemNotice>(Math.max(page, 1), Math.max(pageSize, 1)),
+ new LambdaQueryWrapper<AppSystemNotice>()
+ .eq(AppSystemNotice::getAppUserId, userId)
+ .eq(AppSystemNotice::getNoticeTypeCode, AppSystemNoticeType.ACTIVITY_NOTICE)
+ .eq(AppSystemNotice::getDeleted, RECORD_DELETED_NO)
+ .orderByDesc(AppSystemNotice::getCreatedAt)
+ .orderByDesc(AppSystemNotice::getId)
+ );
+ List<AppActivityNoticeListItemVO> list = new ArrayList<AppActivityNoticeListItemVO>();
+ for (AppSystemNotice record : result.getRecords()) {
+ ActivityNotice activityNotice = requireActivityNotice(record.getReferenceId());
+ if (activityNotice == null) {
+ continue;
+ }
+ list.add(toActivityListItem(record, activityNotice));
+ }
+ return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize());
+ }
+
+ /**
+ * 获取当前登录用户的活动通知详情。
+ */
+ public AppActivityNoticeDetailVO getActivityNoticeDetail(Long id) {
+ AppSystemNotice record = requireActivitySystemNoticeRecord(requireCurrentEnabledUser().getId(), id);
+ ActivityNotice notice = requireActivityNotice(record.getReferenceId());
+ AppActivityNoticeDetailVO vo = new AppActivityNoticeDetailVO();
+ vo.setId(record.getId());
+ vo.setTitle(trimToEmpty(notice.getTitle()));
+ vo.setContentHtml(trimToEmpty(notice.getContentHtml()));
+ vo.setCreatedAt(formatTime(chooseTime(record.getCreatedAt(), record.getPushedAt(), notice.getCreatedAt())));
+ vo.setRead(Boolean.valueOf(record.getReadAt() != null));
+ return vo;
+ }
+
+ /**
+ * 将当前登录用户的一条活动通知标记为已读。
+ */
+ @Transactional
+ public void markActivityRead(Long id) {
+ AppSystemNotice record = requireActivitySystemNoticeRecord(requireCurrentEnabledUser().getId(), id);
+ record.setReadAt(LocalDateTime.now());
+ appSystemNoticeMapper.updateById(record);
}
/**
@@ -181,6 +281,28 @@
"/static/image/notice/service.png",
calculateUnreadBadge(noticeTime, lastReadAt)
);
+ }
+
+ /**
+ * 构建系统客服通知详情。
+ */
+ private AppNoticeDetailVO buildServiceNoticeDetail(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 onlineTime = trimToEmpty(settings == null ? null : settings.getServiceOnlineTime());
+ StringBuilder content = new StringBuilder();
+ content.append("<p>如需帮助,请联系系统客服。</p>");
+ if (!phone.isEmpty()) {
+ content.append("<p>客服电话:").append(phone).append("</p>");
+ }
+ if (!onlineTime.isEmpty()) {
+ content.append("<p>服务时间:").append(onlineTime).append("</p>");
+ }
+ LocalDateTime noticeTime = settings == null ? null : settings.getUpdatedAt();
+ return buildNoticeDetailVO(NOTICE_TYPE_SERVICE, "系统客服", content.toString(), noticeTime, !isUnread(noticeTime, lastReadAt));
}
/**
@@ -208,6 +330,25 @@
"/static/image/notice/lingdang.png",
badge
);
+ }
+
+ /**
+ * 构建会员到期提醒详情。
+ */
+ private AppNoticeDetailVO buildMemberExpireNoticeDetail(AppUser user, LocalDateTime lastReadAt) {
+ LocalDateTime expireAt = user.getMemberExpireAt();
+ LocalDateTime noticeTime = null;
+ String content = "<p>暂无会员到期提醒</p>";
+ if (expireAt != null) {
+ int remindDays = resolveMemberExpireRemindDays();
+ LocalDateTime remindTime = expireAt.minusDays(remindDays);
+ if (!LocalDateTime.now().isBefore(remindTime)) {
+ noticeTime = expireAt;
+ content = "<p>您的会员有效期不足" + remindDays + "天,为避免影响你的学习体验,请及时续费。</p>"
+ + "<p>会员到期时间:" + formatTime(expireAt) + "</p>";
+ }
+ }
+ return buildNoticeDetailVO(NOTICE_TYPE_MEMBER_EXPIRE, "会员到期提醒", content, noticeTime, !isUnread(noticeTime, lastReadAt));
}
/**
@@ -320,6 +461,27 @@
}
/**
+ * 构建直播提醒详情。
+ */
+ private AppNoticeDetailVO buildLiveReserveNoticeDetail(Long userId,
+ AppUserNotificationSetting setting,
+ LocalDateTime lastReadAt) {
+ if (!isEnabled(setting == null ? null : setting.getLiveReservationEnabled())) {
+ return buildNoticeDetailVO(
+ NOTICE_TYPE_LIVE_RESERVE,
+ "直播提醒",
+ "<p>直播预约通知已关闭</p>",
+ null,
+ Boolean.TRUE
+ );
+ }
+ NotificationSnapshot snapshot = resolveLiveReserveSnapshot(userId, setting);
+ String content = snapshot == null ? "<p>暂无直播预约提醒</p>" : wrapParagraph(snapshot.getContent());
+ LocalDateTime noticeTime = snapshot == null ? null : snapshot.getNoticeTime();
+ return buildNoticeDetailVO(NOTICE_TYPE_LIVE_RESERVE, "直播提醒", content, noticeTime, !isUnread(noticeTime, lastReadAt));
+ }
+
+ /**
* 构建活动通知。
*/
private AppNoticeItemVO buildActivityNotice(AppUserNotificationSetting setting, LocalDateTime lastReadAt) {
@@ -333,11 +495,14 @@
0
);
}
- List<ActivityNotice> notices = activityNoticeMapper.selectList(new LambdaQueryWrapper<ActivityNotice>()
- .eq(ActivityNotice::getDeleted, 0)
- .orderByDesc(ActivityNotice::getCreatedAt)
- .orderByDesc(ActivityNotice::getId));
- if (notices == null || notices.isEmpty()) {
+ Long userId = requireCurrentEnabledUser().getId();
+ List<AppSystemNotice> noticeRecords = appSystemNoticeMapper.selectList(new LambdaQueryWrapper<AppSystemNotice>()
+ .eq(AppSystemNotice::getAppUserId, userId)
+ .eq(AppSystemNotice::getNoticeTypeCode, AppSystemNoticeType.ACTIVITY_NOTICE)
+ .eq(AppSystemNotice::getDeleted, RECORD_DELETED_NO)
+ .orderByDesc(AppSystemNotice::getCreatedAt)
+ .orderByDesc(AppSystemNotice::getId));
+ if (noticeRecords == null || noticeRecords.isEmpty()) {
return buildNoticeItem(
NOTICE_TYPE_ACTIVITY,
"活动通知",
@@ -347,10 +512,11 @@
0
);
}
- ActivityNotice latest = notices.get(0);
+ AppSystemNotice latestRecord = noticeRecords.get(0);
+ ActivityNotice latest = requireActivityNotice(latestRecord.getReferenceId());
int unreadCount = 0;
- for (ActivityNotice notice : notices) {
- if (isUnread(notice.getCreatedAt(), lastReadAt)) {
+ for (AppSystemNotice noticeRecord : noticeRecords) {
+ if (noticeRecord.getReadAt() == null) {
unreadCount++;
}
}
@@ -358,7 +524,7 @@
NOTICE_TYPE_ACTIVITY,
"活动通知",
trimToEmpty(latest.getTitle()),
- latest.getCreatedAt(),
+ chooseTime(latestRecord.getCreatedAt(), latestRecord.getPushedAt(), latest.getCreatedAt()),
"/static/image/notice/activity.png",
unreadCount
);
@@ -446,6 +612,21 @@
}
/**
+ * 构建订单状态更新详情。
+ */
+ private AppNoticeDetailVO buildOrderNoticeDetail(Long userId,
+ AppUserNotificationSetting setting,
+ LocalDateTime lastReadAt) {
+ if (!isEnabled(setting == null ? null : setting.getOrderStatusUpdateEnabled())) {
+ return buildNoticeDetailVO(NOTICE_TYPE_ORDER, "订单状态更新", "<p>订单状态更新通知已关闭</p>", null, Boolean.TRUE);
+ }
+ NotificationSnapshot latestSnapshot = resolveOrderSnapshot(userId);
+ String content = latestSnapshot == null ? "<p>暂无订单状态更新</p>" : wrapParagraph(latestSnapshot.getContent());
+ LocalDateTime noticeTime = latestSnapshot == null ? null : latestSnapshot.getNoticeTime();
+ return buildNoticeDetailVO(NOTICE_TYPE_ORDER, "订单状态更新", content, noticeTime, !isUnread(noticeTime, lastReadAt));
+ }
+
+ /**
* 构建课程更新通知。
*/
private AppNoticeItemVO buildCourseUpdateNotice(Long userId,
@@ -524,6 +705,21 @@
}
/**
+ * 构建课程更新通知详情。
+ */
+ private AppNoticeDetailVO buildCourseUpdateNoticeDetail(Long userId,
+ AppUserNotificationSetting setting,
+ LocalDateTime lastReadAt) {
+ if (!isEnabled(setting == null ? null : setting.getCourseUpdateEnabled())) {
+ return buildNoticeDetailVO(NOTICE_TYPE_COURSE_UPDATE, "课程更新通知", "<p>课程更新通知已关闭</p>", null, Boolean.TRUE);
+ }
+ NotificationSnapshot latestSnapshot = resolveCourseUpdateSnapshot(userId);
+ String content = latestSnapshot == null ? "<p>暂无课程更新通知</p>" : wrapParagraph(latestSnapshot.getContent());
+ LocalDateTime noticeTime = latestSnapshot == null ? null : latestSnapshot.getNoticeTime();
+ return buildNoticeDetailVO(NOTICE_TYPE_COURSE_UPDATE, "课程更新通知", content, noticeTime, !isUnread(noticeTime, lastReadAt));
+ }
+
+ /**
* 组装统一通知列表项。
*/
private AppNoticeItemVO buildNoticeItem(String type,
@@ -541,6 +737,23 @@
itemVO.setBadge(Integer.valueOf(Math.max(unreadCount, 0)));
itemVO.setHasRedDot(Boolean.valueOf(unreadCount > 0));
return itemVO;
+ }
+
+ /**
+ * 组装统一通知详情。
+ */
+ private AppNoticeDetailVO buildNoticeDetailVO(String type,
+ String title,
+ String contentHtml,
+ LocalDateTime noticeTime,
+ Boolean read) {
+ AppNoticeDetailVO detailVO = new AppNoticeDetailVO();
+ detailVO.setType(type);
+ detailVO.setTitle(title);
+ detailVO.setContentHtml(trimToEmpty(contentHtml));
+ detailVO.setCreatedAt(formatTime(noticeTime));
+ detailVO.setRead(read);
+ return detailVO;
}
/**
@@ -616,6 +829,83 @@
}
record.setLastReadAt(readAt);
appUserNoticeReadRecordMapper.updateById(record);
+ }
+
+ /**
+ * 将当前用户全部活动通知标记为已读。
+ */
+ private void markAllActivityRead(Long userId, LocalDateTime readAt) {
+ appSystemNoticeMapper.update(
+ null,
+ new LambdaUpdateWrapper<AppSystemNotice>()
+ .eq(AppSystemNotice::getAppUserId, userId)
+ .eq(AppSystemNotice::getNoticeTypeCode, AppSystemNoticeType.ACTIVITY_NOTICE)
+ .eq(AppSystemNotice::getDeleted, RECORD_DELETED_NO)
+ .isNull(AppSystemNotice::getReadAt)
+ .set(AppSystemNotice::getReadAt, readAt)
+ );
+ }
+
+ /**
+ * 校验当前用户可访问的活动通知记录。
+ */
+ private AppUserActivityNotice requireActivityNoticeRecord(Long userId, Long id) {
+ AppUserActivityNotice record = appUserActivityNoticeMapper.selectOne(new LambdaQueryWrapper<AppUserActivityNotice>()
+ .eq(AppUserActivityNotice::getId, id)
+ .eq(AppUserActivityNotice::getAppUserId, userId)
+ .eq(AppUserActivityNotice::getDeleted, RECORD_DELETED_NO)
+ .last("limit 1"));
+ if (record == null) {
+ throw new IllegalArgumentException("通知不存在");
+ }
+ requireActivityNotice(record.getActivityNoticeId());
+ return record;
+ }
+
+ /**
+ * 校验当前用户可访问的统一活动通知记录。
+ */
+ private AppSystemNotice requireActivitySystemNoticeRecord(Long userId, Long id) {
+ AppSystemNotice record = appSystemNoticeMapper.selectOne(new LambdaQueryWrapper<AppSystemNotice>()
+ .eq(AppSystemNotice::getId, id)
+ .eq(AppSystemNotice::getAppUserId, userId)
+ .eq(AppSystemNotice::getNoticeTypeCode, AppSystemNoticeType.ACTIVITY_NOTICE)
+ .eq(AppSystemNotice::getDeleted, RECORD_DELETED_NO)
+ .last("limit 1"));
+ if (record == null) {
+ throw new IllegalArgumentException("通知不存在");
+ }
+ requireActivityNotice(record.getReferenceId());
+ return record;
+ }
+
+ /**
+ * 校验活动通知主表记录仍然有效。
+ */
+ private ActivityNotice requireActivityNotice(Long activityNoticeId) {
+ if (activityNoticeId == null) {
+ return null;
+ }
+ ActivityNotice notice = activityNoticeMapper.selectOne(new LambdaQueryWrapper<ActivityNotice>()
+ .eq(ActivityNotice::getId, activityNoticeId)
+ .eq(ActivityNotice::getDeleted, RECORD_DELETED_NO)
+ .last("limit 1"));
+ if (notice == null) {
+ throw new IllegalArgumentException("通知不存在");
+ }
+ return notice;
+ }
+
+ /**
+ * 组装活动通知列表项。
+ */
+ private AppActivityNoticeListItemVO toActivityListItem(AppSystemNotice record, ActivityNotice notice) {
+ AppActivityNoticeListItemVO itemVO = new AppActivityNoticeListItemVO();
+ itemVO.setId(record.getId());
+ itemVO.setTitle(trimToEmpty(notice.getTitle()));
+ itemVO.setCreatedAt(formatTime(chooseTime(record.getCreatedAt(), record.getPushedAt(), notice.getCreatedAt())));
+ itemVO.setRead(Boolean.valueOf(record.getReadAt() != null));
+ return itemVO;
}
/**
@@ -700,6 +990,169 @@
}
/**
+ * 解析直播提醒最新快照。
+ */
+ private NotificationSnapshot resolveLiveReserveSnapshot(Long userId, AppUserNotificationSetting setting) {
+ 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 null;
+ }
+ List<Long> sessionIds = new ArrayList<Long>();
+ for (LiveReserve reserve : reserves) {
+ if (reserve.getSessionId() != null) {
+ sessionIds.add(reserve.getSessionId());
+ }
+ }
+ if (sessionIds.isEmpty()) {
+ return null;
+ }
+ List<LiveSession> sessions = liveSessionMapper.selectList(new LambdaQueryWrapper<LiveSession>()
+ .in(LiveSession::getId, sessionIds)
+ .eq(LiveSession::getDeleted, 0));
+ if (sessions == null || sessions.isEmpty()) {
+ return null;
+ }
+ 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;
+ 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;
+ }
+ }
+ return snapshot;
+ }
+
+ /**
+ * 解析订单状态更新最新快照。
+ */
+ private NotificationSnapshot resolveOrderSnapshot(Long userId) {
+ NotificationSnapshot latestSnapshot = null;
+ 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());
+ NotificationSnapshot snapshot = new NotificationSnapshot(buildExchangeOrderContent(order), noticeTime);
+ if (latestSnapshot == null || compareTime(snapshot.getNoticeTime(), latestSnapshot.getNoticeTime()) > 0) {
+ latestSnapshot = snapshot;
+ }
+ }
+ 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());
+ NotificationSnapshot snapshot = new NotificationSnapshot("您的会员订单状态已更新", noticeTime);
+ if (latestSnapshot == null || compareTime(snapshot.getNoticeTime(), latestSnapshot.getNoticeTime()) > 0) {
+ latestSnapshot = snapshot;
+ }
+ }
+ 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;
+ }
+ }
+ return latestSnapshot;
+ }
+
+ /**
+ * 解析课程更新通知最新快照。
+ */
+ private NotificationSnapshot resolveCourseUpdateSnapshot(Long userId) {
+ List<Long> ownedCourseIds = resolveOwnedCourseIds(userId);
+ if (ownedCourseIds.isEmpty()) {
+ return null;
+ }
+ 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 null;
+ }
+ 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;
+ 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;
+ }
+ }
+ return latestSnapshot;
+ }
+
+ /**
+ * 将纯文本内容包装为段落,便于 APP 统一使用 rich-text 渲染。
+ */
+ private String wrapParagraph(String content) {
+ String safeContent = trimToEmpty(content);
+ if (safeContent.isEmpty()) {
+ return "<p>暂无通知内容</p>";
+ }
+ return "<p>" + safeContent + "</p>";
+ }
+
+ /**
* 判断通知开关是否开启。
*/
private boolean isEnabled(Integer value) {
--
Gitblit v1.7.1