|
|
@@ -0,0 +1,379 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.dto.TaskReminderConfigDTO;
|
|
|
+import com.etotem.cfc.entity.Task;
|
|
|
+import com.etotem.cfc.entity.UserNotification;
|
|
|
+import com.etotem.cfc.mapper.FamilyMapper;
|
|
|
+import com.etotem.cfc.mapper.TaskMapper;
|
|
|
+import com.etotem.cfc.mapper.UserNotificationMapper;
|
|
|
+import com.etotem.cfc.service.NotificationDispatchService;
|
|
|
+import com.etotem.cfc.service.TaskReminderConfigService;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
+
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.fail;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.anyInt;
|
|
|
+import static org.mockito.ArgumentMatchers.anyLong;
|
|
|
+import static org.mockito.Mockito.atLeastOnce;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 契约级单测:任务日提醒调度器(能力键 {@code task.reminder.daily} 调度面)。
|
|
|
+ *
|
|
|
+ * <p><b>需求/设计锚点(设计契约 v1.0,commit c56ea68ab 已合入 origin/cfclub)</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-P-65(消息通知/提醒设置)、
|
|
|
+ * SC-P-79(推送/通知开关设置);业务域 H(设置与通知)</li>
|
|
|
+ * <li>设计:{@code docs/tdd/orchestration/task-daily-reminder-design-2026-11-14.md}
|
|
|
+ * 用户需求 2026-11-14:现有提前/超期两档外新增日提醒——
|
|
|
+ * 早 07:30 提醒全天关注(今日到期 + 逾期宽限未打卡),
|
|
|
+ * 晚 21:00 全天总结(今日到期已完成 / 未完成分组,催未点完成补打卡)</li>
|
|
|
+ * <li>调度实现(§3.4 拍板):固定 cron 逐用户判断——早上
|
|
|
+ * {@code @Scheduled(cron = "0 30 7 * * ?")}(07:30)、晚上
|
|
|
+ * {@code @Scheduled(cron = "0 0 21 * * ?")}(21:00);按用户配置时间判定当日是否已触发,
|
|
|
+ * 靠 {@code user_notifications} 当日唯一判重保证每日一次</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>宿主契约(TDD-RED 锚点)</b>:{@code com.etotem.cfc.task.TaskDailyReminderScheduledTask} ——
|
|
|
+ * 设计契约 §3.4/§六 规划的新增调度器(保持职责单一),2026-11-14 <b>尚未实现</b>。
|
|
|
+ * 本测试通过反射锚定该类名(避免直接 import 不存在类导致编译失败),运行时
|
|
|
+ * {@code Class.forName} 抛 {@link ClassNotFoundException} 即断言失败(红),
|
|
|
+ * 暴露「实现未落地」真实缺陷;GREEN 阶段实现宿主后转为验证真实行为。</p>
|
|
|
+ *
|
|
|
+ * <p><b>被校验的契约行为(设计 §3.4 + §四 验收 2/3/4)</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li><b>早提醒</b>(07:30):对 daily_enabled=1 的用户配置,扫描「今日到期 +
|
|
|
+ * 逾期宽限(OVERDUE_GRACE_DAYS=3)内未打卡」的 parent 执行型任务,发送
|
|
|
+ * {@code task_daily_morning} 通知(当日唯一);未启用(daily_enabled=0)→ 不发;
|
|
|
+ * 今日无任务 → 不发(避免空打扰);渠道 in_app 恒兜底</li>
|
|
|
+ * <li><b>晚总结</b>(21:00):今日到期任务按「已完成(completedAt 今日)/ 未完成」分组,
|
|
|
+ * 未完成含催补打卡文案,发送 {@code task_daily_evening} 通知(当日唯一);
|
|
|
+ * 历史逾期不进晚总结(由超期提醒兜底)</li>
|
|
|
+ * <li><b>幂等</b>:同一用户同一日早晚各一次——user_notifications 当日同
|
|
|
+ * (type, ref_type=task, createdAt) 判重</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>契约方法签名(实现方按此落地,依赖字段复用 §3.4「现有查询骨架」)</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li>{@code void sendMorningReminder()} —— 早提醒(@Scheduled cron "0 30 7 * * ?")</li>
|
|
|
+ * <li>{@code void sendEveningSummary()} —— 晚总结(@Scheduled cron "0 0 21 * * ?")</li>
|
|
|
+ * <li>依赖:{@code taskMapper} / {@code familyMapper} / {@code userNotificationMapper} /
|
|
|
+ * {@code notificationDispatchService}(与现有 TaskReminderScheduledTask 口径一致)</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>测试范围</b>:纯契约级单测,无 Spring 上下文 / 无 DB / 无外部服务(反射 + Mockito)。
|
|
|
+ * 若实现方采用其他包路径/宿主,GREEN 阶段由编排侧对齐本测试锚点。</p>
|
|
|
+ *
|
|
|
+ * <p><b>RED 预期</b>:宿主类不存在,全部用例在反射探测处失败(红),
|
|
|
+ * 断言原文为「契约宿主 TaskDailyReminderScheduledTask 未实现」。</p>
|
|
|
+ */
|
|
|
+class TaskReminderDailyScheduledTaskContractTest {
|
|
|
+
|
|
|
+ /** 设计蓝图宿主类(新增,§3.4/§六;当前未实现)。 */
|
|
|
+ private static final String HOST_CLASS = "com.etotem.cfc.task.TaskDailyReminderScheduledTask";
|
|
|
+
|
|
|
+ /** 设计 §3.4:早晚固定 cron。 */
|
|
|
+ private static final String MORNING_CRON = "0 30 7 * * ?";
|
|
|
+ private static final String EVENING_CRON = "0 0 21 * * ?";
|
|
|
+
|
|
|
+ // ==================== 宿主结构契约 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("宿主-1:TaskDailyReminderScheduledTask 类应存在(§3.4/§六)")
|
|
|
+ void hostClass_exists() {
|
|
|
+ assertNotNull(loadClass(HOST_CLASS),
|
|
|
+ "RED[task.reminder.daily] 契约宿主 TaskDailyReminderScheduledTask 未实现"
|
|
|
+ + "(设计契约 §3.4/§六 规划的新增调度器,实现未落地)");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("宿主-2:应提供 sendMorningReminder/sendEveningSummary 两个调度入口(§3.4)")
|
|
|
+ void host_hasMorningAndEveningEntries() {
|
|
|
+ Class<?> clazz = loadClass(HOST_CLASS);
|
|
|
+ if (clazz == null) {
|
|
|
+ fail("RED[task.reminder.daily] 契约宿主 TaskDailyReminderScheduledTask 未实现,"
|
|
|
+ + "无法验证早晚调度入口(§3.4)。");
|
|
|
+ }
|
|
|
+ boolean hasMorning = false;
|
|
|
+ boolean hasEvening = false;
|
|
|
+ for (Method m : clazz.getDeclaredMethods()) {
|
|
|
+ if (m.getName().equals("sendMorningReminder")) {
|
|
|
+ hasMorning = true;
|
|
|
+ }
|
|
|
+ if (m.getName().equals("sendEveningSummary")) {
|
|
|
+ hasEvening = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!hasMorning || !hasEvening) {
|
|
|
+ fail("RED[task.reminder.daily] 宿主应提供 sendMorningReminder() 与 sendEveningSummary()"
|
|
|
+ + "两个 @Scheduled 入口(§3.4,缺失:"
|
|
|
+ + "morning=" + hasMorning + ", evening=" + hasEvening + ")。");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("宿主-3:早提醒 cron 应为 0 30 7 * * ?(07:30 触发,§3.4)")
|
|
|
+ void host_morningCron_0730() throws Exception {
|
|
|
+ Class<?> clazz = loadClass(HOST_CLASS);
|
|
|
+ if (clazz == null) {
|
|
|
+ fail("RED[task.reminder.daily] 契约宿主 TaskDailyReminderScheduledTask 未实现,"
|
|
|
+ + "无法验证早提醒 cron(§3.4)。");
|
|
|
+ }
|
|
|
+ String cron = scheduledCron(clazz, "sendMorningReminder");
|
|
|
+ assertEquals(MORNING_CRON, cron,
|
|
|
+ "早提醒固定 cron 应为 0 30 7 * * ?(07:30,§3.4 拍板)");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("宿主-4:晚总结 cron 应为 0 0 21 * * ?(21:00 触发,§3.4)")
|
|
|
+ void host_eveningCron_2100() throws Exception {
|
|
|
+ Class<?> clazz = loadClass(HOST_CLASS);
|
|
|
+ if (clazz == null) {
|
|
|
+ fail("RED[task.reminder.daily] 契约宿主 TaskDailyReminderScheduledTask 未实现,"
|
|
|
+ + "无法验证晚总结 cron(§3.4)。");
|
|
|
+ }
|
|
|
+ String cron = scheduledCron(clazz, "sendEveningSummary");
|
|
|
+ assertEquals(EVENING_CRON, cron,
|
|
|
+ "晚总结固定 cron 应为 0 0 21 * * ?(21:00,§3.4 拍板)");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 早提醒行为(§四 验收 2) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("早提醒-1:启用日提醒用户 + 今日到期任务 → 发送 task_daily_morning(当日唯一)")
|
|
|
+ void morningReminder_enabledUser_todayDue_sends() throws Exception {
|
|
|
+ Object host = contractHost();
|
|
|
+ TaskMapper taskMapper = mock(TaskMapper.class);
|
|
|
+ UserNotificationMapper unMapper = mock(UserNotificationMapper.class);
|
|
|
+ NotificationDispatchService dispatch = mock(NotificationDispatchService.class);
|
|
|
+ inject(host, "taskMapper", taskMapper);
|
|
|
+ inject(host, "userNotificationMapper", unMapper);
|
|
|
+ inject(host, "notificationDispatchService", dispatch);
|
|
|
+
|
|
|
+ Task due = task(1L, "晨起空腹喝水", todayDeadline(10, 0), "pending");
|
|
|
+ when(taskMapper.selectList(any())).thenReturn(new ArrayList<>(Collections.singletonList(due)));
|
|
|
+ when(unMapper.selectCount(any())).thenReturn(0L);
|
|
|
+
|
|
|
+ call(host, "sendMorningReminder");
|
|
|
+
|
|
|
+ ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
|
|
|
+ verify(dispatch, atLeastOnce()).sendByTemplate(typeCaptor.capture(), anyLong(), any(),
|
|
|
+ any(), anyLong(), any(), any());
|
|
|
+ assertNotNull(typeCaptor.getValue(),
|
|
|
+ "应通过 sendByTemplate 发送早提醒");
|
|
|
+ assertEquals("task_daily_morning", typeCaptor.getValue(),
|
|
|
+ "早提醒通知 type 应为 task_daily_morning(§3.4)");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("早提醒-2:daily_enabled=0(未启用)→ 不发早提醒(默认关闭,§3.4/设计决策 #1)")
|
|
|
+ void morningReminder_disabledUser_notSend() throws Exception {
|
|
|
+ Object host = contractHost();
|
|
|
+ TaskMapper taskMapper = mock(TaskMapper.class);
|
|
|
+ UserNotificationMapper unMapper = mock(UserNotificationMapper.class);
|
|
|
+ NotificationDispatchService dispatch = mock(NotificationDispatchService.class);
|
|
|
+ inject(host, "taskMapper", taskMapper);
|
|
|
+ inject(host, "userNotificationMapper", unMapper);
|
|
|
+ inject(host, "notificationDispatchService", dispatch);
|
|
|
+
|
|
|
+ when(taskMapper.selectList(any())).thenReturn(new ArrayList<>());
|
|
|
+ when(unMapper.selectCount(any())).thenReturn(0L);
|
|
|
+
|
|
|
+ call(host, "sendMorningReminder");
|
|
|
+
|
|
|
+ verify(dispatch, never()).sendByTemplate(any(), anyLong(), any(),
|
|
|
+ any(), anyLong(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("早提醒-3:今日无到期任务 → 不发早提醒(避免空打扰,§3.4)")
|
|
|
+ void morningReminder_noTaskToday_notSend() throws Exception {
|
|
|
+ Object host = contractHost();
|
|
|
+ TaskMapper taskMapper = mock(TaskMapper.class);
|
|
|
+ UserNotificationMapper unMapper = mock(UserNotificationMapper.class);
|
|
|
+ NotificationDispatchService dispatch = mock(NotificationDispatchService.class);
|
|
|
+ inject(host, "taskMapper", taskMapper);
|
|
|
+ inject(host, "userNotificationMapper", unMapper);
|
|
|
+ inject(host, "notificationDispatchService", dispatch);
|
|
|
+
|
|
|
+ when(taskMapper.selectList(any())).thenReturn(new ArrayList<>());
|
|
|
+ when(unMapper.selectCount(any())).thenReturn(0L);
|
|
|
+
|
|
|
+ call(host, "sendMorningReminder");
|
|
|
+
|
|
|
+ verify(dispatch, never()).sendByTemplate(any(), anyLong(), any(),
|
|
|
+ any(), anyLong(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 晚总结行为(§四 验收 3) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("晚总结-1:今日到期任务按已完成/未完成分组,未完成含催补打卡文案")
|
|
|
+ void eveningSummary_todayDue_groupedCompletedAndUncompleted() throws Exception {
|
|
|
+ Object host = contractHost();
|
|
|
+ TaskMapper taskMapper = mock(TaskMapper.class);
|
|
|
+ UserNotificationMapper unMapper = mock(UserNotificationMapper.class);
|
|
|
+ NotificationDispatchService dispatch = mock(NotificationDispatchService.class);
|
|
|
+ inject(host, "taskMapper", taskMapper);
|
|
|
+ inject(host, "userNotificationMapper", unMapper);
|
|
|
+ inject(host, "notificationDispatchService", dispatch);
|
|
|
+
|
|
|
+ // 今日到期:1 已完成(completedAt 今日)+ 1 未完成(pending)
|
|
|
+ Task done = task(2L, "已完成的今日任务", todayDeadline(9, 0), "completed");
|
|
|
+ done.setCompletedAt(new Date());
|
|
|
+ Task undone = task(3L, "未完成的今日任务", todayDeadline(18, 0), "pending");
|
|
|
+ when(taskMapper.selectList(any())).thenReturn(
|
|
|
+ new ArrayList<>(Arrays.asList(done, undone)));
|
|
|
+ when(unMapper.selectCount(any())).thenReturn(0L);
|
|
|
+
|
|
|
+ call(host, "sendEveningSummary");
|
|
|
+
|
|
|
+ ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
|
|
|
+ ArgumentCaptor<String> contentCaptor = ArgumentCaptor.forClass(String.class);
|
|
|
+ verify(dispatch, atLeastOnce()).sendByTemplate(typeCaptor.capture(), anyLong(), any(),
|
|
|
+ any(), anyLong(), any(), contentCaptor.capture());
|
|
|
+ assertEquals("task_daily_evening", typeCaptor.getValue(),
|
|
|
+ "晚总结通知 type 应为 task_daily_evening(§3.4)");
|
|
|
+ String content = contentCaptor.getValue();
|
|
|
+ // 设计 §3.4 晚总结文案:「今日总结:完成 X/N。✅ 已完成:…;【未完成】⚠ 标题…别忘了打卡哦~」
|
|
|
+ assertNotNull(content, "晚总结内容不应为空");
|
|
|
+ if (!content.contains("未完成") || !content.contains("已完成")) {
|
|
|
+ fail("RED[task.reminder.daily] 晚总结应区分「已完成/未完成」两组"
|
|
|
+ + "(设计契约 §四 验收 3,实际内容=" + content + ")");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("晚总结-2:今日无到期任务 → 不发晚总结(§3.4)")
|
|
|
+ void eveningSummary_noTaskToday_notSend() throws Exception {
|
|
|
+ Object host = contractHost();
|
|
|
+ TaskMapper taskMapper = mock(TaskMapper.class);
|
|
|
+ UserNotificationMapper unMapper = mock(UserNotificationMapper.class);
|
|
|
+ NotificationDispatchService dispatch = mock(NotificationDispatchService.class);
|
|
|
+ inject(host, "taskMapper", taskMapper);
|
|
|
+ inject(host, "userNotificationMapper", unMapper);
|
|
|
+ inject(host, "notificationDispatchService", dispatch);
|
|
|
+
|
|
|
+ when(taskMapper.selectList(any())).thenReturn(new ArrayList<>());
|
|
|
+ when(unMapper.selectCount(any())).thenReturn(0L);
|
|
|
+
|
|
|
+ call(host, "sendEveningSummary");
|
|
|
+
|
|
|
+ verify(dispatch, never()).sendByTemplate(any(), anyLong(), any(),
|
|
|
+ any(), anyLong(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 幂等(§四 验收 4) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("幂等-1:同一用户同一日早晚各一次——当日已发(user_notifications 判重)则跳过")
|
|
|
+ void idempotent_sameDayAlreadySent_skip() throws Exception {
|
|
|
+ Object host = contractHost();
|
|
|
+ TaskMapper taskMapper = mock(TaskMapper.class);
|
|
|
+ UserNotificationMapper unMapper = mock(UserNotificationMapper.class);
|
|
|
+ NotificationDispatchService dispatch = mock(NotificationDispatchService.class);
|
|
|
+ inject(host, "taskMapper", taskMapper);
|
|
|
+ inject(host, "userNotificationMapper", unMapper);
|
|
|
+ inject(host, "notificationDispatchService", dispatch);
|
|
|
+
|
|
|
+ Task due = task(4L, "今日任务", todayDeadline(15, 0), "pending");
|
|
|
+ when(taskMapper.selectList(any())).thenReturn(new ArrayList<>(Collections.singletonList(due)));
|
|
|
+ // 当日同 type 已发一条 → 判重跳过
|
|
|
+ when(unMapper.selectCount(any())).thenReturn(1L);
|
|
|
+
|
|
|
+ call(host, "sendMorningReminder");
|
|
|
+
|
|
|
+ verify(dispatch, never()).sendByTemplate(any(), anyLong(), any(),
|
|
|
+ any(), anyLong(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 反射辅助 ====================
|
|
|
+
|
|
|
+ private Class<?> loadClass(String name) {
|
|
|
+ try {
|
|
|
+ return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 反射读取 @Scheduled cron 表达式。 */
|
|
|
+ private String scheduledCron(Class<?> clazz, String methodName) throws Exception {
|
|
|
+ Method m = clazz.getMethod(methodName);
|
|
|
+ org.springframework.scheduling.annotation.Scheduled ann =
|
|
|
+ m.getAnnotation(org.springframework.scheduling.annotation.Scheduled.class);
|
|
|
+ if (ann == null) {
|
|
|
+ fail("RED[task.reminder.daily] 方法 " + methodName + " 应标注 @Scheduled(§3.4)");
|
|
|
+ }
|
|
|
+ return ann.cron();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 反射实例化宿主;ClassNotFoundException → RED 失败。 */
|
|
|
+ private Object contractHost() {
|
|
|
+ try {
|
|
|
+ Class<?> clazz = Class.forName(HOST_CLASS);
|
|
|
+ return clazz.getDeclaredConstructor().newInstance();
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
+ fail("RED[task.reminder.daily] 契约宿主 TaskDailyReminderScheduledTask 未实现"
|
|
|
+ + "(实现未落地):" + e.getMessage());
|
|
|
+ throw new IllegalStateException("unreachable");
|
|
|
+ } catch (Exception e) {
|
|
|
+ fail("契约宿主 TaskDailyReminderScheduledTask 实例化失败:" + e.getMessage());
|
|
|
+ throw new IllegalStateException("unreachable");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 反射注入宿主依赖(字段名按设计 §3.4 复用现有骨架;实现方对齐)。 */
|
|
|
+ private void inject(Object host, String field, Object value) {
|
|
|
+ try {
|
|
|
+ ReflectionTestUtils.setField(host, field, value);
|
|
|
+ } catch (IllegalArgumentException e) {
|
|
|
+ fail("RED[task.reminder.daily] 宿主缺依赖字段 " + field
|
|
|
+ + "(设计 §3.4 依赖骨架:" + e.getMessage() + ")");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 反射调用宿主契约方法。 */
|
|
|
+ private void call(Object host, String method) throws Exception {
|
|
|
+ Method m = host.getClass().getMethod(method);
|
|
|
+ m.invoke(host);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 构造今日任务(deadline 落在今天 HH:mm)。 */
|
|
|
+ private Task task(long id, String title, Date deadline, String status) {
|
|
|
+ Task t = new Task();
|
|
|
+ t.setId(id);
|
|
|
+ t.setTitle(title);
|
|
|
+ t.setDeadline(deadline);
|
|
|
+ t.setStatus(status);
|
|
|
+ t.setIsTemplate(0);
|
|
|
+ t.setExecutorType("parent");
|
|
|
+ return t;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 今日 HH:mm 时刻。 */
|
|
|
+ private Date todayDeadline(int hour, int minute) {
|
|
|
+ java.util.Calendar cal = java.util.Calendar.getInstance();
|
|
|
+ cal.set(java.util.Calendar.HOUR_OF_DAY, hour);
|
|
|
+ cal.set(java.util.Calendar.MINUTE, minute);
|
|
|
+ cal.set(java.util.Calendar.SECOND, 0);
|
|
|
+ cal.set(java.util.Calendar.MILLISECOND, 0);
|
|
|
+ return cal.getTime();
|
|
|
+ }
|
|
|
+}
|