feat: 08 数据看板落地(总览 + 转码看板,只读)
为什么: 1) 前面几块把用户、内容、播放、交易、媒资的数据都落库了,运营需要一个地方一次看完, 否则每个指标都要去查库或翻后台列表。 2) 看板最容易出的问题是"数字看起来对但口径不清",所以每个指标都在代码注释和 docs/api-phase1.md 里写死口径(时间范围、统计维度),前端 tooltip 直接复用。 3) 取不到数据源的指标(DAU、CDN 流量成本、商店订单金额)一律返回 null 并在 notes 里 说明缺什么、怎么补 —— 不用 0 冒充,避免运营拿假数字做判断。 4) 站在接入层做只读聚合而不是新建模块:它跨六个域取数,建模块只会把依赖再摊开一层, 而它只读不写,放在 admin-api 改动面最小。
This commit is contained in:
1 parent
1fb8427f28
commit
d67d9daf8c
5 files changed
+359
-3
No files matched your search
+29
-2
@@ -289,7 +289,34 @@ POST /app/auth/register
|
||||
**定时发布**:`DramaPublishJob` 每分钟扫一次,把 `publish_at` 到点的 `approved` 短剧推上线,
|
||||
走的是短剧状态机(不直接改状态字段)。上下架也可通过内容提交的 `OFFLINE` 审核触发。
|
||||
|
||||
### 2.8 数据看板(下一批)
|
||||
### 2.8 数据看板(已实现,只读)
|
||||
|
||||
| 方法 | 路径 | 权限点 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/admin/dashboard/overview` | `dashboard:view` | 总览:用户 / 内容 / 播放 / 收入 / 成本 |
|
||||
| GET | `/admin/dashboard/transcode` | `dashboard:view` | 转码队列积压与失败任务明细(含 `errorMsg` / `retryCount`) |
|
||||
|
||||
**口径(与代码注释一致,前端 tooltip 可直接复用)**
|
||||
|
||||
| 分组 | 指标 | 口径 |
|
||||
| --- | --- | --- |
|
||||
| 用户 | `total` / `newToday` / `new7d` | `app_user` 未删除总数;按 `created_at` 落在当天 / 近 7 天 |
|
||||
| 用户 | `active7d` | `last_login_at` 在近 7 天内。**不是 DAU**,日活需要逐日登录快照,当前无产出 |
|
||||
| 用户 | `banned` / `limited` / `deactivated` | 按 `status` 计数 |
|
||||
| 内容 | `drama*` | 短剧按 `status` 计数(`reviewing` 为待审、`published` 为已上线) |
|
||||
| 内容 | `episodeTotal` / `Published` / `WithMedia` | 剧集总数 / 已上线数 / `video_url` 非空数(均排除逻辑删除) |
|
||||
| 内容 | `commentPending` / `reportPending` | 待审评论、待处理举报 |
|
||||
| 播放 | `viewTotal` / `playTotal` | 短剧表累计计数列求和(累计口径,不限时间) |
|
||||
| 播放 | `watchedDramas` / `finishedDramas` / `finishRate` | `watch_progress` 行数 / 其中 `finished=1` / 完播率 |
|
||||
| 播放 | `playEvents7d` / `playEventsTotal` | `watch_history` 流水口径(与累计口径不可相加) |
|
||||
| 收入 | `order*` / `paidAmount` | 订单按状态计数;金额为已支付+已交付合计(商店未接入前恒为 0) |
|
||||
| 收入 | `coinRecharged` / `coinConsumed` / `coinBalance` | 取自 `coin_ledger`(RECHARGE / UNLOCK)与 `wallet` 余额合计 |
|
||||
| 收入 | `entitlementActive` | 有效权益数 |
|
||||
| 成本 | `transcode*` / `renditions` | `transcode_task` 按状态计数、`media_rendition` 产物数 |
|
||||
| 成本 | `otpSent7d` | 近 7 天验证码发送条数(`app_user_otp`),作为短信成本近似 |
|
||||
| 成本 | `cdnTrafficBytes` | **暂无数据源**,返回 `null` 并在 `notes` 说明 |
|
||||
|
||||
取不到数据源的指标一律返回 `null` 并在 `notes` 里写明缺什么,**不用 0 冒充**。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
@@ -322,6 +349,6 @@ POST /app/auth/register
|
||||
| 05 记录、收藏与下载 | 已建 | 已冻结 | **已实现**:进度、观看流水、收藏、离线下载授权与任务 |
|
||||
| 06 评论、点赞与分享 | 已建 | 已冻结 | **已实现**:评论(游标翻页/审核/删除)、点赞、举报、敏感词;分享埋点在下一批 |
|
||||
| 07 创作者中心 | 已建 | 已冻结 | 下一批(转码任务与产物记账已实现) |
|
||||
| 08 平台运营后台 | 已建 | 已冻结 | 用户管理、内容治理、订单与权益、**创作者与内容审核已实现**;数据看板下一批 |
|
||||
| 08 平台运营后台 | 已建 | 已冻结 | 用户管理、内容治理、订单与权益、创作者与内容审核、**数据看板均已实现**;仅剩后台页面(3-4) |
|
||||
| 09 分集收费与支付 | 已建 | 已冻结 | 下一批(权益判定端口已预留,接上即生效) |
|
||||
| 10 质量、数据与上线 | 已建 | 已冻结 | 压测与埋点 下一批 |
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.fruitshorts.admin.controller;
|
||||
|
||||
import com.fruitshorts.admin.service.DashboardService;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 运营大盘(只读)。
|
||||
*
|
||||
* 指标口径写在 DashboardService 的类注释与 docs/api-phase1.md 里;
|
||||
* 取不到数据源的指标返回 null 并在 notes 里说明,不用 0 冒充。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin/dashboard")
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardAdminController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
|
||||
/** 总览:用户 / 内容 / 播放 / 收入 / 成本。 */
|
||||
@GetMapping("/overview")
|
||||
@PreAuthorize("@ss.hasPermission('dashboard:view')")
|
||||
public Result<Map<String, Object>> overview() {
|
||||
return Result.ok(dashboardService.overview());
|
||||
}
|
||||
|
||||
/** 转码看板:队列积压与失败任务明细。 */
|
||||
@GetMapping("/transcode")
|
||||
@PreAuthorize("@ss.hasPermission('dashboard:view')")
|
||||
public Result<Map<String, Object>> transcode() {
|
||||
return Result.ok(dashboardService.transcode());
|
||||
}
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
package com.fruitshorts.admin.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.fruitshorts.commerce.dal.entity.CoinLedger;
|
||||
import com.fruitshorts.commerce.dal.entity.Entitlement;
|
||||
import com.fruitshorts.commerce.dal.entity.Orders;
|
||||
import com.fruitshorts.commerce.dal.entity.Wallet;
|
||||
import com.fruitshorts.commerce.dal.mapper.CoinLedgerMapper;
|
||||
import com.fruitshorts.commerce.dal.mapper.EntitlementMapper;
|
||||
import com.fruitshorts.commerce.dal.mapper.OrdersMapper;
|
||||
import com.fruitshorts.commerce.dal.mapper.WalletMapper;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
|
||||
import com.fruitshorts.content.dal.mapper.DramaMapper;
|
||||
import com.fruitshorts.content.enums.ContentStatus;
|
||||
import com.fruitshorts.interaction.dal.entity.ContentComment;
|
||||
import com.fruitshorts.interaction.dal.entity.ContentReport;
|
||||
import com.fruitshorts.interaction.dal.mapper.ContentCommentMapper;
|
||||
import com.fruitshorts.interaction.dal.mapper.ContentReportMapper;
|
||||
import com.fruitshorts.media.service.TranscodeService;
|
||||
import com.fruitshorts.playback.dal.entity.WatchHistory;
|
||||
import com.fruitshorts.playback.dal.entity.WatchProgress;
|
||||
import com.fruitshorts.playback.dal.mapper.WatchHistoryMapper;
|
||||
import com.fruitshorts.playback.dal.mapper.WatchProgressMapper;
|
||||
import com.fruitshorts.user.dal.entity.AppUser;
|
||||
import com.fruitshorts.user.dal.mapper.AppUserMapper;
|
||||
import com.fruitshorts.user.dal.mapper.AppUserOtpMapper;
|
||||
import com.fruitshorts.user.dal.entity.AppUserOtp;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 运营大盘(只读聚合)。
|
||||
*
|
||||
* <p><b>为什么放在 admin-api 而不是建一个新模块</b>:它要跨用户/内容/播放/互动/交易/媒资六个域取数,
|
||||
* 建模块只会把依赖再摊开一层;而它**只读、不写**,放在接入层做聚合,改动面最小。
|
||||
*
|
||||
* <p><b>指标口径(每个指标都在注释里写清楚,前端 tooltip 直接复用)</b>:
|
||||
* <ul>
|
||||
* <li>用户:`total` = app_user 未删除总数;`newToday`/`new7d` = 按 created_at 落在当天 / 近 7 天;
|
||||
* `active7d` = last_login_at 在近 7 天内(**不是 DAU**:真正的日活需要按天留存登录快照,
|
||||
* 当前没有逐日快照数据源,见 notes);`banned`/`limited` = 按 status 计数。</li>
|
||||
* <li>内容:短剧按 status 计数(`reviewing` 视为待审、`published` 视为已上线);
|
||||
* 剧集总数与已上线数;`episodeWithMedia` = video_url 非空的剧集数。</li>
|
||||
* <li>播放:`viewTotal`/`playTotal` = 短剧表上累计计数列的求和(累计口径,不限定时间范围);
|
||||
* `watchedDramas` = watch_progress 行数(去重后的"看过的剧");`finishedDramas` = 其中 finished=1 的;
|
||||
* `playEvents7d` = watch_history 近 7 天事件数(**流水口径**,与上面的累计口径不同,不要相加)。</li>
|
||||
* <li>收入:订单按状态计数;`paidAmount` = 已支付/已交付订单金额合计(商店未接入前恒为 0);
|
||||
* `coinRecharged`/`coinConsumed` 取自 coin_ledger(充值按 RECHARGE、消耗按 UNLOCK 的绝对值);
|
||||
* `coinBalance` = wallet 余额合计;`entitlementActive` = 有效权益数。</li>
|
||||
* <li>成本:转码队列与产物数取自 transcode_task / media_rendition;
|
||||
* **CDN 流量与带宽成本暂无数据源**(需要接 CDN 账单或访问日志),短信条数用 app_user_otp 近似。</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardService {
|
||||
|
||||
private static final int ACTIVE_WINDOW_DAYS = 7;
|
||||
|
||||
private final AppUserMapper appUserMapper;
|
||||
private final AppUserOtpMapper appUserOtpMapper;
|
||||
private final DramaMapper dramaMapper;
|
||||
private final DramaEpisodeMapper dramaEpisodeMapper;
|
||||
private final WatchProgressMapper watchProgressMapper;
|
||||
private final WatchHistoryMapper watchHistoryMapper;
|
||||
private final ContentCommentMapper commentMapper;
|
||||
private final ContentReportMapper reportMapper;
|
||||
private final OrdersMapper ordersMapper;
|
||||
private final WalletMapper walletMapper;
|
||||
private final CoinLedgerMapper coinLedgerMapper;
|
||||
private final EntitlementMapper entitlementMapper;
|
||||
private final TranscodeService transcodeService;
|
||||
|
||||
public Map<String, Object> overview() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime sevenDaysAgo = now.minusDays(ACTIVE_WINDOW_DAYS);
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("generatedAt", now);
|
||||
data.put("user", userMetrics(todayStart, sevenDaysAgo));
|
||||
data.put("content", contentMetrics());
|
||||
data.put("playback", playbackMetrics(sevenDaysAgo));
|
||||
data.put("revenue", revenueMetrics());
|
||||
data.put("cost", costMetrics(sevenDaysAgo));
|
||||
data.put("notes", notes());
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 转码看板:队列积压与失败明细。 */
|
||||
public Map<String, Object> transcode() {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("generatedAt", LocalDateTime.now());
|
||||
data.put("queue", transcodeService.stats());
|
||||
data.put("failedTasks", transcodeService.failedTasks(20));
|
||||
data.put("notes", List.of(
|
||||
"queue 为累计口径(不限时间),反映当前积压与历史成败分布",
|
||||
"failedTasks 最多返回 20 条,按 id 倒序"));
|
||||
return data;
|
||||
}
|
||||
|
||||
private Map<String, Object> userMetrics(LocalDateTime todayStart, LocalDateTime sevenDaysAgo) {
|
||||
Map<String, Object> user = new LinkedHashMap<>();
|
||||
user.put("total", countUser(Wrappers.<AppUser>lambdaQuery()));
|
||||
user.put("newToday", countUser(Wrappers.<AppUser>lambdaQuery()
|
||||
.ge(AppUser::getCreatedAt, todayStart)));
|
||||
user.put("new7d", countUser(Wrappers.<AppUser>lambdaQuery()
|
||||
.ge(AppUser::getCreatedAt, sevenDaysAgo)));
|
||||
user.put("active7d", countUser(Wrappers.<AppUser>lambdaQuery()
|
||||
.ge(AppUser::getLastLoginAt, sevenDaysAgo)));
|
||||
user.put("banned", countUser(Wrappers.<AppUser>lambdaQuery().eq(AppUser::getStatus, "banned")));
|
||||
user.put("limited", countUser(Wrappers.<AppUser>lambdaQuery().eq(AppUser::getStatus, "limited")));
|
||||
user.put("deactivated", countUser(Wrappers.<AppUser>lambdaQuery()
|
||||
.eq(AppUser::getStatus, "deactivated")));
|
||||
return user;
|
||||
}
|
||||
|
||||
private Map<String, Object> contentMetrics() {
|
||||
Map<String, Object> content = new LinkedHashMap<>();
|
||||
content.put("dramaTotal", dramaMapper.selectCount(null));
|
||||
content.put("dramaPublished", countDrama(ContentStatus.PUBLISHED.getCode()));
|
||||
content.put("dramaReviewing", countDrama(ContentStatus.REVIEWING.getCode()));
|
||||
content.put("dramaDraft", countDrama(ContentStatus.DRAFT.getCode()));
|
||||
content.put("dramaOffline", countDrama(ContentStatus.OFFLINE.getCode()));
|
||||
content.put("episodeTotal", dramaEpisodeMapper.selectCount(null));
|
||||
content.put("episodePublished", dramaEpisodeMapper.selectCount(
|
||||
Wrappers.<DramaEpisode>lambdaQuery()
|
||||
.eq(DramaEpisode::getStatus, ContentStatus.PUBLISHED.getCode())));
|
||||
content.put("episodeWithMedia", dramaEpisodeMapper.selectCount(
|
||||
Wrappers.<DramaEpisode>lambdaQuery()
|
||||
.isNotNull(DramaEpisode::getVideoUrl)
|
||||
.ne(DramaEpisode::getVideoUrl, "")));
|
||||
content.put("commentPending", commentMapper.selectCount(Wrappers.<ContentComment>lambdaQuery()
|
||||
.eq(ContentComment::getStatus, "pending")));
|
||||
content.put("reportPending", reportMapper.selectCount(Wrappers.<ContentReport>lambdaQuery()
|
||||
.eq(ContentReport::getStatus, "pending")));
|
||||
return content;
|
||||
}
|
||||
|
||||
private Map<String, Object> playbackMetrics(LocalDateTime sevenDaysAgo) {
|
||||
Map<String, Object> playback = new LinkedHashMap<>();
|
||||
long viewTotal = 0L;
|
||||
long playTotal = 0L;
|
||||
for (Drama drama : dramaMapper.selectList(
|
||||
Wrappers.<Drama>lambdaQuery().select(Drama::getViewCount, Drama::getPlayCount))) {
|
||||
viewTotal += drama.getViewCount() == null ? 0L : drama.getViewCount();
|
||||
playTotal += drama.getPlayCount() == null ? 0L : drama.getPlayCount();
|
||||
}
|
||||
playback.put("viewTotal", viewTotal);
|
||||
playback.put("playTotal", playTotal);
|
||||
long watched = orZero(watchProgressMapper.selectCount(Wrappers.<WatchProgress>lambdaQuery()));
|
||||
long finished = orZero(watchProgressMapper.selectCount(Wrappers.<WatchProgress>lambdaQuery()
|
||||
.eq(WatchProgress::getFinished, 1)));
|
||||
playback.put("watchedDramas", watched);
|
||||
playback.put("finishedDramas", finished);
|
||||
playback.put("finishRate", watched == 0 ? 0d : Math.round(finished * 1000.0 / watched) / 1000.0);
|
||||
playback.put("playEvents7d", orZero(watchHistoryMapper.selectCount(
|
||||
Wrappers.<WatchHistory>lambdaQuery().ge(WatchHistory::getWatchedAt, sevenDaysAgo))));
|
||||
playback.put("playEventsTotal", orZero(watchHistoryMapper.selectCount(
|
||||
Wrappers.<WatchHistory>lambdaQuery())));
|
||||
return playback;
|
||||
}
|
||||
|
||||
private Map<String, Object> revenueMetrics() {
|
||||
Map<String, Object> revenue = new LinkedHashMap<>();
|
||||
revenue.put("orderTotal", ordersMapper.selectCount(null));
|
||||
revenue.put("orderDelivered", ordersMapper.selectCount(Wrappers.<Orders>lambdaQuery()
|
||||
.eq(Orders::getStatus, "DELIVERED")));
|
||||
revenue.put("orderRefunded", ordersMapper.selectCount(Wrappers.<Orders>lambdaQuery()
|
||||
.eq(Orders::getStatus, "REFUNDED")));
|
||||
BigDecimal paidAmount = BigDecimal.ZERO;
|
||||
for (Orders order : ordersMapper.selectList(Wrappers.<Orders>lambdaQuery()
|
||||
.select(Orders::getStatus, Orders::getAmount)
|
||||
.in(Orders::getStatus, List.of("PAID", "DELIVERED")))) {
|
||||
if (order.getAmount() != null) {
|
||||
paidAmount = paidAmount.add(order.getAmount());
|
||||
}
|
||||
}
|
||||
revenue.put("paidAmount", paidAmount);
|
||||
revenue.put("coinRecharged", sumLedger("RECHARGE"));
|
||||
revenue.put("coinConsumed", Math.abs(sumLedger("UNLOCK")));
|
||||
long balance = 0L;
|
||||
for (Wallet wallet : walletMapper.selectList(
|
||||
Wrappers.<Wallet>lambdaQuery().select(Wallet::getCoinBalance))) {
|
||||
balance += wallet.getCoinBalance() == null ? 0L : wallet.getCoinBalance();
|
||||
}
|
||||
revenue.put("coinBalance", balance);
|
||||
revenue.put("entitlementActive", entitlementMapper.selectCount(
|
||||
Wrappers.<Entitlement>lambdaQuery().eq(Entitlement::getStatus, "ACTIVE")));
|
||||
return revenue;
|
||||
}
|
||||
|
||||
private Map<String, Object> costMetrics(LocalDateTime sevenDaysAgo) {
|
||||
Map<String, Object> cost = new LinkedHashMap<>();
|
||||
Map<String, Object> queue = transcodeService.stats();
|
||||
cost.put("transcodeQueued", queue.get("queued"));
|
||||
cost.put("transcodeRunning", queue.get("running"));
|
||||
cost.put("transcodeFailed", queue.get("failed"));
|
||||
cost.put("transcodeSuccess", queue.get("success"));
|
||||
cost.put("renditions", queue.get("renditions"));
|
||||
cost.put("otpSent7d", appUserOtpMapper.selectCount(Wrappers.<AppUserOtp>lambdaQuery()
|
||||
.ge(AppUserOtp::getCreatedAt, sevenDaysAgo)));
|
||||
cost.put("cdnTrafficBytes", null);
|
||||
return cost;
|
||||
}
|
||||
|
||||
/** 明确写出哪些指标当前没有数据源,避免前端把 null 当成 0 展示。 */
|
||||
private List<String> notes() {
|
||||
List<String> notes = new ArrayList<>();
|
||||
notes.add("DAU 暂无数据源:需要按天留存登录快照(ops_daily_stat 已建表但当前无产出),"
|
||||
+ "现以 active7d(近 7 天登录过)近似");
|
||||
notes.add("CDN 流量与带宽成本暂无数据源:需接入 CDN 账单或访问日志,cdnTrafficBytes 恒为 null");
|
||||
notes.add("收入金额在商店未接入前恒为 0:订单表无真实商店订单,金币解锁收入请参考 coinConsumed");
|
||||
notes.add("viewTotal/playTotal 是短剧表上的累计计数,与 playEvents7d 的流水口径不可相加");
|
||||
return notes;
|
||||
}
|
||||
|
||||
private Long countUser(com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AppUser> wrapper) {
|
||||
return appUserMapper.selectCount(wrapper);
|
||||
}
|
||||
|
||||
private Long countDrama(String status) {
|
||||
return StringUtils.hasText(status)
|
||||
? dramaMapper.selectCount(Wrappers.<Drama>lambdaQuery().eq(Drama::getStatus, status))
|
||||
: dramaMapper.selectCount(null);
|
||||
}
|
||||
|
||||
private long sumLedger(String bizType) {
|
||||
long sum = 0L;
|
||||
for (CoinLedger ledger : coinLedgerMapper.selectList(Wrappers.<CoinLedger>lambdaQuery()
|
||||
.select(CoinLedger::getChangeAmount)
|
||||
.eq(CoinLedger::getBizType, bizType))) {
|
||||
sum += ledger.getChangeAmount() == null ? 0L : ledger.getChangeAmount();
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private long orZero(Long value) {
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
}
|
||||
+33
@@ -12,7 +12,9 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 转码任务编排。
|
||||
@@ -112,6 +114,37 @@ public class TranscodeService {
|
||||
.orderByAsc(MediaRendition::getVideoBitrate));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转码看板数据。
|
||||
*
|
||||
* 口径:按 transcode_task.status 全量计数(不含逻辑删除),不限定时间范围 ——
|
||||
* 队列类指标看的是"当前积压",用累计值才有意义。
|
||||
* 产物数取自 media_rendition,反映已经生成多少路码率文件。
|
||||
*/
|
||||
public Map<String, Object> stats() {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("queued", countByStatus(STATUS_QUEUED));
|
||||
data.put("running", countByStatus(STATUS_RUNNING));
|
||||
data.put("success", countByStatus(STATUS_SUCCESS));
|
||||
data.put("failed", countByStatus(STATUS_FAILED));
|
||||
data.put("canceled", countByStatus("CANCELED"));
|
||||
data.put("renditions", mediaRenditionMapper.selectCount(null));
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 失败任务明细:带 error_msg 与 retry_count,便于运营判断是重试还是排查片源。 */
|
||||
public List<TranscodeTask> failedTasks(int limit) {
|
||||
return transcodeTaskMapper.selectList(Wrappers.<TranscodeTask>lambdaQuery()
|
||||
.eq(TranscodeTask::getStatus, STATUS_FAILED)
|
||||
.orderByDesc(TranscodeTask::getId)
|
||||
.last("limit " + (limit <= 0 || limit > 100 ? 20 : limit)));
|
||||
}
|
||||
|
||||
private Long countByStatus(String status) {
|
||||
return transcodeTaskMapper.selectCount(Wrappers.<TranscodeTask>lambdaQuery()
|
||||
.eq(TranscodeTask::getStatus, status));
|
||||
}
|
||||
|
||||
private TranscodeTask getExisting(Long id) {
|
||||
TranscodeTask task = id == null ? null : transcodeTaskMapper.selectById(id);
|
||||
if (task == null) {
|
||||
|
||||
+6
-1
@@ -63,6 +63,11 @@ INSERT INTO sys_permission (code, name, group_name, type, sort) VALUES
|
||||
('price:update', '配置分集价格', '内容管理', 'BUTTON', 135)
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- 看板权限:菜单在 3-4 补页面时一起加,避免菜单指向不存在的组件
|
||||
INSERT INTO sys_permission (code, name, group_name, type, sort) VALUES
|
||||
('dashboard:view', '查看运营大盘', '数据看板', 'BUTTON', 141)
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- 超管默认拥有全部权限点
|
||||
INSERT INTO sys_role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
@@ -75,7 +80,7 @@ WHERE r.code = 'super_admin'
|
||||
'order:list', 'order:update', 'entitlement:list', 'entitlement:update',
|
||||
'wallet:update', 'reconcile:run',
|
||||
'creator:list', 'creator:audit', 'submission:list', 'submission:audit',
|
||||
'price:update')
|
||||
'price:update', 'dashboard:view')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ---------- 菜单:创作者管理 ----------
|
||||
|
||||
Reference in new issue
Block a user