feat: add prioritized workbench actions
This commit is contained in:
parent
133f61e4ad
commit
0ad8a4d596
@ -3,6 +3,7 @@ package com.petstore.controller;
|
||||
import com.petstore.auth.CurrentUser;
|
||||
import com.petstore.auth.CurrentUserContext;
|
||||
import com.petstore.service.AdminBusinessEventService;
|
||||
import com.petstore.service.AdminWorkbenchService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
@ -22,6 +23,24 @@ import java.util.Map;
|
||||
public class AdminBusinessEventController {
|
||||
|
||||
private final AdminBusinessEventService adminBusinessEventService;
|
||||
private final AdminWorkbenchService adminWorkbenchService;
|
||||
|
||||
/** 今日行动聚合:只返回本店准确计数和低敏预览。 */
|
||||
@GetMapping("/actions")
|
||||
public Map<String, Object> actions() {
|
||||
CurrentUser user = CurrentUserContext.require();
|
||||
if (!user.isStoreUser() || user.storeId() == null) {
|
||||
return Map.of(
|
||||
"code", 403,
|
||||
"message", "仅门店员工可查看今日行动",
|
||||
"bizCode", "FORBIDDEN"
|
||||
);
|
||||
}
|
||||
return Map.of(
|
||||
"code", 200,
|
||||
"data", adminWorkbenchService.today(user.storeId())
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/report-funnel")
|
||||
public Map<String, Object> reportFunnel(
|
||||
|
||||
@ -20,6 +20,7 @@ public interface AppointmentMapper extends JpaRepository<Appointment, Long> {
|
||||
List<Appointment> findByUserIdAndStatusAndDeletedFalse(Long userId, String status);
|
||||
List<Appointment> findByStoreIdAndDeletedFalse(Long storeId);
|
||||
List<Appointment> findByStoreIdAndStatusAndDeletedFalse(Long storeId, String status);
|
||||
List<Appointment> findByStoreIdAndStatusAndDeletedFalseOrderByAppointmentTimeAsc(Long storeId, String status);
|
||||
|
||||
/** @deprecated 兼容旧调用;新代码使用 customer_user_id 语义查询。 */
|
||||
@Deprecated
|
||||
@ -31,6 +32,12 @@ public interface AppointmentMapper extends JpaRepository<Appointment, Long> {
|
||||
Page<Appointment> findByStoreIdAndStatusAndDeletedFalse(Long storeId, String status, Pageable pageable);
|
||||
Optional<Appointment> findByIdAndDeletedFalse(Long id);
|
||||
|
||||
long countByStoreIdAndDeletedFalseAndAppointmentTimeGreaterThanEqualAndAppointmentTimeLessThan(
|
||||
Long storeId, LocalDateTime from, LocalDateTime to);
|
||||
|
||||
long countByStoreIdAndStatusAndDeletedFalseAndUpdateTimeGreaterThanEqualAndUpdateTimeLessThan(
|
||||
Long storeId, String status, LocalDateTime from, LocalDateTime to);
|
||||
|
||||
@Query("SELECT a FROM Appointment a WHERE a.deleted = false AND "
|
||||
+ "(a.customerUserId = :customerUserId OR (a.customerUserId IS NULL AND a.userId = :customerUserId)) "
|
||||
+ "ORDER BY a.appointmentTime DESC")
|
||||
|
||||
@ -20,6 +20,9 @@ public interface ReportMapper extends JpaRepository<Report, Long> {
|
||||
|
||||
List<Report> findByStoreIdAndDeletedFalseOrderByCreateTimeDesc(Long storeId);
|
||||
|
||||
List<Report> findByStoreIdAndDeletedFalseAndHighlightVideoStatusInOrderByUpdateTimeAsc(
|
||||
Long storeId, List<String> highlightVideoStatuses);
|
||||
|
||||
/** @deprecated user_id 表示旧报告作者,不再用于客户归属查询。 */
|
||||
@Deprecated
|
||||
List<Report> findByUserIdAndDeletedFalseOrderByCreateTimeDesc(Long userId);
|
||||
|
||||
228
src/main/java/com/petstore/service/AdminWorkbenchService.java
Normal file
228
src/main/java/com/petstore/service/AdminWorkbenchService.java
Normal file
@ -0,0 +1,228 @@
|
||||
package com.petstore.service;
|
||||
|
||||
import com.petstore.entity.Appointment;
|
||||
import com.petstore.entity.Report;
|
||||
import com.petstore.entity.ReportLead;
|
||||
import com.petstore.mapper.AppointmentMapper;
|
||||
import com.petstore.mapper.ReportLeadMapper;
|
||||
import com.petstore.mapper.ReportMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** 单店今日行动读模型:准确计数、低敏预览,不返回手机号、报告 token 或媒体地址。 */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AdminWorkbenchService {
|
||||
static final int PREVIEW_LIMIT = 3;
|
||||
static final int HIGHLIGHT_TIMEOUT_MINUTES = 30;
|
||||
|
||||
public static final String OVERDUE_APPOINTMENT = "overdue_appointment";
|
||||
public static final String IN_SERVICE = "in_service";
|
||||
public static final String UPCOMING_TODAY = "upcoming_today";
|
||||
public static final String HIGHLIGHT_ANOMALY = "highlight_anomaly";
|
||||
public static final String FOLLOW_UP_DUE = "follow_up_due";
|
||||
|
||||
private final AppointmentMapper appointmentMapper;
|
||||
private final ReportMapper reportMapper;
|
||||
private final ReportLeadMapper reportLeadMapper;
|
||||
private final AdminBusinessEventService adminBusinessEventService;
|
||||
|
||||
public WorkbenchData today(Long storeId) {
|
||||
return build(storeId, LocalDateTime.now());
|
||||
}
|
||||
|
||||
WorkbenchData build(Long storeId, LocalDateTime now) {
|
||||
if (storeId == null) {
|
||||
throw new IllegalArgumentException("storeId required");
|
||||
}
|
||||
LocalDate day = now.toLocalDate();
|
||||
LocalDateTime dayStart = day.atStartOfDay();
|
||||
LocalDateTime dayEnd = day.plusDays(1).atStartOfDay();
|
||||
|
||||
List<Appointment> newAppointments = sortedAppointments(
|
||||
appointmentMapper.findByStoreIdAndStatusAndDeletedFalseOrderByAppointmentTimeAsc(storeId, "new")
|
||||
);
|
||||
List<Appointment> inService = sortedAppointments(
|
||||
appointmentMapper.findByStoreIdAndStatusAndDeletedFalseOrderByAppointmentTimeAsc(storeId, "doing")
|
||||
);
|
||||
List<Appointment> overdueAppointments = newAppointments.stream()
|
||||
.filter(appointment -> appointment.getAppointmentTime() != null
|
||||
&& appointment.getAppointmentTime().isBefore(now))
|
||||
.toList();
|
||||
List<Appointment> upcomingToday = newAppointments.stream()
|
||||
.filter(appointment -> appointment.getAppointmentTime() != null
|
||||
&& !appointment.getAppointmentTime().isBefore(now)
|
||||
&& appointment.getAppointmentTime().isBefore(dayEnd))
|
||||
.toList();
|
||||
|
||||
LocalDateTime staleBefore = now.minusMinutes(HIGHLIGHT_TIMEOUT_MINUTES);
|
||||
List<Report> highlightAnomalies = safeReports(
|
||||
reportMapper.findByStoreIdAndDeletedFalseAndHighlightVideoStatusInOrderByUpdateTimeAsc(
|
||||
storeId, List.of("failed", "processing"))
|
||||
).stream()
|
||||
.filter(report -> isHighlightAnomaly(report, staleBefore))
|
||||
.sorted(Comparator
|
||||
.comparing((Report report) -> !"failed".equals(report.getHighlightVideoStatus()))
|
||||
.thenComparing(AdminWorkbenchService::reportActivityTime,
|
||||
Comparator.nullsFirst(Comparator.naturalOrder())))
|
||||
.toList();
|
||||
|
||||
List<ReportLead> dueFollowUps = safeLeads(
|
||||
reportLeadMapper.findByStoreIdAndRemindStatusOrderByRemindDateAsc(storeId, "pending")
|
||||
).stream()
|
||||
.filter(lead -> lead.getRemindDate() == null || !lead.getRemindDate().isAfter(day))
|
||||
.toList();
|
||||
|
||||
long todayAppointments = appointmentMapper
|
||||
.countByStoreIdAndDeletedFalseAndAppointmentTimeGreaterThanEqualAndAppointmentTimeLessThan(
|
||||
storeId, dayStart, dayEnd);
|
||||
long todayDone = appointmentMapper
|
||||
.countByStoreIdAndStatusAndDeletedFalseAndUpdateTimeGreaterThanEqualAndUpdateTimeLessThan(
|
||||
storeId, "done", dayStart, dayEnd);
|
||||
|
||||
WorkbenchMetrics metrics = new WorkbenchMetrics(
|
||||
todayAppointments,
|
||||
todayDone,
|
||||
overdueAppointments.size(),
|
||||
inService.size(),
|
||||
upcomingToday.size(),
|
||||
highlightAnomalies.size(),
|
||||
dueFollowUps.size()
|
||||
);
|
||||
|
||||
List<ActionGroup> actions = new ArrayList<>();
|
||||
addAppointmentGroup(actions, OVERDUE_APPOINTMENT, "critical", overdueAppointments, now);
|
||||
addAppointmentGroup(actions, IN_SERVICE, "high", inService, now);
|
||||
addAppointmentGroup(actions, UPCOMING_TODAY, "normal", upcomingToday, now);
|
||||
addReportGroup(actions, highlightAnomalies);
|
||||
addLeadGroup(actions, dueFollowUps);
|
||||
|
||||
Map<String, Object> reportFunnel = adminBusinessEventService.reportFunnel(storeId, day);
|
||||
return new WorkbenchData(day.toString(), now, metrics, List.copyOf(actions), reportFunnel);
|
||||
}
|
||||
|
||||
private static List<Appointment> sortedAppointments(List<Appointment> appointments) {
|
||||
return (appointments == null ? List.<Appointment>of() : appointments).stream()
|
||||
.sorted(Comparator.comparing(
|
||||
Appointment::getAppointmentTime,
|
||||
Comparator.nullsLast(Comparator.naturalOrder())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static List<Report> safeReports(List<Report> reports) {
|
||||
return reports == null ? List.of() : reports;
|
||||
}
|
||||
|
||||
private static List<ReportLead> safeLeads(List<ReportLead> leads) {
|
||||
return leads == null ? List.of() : leads;
|
||||
}
|
||||
|
||||
private static boolean isHighlightAnomaly(Report report, LocalDateTime staleBefore) {
|
||||
if ("failed".equals(report.getHighlightVideoStatus())) {
|
||||
return true;
|
||||
}
|
||||
if (!"processing".equals(report.getHighlightVideoStatus())) {
|
||||
return false;
|
||||
}
|
||||
LocalDateTime activity = reportActivityTime(report);
|
||||
return activity == null || activity.isBefore(staleBefore);
|
||||
}
|
||||
|
||||
private static LocalDateTime reportActivityTime(Report report) {
|
||||
return report.getUpdateTime() != null ? report.getUpdateTime() : report.getCreateTime();
|
||||
}
|
||||
|
||||
private static void addAppointmentGroup(
|
||||
List<ActionGroup> groups,
|
||||
String kind,
|
||||
String urgency,
|
||||
List<Appointment> appointments,
|
||||
LocalDateTime now) {
|
||||
if (appointments.isEmpty()) return;
|
||||
List<ActionItem> items = appointments.stream()
|
||||
.limit(PREVIEW_LIMIT)
|
||||
.map(appointment -> new ActionItem(
|
||||
"appointment",
|
||||
appointment.getId(),
|
||||
appointment.getPetName(),
|
||||
appointment.getServiceType(),
|
||||
appointment.getAppointmentTime() == null ? null : appointment.getAppointmentTime().toString(),
|
||||
appointment.getStatus(),
|
||||
appointment.getAppointmentTime() != null && appointment.getAppointmentTime().isBefore(now)
|
||||
))
|
||||
.toList();
|
||||
groups.add(new ActionGroup(kind, urgency, appointments.size(), items));
|
||||
}
|
||||
|
||||
private static void addReportGroup(List<ActionGroup> groups, List<Report> reports) {
|
||||
if (reports.isEmpty()) return;
|
||||
List<ActionItem> items = reports.stream()
|
||||
.limit(PREVIEW_LIMIT)
|
||||
.map(report -> new ActionItem(
|
||||
"report",
|
||||
report.getId(),
|
||||
report.getPetName(),
|
||||
report.getServiceType(),
|
||||
reportActivityTime(report) == null ? null : reportActivityTime(report).toString(),
|
||||
report.getHighlightVideoStatus(),
|
||||
true
|
||||
))
|
||||
.toList();
|
||||
groups.add(new ActionGroup(HIGHLIGHT_ANOMALY, "high", reports.size(), items));
|
||||
}
|
||||
|
||||
private static void addLeadGroup(List<ActionGroup> groups, List<ReportLead> leads) {
|
||||
if (leads.isEmpty()) return;
|
||||
List<ActionItem> items = leads.stream()
|
||||
.limit(PREVIEW_LIMIT)
|
||||
.map(lead -> new ActionItem(
|
||||
"lead",
|
||||
lead.getId(),
|
||||
lead.getPetName(),
|
||||
lead.getServiceType(),
|
||||
lead.getRemindDate() == null ? null : lead.getRemindDate().toString(),
|
||||
lead.getRemindStatus(),
|
||||
true
|
||||
))
|
||||
.toList();
|
||||
groups.add(new ActionGroup(FOLLOW_UP_DUE, "normal", leads.size(), items));
|
||||
}
|
||||
|
||||
public record WorkbenchData(
|
||||
String date,
|
||||
LocalDateTime generatedAt,
|
||||
WorkbenchMetrics metrics,
|
||||
List<ActionGroup> actions,
|
||||
Map<String, Object> reportFunnel) {
|
||||
}
|
||||
|
||||
public record WorkbenchMetrics(
|
||||
long todayAppointmentCount,
|
||||
long todayDoneCount,
|
||||
long overdueAppointmentCount,
|
||||
long inServiceCount,
|
||||
long upcomingTodayCount,
|
||||
long highlightAnomalyCount,
|
||||
long dueFollowUpCount) {
|
||||
}
|
||||
|
||||
public record ActionGroup(String kind, String urgency, long count, List<ActionItem> items) {
|
||||
}
|
||||
|
||||
public record ActionItem(
|
||||
String resourceType,
|
||||
Long resourceId,
|
||||
String petName,
|
||||
String serviceType,
|
||||
String scheduledAt,
|
||||
String status,
|
||||
boolean overdue) {
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package com.petstore.controller;
|
||||
import com.petstore.auth.CurrentUser;
|
||||
import com.petstore.auth.CurrentUserContext;
|
||||
import com.petstore.service.AdminBusinessEventService;
|
||||
import com.petstore.service.AdminWorkbenchService;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@ -11,6 +12,8 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@ -22,6 +25,7 @@ import static org.mockito.Mockito.when;
|
||||
class AdminBusinessEventControllerTest {
|
||||
|
||||
@Mock private AdminBusinessEventService adminBusinessEventService;
|
||||
@Mock private AdminWorkbenchService adminWorkbenchService;
|
||||
@InjectMocks private AdminBusinessEventController controller;
|
||||
|
||||
@AfterEach
|
||||
@ -53,4 +57,33 @@ class AdminBusinessEventControllerTest {
|
||||
assertEquals(200, result.get("code"));
|
||||
verify(adminBusinessEventService).reportFunnel(10L, date);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customerCannotReadStoreActions() {
|
||||
CurrentUserContext.set(new CurrentUser(99L, null, "customer"));
|
||||
|
||||
Map<String, Object> result = controller.actions();
|
||||
|
||||
assertEquals(403, result.get("code"));
|
||||
verifyNoInteractions(adminWorkbenchService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void staffActionsUseDerivedStoreAndLowSensitivityReadModel() {
|
||||
CurrentUserContext.set(new CurrentUser(7L, 10L, "staff"));
|
||||
AdminWorkbenchService.WorkbenchData data = new AdminWorkbenchService.WorkbenchData(
|
||||
"2026-08-01",
|
||||
LocalDateTime.of(2026, 8, 1, 10, 0),
|
||||
new AdminWorkbenchService.WorkbenchMetrics(3, 1, 1, 1, 1, 0, 0),
|
||||
List.of(),
|
||||
Map.of()
|
||||
);
|
||||
when(adminWorkbenchService.today(10L)).thenReturn(data);
|
||||
|
||||
Map<String, Object> result = controller.actions();
|
||||
|
||||
assertEquals(200, result.get("code"));
|
||||
assertEquals(data, result.get("data"));
|
||||
verify(adminWorkbenchService).today(10L);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,169 @@
|
||||
package com.petstore.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.petstore.entity.Appointment;
|
||||
import com.petstore.entity.Report;
|
||||
import com.petstore.entity.ReportLead;
|
||||
import com.petstore.mapper.AppointmentMapper;
|
||||
import com.petstore.mapper.ReportLeadMapper;
|
||||
import com.petstore.mapper.ReportMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AdminWorkbenchServiceTest {
|
||||
|
||||
@Mock private AppointmentMapper appointmentMapper;
|
||||
@Mock private ReportMapper reportMapper;
|
||||
@Mock private ReportLeadMapper reportLeadMapper;
|
||||
@Mock private AdminBusinessEventService adminBusinessEventService;
|
||||
@InjectMocks private AdminWorkbenchService service;
|
||||
|
||||
@Test
|
||||
void buildsPrioritizedLowSensitivityActionGroups() throws Exception {
|
||||
LocalDateTime now = LocalDateTime.of(2026, 8, 1, 10, 0);
|
||||
List<Appointment> newAppointments = List.of(
|
||||
appointment(1L, "迟到的豆豆", "洗护", "new", now.minusMinutes(30)),
|
||||
appointment(2L, "即将到店的可乐", "美容", "new", now.plusHours(1)),
|
||||
appointment(3L, "明天的奶糖", "洗护", "new", now.plusDays(1))
|
||||
);
|
||||
List<Appointment> doingAppointments = List.of(
|
||||
appointment(4L, "服务中的团子", "洗护", "doing", now.minusHours(1))
|
||||
);
|
||||
when(appointmentMapper.findByStoreIdAndStatusAndDeletedFalseOrderByAppointmentTimeAsc(10L, "new"))
|
||||
.thenReturn(newAppointments);
|
||||
when(appointmentMapper.findByStoreIdAndStatusAndDeletedFalseOrderByAppointmentTimeAsc(10L, "doing"))
|
||||
.thenReturn(doingAppointments);
|
||||
when(appointmentMapper
|
||||
.countByStoreIdAndDeletedFalseAndAppointmentTimeGreaterThanEqualAndAppointmentTimeLessThan(
|
||||
eq(10L), any(LocalDateTime.class), any(LocalDateTime.class)))
|
||||
.thenReturn(5L);
|
||||
when(appointmentMapper
|
||||
.countByStoreIdAndStatusAndDeletedFalseAndUpdateTimeGreaterThanEqualAndUpdateTimeLessThan(
|
||||
eq(10L), eq("done"), any(LocalDateTime.class), any(LocalDateTime.class)))
|
||||
.thenReturn(2L);
|
||||
|
||||
Report failed = report(20L, "成片失败", "洗护", "failed", now.minusMinutes(5));
|
||||
Report stale = report(21L, "成片超时", "美容", "processing", now.minusMinutes(31));
|
||||
Report fresh = report(22L, "正常处理中", "洗护", "processing", now.minusMinutes(10));
|
||||
when(reportMapper.findByStoreIdAndDeletedFalseAndHighlightVideoStatusInOrderByUpdateTimeAsc(
|
||||
10L, List.of("failed", "processing")))
|
||||
.thenReturn(List.of(stale, fresh, failed));
|
||||
|
||||
ReportLead noDate = lead(30L, "无日期线索", null, "pending");
|
||||
ReportLead due = lead(31L, "今日回访", LocalDate.of(2026, 8, 1), "pending");
|
||||
ReportLead future = lead(32L, "未来回访", LocalDate.of(2026, 8, 2), "pending");
|
||||
when(reportLeadMapper.findByStoreIdAndRemindStatusOrderByRemindDateAsc(10L, "pending"))
|
||||
.thenReturn(List.of(noDate, due, future));
|
||||
when(adminBusinessEventService.reportFunnel(10L, LocalDate.of(2026, 8, 1)))
|
||||
.thenReturn(Map.of("reportSubmittedCount", 2L));
|
||||
|
||||
AdminWorkbenchService.WorkbenchData data = service.build(10L, now);
|
||||
|
||||
assertEquals(5L, data.metrics().todayAppointmentCount());
|
||||
assertEquals(2L, data.metrics().todayDoneCount());
|
||||
assertEquals(1L, data.metrics().overdueAppointmentCount());
|
||||
assertEquals(1L, data.metrics().inServiceCount());
|
||||
assertEquals(1L, data.metrics().upcomingTodayCount());
|
||||
assertEquals(2L, data.metrics().highlightAnomalyCount());
|
||||
assertEquals(2L, data.metrics().dueFollowUpCount());
|
||||
assertEquals(List.of(
|
||||
AdminWorkbenchService.OVERDUE_APPOINTMENT,
|
||||
AdminWorkbenchService.IN_SERVICE,
|
||||
AdminWorkbenchService.UPCOMING_TODAY,
|
||||
AdminWorkbenchService.HIGHLIGHT_ANOMALY,
|
||||
AdminWorkbenchService.FOLLOW_UP_DUE
|
||||
),
|
||||
data.actions().stream().map(AdminWorkbenchService.ActionGroup::kind).toList());
|
||||
assertEquals("迟到的豆豆", data.actions().get(0).items().get(0).petName());
|
||||
assertEquals("report", data.actions().get(3).items().get(0).resourceType());
|
||||
assertEquals(2L, data.reportFunnel().get("reportSubmittedCount"));
|
||||
|
||||
String json = new ObjectMapper().findAndRegisterModules().writeValueAsString(data);
|
||||
assertFalse(json.contains("13800000000"));
|
||||
assertFalse(json.contains("report-secret-token"));
|
||||
assertFalse(json.contains("/private/video.mp4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void capsPreviewWithoutLosingAccurateCount() {
|
||||
LocalDateTime now = LocalDateTime.of(2026, 8, 1, 10, 0);
|
||||
List<Appointment> overdue = new ArrayList<>();
|
||||
for (long id = 1; id <= 5; id++) {
|
||||
overdue.add(appointment(id, "宠物" + id, "洗护", "new", now.minusMinutes(id)));
|
||||
}
|
||||
when(appointmentMapper.findByStoreIdAndStatusAndDeletedFalseOrderByAppointmentTimeAsc(10L, "new"))
|
||||
.thenReturn(overdue);
|
||||
when(appointmentMapper.findByStoreIdAndStatusAndDeletedFalseOrderByAppointmentTimeAsc(10L, "doing"))
|
||||
.thenReturn(List.of());
|
||||
when(reportMapper.findByStoreIdAndDeletedFalseAndHighlightVideoStatusInOrderByUpdateTimeAsc(
|
||||
10L, List.of("failed", "processing")))
|
||||
.thenReturn(List.of());
|
||||
when(reportLeadMapper.findByStoreIdAndRemindStatusOrderByRemindDateAsc(10L, "pending"))
|
||||
.thenReturn(List.of());
|
||||
when(adminBusinessEventService.reportFunnel(10L, LocalDate.of(2026, 8, 1))).thenReturn(Map.of());
|
||||
|
||||
AdminWorkbenchService.WorkbenchData data = service.build(10L, now);
|
||||
|
||||
assertEquals(5L, data.actions().get(0).count());
|
||||
assertEquals(AdminWorkbenchService.PREVIEW_LIMIT, data.actions().get(0).items().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingStoreScope() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> service.build(null, LocalDateTime.of(2026, 8, 1, 10, 0)));
|
||||
}
|
||||
|
||||
private static Appointment appointment(
|
||||
Long id, String petName, String serviceType, String status, LocalDateTime appointmentTime) {
|
||||
Appointment appointment = new Appointment();
|
||||
appointment.setId(id);
|
||||
appointment.setPetName(petName);
|
||||
appointment.setServiceType(serviceType);
|
||||
appointment.setStatus(status);
|
||||
appointment.setAppointmentTime(appointmentTime);
|
||||
appointment.setUpdateTime(appointmentTime);
|
||||
return appointment;
|
||||
}
|
||||
|
||||
private static Report report(
|
||||
Long id, String petName, String serviceType, String status, LocalDateTime updateTime) {
|
||||
Report report = new Report();
|
||||
report.setId(id);
|
||||
report.setPetName(petName);
|
||||
report.setServiceType(serviceType);
|
||||
report.setHighlightVideoStatus(status);
|
||||
report.setReportToken("report-secret-token");
|
||||
report.setHighlightVideoUrl("/private/video.mp4");
|
||||
report.setUpdateTime(updateTime);
|
||||
return report;
|
||||
}
|
||||
|
||||
private static ReportLead lead(Long id, String petName, LocalDate remindDate, String status) {
|
||||
ReportLead lead = new ReportLead();
|
||||
lead.setId(id);
|
||||
lead.setPetName(petName);
|
||||
lead.setServiceType("洗护");
|
||||
lead.setPhone("13800000000");
|
||||
lead.setRemindDate(remindDate);
|
||||
lead.setRemindStatus(status);
|
||||
return lead;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user