feat: add TaurusX API performance data
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fetch recent TaurusX performance data without persisting API credentials."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
API_URL = "https://scaler.taurusx.com/openapi/performance_data"
|
||||
TIMEZONE = "+8"
|
||||
|
||||
|
||||
def build_token(secret_key: str, timestamp: str) -> str:
|
||||
timestamp_hash = hashlib.md5(timestamp.encode()).hexdigest()
|
||||
return hashlib.md5((secret_key + timestamp_hash).encode()).hexdigest()
|
||||
|
||||
|
||||
def request_performance(
|
||||
access_key: str,
|
||||
secret_key: str,
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
) -> list[dict[str, Any]]:
|
||||
timestamp = str(int(time.time()))
|
||||
query = urllib.parse.urlencode(
|
||||
{
|
||||
"timezone": TIMEZONE,
|
||||
"report_type": "event",
|
||||
"start_time": start_date.isoformat(),
|
||||
"end_time": end_date.isoformat(),
|
||||
"dimension_option": "Campaign,Country",
|
||||
}
|
||||
)
|
||||
request = urllib.request.Request(
|
||||
f"{API_URL}?{query}",
|
||||
headers={
|
||||
"access-key": access_key,
|
||||
"token": build_token(secret_key, timestamp),
|
||||
"timestamp": timestamp,
|
||||
"Accept": "application/json",
|
||||
"User-Agent": "AgentStage-TaurusX-Report/1.0",
|
||||
},
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=60) as response:
|
||||
payload = json.loads(response.read())
|
||||
if payload.get("code") != 200:
|
||||
raise RuntimeError(f"TaurusX API returned code {payload.get('code')}: {payload.get('msg', 'unknown error')}")
|
||||
rows = (payload.get("data") or {}).get("data_list")
|
||||
if not isinstance(rows, list):
|
||||
raise RuntimeError("TaurusX API response is missing data.data_list")
|
||||
return rows
|
||||
|
||||
|
||||
def match_pkg(campaign_name: str, packages: list[str]) -> str:
|
||||
lowered = campaign_name.casefold()
|
||||
for package in packages:
|
||||
if package.casefold() in lowered:
|
||||
return package
|
||||
return ""
|
||||
|
||||
|
||||
def build_dataset(
|
||||
api_rows: list[dict[str, Any]],
|
||||
report_payload: dict[str, Any],
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
) -> dict[str, Any]:
|
||||
coverage_records = report_payload.get("records") or []
|
||||
packages = sorted({str(row["pkg"]) for row in coverage_records}, key=len, reverse=True)
|
||||
coverage_by_key = {
|
||||
(str(row["pkg"]).casefold(), str(row["country"]).upper()): row
|
||||
for row in coverage_records
|
||||
}
|
||||
|
||||
records: list[dict[str, Any]] = []
|
||||
for api_row in api_rows:
|
||||
campaign_name = str(api_row.get("campaign_name") or "").strip()
|
||||
pkg = match_pkg(campaign_name, packages)
|
||||
country = str(api_row.get("country") or "").strip().upper()
|
||||
coverage = coverage_by_key.get((pkg.casefold(), country)) if pkg and country else None
|
||||
records.append(
|
||||
{
|
||||
"date": str(api_row.get("date") or ""),
|
||||
"campaign_id": str(api_row.get("campaign_id") or ""),
|
||||
"campaign_name": campaign_name,
|
||||
"pkg": pkg,
|
||||
"country": country,
|
||||
"impressions": int(float(api_row.get("impressions") or 0)),
|
||||
"clicks": int(float(api_row.get("clicks") or 0)),
|
||||
"conversions": int(float(api_row.get("conversions") or 0)),
|
||||
"cost": round(float(api_row.get("cost") or 0), 4),
|
||||
"order_match": coverage is not None,
|
||||
"adpingids": list(coverage.get("adpingids") or []) if coverage else [],
|
||||
"order_statuses": list(coverage.get("order_statuses") or []) if coverage else [],
|
||||
"listed_in_taurusx_orders": bool(coverage and coverage.get("target_present")),
|
||||
}
|
||||
)
|
||||
|
||||
records.sort(key=lambda row: (row["date"], row["cost"], row["clicks"]), reverse=True)
|
||||
fetched_at = datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds")
|
||||
return {
|
||||
"meta": {
|
||||
"source": API_URL,
|
||||
"timezone": TIMEZONE,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
"fetched_at": fetched_at,
|
||||
"rows": len(records),
|
||||
"campaigns": len({row["campaign_id"] for row in records}),
|
||||
"pkg_country_count": len({(row["pkg"].casefold(), row["country"]) for row in records if row["pkg"]}),
|
||||
"countries": len({row["country"] for row in records if row["country"]}),
|
||||
"matched_order_rows": sum(row["order_match"] for row in records),
|
||||
"unmatched_order_rows": sum(not row["order_match"] for row in records),
|
||||
"impressions": sum(row["impressions"] for row in records),
|
||||
"clicks": sum(row["clicks"] for row in records),
|
||||
"conversions": sum(row["conversions"] for row in records),
|
||||
"cost": round(sum(row["cost"] for row in records), 4),
|
||||
},
|
||||
"records": records,
|
||||
}
|
||||
|
||||
|
||||
def atomic_write_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
||||
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
||||
handle.write("\n")
|
||||
temporary_path = Path(handle.name)
|
||||
temporary_path.replace(path)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--data-file", type=Path, default=Path(__file__).with_name("data.json"))
|
||||
parser.add_argument("--days", type=int, default=7, choices=range(1, 8), metavar="1-7")
|
||||
parser.add_argument("--end-date", type=date.fromisoformat, help="Last report date; defaults to yesterday in UTC+8")
|
||||
args = parser.parse_args()
|
||||
|
||||
access_key = os.environ.get("TAURUSX_ACCESS_KEY", "").strip()
|
||||
secret_key = os.environ.get("TAURUSX_SECRET_KEY", "").strip()
|
||||
if not access_key or not secret_key:
|
||||
raise SystemExit("TAURUSX_ACCESS_KEY and TAURUSX_SECRET_KEY environment variables are required")
|
||||
if not args.data_file.is_file():
|
||||
raise SystemExit(f"Report data file not found: {args.data_file}")
|
||||
|
||||
utc8_today = datetime.now(timezone(timedelta(hours=8))).date()
|
||||
end_date = args.end_date or (utc8_today - timedelta(days=1))
|
||||
start_date = end_date - timedelta(days=args.days - 1)
|
||||
report_payload = json.loads(args.data_file.read_text(encoding="utf-8"))
|
||||
api_rows = request_performance(access_key, secret_key, start_date, end_date)
|
||||
report_payload["taurusx_perf"] = build_dataset(api_rows, report_payload, start_date, end_date)
|
||||
atomic_write_json(args.data_file, report_payload)
|
||||
meta = report_payload["taurusx_perf"]["meta"]
|
||||
print(
|
||||
f"Updated {args.data_file}: {meta['rows']} TaurusX rows, "
|
||||
f"{meta['matched_order_rows']} matched orders, {meta['start_date']}..{meta['end_date']}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+160
@@ -697,6 +697,165 @@
|
||||
document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
const txState = { filtered: [], page: 1 };
|
||||
|
||||
function renderTaurusxPerf() {
|
||||
const data = state.data.taurusx_perf;
|
||||
if (!data || !Array.isArray(data.records)) return;
|
||||
const panel = el('section', 'panel workspace');
|
||||
panel.id = 'taurusxPerfPanel';
|
||||
const header = el('div', 'workspace-head');
|
||||
const head = el('div', 'panel-head');
|
||||
const title = el('div');
|
||||
title.append(
|
||||
el('h2', '', 'TaurusX 近 7 日投放表现'),
|
||||
el('div', 'panel-desc', `${data.meta.start_date} 至 ${data.meta.end_date} · UTC${data.meta.timezone} · 拉取于 ${formatTime(data.meta.fetched_at)}`)
|
||||
);
|
||||
head.append(title);
|
||||
|
||||
const summary = el('section', 'kpis');
|
||||
summary.append(
|
||||
kpi('Impressions', nf.format(data.meta.impressions), `${nf.format(data.meta.campaigns)} 个 Campaign`),
|
||||
kpi('Clicks', nf.format(data.meta.clicks), `${nf.format(data.meta.pkg_country_count)} 个 Pkg_国家`),
|
||||
kpi('Conversions', nf.format(data.meta.conversions), `${nf.format(data.meta.matched_order_rows)} 条已匹配订单`),
|
||||
kpi('Cost', money(data.meta.cost), `${nf.format(data.meta.unmatched_order_rows)} 条订单未登记`)
|
||||
);
|
||||
summary.style.marginBottom = '16px';
|
||||
|
||||
const filters = el('div', 'filters');
|
||||
filters.style.gridTemplateColumns = 'minmax(210px, 1.2fr) repeat(4, minmax(125px, .55fr)) auto';
|
||||
const search = el('input', 'control');
|
||||
search.id = 'txSearch'; search.type = 'search'; search.placeholder = '搜索 Campaign、Pkg、Adping ID'; search.autocomplete = 'off';
|
||||
const reportDate = selectControl('txDate', '全部日期');
|
||||
[...new Set(data.records.map(row => row.date))].sort().reverse().forEach(value => reportDate.append(new Option(value, value)));
|
||||
const country = selectControl('txCountry', '全部国家');
|
||||
[...new Set(data.records.map(row => row.country))].sort().forEach(value => country.append(new Option(value, value)));
|
||||
const orderMatch = selectControl('txOrderMatch', '');
|
||||
[['all','全部订单匹配'], ['matched','订单已登记'], ['unmatched','API有数据·订单未登记']]
|
||||
.forEach(([value,label]) => orderMatch.append(new Option(label, value)));
|
||||
const sort = selectControl('txSort', '');
|
||||
[
|
||||
['date-desc','日期:新到旧'], ['cost-desc','Cost:高到低'],
|
||||
['impressions-desc','Impressions:高到低'], ['clicks-desc','Clicks:高到低'],
|
||||
['conversions-desc','Conversions:高到低'], ['pkg-asc','Pkg:A-Z']
|
||||
].forEach(([value,label]) => sort.append(new Option(`排序 · ${label}`, value)));
|
||||
const exportButton = el('button', 'button', '导出 TaurusX 数据');
|
||||
exportButton.type = 'button';
|
||||
filters.append(search, reportDate, country, orderMatch, sort, exportButton);
|
||||
header.append(head, summary, filters);
|
||||
panel.append(header, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination'));
|
||||
document.getElementById('app').insertBefore(panel, document.querySelector('footer'));
|
||||
|
||||
search.addEventListener('input', () => { txState.page = 1; applyTxFilters(); });
|
||||
[reportDate, country, orderMatch, sort].forEach(control => control.addEventListener('change', () => { txState.page = 1; applyTxFilters(); }));
|
||||
exportButton.addEventListener('click', exportTxCsv);
|
||||
applyTxFilters();
|
||||
}
|
||||
|
||||
function applyTxFilters() {
|
||||
const query = document.getElementById('txSearch').value.trim().toLocaleLowerCase();
|
||||
const reportDate = document.getElementById('txDate').value;
|
||||
const country = document.getElementById('txCountry').value;
|
||||
const orderMatch = document.getElementById('txOrderMatch').value;
|
||||
const sort = document.getElementById('txSort').value;
|
||||
txState.filtered = state.data.taurusx_perf.records.filter(row => {
|
||||
const searchable = `${row.campaign_id} ${row.campaign_name} ${row.pkg} ${row.adpingids.join(' ')}`.toLocaleLowerCase();
|
||||
return (!query || searchable.includes(query))
|
||||
&& (reportDate === 'all' || row.date === reportDate)
|
||||
&& (country === 'all' || row.country === country)
|
||||
&& (orderMatch === 'all' || (orderMatch === 'matched' ? row.order_match : !row.order_match));
|
||||
});
|
||||
const [field, direction] = sort.split('-');
|
||||
const multiplier = direction === 'asc' ? 1 : -1;
|
||||
txState.filtered.sort((left, right) => {
|
||||
const a = left[field]; const b = right[field];
|
||||
if (typeof a === 'string' || typeof b === 'string') {
|
||||
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' }) * multiplier
|
||||
|| left.campaign_id.localeCompare(right.campaign_id, undefined, { numeric: true });
|
||||
}
|
||||
return (Number(a) - Number(b)) * multiplier
|
||||
|| left.campaign_id.localeCompare(right.campaign_id, undefined, { numeric: true });
|
||||
});
|
||||
renderTxResults();
|
||||
}
|
||||
|
||||
function renderTxResults() {
|
||||
const panel = document.getElementById('taurusxPerfPanel');
|
||||
const totalPages = Math.max(1, Math.ceil(txState.filtered.length / PAGE_SIZE));
|
||||
txState.page = Math.min(txState.page, totalPages);
|
||||
const start = (txState.page - 1) * PAGE_SIZE;
|
||||
const rows = txState.filtered.slice(start, start + PAGE_SIZE);
|
||||
const impressions = txState.filtered.reduce((sum, row) => sum + row.impressions, 0);
|
||||
const clicks = txState.filtered.reduce((sum, row) => sum + row.clicks, 0);
|
||||
const conversions = txState.filtered.reduce((sum, row) => sum + row.conversions, 0);
|
||||
const cost = txState.filtered.reduce((sum, row) => sum + row.cost, 0);
|
||||
const cards = panel.querySelectorAll('.kpi');
|
||||
const setCard = (index, value, sub) => {
|
||||
cards[index].querySelector('.value').textContent = value;
|
||||
cards[index].querySelector('.sub').textContent = sub;
|
||||
};
|
||||
setCard(0, nf.format(impressions), `${nf.format(txState.filtered.length)} 条筛选明细`);
|
||||
setCard(1, nf.format(clicks), `CTR ${impressions ? percent(clicks / impressions) : '-'}`);
|
||||
setCard(2, nf.format(conversions), `CVR ${clicks ? percent(conversions / clicks) : '-'}`);
|
||||
setCard(3, money(cost), `${nf.format(txState.filtered.filter(row => !row.order_match).length)} 条订单未登记`);
|
||||
|
||||
const meta = panel.querySelector('.result-meta');
|
||||
meta.textContent = '';
|
||||
meta.append(el('span', '', `筛选结果 ${nf.format(txState.filtered.length)} 条`), el('span', '', `第 ${txState.page} / ${totalPages} 页`));
|
||||
const wrap = panel.querySelector('.table-wrap');
|
||||
wrap.textContent = '';
|
||||
const table = el('table');
|
||||
const thead = el('thead'); const header = el('tr');
|
||||
['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Adpingid', '订单状态', 'Impressions', 'Clicks', 'Conversions', 'Cost', '订单关联']
|
||||
.forEach(label => header.append(el('th', '', label)));
|
||||
thead.append(header); table.append(thead);
|
||||
const tbody = el('tbody');
|
||||
if (!rows.length) {
|
||||
const cell = el('td', 'empty', '没有符合当前条件的 TaurusX 记录'); cell.colSpan = 12;
|
||||
const row = el('tr'); row.append(cell); tbody.append(row);
|
||||
}
|
||||
rows.forEach(record => {
|
||||
const row = el('tr');
|
||||
const country = el('td'); country.append(el('span', 'country-pill', record.country || '-'));
|
||||
const status = el('td');
|
||||
const statusPills = el('div', 'platforms');
|
||||
(record.order_statuses.length ? record.order_statuses : ['未登记']).forEach(value => statusPills.append(el('span', 'platform-pill', value)));
|
||||
status.append(statusPills);
|
||||
const match = el('td');
|
||||
match.append(el('span', `badge ${record.order_match ? 'covered' : 'target-only'}`, record.order_match ? '订单已登记' : 'API有数据·订单未登记'));
|
||||
row.append(
|
||||
el('td', '', record.date), el('td', 'pkg', record.campaign_id), el('td', 'product', record.campaign_name),
|
||||
el('td', 'pkg', record.pkg || '-'), country, el('td', 'pkg', record.adpingids.join(' / ') || '-'), status,
|
||||
el('td', '', nf.format(record.impressions)), el('td', '', nf.format(record.clicks)),
|
||||
el('td', '', nf.format(record.conversions)), el('td', '', money(record.cost)), match
|
||||
);
|
||||
tbody.append(row);
|
||||
});
|
||||
table.append(tbody); wrap.append(table);
|
||||
|
||||
const pager = panel.querySelector('.pagination');
|
||||
pager.textContent = '';
|
||||
const prev = el('button', 'page-button', '‹'); prev.type = 'button'; prev.disabled = txState.page <= 1;
|
||||
const next = el('button', 'page-button', '›'); next.type = 'button'; next.disabled = txState.page >= totalPages;
|
||||
prev.addEventListener('click', () => { txState.page -= 1; renderTxResults(); panel.scrollIntoView({ behavior: 'smooth' }); });
|
||||
next.addEventListener('click', () => { txState.page += 1; renderTxResults(); panel.scrollIntoView({ behavior: 'smooth' }); });
|
||||
pager.append(prev, el('span', 'panel-desc', `${txState.page} / ${totalPages}`), next);
|
||||
}
|
||||
|
||||
function exportTxCsv() {
|
||||
const headers = ['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Adpingid', '订单状态', 'Impressions', 'Clicks', 'Conversions', 'Cost', '订单关联'];
|
||||
const rows = [headers, ...txState.filtered.map(record => [
|
||||
record.date, record.campaign_id, record.campaign_name, record.pkg, record.country,
|
||||
record.adpingids.join(' / '), record.order_statuses.join(' / '), record.impressions,
|
||||
record.clicks, record.conversions, record.cost, record.order_match ? '订单已登记' : 'API有数据·订单未登记'
|
||||
])];
|
||||
const csv = '\ufeff' + rows.map(row => row.map(value => `"${String(value).replaceAll('"', '""')}"`).join(',')).join('\n');
|
||||
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
|
||||
const link = document.createElement('a');
|
||||
link.href = url; link.download = `TaurusX近7日_${new Date().toISOString().slice(0,10)}.csv`;
|
||||
document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
fetch(`data.json?v=${Date.now()}`, { cache: 'no-store' })
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
@@ -707,6 +866,7 @@
|
||||
state.data = data;
|
||||
renderShell();
|
||||
renderMtg1013();
|
||||
renderTaurusxPerf();
|
||||
})
|
||||
.catch(error => {
|
||||
const app = document.getElementById('app');
|
||||
|
||||
Reference in New Issue
Block a user