fix(douyu): aggregate audience trend by minute

This commit is contained in:
liuwei
2026-04-09 09:50:21 +08:00
parent 4e2dea86af
commit 5dfc17f811
2 changed files with 86 additions and 22 deletions

View File

@@ -891,18 +891,18 @@ class DouyuPlugin(MessagePluginInterface):
@staticmethod
def _normalize_audience_points(points: List[Dict[str, Any]], limit: int = 720) -> List[Dict[str, Any]]:
normalized: List[Dict[str, Any]] = []
seen = set()
normalized_map: Dict[str, Dict[str, Any]] = {}
for item in points or []:
timestamp = str(item.get("timestamp") or "").strip()
if not timestamp or timestamp in seen:
if not timestamp:
continue
seen.add(timestamp)
normalized.append({
minute_key = timestamp[:16]
normalized_map[minute_key] = {
"timestamp": timestamp,
"vip_count": int(item.get("vip_count", 0) or 0),
"diamond_count": int(item.get("diamond_count", 0) or 0),
})
}
normalized = list(normalized_map.values())
normalized.sort(key=lambda row: row.get("timestamp", ""))
if len(normalized) > limit:
normalized = normalized[-limit:]
@@ -1183,7 +1183,16 @@ class DouyuPlugin(MessagePluginInterface):
def _build_audience_trend(self, sessions: List[Dict[str, Any]]) -> Dict[str, Any]:
points: List[Dict[str, Any]] = []
segment_start_times: List[str] = []
segment_end_times: List[str] = []
for session in sessions:
for segment in session.get("segments", []) or []:
start_time = str(segment.get("start_time") or "").strip()
end_time = str(segment.get("end_time") or "").strip()
if start_time:
segment_start_times.append(start_time)
if end_time:
segment_end_times.append(end_time)
for item in session.get("audience_points", []) or []:
point = {
"timestamp": str(item.get("timestamp") or "").strip(),
@@ -1198,6 +1207,16 @@ class DouyuPlugin(MessagePluginInterface):
vip_values = [int(item.get("vip_count", 0) or 0) for item in points]
diamond_values = [int(item.get("diamond_count", 0) or 0) for item in points]
labels = [str(item.get("timestamp") or "")[-8:-3] for item in points]
session_start = min(segment_start_times) if segment_start_times else ""
session_end = max(segment_end_times) if segment_end_times else str(points[-1].get("timestamp") or "")
first_point_time = str(points[0].get("timestamp") or "")
last_point_time = str(points[-1].get("timestamp") or "")
leading_gap_minutes = 0
if session_start and first_point_time:
start_dt = self._parse_session_time(session_start)
point_dt = self._parse_session_time(first_point_time)
if start_dt and point_dt:
leading_gap_minutes = max(int((point_dt - start_dt).total_seconds() // 60), 0)
return {
"points": points,
"summary": {
@@ -1209,6 +1228,11 @@ class DouyuPlugin(MessagePluginInterface):
"diamond_max": max(diamond_values),
"diamond_latest": diamond_values[-1],
"labels": labels,
"session_start": session_start,
"session_end": session_end,
"first_point_time": first_point_time,
"last_point_time": last_point_time,
"leading_gap_minutes": leading_gap_minutes,
},
}

View File

@@ -209,28 +209,56 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str:
plot_width = width - padding_left - padding_right
plot_height = height - padding_top - padding_bottom
vip_values = [int(item.get("vip_count", 0) or 0) for item in points]
diamond_values = [int(item.get("diamond_count", 0) or 0) for item in points]
labels = [str(item.get("timestamp") or "")[-8:-3] for item in points]
summary = audience_trend.get("summary", {}) or {}
def _compress_chart_points(raw_points: List[Dict[str, Any]], max_points: int = 36) -> List[Dict[str, Any]]:
if len(raw_points) <= max_points:
return list(raw_points)
bucket_size = max(len(raw_points) / max_points, 1)
compressed: List[Dict[str, Any]] = []
for idx in range(max_points):
start = int(round(idx * bucket_size))
end = int(round((idx + 1) * bucket_size))
bucket = raw_points[start:end] or raw_points[start:start + 1]
if not bucket:
continue
compressed.append(dict(bucket[-1]))
first = raw_points[0]
last = raw_points[-1]
if compressed:
compressed[0] = dict(first)
compressed[-1] = dict(last)
return compressed
chart_points = _compress_chart_points(points, max_points=36)
vip_values = [int(item.get("vip_count", 0) or 0) for item in chart_points]
diamond_values = [int(item.get("diamond_count", 0) or 0) for item in chart_points]
labels = [str(item.get("timestamp") or "")[-8:-3] for item in chart_points]
vip_min = min(vip_values)
vip_max = max(vip_values)
diamond_min = min(diamond_values)
diamond_max = max(diamond_values)
vip_span = max(vip_max - vip_min, 1)
diamond_span = max(diamond_max - diamond_min, 1)
step_x = plot_width / max(len(points) - 1, 1)
vip_padding = max(int((vip_max - vip_min) * 0.12), 60)
diamond_padding = 1 if diamond_max - diamond_min <= 2 else max(int((diamond_max - diamond_min) * 0.2), 1)
vip_display_min = max(vip_min - vip_padding, 0)
vip_display_max = vip_max + vip_padding
diamond_display_min = max(diamond_min - diamond_padding, 0)
diamond_display_max = diamond_max + diamond_padding
vip_span = max(vip_display_max - vip_display_min, 1)
diamond_span = max(diamond_display_max - diamond_display_min, 1)
step_x = plot_width / max(len(chart_points) - 1, 1)
bar_width = max(min(step_x * 0.58, 18), 6)
def x_at(index: int) -> float:
return padding_left + step_x * index
def y_vip(value: int) -> float:
ratio = (value - vip_min) / vip_span if vip_span else 0
ratio = (value - vip_display_min) / vip_span if vip_span else 0
return padding_top + plot_height - ratio * plot_height
def y_diamond(value: int) -> float:
ratio = (value - diamond_min) / diamond_span if diamond_span else 0
ratio = (value - diamond_display_min) / diamond_span if diamond_span else 0
return padding_top + plot_height - ratio * plot_height
grid_lines = []
@@ -239,8 +267,8 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str:
for idx in range(5):
ratio = idx / 4
y = padding_top + plot_height - ratio * plot_height
vip_tick = round(vip_min + vip_span * ratio)
diamond_tick = round(diamond_min + diamond_span * ratio)
vip_tick = round(vip_display_min + vip_span * ratio)
diamond_tick = round(diamond_display_min + diamond_span * ratio)
grid_lines.append(
f'<line x1="{padding_left}" y1="{y:.1f}" x2="{width - padding_right}" y2="{y:.1f}" class="chart-grid" />'
)
@@ -256,14 +284,14 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str:
dot_points = []
label_marks = []
annotations = []
if len(points) <= 12:
label_indexes = list(range(len(points)))
if len(chart_points) <= 12:
label_indexes = list(range(len(chart_points)))
else:
label_indexes = sorted(set([0, len(points) - 1] + [int(round((len(points) - 1) * i / 6)) for i in range(1, 6)]))
label_indexes = sorted(set([0, len(chart_points) - 1] + [int(round((len(chart_points) - 1) * i / 6)) for i in range(1, 6)]))
vip_peak_index = vip_values.index(vip_max)
vip_valley_index = vip_values.index(vip_min)
diamond_latest_index = len(points) - 1
for idx, item in enumerate(points):
diamond_latest_index = len(chart_points) - 1
for idx, item in enumerate(chart_points):
x = x_at(idx)
vip_y = y_vip(int(item.get("vip_count", 0) or 0))
diamond_y = y_diamond(int(item.get("diamond_count", 0) or 0))
@@ -300,9 +328,14 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str:
)
polyline = f'<polyline points="{" ".join(line_points)}" class="diamond-line" />'
summary = audience_trend.get("summary", {}) or {}
vip_latest = int(summary.get("vip_latest", vip_values[-1]) or 0)
diamond_latest = int(summary.get("diamond_latest", diamond_values[-1]) or 0)
session_start = str(summary.get("session_start") or "")
first_point_time = str(summary.get("first_point_time") or "")
leading_gap_minutes = int(summary.get("leading_gap_minutes", 0) or 0)
note_text = ""
if session_start and first_point_time and leading_gap_minutes >= 20:
note_text = f"采样起点 {first_point_time[-8:-3]},早于该时段的人数数据未记录"
return f"""
<div class="chart-wrap">
@@ -314,6 +347,7 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str:
<span class="legend-meta">最新:贵宾 {_escape(vip_latest)} / 钻粉 {_escape(diamond_latest)}</span>
</div>
</div>
{'<div class="chart-note">' + _escape(note_text) + '</div>' if note_text else ''}
<svg viewBox="0 0 {width} {height}" class="audience-chart" role="img" aria-label="贵宾与钻粉趋势图">
<defs>
<linearGradient id="vipBarGradient" x1="0" y1="0" x2="0" y2="1">
@@ -744,6 +778,12 @@ def render_daily_report_html(
gap: 12px;
margin-bottom: 10px;
}}
.chart-note {{
margin: -2px 0 8px;
color: #7b8798;
font-size: 12px;
line-height: 1.5;
}}
.chart-title {{
font-size: 17px;
font-weight: 800;