diff --git a/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/DashboardAdminController.java b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/DashboardAdminController.java index 854e82b..ca0f7fc 100644 --- a/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/DashboardAdminController.java +++ b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/DashboardAdminController.java @@ -3,6 +3,8 @@ package com.fruitshorts.admin.controller; import com.fruitshorts.admin.service.DashboardService; +import com.fruitshorts.admin.vo.DashboardDistributionRespVO; +import com.fruitshorts.admin.vo.DashboardTrendRespVO; import com.fruitshorts.admin.vo.TranscodeDashboardRespVO; import com.fruitshorts.common.pojo.Result; import io.swagger.v3.oas.annotations.Operation; @@ -44,4 +46,20 @@ public class DashboardAdminController { public Result transcode() { return Result.ok(dashboardService.transcode()); } + + /** 近 7 天每日趋势(用户注册 / 播放事件 / 转码任务)。 */ + @Operation(summary = "大盘趋势(近 7 天每日数据,给折线图用)") + @GetMapping("/trend") + @PreAuthorize("@ss.hasPermission('dashboard:view')") + public Result trend() { + return Result.ok(dashboardService.trend()); + } + + /** 分布:短剧状态 / 转码状态 / 片源覆盖 / 待处理审核。 */ + @Operation(summary = "大盘分布(给饼图用)") + @GetMapping("/distribution") + @PreAuthorize("@ss.hasPermission('dashboard:view')") + public Result distribution() { + return Result.ok(dashboardService.distribution()); + } } diff --git a/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/service/DashboardService.java b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/service/DashboardService.java index fcec80c..277d645 100644 --- a/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/service/DashboardService.java +++ b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/service/DashboardService.java @@ -3,6 +3,8 @@ package com.fruitshorts.admin.service; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.fruitshorts.admin.vo.DashboardDistributionRespVO; +import com.fruitshorts.admin.vo.DashboardTrendRespVO; import com.fruitshorts.admin.vo.TranscodeDashboardRespVO; import com.fruitshorts.commerce.dal.entity.CoinLedger; import com.fruitshorts.commerce.dal.entity.Entitlement; @@ -44,6 +46,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.springframework.jdbc.core.JdbcTemplate; + /** * 运营大盘(只读聚合)。 * @@ -86,6 +90,7 @@ public class DashboardService { private final CoinLedgerMapper coinLedgerMapper; private final EntitlementMapper entitlementMapper; private final TranscodeService transcodeService; + private final JdbcTemplate jdbcTemplate; public Map overview() { LocalDateTime now = LocalDateTime.now(); @@ -117,6 +122,58 @@ public class DashboardService { return data; } + /** 近 7 天每日趋势,给折线图用。 */ + public DashboardTrendRespVO trend() { + int days = 7; + LocalDate today = LocalDate.now(); + List dates = new ArrayList<>(days); + List newUsers = new ArrayList<>(days); + List playEvents = new ArrayList<>(days); + List newTranscodes = new ArrayList<>(days); + List transcodeSuccess = new ArrayList<>(days); + for (int i = days - 1; i >= 0; i--) { + LocalDate d = today.minusDays(i); + dates.add(d.toString()); + newUsers.add(countForDate("SELECT COUNT(*) FROM app_user WHERE deleted = 0 AND DATE(created_at) = ?", d)); + playEvents.add(countForDate("SELECT COUNT(*) FROM watch_history WHERE DATE(watched_at) = ?", d)); + newTranscodes.add(countForDate("SELECT COUNT(*) FROM transcode_task WHERE DATE(created_at) = ?", d)); + transcodeSuccess.add(countForDate("SELECT COUNT(*) FROM transcode_task WHERE status = 'SUCCESS' AND DATE(created_at) = ?", d)); + } + return new DashboardTrendRespVO(dates, newUsers, playEvents, newTranscodes, transcodeSuccess); + } + + /** 各域状态分布,给饼图用。 */ + public DashboardDistributionRespVO distribution() { + DashboardDistributionRespVO vo = new DashboardDistributionRespVO(); + vo.setDramaStatus(groupBy("SELECT status, COUNT(*) FROM drama WHERE deleted = 0 GROUP BY status")); + vo.setTranscodeStatus(groupBy("SELECT status, COUNT(*) FROM transcode_task GROUP BY status")); + vo.setEpisodeMedia(groupBy(""" + SELECT CASE WHEN video_url IS NOT NULL AND video_url <> '' THEN '有片源' ELSE '无片源' END AS bucket, + COUNT(*) FROM drama_episode WHERE deleted = 0 GROUP BY bucket""")); + List pending = new ArrayList<>(); + pending.add(new DashboardDistributionRespVO.Item("评论待审", orZero(commentMapper.selectCount( + Wrappers.lambdaQuery().eq(ContentComment::getStatus, "pending"))))); + pending.add(new DashboardDistributionRespVO.Item("举报待处理", orZero(reportMapper.selectCount( + Wrappers.lambdaQuery().eq(ContentReport::getStatus, "pending"))))); + vo.setPendingModeration(pending); + return vo; + } + + /** 按日期查计数,空结果返回 0。 */ + private long countForDate(String sql, LocalDate date) { + Long count = jdbcTemplate.queryForObject(sql, Long.class, date); + return count == null ? 0L : count; + } + + /** 两列 GROUP BY 查询返回 Map。 */ + private Map groupBy(String sql) { + Map result = new LinkedHashMap<>(); + jdbcTemplate.query(sql, rs -> { + result.put(rs.getString(1), rs.getLong(2)); + }); + return result; + } + private Map userMetrics(LocalDateTime todayStart, LocalDateTime sevenDaysAgo) { Map user = new LinkedHashMap<>(); user.put("total", countUser(Wrappers.lambdaQuery())); diff --git a/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/vo/DashboardDistributionRespVO.java b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/vo/DashboardDistributionRespVO.java new file mode 100644 index 0000000..93f843e --- /dev/null +++ b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/vo/DashboardDistributionRespVO.java @@ -0,0 +1,39 @@ +// Author: fruit.com.cn + +package com.fruitshorts.admin.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** 运营大盘分布(给饼图用)。 */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DashboardDistributionRespVO { + + /** 短剧状态分布:key = 状态码,value = 数量;含 total 汇总。 */ + private Map dramaStatus; + + /** 转码任务状态分布。 */ + private Map transcodeStatus; + + /** 有片源 / 无片源剧集分布。 */ + private Map episodeMedia; + + /** 互动待处理:评论待审、举报待审。 */ + private List pendingModeration; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Item { + + private String label; + + private Long value; + } +} diff --git a/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/vo/DashboardTrendRespVO.java b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/vo/DashboardTrendRespVO.java new file mode 100644 index 0000000..20cac86 --- /dev/null +++ b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/vo/DashboardTrendRespVO.java @@ -0,0 +1,31 @@ +// Author: fruit.com.cn + +package com.fruitshorts.admin.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** 运营大盘趋势(近 N 天每日数据,给折线图用)。 */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DashboardTrendRespVO { + + /** 日期标签,格式 yyyy-MM-dd,从最早到最新。 */ + private List dates; + + /** 每日新增用户数(与 dates 对齐)。 */ + private List newUsers; + + /** 每日播放事件数(watch_history 行数,与 dates 对齐)。 */ + private List playEvents; + + /** 每日新增转码任务数(transcode_task 行数,与 dates 对齐)。 */ + private List newTranscodes; + + /** 每日转码成功数(与 dates 对齐)。 */ + private List transcodeSuccess; +}