5 Commits
5 changed files with 49 additions and 28 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"title": "King · 当月 Campaign 表现", "title": "King · 当月 Campaign 表现",
"generated_at": "2026-08-27T15:26:33+08:00", "generated_at": "2026-08-27T15:50:27+08:00",
"timezone": "Asia/Shanghai", "timezone": "Asia/Shanghai",
"period": { "period": {
"start": "2026-08-01", "start": "2026-08-01",
@@ -29,7 +29,7 @@
"completion": { "completion": {
"revenue": 27.47, "revenue": 27.47,
"gp": 19.52, "gp": 19.52,
"gp_rate": 71.05 "gp_rate": null
}, },
"time_progress_percent": 83.87, "time_progress_percent": 83.87,
"month_days": 31, "month_days": 31,
@@ -1196,6 +1196,6 @@
"performance_cost": "Cost 取 Sands Report 2.0 的 Cost 合计,需本地抓取", "performance_cost": "Cost 取 Sands Report 2.0 的 Cost 合计,需本地抓取",
"performance_gp": "GP = Revenue − Cost;GP rate = GP ÷ Revenue × 100", "performance_gp": "GP = Revenue − Cost;GP rate = GP ÷ Revenue × 100",
"performance_target": "Target 来自 Oversea FY2627 Monthly FFR 表 King 行", "performance_target": "Target 来自 Oversea FY2627 Monthly FFR 表 King 行",
"performance_completion": "完成率 = 实际 ÷ Target × 100;Target 为整月目标" "performance_completion": "完成率 = 实际 ÷ Target × 100,仅用于 Revenue 与 GP;GP rate 为比率,只看与目标的百分点差距"
} }
} }
+18 -9
View File
@@ -36,14 +36,6 @@ td.name{max-width:340px;overflow:hidden;text-overflow:ellipsis}
</style></head><body><main class="shell"> </style></head><body><main class="shell">
<header class="hero"><div><h1>King · Campaign 表现</h1><p id="period">正在加载数据…</p></div><div class="updated" id="updated">数据加载中</div></header> <header class="hero"><div><h1>King · Campaign 表现</h1><p id="period">正在加载数据…</p></div><div class="updated" id="updated">数据加载中</div></header>
<div class="scope">
<label>Campaign <select id="scope"></select></label>
<label>App <select id="appFilter"></select></label>
<label>搜索 <input id="search" type="search" placeholder="Campaign 关键词" autocomplete="off"></label>
<span id="scopeHint"></span>
</div>
<div class="grid" id="kpis"></div>
<section class="panel" id="perfPanel" hidden> <section class="panel" id="perfPanel" hidden>
<div class="panel-head"><div class="panel-title">Target 完成情况</div><span class="hint" id="perfHint"></span></div> <div class="panel-head"><div class="panel-title">Target 完成情况</div><span class="hint" id="perfHint"></span></div>
<div class="table-wrap"><table> <div class="table-wrap"><table>
@@ -52,6 +44,14 @@ td.name{max-width:340px;overflow:hidden;text-overflow:ellipsis}
<div class="legend" id="perfNote"></div> <div class="legend" id="perfNote"></div>
</section> </section>
<div class="scope">
<label>Campaign <select id="scope"></select></label>
<label>App <select id="appFilter"></select></label>
<label>搜索 <input id="search" type="search" placeholder="Campaign 关键词" autocomplete="off"></label>
<span id="scopeHint"></span>
</div>
<div class="grid" id="kpis"></div>
<section class="panel"> <section class="panel">
<div class="panel-head"><div class="panel-title">Install 与 Cost 趋势</div></div> <div class="panel-head"><div class="panel-title">Install 与 Cost 趋势</div></div>
<div class="chart-wrap"><svg id="mainChart" class="chart" viewBox="0 0 1120 380" role="img" aria-label="Install 与 Cost 趋势图"></svg><div class="tip" id="mainTip"></div></div> <div class="chart-wrap"><svg id="mainChart" class="chart" viewBox="0 0 1120 380" role="img" aria-label="Install 与 Cost 趋势图"></svg><div class="tip" id="mainTip"></div></div>
@@ -232,10 +232,17 @@ function renderPerformance(){
const color=diff>=0?'#23a673':'#e4568b'; const color=diff>=0?'#23a673':'#e4568b';
return `<span style="color:${color}">${diff>=0?'+':''}${num(diff,2)} pt</span>`; return `<span style="color:${color}">${diff>=0?'+':''}${num(diff,2)} pt</span>`;
}; };
/* 比率不做完成率,只看与目标的百分点差距 */
const ratePoints=(actualRate,targetRate)=>{
if(!ok(actualRate)||!ok(targetRate))return '—';
const diff=Number(actualRate)-Number(targetRate);
const color=diff>=0?'#23a673':'#e4568b';
return `<span style="color:${color}" title="实际 GP rate 与目标的差距">${diff>=0?'+':''}${num(diff,2)} pt</span>`;
};
const rows=[ const rows=[
['Revenue',usd(target.revenue),usd(actual.revenue),pct(done.revenue),gap(done.revenue)], ['Revenue',usd(target.revenue),usd(actual.revenue),pct(done.revenue),gap(done.revenue)],
['GP',usd(target.gp),usd(actual.gp),pct(done.gp),gap(done.gp)], ['GP',usd(target.gp),usd(actual.gp),pct(done.gp),gap(done.gp)],
['GP rate',pct(target.gp_rate),pct(actual.gp_rate),pct(done.gp_rate),'—'] ['GP rate',pct(target.gp_rate),pct(actual.gp_rate),'—',ratePoints(actual.gp_rate,target.gp_rate)]
]; ];
$('perfRows').innerHTML=rows.map(row=>`<tr><td>${row[0]}</td><td>${row[1]}</td><td>${row[2]}</td><td>${row[3]}</td><td>${row[4]}</td></tr>`).join('') $('perfRows').innerHTML=rows.map(row=>`<tr><td>${row[0]}</td><td>${row[1]}</td><td>${row[2]}</td><td>${row[3]}</td><td>${row[4]}</td></tr>`).join('')
+`<tr class="total-row"><td>Cost(Sands)</td><td>—</td><td>${usd(actual.cost)}</td><td>—</td><td>—</td></tr>`; +`<tr class="total-row"><td>Cost(Sands)</td><td>—</td><td>${usd(actual.cost)}</td><td>—</td><td>—</td></tr>`;
@@ -249,6 +256,8 @@ function renderPerformance(){
else{ else{
if(sands.fetched_at)notes.push(`Sands Cost 抓取于 ${esc(String(sands.fetched_at).replace('T',' '))},不随每日自动更新`); if(sands.fetched_at)notes.push(`Sands Cost 抓取于 ${esc(String(sands.fetched_at).replace('T',' '))},不随每日自动更新`);
if(sands.scope)notes.push(`Cost 范围:${esc(sands.scope)}`); if(sands.scope)notes.push(`Cost 范围:${esc(sands.scope)}`);
if(sands.carried_over)notes.push('<b style="color:#b45309">Cost 沿用上次抓取值,需本地重新抓取才会更新</b>');
if(sands.stale)notes.push(`<b style="color:#b42318">Cost 数据滞后:抓取于 ${esc(sands.fetched_date||'')},周期到 ${esc((report.period||{}).end||'')},滞后约 ${num(sands.lag_days)} 天,GP 会偏高</b>`);
if(Array.isArray(sands.missing_pkgs)&&sands.missing_pkgs.length) if(Array.isArray(sands.missing_pkgs)&&sands.missing_pkgs.length)
notes.push(`<b style="color:#b42318">Cost 不完整,缺少包:${esc(sands.missing_pkgs.join(', '))}</b>`); notes.push(`<b style="color:#b42318">Cost 不完整,缺少包:${esc(sands.missing_pkgs.join(', '))}</b>`);
} }
-1
View File
@@ -1 +0,0 @@
24
-13
View File
@@ -1,13 +0,0 @@
#!/bin/sh
set -eu
# 平台按 schedule.conf 每 24 小时执行一次。
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
TMP_FILE="$SCRIPT_DIR/data.json.tmp"
OUTPUT_FILE="$SCRIPT_DIR/data.json"
rm -f "$TMP_FILE"
python3 "$SCRIPT_DIR/update_report_data.py" "$TMP_FILE"
mv "$TMP_FILE" "$OUTPUT_FILE"
echo "Updated $OUTPUT_FILE"
+28 -2
View File
@@ -364,6 +364,18 @@ def load_sands_cost():
] ]
path = next((item for item in candidates if item.exists()), None) path = next((item for item in candidates if item.exists()), None)
if path is None: if path is None:
# 服务器上没有 sands_cost.json(Sands 需登录),沿用上一版 data.json 里的值
previous = here.parent / "data.json"
if previous.exists():
try:
sands = (json.loads(previous.read_text(encoding="utf-8")).get("performance") or {}).get("sands")
except Exception:
sands = None
if sands and sands.get("cost"):
sands = dict(sands)
sands["carried_over"] = True
print(f" Sands Cost:沿用上一版 {sands['cost']}(抓取于 {sands.get('fetched_at') or '未知'})")
return sands
print(" Sands Cost:未找到 sands_cost.json,将留空") print(" Sands Cost:未找到 sands_cost.json,将留空")
return None return None
data = json.loads(path.read_text(encoding="utf-8")) data = json.loads(path.read_text(encoding="utf-8"))
@@ -413,7 +425,8 @@ def build_performance(revenue, sands, targets, period_days, month_days):
"completion": { "completion": {
"revenue": ratio(revenue, targets.get("revenue") if targets else None), "revenue": ratio(revenue, targets.get("revenue") if targets else None),
"gp": ratio(gp, targets.get("gp") if targets else None), "gp": ratio(gp, targets.get("gp") if targets else None),
"gp_rate": ratio(gp_rate, target_gp_rate), # GP rate 是比率,不计算完成率;页面显示与目标的百分点差距
"gp_rate": None,
}, },
"time_progress_percent": round(period_days / month_days * 100, 2) if month_days else None, "time_progress_percent": round(period_days / month_days * 100, 2) if month_days else None,
"month_days": month_days, "month_days": month_days,
@@ -445,6 +458,19 @@ def build_payload(now):
except Exception as error: except Exception as error:
print(f" Target 读取失败({error});请把该表共享给服务账号或开放链接查看") print(f" Target 读取失败({error});请把该表共享给服务账号或开放链接查看")
sands = load_sands_cost() sands = load_sands_cost()
# 判断 Cost 是否滞后于数据周期
if sands and sands.get("fetched_at"):
try:
fetched = datetime.fromisoformat(sands["fetched_at"]).date()
except ValueError:
fetched = None
if fetched:
lag = (end_date - fetched).days + 1
sands["fetched_date"] = fetched.isoformat()
sands["lag_days"] = max(0, lag)
sands["stale"] = lag > 0
if sands["stale"]:
print(f" 警告:Sands Cost 抓取于 {fetched},可能未覆盖到 {end_date},滞后约 {lag} 天")
# 消耗表里可能出现 Cost_Client 未覆盖的 Campaign,一并纳入 # 消耗表里可能出现 Cost_Client 未覆盖的 Campaign,一并纳入
for key, _day in wide_costs: for key, _day in wide_costs:
@@ -546,7 +572,7 @@ def build_payload(now):
"performance_cost": "Cost 取 Sands Report 2.0 的 Cost 合计,需本地抓取", "performance_cost": "Cost 取 Sands Report 2.0 的 Cost 合计,需本地抓取",
"performance_gp": "GP = Revenue − Cost;GP rate = GP ÷ Revenue × 100", "performance_gp": "GP = Revenue − Cost;GP rate = GP ÷ Revenue × 100",
"performance_target": "Target 来自 Oversea FY2627 Monthly FFR 表 King 行", "performance_target": "Target 来自 Oversea FY2627 Monthly FFR 表 King 行",
"performance_completion": "完成率 = 实际 ÷ Target × 100;Target 为整月目标", "performance_completion": "完成率 = 实际 ÷ Target × 100,仅用于 Revenue 与 GP;GP rate 为比率,只看与目标的百分点差距",
}, },
} }