Compare commits
9
Commits
82e64d1512
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ef5d5f1a7 | ||
|
|
f62a78b8ca | ||
|
|
fc3fdb4764 | ||
|
|
1b3faaf925 | ||
|
|
d2b6417e80 | ||
|
|
9960b516da | ||
|
|
15748d4ee5 | ||
|
|
f53fd78b2a | ||
|
|
03944f2aef |
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"title": "Ozon · 过去14天国家投放表现",
|
"title": "Ozon · 当月投放表现",
|
||||||
"generated_at": "2026-08-24T15:42:16+08:00",
|
"generated_at": "2026-08-26T16:26:52+08:00",
|
||||||
"timezone": "Asia/Shanghai",
|
"timezone": "Asia/Shanghai",
|
||||||
"period": {
|
"period": {
|
||||||
"start": "2026-08-10",
|
"start": "2026-08-01",
|
||||||
"end": "2026-08-23",
|
"end": "2026-08-25",
|
||||||
"days": 14
|
"days": 25
|
||||||
},
|
},
|
||||||
"countries": [
|
"countries": [
|
||||||
"KZ",
|
"KZ",
|
||||||
@@ -13,433 +13,784 @@
|
|||||||
"KG",
|
"KG",
|
||||||
"AM"
|
"AM"
|
||||||
],
|
],
|
||||||
|
"kpis": {
|
||||||
|
"TOTAL": {
|
||||||
|
"installs": 40028.0,
|
||||||
|
"revenue": 24820.6,
|
||||||
|
"budget_usd": 33930.0,
|
||||||
|
"ffr_percent": 69.72
|
||||||
|
},
|
||||||
|
"KZ": {
|
||||||
|
"installs": 24033.0,
|
||||||
|
"revenue": 16823.1,
|
||||||
|
"budget_usd": 23450.0,
|
||||||
|
"ffr_percent": 71.74
|
||||||
|
},
|
||||||
|
"UZ": {
|
||||||
|
"installs": 10048.0,
|
||||||
|
"revenue": 5024.0,
|
||||||
|
"budget_usd": 7400.0,
|
||||||
|
"ffr_percent": 67.89
|
||||||
|
},
|
||||||
|
"KG": {
|
||||||
|
"installs": 3619.0,
|
||||||
|
"revenue": 1809.5,
|
||||||
|
"budget_usd": 3080.0,
|
||||||
|
"ffr_percent": 58.75
|
||||||
|
},
|
||||||
|
"AM": {
|
||||||
|
"installs": 2328.0,
|
||||||
|
"revenue": 1164.0,
|
||||||
|
"budget_usd": 1850.0,
|
||||||
|
"ffr_percent": 62.92
|
||||||
|
}
|
||||||
|
},
|
||||||
"summary": [
|
"summary": [
|
||||||
{
|
{
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 12980.0,
|
"installs": 24033.0,
|
||||||
"spend": 9086.0,
|
"revenue": 16823.1,
|
||||||
"avg_cac": 807.3846,
|
"avg_cac": 743.625,
|
||||||
"cac_days": 13
|
"cac_days": 24,
|
||||||
|
"budget_usd": 23450.0,
|
||||||
|
"ffr_percent": 71.74
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 5630.0,
|
"installs": 10048.0,
|
||||||
"spend": 2815.0,
|
"revenue": 5024.0,
|
||||||
"avg_cac": 5723.6923,
|
"avg_cac": 4621.5833,
|
||||||
"cac_days": 13
|
"cac_days": 24,
|
||||||
|
"budget_usd": 7400.0,
|
||||||
|
"ffr_percent": 67.89
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 1724.0,
|
"installs": 3619.0,
|
||||||
"spend": 862.0,
|
"revenue": 1809.5,
|
||||||
"avg_cac": 5302.0,
|
"avg_cac": 5340.2917,
|
||||||
"cac_days": 10
|
"cac_days": 24,
|
||||||
|
"budget_usd": 3080.0,
|
||||||
|
"ffr_percent": 58.75
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 971.0,
|
"installs": 2328.0,
|
||||||
"spend": 485.5,
|
"revenue": 1164.0,
|
||||||
"avg_cac": 3127.553,
|
"avg_cac": 2888.4008,
|
||||||
"cac_days": 10
|
"cac_days": 24,
|
||||||
|
"budget_usd": 1850.0,
|
||||||
|
"ffr_percent": 62.92
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"records": [
|
"records": [
|
||||||
|
{
|
||||||
|
"date": "2026-08-01",
|
||||||
|
"country": "KZ",
|
||||||
|
"installs": 890.0,
|
||||||
|
"revenue": 623.0,
|
||||||
|
"cac": 640.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-02",
|
||||||
|
"country": "KZ",
|
||||||
|
"installs": 893.0,
|
||||||
|
"revenue": 625.1,
|
||||||
|
"cac": 486.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-03",
|
||||||
|
"country": "KZ",
|
||||||
|
"installs": 939.0,
|
||||||
|
"revenue": 657.3,
|
||||||
|
"cac": 592.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-04",
|
||||||
|
"country": "KZ",
|
||||||
|
"installs": 927.0,
|
||||||
|
"revenue": 648.9,
|
||||||
|
"cac": 620.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-05",
|
||||||
|
"country": "KZ",
|
||||||
|
"installs": 928.0,
|
||||||
|
"revenue": 649.6,
|
||||||
|
"cac": 542.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-06",
|
||||||
|
"country": "KZ",
|
||||||
|
"installs": 982.0,
|
||||||
|
"revenue": 687.4,
|
||||||
|
"cac": 672.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-07",
|
||||||
|
"country": "KZ",
|
||||||
|
"installs": 997.0,
|
||||||
|
"revenue": 697.9,
|
||||||
|
"cac": 561.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-08",
|
||||||
|
"country": "KZ",
|
||||||
|
"installs": 969.0,
|
||||||
|
"revenue": 678.3,
|
||||||
|
"cac": 573.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-09",
|
||||||
|
"country": "KZ",
|
||||||
|
"installs": 1055.0,
|
||||||
|
"revenue": 738.5,
|
||||||
|
"cac": 838.0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-10",
|
"date": "2026-08-10",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 1075.0,
|
"installs": 1075.0,
|
||||||
"spend": 752.5,
|
"revenue": 752.5,
|
||||||
"cac": 636.0
|
"cac": 636.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-11",
|
"date": "2026-08-11",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 1027.0,
|
"installs": 1027.0,
|
||||||
"spend": 718.9,
|
"revenue": 718.9,
|
||||||
"cac": 745.0
|
"cac": 745.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-12",
|
"date": "2026-08-12",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 1034.0,
|
"installs": 1034.0,
|
||||||
"spend": 723.8,
|
"revenue": 723.8,
|
||||||
"cac": 954.0
|
"cac": 929.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-13",
|
"date": "2026-08-13",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 1097.0,
|
"installs": 1097.0,
|
||||||
"spend": 767.9,
|
"revenue": 767.9,
|
||||||
"cac": 837.0
|
"cac": 837.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-14",
|
"date": "2026-08-14",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 1072.0,
|
"installs": 1072.0,
|
||||||
"spend": 750.4,
|
"revenue": 750.4,
|
||||||
"cac": 724.0
|
"cac": 724.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-15",
|
"date": "2026-08-15",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 718.0,
|
"installs": 718.0,
|
||||||
"spend": 502.6,
|
"revenue": 502.6,
|
||||||
"cac": 837.0
|
"cac": 788.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-16",
|
"date": "2026-08-16",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 675.0,
|
"installs": 675.0,
|
||||||
"spend": 472.5,
|
"revenue": 472.5,
|
||||||
"cac": 1062.0
|
"cac": 1062.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-17",
|
"date": "2026-08-17",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 715.0,
|
"installs": 715.0,
|
||||||
"spend": 500.5,
|
"revenue": 500.5,
|
||||||
"cac": 702.0
|
"cac": 683.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-18",
|
"date": "2026-08-18",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 682.0,
|
"installs": 682.0,
|
||||||
"spend": 477.4,
|
"revenue": 477.4,
|
||||||
"cac": 677.0
|
"cac": 677.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-19",
|
"date": "2026-08-19",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 612.0,
|
"installs": 612.0,
|
||||||
"spend": 428.4,
|
"revenue": 428.4,
|
||||||
"cac": 597.0
|
"cac": 582.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-20",
|
"date": "2026-08-20",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 602.0,
|
"installs": 602.0,
|
||||||
"spend": 421.4,
|
"revenue": 421.4,
|
||||||
"cac": 770.0
|
"cac": 770.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-21",
|
"date": "2026-08-21",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 1117.0,
|
"installs": 1117.0,
|
||||||
"spend": 781.9,
|
"revenue": 781.9,
|
||||||
"cac": 909.0
|
"cac": 851.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-22",
|
"date": "2026-08-22",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 1298.0,
|
"installs": 1298.0,
|
||||||
"spend": 908.6,
|
"revenue": 908.6,
|
||||||
"cac": 1046.0
|
"cac": 856.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-23",
|
"date": "2026-08-23",
|
||||||
"country": "KZ",
|
"country": "KZ",
|
||||||
"installs": 1256.0,
|
"installs": 1256.0,
|
||||||
"spend": 879.2,
|
"revenue": 879.2,
|
||||||
|
"cac": 1237.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-24",
|
||||||
|
"country": "KZ",
|
||||||
|
"installs": 1215.0,
|
||||||
|
"revenue": 850.5,
|
||||||
|
"cac": 946.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-25",
|
||||||
|
"country": "KZ",
|
||||||
|
"installs": 1258.0,
|
||||||
|
"revenue": 880.6,
|
||||||
"cac": null
|
"cac": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-01",
|
||||||
|
"country": "UZ",
|
||||||
|
"installs": 248.0,
|
||||||
|
"revenue": 124.0,
|
||||||
|
"cac": 3449.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-02",
|
||||||
|
"country": "UZ",
|
||||||
|
"installs": 278.0,
|
||||||
|
"revenue": 139.0,
|
||||||
|
"cac": 1737.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-03",
|
||||||
|
"country": "UZ",
|
||||||
|
"installs": 249.0,
|
||||||
|
"revenue": 124.5,
|
||||||
|
"cac": 3574.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-04",
|
||||||
|
"country": "UZ",
|
||||||
|
"installs": 265.0,
|
||||||
|
"revenue": 132.5,
|
||||||
|
"cac": 2146.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-05",
|
||||||
|
"country": "UZ",
|
||||||
|
"installs": 345.0,
|
||||||
|
"revenue": 172.5,
|
||||||
|
"cac": 3618.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-06",
|
||||||
|
"country": "UZ",
|
||||||
|
"installs": 374.0,
|
||||||
|
"revenue": 187.0,
|
||||||
|
"cac": 4598.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-07",
|
||||||
|
"country": "UZ",
|
||||||
|
"installs": 349.0,
|
||||||
|
"revenue": 174.5,
|
||||||
|
"cac": 2519.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-08",
|
||||||
|
"country": "UZ",
|
||||||
|
"installs": 402.0,
|
||||||
|
"revenue": 201.0,
|
||||||
|
"cac": 2416.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-09",
|
||||||
|
"country": "UZ",
|
||||||
|
"installs": 400.0,
|
||||||
|
"revenue": 200.0,
|
||||||
|
"cac": 3257.0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-10",
|
"date": "2026-08-10",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 550.0,
|
"installs": 550.0,
|
||||||
"spend": 275.0,
|
"revenue": 275.0,
|
||||||
"cac": 4493.0
|
"cac": 4493.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-11",
|
"date": "2026-08-11",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 614.0,
|
"installs": 614.0,
|
||||||
"spend": 307.0,
|
"revenue": 307.0,
|
||||||
"cac": 5103.0
|
"cac": 4536.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-12",
|
"date": "2026-08-12",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 614.0,
|
"installs": 614.0,
|
||||||
"spend": 307.0,
|
"revenue": 307.0,
|
||||||
"cac": 3349.0
|
"cac": 3349.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-13",
|
"date": "2026-08-13",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 608.0,
|
"installs": 608.0,
|
||||||
"spend": 304.0,
|
"revenue": 304.0,
|
||||||
"cac": 7011.0
|
"cac": 7011.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-14",
|
"date": "2026-08-14",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 587.0,
|
"installs": 587.0,
|
||||||
"spend": 293.5,
|
"revenue": 293.5,
|
||||||
"cac": 2545.0
|
"cac": 2545.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-15",
|
"date": "2026-08-15",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 224.0,
|
"installs": 224.0,
|
||||||
"spend": 112.0,
|
"revenue": 112.0,
|
||||||
"cac": 3768.0
|
"cac": 3768.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-16",
|
"date": "2026-08-16",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 176.0,
|
"installs": 176.0,
|
||||||
"spend": 88.0,
|
"revenue": 88.0,
|
||||||
"cac": 0.0
|
"cac": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-17",
|
"date": "2026-08-17",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 211.0,
|
"installs": 211.0,
|
||||||
"spend": 105.5,
|
"revenue": 105.5,
|
||||||
"cac": 8147.0
|
"cac": 8147.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-18",
|
"date": "2026-08-18",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 170.0,
|
"installs": 170.0,
|
||||||
"spend": 85.0,
|
"revenue": 85.0,
|
||||||
"cac": 20293.0
|
"cac": 20293.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-19",
|
"date": "2026-08-19",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 141.0,
|
"installs": 141.0,
|
||||||
"spend": 70.5,
|
"revenue": 70.5,
|
||||||
"cac": 8912.0
|
"cac": 8912.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-20",
|
"date": "2026-08-20",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 124.0,
|
"installs": 124.0,
|
||||||
"spend": 62.0,
|
"revenue": 62.0,
|
||||||
"cac": 3829.0
|
"cac": 3829.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-21",
|
"date": "2026-08-21",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 509.0,
|
"installs": 509.0,
|
||||||
"spend": 254.5,
|
"revenue": 254.5,
|
||||||
"cac": 3233.0
|
"cac": 2985.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-22",
|
"date": "2026-08-22",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 530.0,
|
"installs": 530.0,
|
||||||
"spend": 265.0,
|
"revenue": 265.0,
|
||||||
"cac": 3725.0
|
"cac": 3415.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-23",
|
"date": "2026-08-23",
|
||||||
"country": "UZ",
|
"country": "UZ",
|
||||||
"installs": 572.0,
|
"installs": 572.0,
|
||||||
"spend": 286.0,
|
"revenue": 286.0,
|
||||||
|
"cac": 5181.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-24",
|
||||||
|
"country": "UZ",
|
||||||
|
"installs": 704.0,
|
||||||
|
"revenue": 352.0,
|
||||||
|
"cac": 5140.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-25",
|
||||||
|
"country": "UZ",
|
||||||
|
"installs": 804.0,
|
||||||
|
"revenue": 402.0,
|
||||||
"cac": null
|
"cac": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-01",
|
||||||
|
"country": "KG",
|
||||||
|
"installs": 150.0,
|
||||||
|
"revenue": 75.0,
|
||||||
|
"cac": 2203.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-02",
|
||||||
|
"country": "KG",
|
||||||
|
"installs": 150.0,
|
||||||
|
"revenue": 75.0,
|
||||||
|
"cac": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-03",
|
||||||
|
"country": "KG",
|
||||||
|
"installs": 143.0,
|
||||||
|
"revenue": 71.5,
|
||||||
|
"cac": 5062.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-04",
|
||||||
|
"country": "KG",
|
||||||
|
"installs": 162.0,
|
||||||
|
"revenue": 81.0,
|
||||||
|
"cac": 3550.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-05",
|
||||||
|
"country": "KG",
|
||||||
|
"installs": 165.0,
|
||||||
|
"revenue": 82.5,
|
||||||
|
"cac": 11642.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-06",
|
||||||
|
"country": "KG",
|
||||||
|
"installs": 176.0,
|
||||||
|
"revenue": 88.0,
|
||||||
|
"cac": 7450.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-07",
|
||||||
|
"country": "KG",
|
||||||
|
"installs": 170.0,
|
||||||
|
"revenue": 85.0,
|
||||||
|
"cac": 7380.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-08",
|
||||||
|
"country": "KG",
|
||||||
|
"installs": 162.0,
|
||||||
|
"revenue": 81.0,
|
||||||
|
"cac": 5100.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-09",
|
||||||
|
"country": "KG",
|
||||||
|
"installs": 171.0,
|
||||||
|
"revenue": 85.5,
|
||||||
|
"cac": 2684.0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-10",
|
"date": "2026-08-10",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 197.0,
|
"installs": 197.0,
|
||||||
"spend": 98.5,
|
"revenue": 98.5,
|
||||||
"cac": 2186.0
|
"cac": 3278.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-11",
|
"date": "2026-08-11",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 196.0,
|
"installs": 196.0,
|
||||||
"spend": 98.0,
|
"revenue": 98.0,
|
||||||
"cac": 2537.0
|
"cac": 6765.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-12",
|
"date": "2026-08-12",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 186.0,
|
"installs": 186.0,
|
||||||
"spend": 93.0,
|
"revenue": 93.0,
|
||||||
"cac": 3633.0
|
"cac": 7265.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-13",
|
"date": "2026-08-13",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 179.0,
|
"installs": 179.0,
|
||||||
"spend": 89.5,
|
"revenue": 89.5,
|
||||||
"cac": 20857.0
|
"cac": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-14",
|
"date": "2026-08-14",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 167.0,
|
"installs": 167.0,
|
||||||
"spend": 83.5,
|
"revenue": 83.5,
|
||||||
"cac": 3520.0
|
"cac": 5280.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-15",
|
"date": "2026-08-15",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 31.0,
|
"installs": 31.0,
|
||||||
"spend": 15.5,
|
"revenue": 15.5,
|
||||||
"cac": 2545.0
|
"cac": 12724.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-16",
|
"date": "2026-08-16",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 26.0,
|
"installs": 26.0,
|
||||||
"spend": 13.0,
|
"revenue": 13.0,
|
||||||
"cac": 15506.0
|
"cac": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-17",
|
"date": "2026-08-17",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 30.0,
|
"installs": 30.0,
|
||||||
"spend": 15.0,
|
"revenue": 15.0,
|
||||||
"cac": 0.0
|
"cac": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-18",
|
"date": "2026-08-18",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 31.0,
|
"installs": 31.0,
|
||||||
"spend": 15.5,
|
"revenue": 15.5,
|
||||||
"cac": 0.0
|
"cac": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-19",
|
"date": "2026-08-19",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 22.0,
|
"installs": 22.0,
|
||||||
"spend": 11.0,
|
"revenue": 11.0,
|
||||||
"cac": 2236.0
|
"cac": 4471.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-20",
|
"date": "2026-08-20",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 28.0,
|
"installs": 28.0,
|
||||||
"spend": 14.0,
|
"revenue": 14.0,
|
||||||
"cac": null
|
"cac": 6257.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-21",
|
"date": "2026-08-21",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 192.0,
|
"installs": 192.0,
|
||||||
"spend": 96.0,
|
"revenue": 96.0,
|
||||||
"cac": null
|
"cac": 15812.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-22",
|
"date": "2026-08-22",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 210.0,
|
"installs": 210.0,
|
||||||
"spend": 105.0,
|
"revenue": 105.0,
|
||||||
"cac": null
|
"cac": 9055.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-23",
|
"date": "2026-08-23",
|
||||||
"country": "KG",
|
"country": "KG",
|
||||||
"installs": 229.0,
|
"installs": 229.0,
|
||||||
"spend": 114.5,
|
"revenue": 114.5,
|
||||||
|
"cac": 6249.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-24",
|
||||||
|
"country": "KG",
|
||||||
|
"installs": 230.0,
|
||||||
|
"revenue": 115.0,
|
||||||
|
"cac": 5940.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-25",
|
||||||
|
"country": "KG",
|
||||||
|
"installs": 216.0,
|
||||||
|
"revenue": 108.0,
|
||||||
"cac": null
|
"cac": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-01",
|
||||||
|
"country": "AM",
|
||||||
|
"installs": 84.0,
|
||||||
|
"revenue": 42.0,
|
||||||
|
"cac": 2308.42
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-02",
|
||||||
|
"country": "AM",
|
||||||
|
"installs": 81.0,
|
||||||
|
"revenue": 40.5,
|
||||||
|
"cac": 2224.98
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-03",
|
||||||
|
"country": "AM",
|
||||||
|
"installs": 127.0,
|
||||||
|
"revenue": 63.5,
|
||||||
|
"cac": 3587.78
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-04",
|
||||||
|
"country": "AM",
|
||||||
|
"installs": 122.0,
|
||||||
|
"revenue": 61.0,
|
||||||
|
"cac": 1751.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-05",
|
||||||
|
"country": "AM",
|
||||||
|
"installs": 107.0,
|
||||||
|
"revenue": 53.5,
|
||||||
|
"cac": 6019.78
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-06",
|
||||||
|
"country": "AM",
|
||||||
|
"installs": 143.0,
|
||||||
|
"revenue": 71.5,
|
||||||
|
"cac": 4135.48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-07",
|
||||||
|
"country": "AM",
|
||||||
|
"installs": 141.0,
|
||||||
|
"revenue": 70.5,
|
||||||
|
"cac": 4074.46
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-08",
|
||||||
|
"country": "AM",
|
||||||
|
"installs": 136.0,
|
||||||
|
"revenue": 68.0,
|
||||||
|
"cac": 3824.85
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-09",
|
||||||
|
"country": "AM",
|
||||||
|
"installs": 129.0,
|
||||||
|
"revenue": 64.5,
|
||||||
|
"cac": 0.0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-10",
|
"date": "2026-08-10",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 124.0,
|
"installs": 124.0,
|
||||||
"spend": 62.0,
|
"revenue": 62.0,
|
||||||
"cac": 7132.05
|
"cac": 7132.05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-11",
|
"date": "2026-08-11",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 136.0,
|
"installs": 136.0,
|
||||||
"spend": 68.0,
|
"revenue": 68.0,
|
||||||
"cac": 7806.27
|
"cac": 7806.27
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-12",
|
"date": "2026-08-12",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 126.0,
|
"installs": 126.0,
|
||||||
"spend": 63.0,
|
"revenue": 63.0,
|
||||||
"cac": 3517.38
|
"cac": 3517.38
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-13",
|
"date": "2026-08-13",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 124.0,
|
"installs": 124.0,
|
||||||
"spend": 62.0,
|
"revenue": 62.0,
|
||||||
"cac": 7146.1
|
"cac": 7146.1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-14",
|
"date": "2026-08-14",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 112.0,
|
"installs": 112.0,
|
||||||
"spend": 56.0,
|
"revenue": 56.0,
|
||||||
"cac": 1686.59
|
"cac": 2248.79
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-15",
|
"date": "2026-08-15",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 19.0,
|
"installs": 19.0,
|
||||||
"spend": 9.5,
|
"revenue": 9.5,
|
||||||
"cac": 0.0
|
"cac": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-16",
|
"date": "2026-08-16",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 25.0,
|
"installs": 25.0,
|
||||||
"spend": 12.5,
|
"revenue": 12.5,
|
||||||
"cac": 0.0
|
"cac": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-17",
|
"date": "2026-08-17",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 30.0,
|
"installs": 30.0,
|
||||||
"spend": 15.0,
|
"revenue": 15.0,
|
||||||
"cac": 0.0
|
"cac": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-18",
|
"date": "2026-08-18",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 28.0,
|
"installs": 28.0,
|
||||||
"spend": 14.0,
|
"revenue": 14.0,
|
||||||
"cac": 3987.14
|
"cac": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-19",
|
"date": "2026-08-19",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 19.0,
|
"installs": 19.0,
|
||||||
"spend": 9.5,
|
"revenue": 9.5,
|
||||||
"cac": 0.0
|
"cac": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-20",
|
"date": "2026-08-20",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 15.0,
|
"installs": 15.0,
|
||||||
"spend": 7.5,
|
"revenue": 7.5,
|
||||||
"cac": null
|
"cac": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-21",
|
"date": "2026-08-21",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 75.0,
|
"installs": 75.0,
|
||||||
"spend": 37.5,
|
"revenue": 37.5,
|
||||||
"cac": null
|
"cac": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-22",
|
"date": "2026-08-22",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 67.0,
|
"installs": 67.0,
|
||||||
"spend": 33.5,
|
"revenue": 33.5,
|
||||||
"cac": null
|
"cac": 2283.09
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-08-23",
|
"date": "2026-08-23",
|
||||||
"country": "AM",
|
"country": "AM",
|
||||||
"installs": 71.0,
|
"installs": 71.0,
|
||||||
"spend": 35.5,
|
"revenue": 35.5,
|
||||||
|
"cac": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-24",
|
||||||
|
"country": "AM",
|
||||||
|
"installs": 119.0,
|
||||||
|
"revenue": 59.5,
|
||||||
|
"cac": 11260.69
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-08-25",
|
||||||
|
"country": "AM",
|
||||||
|
"installs": 168.0,
|
||||||
|
"revenue": 84.0,
|
||||||
"cac": null
|
"cac": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"installs": "Google Sheet 各国家区块的 Install",
|
"period": "当月 1 号至昨天",
|
||||||
"spend": "Google Sheet 各国家区块的 Daily Spend(USD)",
|
"installs": "各国家区块的 Install",
|
||||||
"cac": "Google Sheet 各国家区块的 CAC from client report;汇总值为有数据日期的日均 CAC"
|
"revenue": "各国家区块的 Revenue(USD),取源表 Daily Spend 列",
|
||||||
|
"cac": "各国家区块的 CAC from client report 原始值;不改算、不标记为 USD",
|
||||||
|
"budget_usd": "源表 Current Month Budget,按 Total 与各国读取",
|
||||||
|
"ffr_percent": "源表 FFR,按 Total 与各国读取"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+249
-27
@@ -1,30 +1,252 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<head>
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"><meta http-equiv="Pragma" content="no-cache"><meta http-equiv="Expires" content="0">
|
||||||
<meta charset="utf-8">
|
<title>Ozon 投放表现</title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<title>Ozon 国家投放表现</title>
|
|
||||||
<style>
|
<style>
|
||||||
:root{--bg:#f4f7fb;--card:#fff;--text:#14213d;--muted:#6b7280;--line:#e6eaf0;--blue:#5577ee;--shadow:0 8px 30px rgba(24,39,75,.06)}
|
:root{--bg:#f4f7fb;--card:#fff;--text:#14213d;--muted:#6b7280;--line:#e5eaf1;--blue:#5577ee;--green:#23a673;--pink:#e4568b}
|
||||||
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,"Microsoft YaHei",system-ui,sans-serif}.shell{max-width:1380px;margin:auto;padding:30px}.hero{background:linear-gradient(125deg,#111827,#26355e);color:white;border-radius:22px;padding:28px 30px;display:flex;justify-content:space-between;gap:20px;align-items:end;box-shadow:0 16px 40px rgba(17,24,39,.15)}h1{margin:0 0 8px;font-size:29px}.hero p{margin:0;color:#cbd5e1;font-size:13px}.updated{text-align:right;font-size:12px;color:#cbd5e1;line-height:1.6}.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:18px 0}.card,.panel{background:var(--card);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow)}.card{padding:18px}.label{font-size:12px;color:var(--muted);margin-bottom:8px}.value{font-size:26px;font-weight:800}.hint{font-size:11px;color:#94a3b8;margin-top:6px}.panel{padding:20px;margin-top:15px}.panel-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:16px}.panel-title{font-size:17px;font-weight:800}.controls{display:flex;gap:9px;flex-wrap:wrap}button,select{border:1px solid var(--line);background:white;color:var(--text);padding:9px 12px;border-radius:9px;font:inherit;cursor:pointer}button.active{background:var(--blue);border-color:var(--blue);color:white}.chart-wrap{width:100%;overflow:auto}.chart{width:100%;min-width:760px;height:auto;display:block}.legend{display:flex;justify-content:center;gap:18px;flex-wrap:wrap;margin-top:8px;font-size:12px}.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px}.tables{display:grid;grid-template-columns:.8fr 1.2fr;gap:15px}.table-wrap{overflow:auto;max-height:560px;border:1px solid var(--line);border-radius:11px}table{border-collapse:separate;border-spacing:0;width:100%;font-size:12px}th,td{padding:10px 12px;border-bottom:1px solid #edf0f5;text-align:right;white-space:nowrap}th{position:sticky;top:0;background:#f8fafc;color:#64748b;z-index:1}th:first-child,td:first-child{text-align:left}.country{font-weight:800}.error{padding:50px;text-align:center;color:#b42318}.footer{text-align:center;color:#94a3b8;font-size:11px;margin:20px}.skeleton{animation:pulse 1.2s infinite;background:#e9edf5;color:transparent;border-radius:5px}@keyframes pulse{50%{opacity:.55}}@media(max-width:950px){.grid{grid-template-columns:repeat(2,1fr)}.tables{grid-template-columns:1fr}}@media(max-width:620px){.shell{padding:14px}.hero{display:block}.updated{text-align:left;margin-top:14px}.grid{grid-template-columns:1fr 1fr}.value{font-size:21px}}
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,"Microsoft YaHei",system-ui,sans-serif}
|
||||||
</style>
|
.shell{max-width:1380px;margin:auto;padding:26px}
|
||||||
</head>
|
.hero{display:flex;justify-content:space-between;gap:18px;align-items:end;padding:24px 27px;border-radius:20px;color:#fff;background:linear-gradient(125deg,#111827,#26355e)}
|
||||||
<body>
|
h1{margin:0 0 6px;font-size:27px}.hero p,.updated{margin:0;color:#cbd5e1;font-size:12px;line-height:1.7}.updated{text-align:right}
|
||||||
<div class="shell">
|
.scope{display:flex;align-items:center;gap:10px;margin:16px 0 0;font-size:13px;color:var(--muted)}
|
||||||
<header class="hero"><div><h1>Ozon · 国家投放表现</h1><p id="period">正在加载过去 14 天数据…</p></div><div class="updated" id="updated">数据加载中</div></header>
|
select{border:1px solid var(--line);background:#fff;padding:8px 11px;border-radius:9px;color:var(--text);font:inherit}
|
||||||
<div class="grid" id="kpis"><div class="card"><div class="label">总安装</div><div class="value skeleton">00000</div></div></div>
|
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:13px;margin:13px 0}
|
||||||
<section class="panel"><div class="panel-head"><div class="panel-title">14 天趋势</div><div class="controls" id="metricButtons"><button data-metric="installs" class="active">安装数</button><button data-metric="spend">消耗</button><button data-metric="cac">CAC</button></div></div><div class="chart-wrap"><svg class="chart" id="chart" viewBox="0 0 1100 390" role="img" aria-label="国家趋势图"></svg></div><div class="legend" id="legend"></div></section>
|
.card,.panel{background:var(--card);border:1px solid var(--line);border-radius:15px;box-shadow:0 8px 28px rgba(24,39,75,.05)}
|
||||||
<div class="tables"><section class="panel"><div class="panel-head"><div class="panel-title">国家汇总</div></div><div class="table-wrap"><table><thead><tr><th>国家</th><th>安装数</th><th>消耗</th><th>日均 CAC</th></tr></thead><tbody id="summaryRows"></tbody></table></div></section><section class="panel"><div class="panel-head"><div class="panel-title">每日明细</div><select id="countryFilter"><option value="">全部国家</option></select></div><div class="table-wrap"><table><thead><tr><th>日期</th><th>国家</th><th>安装数</th><th>消耗</th><th>CAC</th></tr></thead><tbody id="detailRows"></tbody></table></div></section></div>
|
.card{padding:17px}.label{font-size:12px;color:var(--muted)}.value{font-size:26px;font-weight:800;margin-top:6px}.hint{font-size:11px;color:#94a3b8;margin-top:5px}
|
||||||
<div class="footer">数据来源:Google Sheets · Ozon_IronSpace · 每 24 小时自动更新</div>
|
.panel{padding:18px;margin-top:14px}.panel-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px}.panel-title{font-size:17px;font-weight:800}
|
||||||
</div>
|
.chart-wrap{position:relative;overflow:auto}.chart{display:block;width:100%;min-width:760px;height:auto}
|
||||||
|
.tip{position:absolute;pointer-events:none;opacity:0;transform:translate(-50%,-100%);background:rgba(17,24,39,.94);color:#fff;font-size:12px;line-height:1.6;padding:8px 11px;border-radius:9px;white-space:nowrap;z-index:5;transition:opacity .12s}
|
||||||
|
.legend{text-align:center;color:var(--muted);font-size:12px;margin-top:7px}.legend span{margin:0 9px;white-space:nowrap}
|
||||||
|
.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:5px}
|
||||||
|
.tables{display:grid;grid-template-columns:1fr 1.25fr;gap:14px}
|
||||||
|
.table-wrap{overflow:auto;max-height:520px;border:1px solid var(--line);border-radius:10px}
|
||||||
|
table{width:100%;border-collapse:separate;border-spacing:0;font-size:12px}
|
||||||
|
th,td{padding:9px 11px;text-align:right;white-space:nowrap;border-bottom:1px solid #edf0f5}
|
||||||
|
th{position:sticky;top:0;background:#f8fafc;color:#64748b;z-index:1}th:first-child,td:first-child{text-align:left}
|
||||||
|
.country{font-weight:800}.total-row{background:#f8fafc;font-weight:800}
|
||||||
|
.error{margin:40px auto;max-width:700px;text-align:center;padding:44px;color:#b42318}
|
||||||
|
.footer{text-align:center;color:#94a3b8;font-size:11px;margin:18px}
|
||||||
|
@media(max-width:900px){.grid{grid-template-columns:repeat(2,1fr)}.tables{grid-template-columns:1fr}}
|
||||||
|
@media(max-width:560px){.shell{padding:12px}.hero{display:block}.updated{text-align:left;margin-top:12px}.grid{gap:9px}.card{padding:13px}.value{font-size:20px}.panel{padding:13px}.panel-head{flex-direction:column;align-items:flex-start}}
|
||||||
|
</style></head><body><main class="shell">
|
||||||
|
<header class="hero"><div><h1>Ozon · 投放表现</h1><p id="period">正在加载数据…</p></div><div class="updated" id="updated">数据加载中</div></header>
|
||||||
|
|
||||||
|
<section class="panel"><div class="panel-title" style="margin-bottom:11px">国家汇总</div><div class="table-wrap"><table><thead><tr><th>国家</th><th>Install</th><th>CPI</th><th>Revenue</th><th>Budget</th><th>Left Budget</th><th>FFR</th><th>L3D avr Install</th><th>Rec. Daily Install</th></tr></thead><tbody id="summaryRows"></tbody></table></div></section>
|
||||||
|
|
||||||
|
<div class="scope"><label>统计范围 <select id="scope"></select></label><span id="scopeHint"></span></div>
|
||||||
|
<div class="grid" id="kpis"></div>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head"><div class="panel-title">Install · Revenue · CAC</div></div>
|
||||||
|
<div class="chart-wrap"><svg id="mainChart" class="chart" viewBox="0 0 1100 380" role="img" aria-label="Install、Revenue 与 CAC 趋势图"></svg><div class="tip" id="mainTip"></div></div>
|
||||||
|
<div class="legend"><span><i class="dot" style="background:var(--blue)"></i>Install(左轴,实线)</span><span><i class="dot" style="background:var(--green)"></i>Revenue(右轴 USD,虚线)</span><span><i class="dot" style="background:var(--pink)"></i>CAC(最右轴,源表原始值)</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel"><div class="panel-head"><div class="panel-title">每日明细</div><label>国家 <select id="detailScope"></select></label></div><div class="table-wrap"><table><thead><tr><th>日期</th><th>国家</th><th>Install</th><th>Revenue</th><th>CAC</th></tr></thead><tbody id="detailRows"></tbody></table></div></section>
|
||||||
|
<div class="footer">数据来源:Google Sheets · Ozon_IronSpace · 每 24 小时自动更新</div></main>
|
||||||
<script>
|
<script>
|
||||||
const colors={KZ:'#5577ee',UZ:'#32b67a',KG:'#f59e0b',AM:'#e4568b'};let report=null;let metric='installs';
|
'use strict';
|
||||||
const $=id=>document.getElementById(id);const number=(v,d=0)=>Number(v||0).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d});const money=v=>v==null?'—':'$'+number(v,2);const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
const $=id=>document.getElementById(id),COUNTRIES=['KZ','UZ','KG','AM'],
|
||||||
function render(){const totalI=report.summary.reduce((a,x)=>a+x.installs,0),totalS=report.summary.reduce((a,x)=>a+x.spend,0),cac=report.summary.map(x=>x.avg_cac).filter(x=>x!=null);$('period').textContent=`${report.period.start} 至 ${report.period.end} · ${report.period.days} 个完整自然日`;$('updated').innerHTML=`最近更新:${report.generated_at.replace('T',' ')}<br>时区:${report.timezone}`;$('kpis').innerHTML=[['总安装',number(totalI),'四国合计'],['总消耗',money(totalS),'USD'],['平均 CAC',cac.length?number(cac.reduce((a,b)=>a+b,0)/cac.length,2):'—','来源表原始单位 · 各国家日均值'],['覆盖国家',report.countries.length,'KZ · UZ · KG · AM']].map(x=>`<div class="card"><div class="label">${x[0]}</div><div class="value">${x[1]}</div><div class="hint">${x[2]}</div></div>`).join('');$('summaryRows').innerHTML=report.summary.map(x=>`<tr><td class="country" style="color:${colors[x.country]}">${x.country}</td><td>${number(x.installs)}</td><td>${money(x.spend)}</td><td>${number(x.avg_cac,2)}</td></tr>`).join('');$('countryFilter').innerHTML='<option value="">全部国家</option>'+report.countries.map(x=>`<option>${x}</option>`).join('');renderDetails();drawChart()}
|
colors={KZ:'#5577ee',UZ:'#23a673',KG:'#f59e0b',AM:'#e4568b'},
|
||||||
function renderDetails(){const country=$('countryFilter').value,rows=report.records.filter(x=>!country||x.country===country).sort((a,b)=>b.date.localeCompare(a.date)||a.country.localeCompare(b.country));$('detailRows').innerHTML=rows.map(x=>`<tr><td>${x.date}</td><td class="country" style="color:${colors[x.country]}">${x.country}</td><td>${number(x.installs)}</td><td>${money(x.spend)}</td><td>${x.cac==null?'—':number(x.cac,2)}</td></tr>`).join('')}
|
esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])),
|
||||||
function drawChart(){const svg=$('chart'),W=1100,H=390,m={l:70,r:25,t:30,b:58},iw=W-m.l-m.r,ih=H-m.t-m.b,dates=[...new Set(report.records.map(x=>x.date))].sort(),values=report.records.map(x=>x[metric]).filter(x=>x!=null),max=Math.max(...values,1)*1.12,x=i=>m.l+(dates.length===1?iw/2:i*iw/(dates.length-1)),y=v=>m.t+ih-(v/max)*ih,fmt=metric==='installs'?v=>number(v):(metric==='spend'?v=>money(v):v=>number(v,2));let h='';for(let i=0;i<=5;i++){const yy=m.t+ih*i/5,val=max*(1-i/5);h+=`<line x1="${m.l}" y1="${yy}" x2="${W-m.r}" y2="${yy}" stroke="#e8edf5"/><text x="${m.l-12}" y="${yy+4}" text-anchor="end" fill="#8190a5" font-size="11">${esc(fmt(val))}</text>`}dates.forEach((d,i)=>{if(i%2===0||dates.length<9)h+=`<text x="${x(i)}" y="${H-27}" text-anchor="middle" fill="#8190a5" font-size="11">${d.slice(5)}</text>`});for(const country of report.countries){const rows=dates.map(d=>report.records.find(r=>r.country===country&&r.date===d)),points=rows.map((r,i)=>r&&r[metric]!=null?`${x(i)},${y(r[metric])}`:null);let segment=[];const flush=()=>{if(segment.length>1)h+=`<polyline points="${segment.join(' ')}" fill="none" stroke="${colors[country]}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>`;segment=[]};points.forEach(p=>p?segment.push(p):flush());flush();rows.forEach((r,i)=>{if(r&&r[metric]!=null)h+=`<circle cx="${x(i)}" cy="${y(r[metric])}" r="4" fill="${colors[country]}" stroke="white" stroke-width="2"><title>${country} · ${r.date} · ${fmt(r[metric])}</title></circle>`})}svg.innerHTML=h;$('legend').innerHTML=report.countries.map(x=>`<span><i class="dot" style="background:${colors[x]}"></i>${x}</span>`).join('')}
|
ok=v=>v!==null&&v!==undefined&&v!==''&&Number.isFinite(Number(v)),
|
||||||
document.querySelectorAll('#metricButtons button').forEach(btn=>btn.onclick=()=>{metric=btn.dataset.metric;document.querySelectorAll('#metricButtons button').forEach(x=>x.classList.toggle('active',x===btn));drawChart()});$('countryFilter').onchange=renderDetails;
|
n=v=>ok(v)?Number(v):0,
|
||||||
fetch('data.json',{cache:'no-store'}).then(r=>{if(!r.ok)throw new Error(`HTTP ${r.status}`);return r.json()}).then(data=>{report=data;render()}).catch(error=>{document.querySelector('.shell').innerHTML=`<div class="panel error"><h2>数据加载失败</h2><p>${esc(error.message)}</p><p>请稍后刷新页面。</p></div>`});
|
num=(v,d=0)=>n(v).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d}),
|
||||||
</script>
|
usd=v=>ok(v)?'$'+num(v,2):'—',
|
||||||
</body>
|
pct=v=>ok(v)?num(v,2)+'%':'—';
|
||||||
</html>
|
let report=null,countries=COUNTRIES.slice();
|
||||||
|
|
||||||
|
function records(){return Array.isArray(report.records)?report.records:[]}
|
||||||
|
function summary(){return Array.isArray(report.summary)?report.summary:[]}
|
||||||
|
function hasRevenue(){return records().some(r=>ok(r.revenue)||ok(r.spend))}
|
||||||
|
function scopeRows(scope){const rows=records();return scope==='TOTAL'?rows:rows.filter(r=>r.country===scope)}
|
||||||
|
function dates(){return [...new Set(records().filter(r=>r.date).map(r=>r.date))].sort()}
|
||||||
|
|
||||||
|
/* 按日期聚合所选范围:Install/Revenue/Spend 求和;TOTAL 下 CAC 不合并 */
|
||||||
|
function series(scope){
|
||||||
|
return dates().map(date=>{
|
||||||
|
const rows=scopeRows(scope).filter(r=>r.date===date);
|
||||||
|
const cacRows=rows.filter(r=>ok(r.cac));
|
||||||
|
return {date,
|
||||||
|
installs:rows.reduce((s,r)=>s+n(r.installs),0),
|
||||||
|
revenue:rows.some(r=>ok(r.revenue)||ok(r.spend))?rows.reduce((s,r)=>s+n(ok(r.revenue)?r.revenue:r.spend),0):null,
|
||||||
|
cac:scope==='TOTAL'?null:(cacRows.length?Number(cacRows[0].cac):null)};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function kpi(scope){
|
||||||
|
const stored=(report.kpis&&report.kpis[scope])||null,rows=scopeRows(scope),
|
||||||
|
sums=field=>{const pick=r=>field==='revenue'&&!ok(r.revenue)?r.spend:r[field];
|
||||||
|
return rows.some(r=>ok(pick(r)))?rows.reduce((s,r)=>s+n(pick(r)),0):null},
|
||||||
|
fromSummary=field=>{const list=scope==='TOTAL'?summary():summary().filter(r=>r.country===scope),
|
||||||
|
vals=list.map(r=>r[field]).filter(ok);return vals.length?vals.reduce((a,b)=>a+Number(b),0):null};
|
||||||
|
const legacy=report.kpis||{};
|
||||||
|
return {
|
||||||
|
installs:stored&&ok(stored.installs)?Number(stored.installs):(scope==='TOTAL'&&ok(legacy.total_installs)?Number(legacy.total_installs):sums('installs')),
|
||||||
|
revenue:stored&&ok(stored.revenue)?Number(stored.revenue):(sums('revenue')!==null?sums('revenue'):(scope==='TOTAL'&&ok(legacy.spend_usd)?Number(legacy.spend_usd):null)),
|
||||||
|
budget:stored&&ok(stored.budget_usd)?Number(stored.budget_usd):(scope==='TOTAL'&&ok(legacy.budget_usd)?Number(legacy.budget_usd):fromSummary('budget_usd')),
|
||||||
|
ffr:stored&&ok(stored.ffr_percent)?Number(stored.ffr_percent):(scope==='TOTAL'&&ok(legacy.ffr_percent)?Number(legacy.ffr_percent):(scope==='TOTAL'?null:fromSummary('ffr_percent')))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 月度天数进度:已统计天数 / 当月总天数 */
|
||||||
|
function monthProgress(period){
|
||||||
|
const end=String(period.end||''),match=end.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if(!match||!ok(period.days))return '';
|
||||||
|
const year=Number(match[1]),month=Number(match[2]),
|
||||||
|
totalDays=new Date(year,month,0).getDate(),
|
||||||
|
passed=Math.min(Number(period.days),totalDays);
|
||||||
|
return ` · 月度进度 ${num(passed/totalDays*100,1)}%(${passed}/${totalDays} 天)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillSelect(select,withTotal,totalLabel){
|
||||||
|
select.innerHTML=(withTotal?`<option value="TOTAL">${totalLabel}</option>`:'')+countries.map(c=>`<option value="${c}">${c}</option>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function px(i,count,m,iw){return count>1?m.l+i*iw/(count-1):m.l+iw/2}
|
||||||
|
function empty(svg,message){svg.innerHTML=`<text x="550" y="160" text-anchor="middle" fill="#8190a5" font-size="14">${esc(message)}</text>`}
|
||||||
|
|
||||||
|
function xLabels(rows,m,W,H,iw){
|
||||||
|
return rows.map((row,i)=>(i%2===0||i===rows.length-1)
|
||||||
|
?`<text x="${px(i,rows.length,m,iw)}" y="${H-18}" text-anchor="middle" fill="#8190a5" font-size="11">${esc(String(row.date).slice(5))}</text>`:'').join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawMain(){
|
||||||
|
const scope=$('scope').value,svg=$('mainChart'),rows=series(scope).filter(r=>r);
|
||||||
|
if(!rows.length){empty(svg,'暂无趋势数据');return}
|
||||||
|
const W=1100,H=380,m={l:66,r:132,t:22,b:50},iw=W-m.l-m.r,ih=H-m.t-m.b,
|
||||||
|
maxI=Math.max(1,...rows.map(r=>n(r.installs)))*1.12,
|
||||||
|
revVals=rows.filter(r=>ok(r.revenue)).map(r=>Number(r.revenue)),
|
||||||
|
maxR=revVals.length?Math.max(1,...revVals)*1.12:null,
|
||||||
|
cacVals=rows.filter(r=>ok(r.cac)).map(r=>Number(r.cac)),
|
||||||
|
maxC=cacVals.length?Math.max(1,...cacVals)*1.12:null,
|
||||||
|
x=i=>px(i,rows.length,m,iw),
|
||||||
|
yI=v=>m.t+ih-n(v)/maxI*ih,
|
||||||
|
yR=v=>m.t+ih-Number(v)/maxR*ih,
|
||||||
|
yC=v=>m.t+ih-Number(v)/maxC*ih;
|
||||||
|
let h='';
|
||||||
|
for(let i=0;i<=5;i++){
|
||||||
|
const y=m.t+ih*i/5,ratio=1-i/5;
|
||||||
|
h+=`<line x1="${m.l}" y1="${y}" x2="${W-m.r}" y2="${y}" stroke="#e8edf5"/>`;
|
||||||
|
h+=`<text x="${m.l-9}" y="${y+4}" text-anchor="end" fill="#8190a5" font-size="11">${num(maxI*ratio)}</text>`;
|
||||||
|
if(maxR!=null)h+=`<text x="${W-m.r+8}" y="${y+4}" fill="#23a673" font-size="11">${num(maxR*ratio)}</text>`;
|
||||||
|
if(maxC!=null)h+=`<text x="${W-m.r+72}" y="${y+4}" fill="#e4568b" font-size="11">${num(maxC*ratio)}</text>`;
|
||||||
|
}
|
||||||
|
h+=xLabels(rows,m,W,H,iw);
|
||||||
|
h+=`<polyline points="${rows.map((r,i)=>`${x(i)},${yI(r.installs)}`).join(' ')}" fill="none" stroke="#5577ee" stroke-width="3"/>`;
|
||||||
|
const line=(getY,valid,color,dash)=>{let out='',seg=[];const flush=()=>{if(seg.length>1)out+=`<polyline points="${seg.join(' ')}" fill="none" stroke="${color}" stroke-width="2.5"${dash?` stroke-dasharray="${dash}"`:''}/>`;seg=[]};
|
||||||
|
rows.forEach((r,i)=>{valid(r)?seg.push(`${x(i)},${getY(r)}`):flush()});flush();return out};
|
||||||
|
/* Revenue 常与 Install 成固定比例、走势重合,用虚线让下方的 Install 实线透出 */
|
||||||
|
if(maxR!=null)h+=line(r=>yR(r.revenue),r=>ok(r.revenue),'#23a673','8 5');
|
||||||
|
if(maxC!=null)h+=line(r=>yC(r.cac),r=>ok(r.cac),'#e4568b','3 4');
|
||||||
|
rows.forEach((r,i)=>{
|
||||||
|
if(maxR!=null&&ok(r.revenue))h+=`<circle cx="${x(i)}" cy="${yR(r.revenue)}" r="4.6" fill="#fff" stroke="#23a673" stroke-width="2"/>`;
|
||||||
|
if(maxC!=null&&ok(r.cac))h+=`<circle cx="${x(i)}" cy="${yC(r.cac)}" r="3.4" fill="#e4568b"/>`;
|
||||||
|
h+=`<circle cx="${x(i)}" cy="${yI(r.installs)}" r="3.2" fill="#5577ee"/>`;
|
||||||
|
});
|
||||||
|
h+=`<line id="mainCursor" x1="0" y1="${m.t}" x2="0" y2="${m.t+ih}" stroke="#94a3b8" stroke-dasharray="4 4" opacity="0"/>`;
|
||||||
|
h+=`<rect id="mainHit" x="${m.l}" y="${m.t}" width="${iw}" height="${ih}" fill="transparent"/>`;
|
||||||
|
svg.innerHTML=h;
|
||||||
|
hover(svg,$('mainTip'),rows,m,iw,'mainCursor',(row)=>
|
||||||
|
`${esc(row.date)} · ${scope==='TOTAL'?'四国合计':scope}<br>Install:${num(row.installs)}<br>Revenue:${usd(row.revenue)}<br>CAC:${scope==='TOTAL'?'按国家查看':(ok(row.cac)?num(row.cac,2):'—')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 鼠标移动时定位最近日期,并在光标旁显示当天数值 */
|
||||||
|
function hover(svg,tip,rows,m,iw,cursorId,html){
|
||||||
|
const move=event=>{
|
||||||
|
const box=svg.getBoundingClientRect(),vb=svg.viewBox.baseVal,
|
||||||
|
vx=(event.clientX-box.left)/box.width*vb.width;
|
||||||
|
let index=0,best=Infinity;
|
||||||
|
rows.forEach((row,i)=>{const d=Math.abs(px(i,rows.length,m,iw)-vx);if(d<best){best=d;index=i}});
|
||||||
|
const row=rows[index],cx=px(index,rows.length,m,iw),cursor=svg.querySelector('#'+cursorId);
|
||||||
|
if(cursor){cursor.setAttribute('x1',cx);cursor.setAttribute('x2',cx);cursor.setAttribute('opacity','1')}
|
||||||
|
tip.innerHTML=html(row);
|
||||||
|
tip.style.left=(cx/vb.width*box.width)+'px';
|
||||||
|
tip.style.top=Math.max(38,event.clientY-box.top-14)+'px';
|
||||||
|
tip.style.opacity='1';
|
||||||
|
};
|
||||||
|
svg.onmousemove=move;
|
||||||
|
svg.onmouseleave=()=>{tip.style.opacity='0';const cursor=svg.querySelector('#'+cursorId);if(cursor)cursor.setAttribute('opacity','0')};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderKpis(){
|
||||||
|
const scope=$('scope').value,k=kpi(scope),label=scope==='TOTAL'?'四国合计':scope,
|
||||||
|
period=report.period||{},range=`${period.start||'—'} 至 ${period.end||'—'}`;
|
||||||
|
$('scopeHint').textContent=`${label} · ${range}`;
|
||||||
|
$('kpis').innerHTML=[
|
||||||
|
['Install',ok(k.installs)?num(k.installs):'—',label],
|
||||||
|
['Revenue',usd(k.revenue),hasRevenue()?label+' · USD':'源表 Revenue 待刷新'],
|
||||||
|
['Budget',usd(k.budget),ok(k.budget)?'Current Month Budget':'数据源待刷新'],
|
||||||
|
['FFR',pct(k.ffr),ok(k.ffr)?'源表 FFR':'数据源待刷新']
|
||||||
|
].map(item=>`<div class="card"><div class="label">${item[0]}</div><div class="value">${item[1]}</div><div class="hint">${esc(item[2])}</div></div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当月剩余天数 = 当月总天数 - 已统计天数 */
|
||||||
|
function remainingDays(){
|
||||||
|
const period=report.period||{},match=String(period.end||'').match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if(!match||!ok(period.days))return null;
|
||||||
|
const totalDays=new Date(Number(match[1]),Number(match[2]),0).getDate();
|
||||||
|
return Math.max(0,totalDays-Math.min(Number(period.days),totalDays));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 最近 3 个自然日的平均 Install */
|
||||||
|
function last3AvgInstall(scope){
|
||||||
|
const list=dates().slice(-3);
|
||||||
|
if(!list.length)return null;
|
||||||
|
const rows=scopeRows(scope).filter(r=>list.includes(r.date));
|
||||||
|
if(!rows.length)return null;
|
||||||
|
return rows.reduce((s,r)=>s+n(r.installs),0)/list.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 建议日均 Install = 剩余预算 / CPI / 剩余天数 */
|
||||||
|
function recDailyInstall(leftBudget,cpi){
|
||||||
|
const left=remainingDays();
|
||||||
|
if(!ok(leftBudget)||!ok(cpi)||cpi<=0||!ok(left)||left<=0)return null;
|
||||||
|
return leftBudget/cpi/left;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summaryCells(name,installs,revenue,budget,ffr,scope){
|
||||||
|
const cpi=ok(installs)&&installs>0&&ok(revenue)?revenue/installs:null,
|
||||||
|
left=ok(budget)&&ok(revenue)?budget-revenue:null,
|
||||||
|
l3d=last3AvgInstall(scope),
|
||||||
|
rec=recDailyInstall(left,cpi);
|
||||||
|
return `<td>${num(installs)}</td><td>${ok(cpi)?'$'+num(cpi,2):'—'}</td><td>${usd(revenue)}</td>`
|
||||||
|
+`<td>${usd(budget)}</td><td>${usd(left)}</td><td>${pct(ffr)}</td>`
|
||||||
|
+`<td>${ok(l3d)?num(l3d):'—'}</td><td>${ok(rec)?num(rec):'—'}</td>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSummary(){
|
||||||
|
const rows=summary(),total=kpi('TOTAL');
|
||||||
|
$('summaryRows').innerHTML=rows.map(row=>{
|
||||||
|
const revenue=ok(row.revenue)?Number(row.revenue):n(row.spend);
|
||||||
|
return `<tr><td class="country" style="color:${colors[row.country]||'inherit'}">${esc(row.country)}</td>`
|
||||||
|
+summaryCells(row.country,n(row.installs),revenue,ok(row.budget_usd)?Number(row.budget_usd):null,row.ffr_percent,row.country)+'</tr>';
|
||||||
|
}).join('')
|
||||||
|
+`<tr class="total-row"><td>Total</td>${summaryCells('Total',total.installs,total.revenue,total.budget,total.ffr,'TOTAL')}</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDetails(){
|
||||||
|
const scope=$('detailScope').value,
|
||||||
|
rows=scopeRows(scope).slice().sort((a,b)=>String(b.date).localeCompare(String(a.date))||String(a.country).localeCompare(String(b.country)));
|
||||||
|
$('detailRows').innerHTML=rows.map(row=>`<tr><td>${esc(row.date)}</td><td class="country" style="color:${colors[row.country]||'inherit'}">${esc(row.country)}</td><td>${num(row.installs)}</td><td>${usd(ok(row.revenue)?row.revenue:row.spend)}</td><td>${ok(row.cac)?num(row.cac,2):'—'}</td></tr>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(){
|
||||||
|
if(Array.isArray(report.countries)&&report.countries.length)countries=report.countries;
|
||||||
|
const period=report.period||{};
|
||||||
|
$('period').textContent=`${period.start||'—'} 至 ${period.end||'—'}${ok(period.days)?` · ${num(period.days)} 天`:''}${monthProgress(period)}`;
|
||||||
|
$('updated').innerHTML=`最近更新:${esc(String(report.generated_at||'—').replace('T',' '))}<br>时区:${esc(report.timezone||'—')}`;
|
||||||
|
fillSelect($('scope'),true,'Total(四国合计)');
|
||||||
|
fillSelect($('detailScope'),true,'全部国家');
|
||||||
|
renderKpis();renderSummary();renderDetails();drawMain();
|
||||||
|
}
|
||||||
|
|
||||||
|
$('scope').onchange=()=>{renderKpis();drawMain()};
|
||||||
|
$('detailScope').onchange=renderDetails;
|
||||||
|
window.addEventListener('resize',()=>{if(report)drawMain()});
|
||||||
|
|
||||||
|
fetch('data.json',{cache:'no-store'})
|
||||||
|
.then(response=>{if(!response.ok)throw new Error(`HTTP ${response.status}`);return response.json()})
|
||||||
|
.then(data=>{report=data;render()})
|
||||||
|
.catch(error=>{document.querySelector('.shell').innerHTML=`<section class="panel error"><h2>数据加载失败</h2><p>${esc(error.message)}</p><p>请稍后刷新,页面不会使用旧值冒充最新数据。</p></section>`});
|
||||||
|
</script></body></html>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
24
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
||||||
TMP_FILE="$SCRIPT_DIR/data.json.tmp"
|
|
||||||
OUTPUT_FILE="$SCRIPT_DIR/data.json"
|
|
||||||
|
|
||||||
python3 - "$TMP_FILE" <<'PY'
|
|
||||||
import csv
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import urllib.request
|
|
||||||
from collections import defaultdict
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
CSV_URL = (
|
|
||||||
"https://docs.google.com/spreadsheets/d/"
|
|
||||||
"1KaNOwaZ3DqjTXRK8XhF1OGF9qUmOh0NLXOgxVrh14h0/"
|
|
||||||
"export?format=csv&gid=1445753035"
|
|
||||||
)
|
|
||||||
TIMEZONE = ZoneInfo("Asia/Shanghai")
|
|
||||||
DAYS = 14
|
|
||||||
BLOCKS = [
|
|
||||||
{"country": "KZ", "date": 2, "geo": 3, "installs": 5, "spend": 8, "cac": 10},
|
|
||||||
{"country": "UZ", "date": 14, "geo": 15, "installs": 17, "spend": 18, "cac": 20},
|
|
||||||
{"country": "KG", "date": 25, "geo": 26, "installs": 28, "spend": 29, "cac": 31},
|
|
||||||
{"country": "AM", "date": 35, "geo": 36, "installs": 38, "spend": 39, "cac": 41},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def parse_date(value):
|
|
||||||
text = str(value or "").strip()
|
|
||||||
for fmt in ("%Y/%m/%d", "%Y-%m-%d", "%Y%m%d", "%m/%d/%Y"):
|
|
||||||
try:
|
|
||||||
return datetime.strptime(text, fmt).date()
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
match = re.fullmatch(r"(\d{4})[/.-](\d{1,2})[/.-](\d{1,2})", text)
|
|
||||||
if not match:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return datetime(*map(int, match.groups())).date()
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_number(value):
|
|
||||||
text = str(value or "").strip().replace("\xa0", "").replace(",", "")
|
|
||||||
if not text or text in {"-", "—"}:
|
|
||||||
return None
|
|
||||||
negative = text.startswith("(") and text.endswith(")")
|
|
||||||
cleaned = re.sub(r"[^0-9.\-]", "", text)
|
|
||||||
try:
|
|
||||||
number = float(cleaned)
|
|
||||||
return -number if negative else number
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def cell(row, index):
|
|
||||||
return row[index] if index < len(row) else ""
|
|
||||||
|
|
||||||
|
|
||||||
request = urllib.request.Request(CSV_URL, headers={"User-Agent": "AgentStage-Ozon-Report/1.0"})
|
|
||||||
with urllib.request.urlopen(request, timeout=90) as response:
|
|
||||||
text = response.read().decode("utf-8-sig")
|
|
||||||
rows = list(csv.reader(text.splitlines()))
|
|
||||||
|
|
||||||
now = datetime.now(TIMEZONE)
|
|
||||||
end_date = now.date() - timedelta(days=1)
|
|
||||||
start_date = end_date - timedelta(days=DAYS - 1)
|
|
||||||
aggregated = defaultdict(lambda: {"installs": 0.0, "spend": 0.0, "cac_values": []})
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
for block in BLOCKS:
|
|
||||||
day = parse_date(cell(row, block["date"]))
|
|
||||||
if day is None or not (start_date <= day <= end_date):
|
|
||||||
continue
|
|
||||||
country = str(cell(row, block["geo"])).strip().upper() or block["country"]
|
|
||||||
if country != block["country"]:
|
|
||||||
continue
|
|
||||||
key = (country, day.isoformat())
|
|
||||||
installs = parse_number(cell(row, block["installs"]))
|
|
||||||
spend = parse_number(cell(row, block["spend"]))
|
|
||||||
cac = parse_number(cell(row, block["cac"]))
|
|
||||||
aggregated[key]["installs"] += installs or 0
|
|
||||||
aggregated[key]["spend"] += spend or 0
|
|
||||||
if cac is not None:
|
|
||||||
aggregated[key]["cac_values"].append(cac)
|
|
||||||
|
|
||||||
records = []
|
|
||||||
for country in [item["country"] for item in BLOCKS]:
|
|
||||||
for offset in range(DAYS):
|
|
||||||
day = (start_date + timedelta(days=offset)).isoformat()
|
|
||||||
values = aggregated.get((country, day), {"installs": 0, "spend": 0, "cac_values": []})
|
|
||||||
cac_values = values["cac_values"]
|
|
||||||
records.append({
|
|
||||||
"date": day,
|
|
||||||
"country": country,
|
|
||||||
"installs": round(values["installs"], 2),
|
|
||||||
"spend": round(values["spend"], 4),
|
|
||||||
"cac": round(sum(cac_values) / len(cac_values), 4) if cac_values else None,
|
|
||||||
})
|
|
||||||
|
|
||||||
summary = []
|
|
||||||
for country in [item["country"] for item in BLOCKS]:
|
|
||||||
country_rows = [row for row in records if row["country"] == country]
|
|
||||||
cac_values = [row["cac"] for row in country_rows if row["cac"] is not None]
|
|
||||||
summary.append({
|
|
||||||
"country": country,
|
|
||||||
"installs": round(sum(row["installs"] for row in country_rows), 2),
|
|
||||||
"spend": round(sum(row["spend"] for row in country_rows), 4),
|
|
||||||
"avg_cac": round(sum(cac_values) / len(cac_values), 4) if cac_values else None,
|
|
||||||
"cac_days": len(cac_values),
|
|
||||||
})
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"title": "Ozon · 过去14天国家投放表现",
|
|
||||||
"generated_at": now.isoformat(timespec="seconds"),
|
|
||||||
"timezone": "Asia/Shanghai",
|
|
||||||
"period": {"start": start_date.isoformat(), "end": end_date.isoformat(), "days": DAYS},
|
|
||||||
"countries": [item["country"] for item in BLOCKS],
|
|
||||||
"summary": summary,
|
|
||||||
"records": records,
|
|
||||||
"definitions": {
|
|
||||||
"installs": "Google Sheet 各国家区块的 Install",
|
|
||||||
"spend": "Google Sheet 各国家区块的 Daily Spend(USD)",
|
|
||||||
"cac": "Google Sheet 各国家区块的 CAC from client report;汇总值为有数据日期的日均 CAC",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
with open(sys.argv[1], "w", encoding="utf-8") as handle:
|
|
||||||
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
|
||||||
PY
|
|
||||||
|
|
||||||
mv "$TMP_FILE" "$OUTPUT_FILE"
|
|
||||||
echo "Updated $OUTPUT_FILE"
|
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
"""生成 Ozon 报告数据。
|
||||||
|
|
||||||
|
取数范围:当月 1 号至昨天。
|
||||||
|
指标:Install、Revenue、CAC from client report。
|
||||||
|
Revenue 取源表的 Daily Spend 列(该列即收入口径)。
|
||||||
|
月度指标:Current Month Budget、FFR,按 Total/KZ/UZ/KG/AM 读取源表。
|
||||||
|
|
||||||
|
列位置通过表头识别,不依赖固定列号。脚本不包含任何账号、Token 或私钥。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
CSV_URL = (
|
||||||
|
"https://docs.google.com/spreadsheets/d/"
|
||||||
|
"1KaNOwaZ3DqjTXRK8XhF1OGF9qUmOh0NLXOgxVrh14h0/"
|
||||||
|
"export?format=csv&gid=1445753035"
|
||||||
|
)
|
||||||
|
TIMEZONE = ZoneInfo("Asia/Shanghai")
|
||||||
|
COUNTRIES = ["KZ", "UZ", "KG", "AM"]
|
||||||
|
|
||||||
|
DATE_HEADERS = {"date"}
|
||||||
|
GEO_HEADERS = {"geo", "country"}
|
||||||
|
INSTALL_HEADERS = {"install", "installs"}
|
||||||
|
"""Revenue 使用源表 Daily Spend 列;Revenue 表头若存在则优先使用。"""
|
||||||
|
REVENUE_HEADERS = {"revenue", "income from client", "income from client($)"}
|
||||||
|
REVENUE_FALLBACK_HEADERS = {"daily spend", "daily spend($)", "spend"}
|
||||||
|
CAC_HEADERS = {"cac from client report", "cac from client report($)", "cac"}
|
||||||
|
BUDGET_LABELS = {"current month budget", "month budget", "budget"}
|
||||||
|
FFR_LABELS = {"ffr%", "ffr", "ffr(%)"}
|
||||||
|
|
||||||
|
|
||||||
|
def clean(value):
|
||||||
|
return " ".join(str(value or "").replace("\xa0", " ").split())
|
||||||
|
|
||||||
|
|
||||||
|
def key(value):
|
||||||
|
return clean(value).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def cell(row, index):
|
||||||
|
if index is None or index < 0 or index >= len(row):
|
||||||
|
return ""
|
||||||
|
return row[index]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_date(value):
|
||||||
|
text = clean(value)
|
||||||
|
for fmt in ("%Y/%m/%d", "%Y-%m-%d", "%Y%m%d", "%m/%d/%Y", "%d/%m/%Y"):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(text, fmt).date()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
match = re.fullmatch(r"(\d{4})\D{1,3}(\d{1,2})\D{1,3}(\d{1,2})", text)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime(*map(int, match.groups())).date()
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_number(value):
|
||||||
|
text = clean(value).replace(",", "").replace("%", "")
|
||||||
|
if not text or text in {"-", "—", "#DIV/0!", "#N/A", "#REF!", "#VALUE!"}:
|
||||||
|
return None
|
||||||
|
negative = text.startswith("(") and text.endswith(")")
|
||||||
|
cleaned = re.sub(r"[^0-9.\-]", "", text)
|
||||||
|
if cleaned in {"", "-", ".", "-."}:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
number = float(cleaned)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
return -number if negative else number
|
||||||
|
|
||||||
|
|
||||||
|
def find_blocks(rows):
|
||||||
|
"""按表头识别每个国家的横向数据区块。"""
|
||||||
|
blocks = []
|
||||||
|
for row_index, row in enumerate(rows):
|
||||||
|
date_columns = [i for i, value in enumerate(row) if key(value) in DATE_HEADERS]
|
||||||
|
if not date_columns:
|
||||||
|
continue
|
||||||
|
for position, start in enumerate(date_columns):
|
||||||
|
end = date_columns[position + 1] if position + 1 < len(date_columns) else len(row)
|
||||||
|
block = {"header_row": row_index, "date": start}
|
||||||
|
for column in range(start, end):
|
||||||
|
name = key(cell(row, column))
|
||||||
|
if name in GEO_HEADERS:
|
||||||
|
block.setdefault("geo", column)
|
||||||
|
elif name in INSTALL_HEADERS:
|
||||||
|
block.setdefault("installs", column)
|
||||||
|
elif name in REVENUE_HEADERS:
|
||||||
|
block["revenue"] = column
|
||||||
|
elif name in REVENUE_FALLBACK_HEADERS:
|
||||||
|
block.setdefault("revenue_fallback", column)
|
||||||
|
elif name in CAC_HEADERS:
|
||||||
|
block.setdefault("cac", column)
|
||||||
|
if "revenue" not in block and "revenue_fallback" in block:
|
||||||
|
block["revenue"] = block["revenue_fallback"]
|
||||||
|
if "geo" in block and "installs" in block:
|
||||||
|
blocks.append(block)
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def find_monthly_metrics(rows):
|
||||||
|
"""读取顶部 Current Month Budget 与 FFR 的 Total/各国数值。"""
|
||||||
|
metrics = {"budget": {}, "ffr": {}}
|
||||||
|
for row_index, row in enumerate(rows[:30]):
|
||||||
|
labels = {key(value) for value in row}
|
||||||
|
if "total" not in labels:
|
||||||
|
continue
|
||||||
|
columns = {}
|
||||||
|
for column, value in enumerate(row):
|
||||||
|
name = clean(value).upper()
|
||||||
|
if name == "TOTAL":
|
||||||
|
columns["TOTAL"] = column
|
||||||
|
elif name in COUNTRIES:
|
||||||
|
columns[name] = column
|
||||||
|
if len(columns) < 2:
|
||||||
|
continue
|
||||||
|
window = rows[row_index: min(row_index + 12, len(rows))]
|
||||||
|
for line in window:
|
||||||
|
labels_in_line = [key(value) for value in line]
|
||||||
|
target = None
|
||||||
|
if any(name in BUDGET_LABELS for name in labels_in_line):
|
||||||
|
target = "budget"
|
||||||
|
elif any(name in FFR_LABELS for name in labels_in_line):
|
||||||
|
target = "ffr"
|
||||||
|
if target is None:
|
||||||
|
continue
|
||||||
|
for name, column in columns.items():
|
||||||
|
number = parse_number(cell(line, column))
|
||||||
|
if number is not None and name not in metrics[target]:
|
||||||
|
metrics[target][name] = number
|
||||||
|
if metrics["budget"] or metrics["ffr"]:
|
||||||
|
break
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
|
||||||
|
def build_payload(rows, now):
|
||||||
|
end_date = now.date() - timedelta(days=1)
|
||||||
|
start_date = end_date.replace(day=1)
|
||||||
|
days = (end_date - start_date).days + 1
|
||||||
|
|
||||||
|
blocks = find_blocks(rows)
|
||||||
|
if not blocks:
|
||||||
|
raise SystemExit("未能通过表头识别国家数据区块,已中止,不覆盖旧数据。")
|
||||||
|
|
||||||
|
daily = {}
|
||||||
|
for row in rows:
|
||||||
|
for block in blocks:
|
||||||
|
day = parse_date(cell(row, block["date"]))
|
||||||
|
if day is None or not (start_date <= day <= end_date):
|
||||||
|
continue
|
||||||
|
country = clean(cell(row, block["geo"])).upper()
|
||||||
|
if country not in COUNTRIES:
|
||||||
|
continue
|
||||||
|
item = daily.setdefault(
|
||||||
|
(country, day.isoformat()),
|
||||||
|
{"installs": 0.0, "revenue": 0.0, "cac": None},
|
||||||
|
)
|
||||||
|
item["installs"] += parse_number(cell(row, block.get("installs"))) or 0.0
|
||||||
|
item["revenue"] += parse_number(cell(row, block.get("revenue"))) or 0.0
|
||||||
|
cac = parse_number(cell(row, block.get("cac")))
|
||||||
|
if cac is not None and item["cac"] is None:
|
||||||
|
item["cac"] = cac
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for country in COUNTRIES:
|
||||||
|
for offset in range(days):
|
||||||
|
day = (start_date + timedelta(days=offset)).isoformat()
|
||||||
|
values = daily.get((country, day))
|
||||||
|
records.append({
|
||||||
|
"date": day,
|
||||||
|
"country": country,
|
||||||
|
"installs": round(values["installs"], 2) if values else 0,
|
||||||
|
"revenue": round(values["revenue"], 4) if values else 0,
|
||||||
|
"cac": values["cac"] if values else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
monthly = find_monthly_metrics(rows)
|
||||||
|
summary = []
|
||||||
|
for country in COUNTRIES:
|
||||||
|
country_rows = [row for row in records if row["country"] == country]
|
||||||
|
cac_values = [row["cac"] for row in country_rows if row["cac"] is not None]
|
||||||
|
summary.append({
|
||||||
|
"country": country,
|
||||||
|
"installs": round(sum(row["installs"] for row in country_rows), 2),
|
||||||
|
"revenue": round(sum(row["revenue"] for row in country_rows), 4),
|
||||||
|
"avg_cac": round(sum(cac_values) / len(cac_values), 4) if cac_values else None,
|
||||||
|
"cac_days": len(cac_values),
|
||||||
|
"budget_usd": monthly["budget"].get(country),
|
||||||
|
"ffr_percent": monthly["ffr"].get(country),
|
||||||
|
})
|
||||||
|
|
||||||
|
total_budget = monthly["budget"].get("TOTAL")
|
||||||
|
if total_budget is None:
|
||||||
|
country_budgets = [row["budget_usd"] for row in summary if row["budget_usd"] is not None]
|
||||||
|
total_budget = round(sum(country_budgets), 4) if country_budgets else None
|
||||||
|
|
||||||
|
kpis = {
|
||||||
|
"TOTAL": {
|
||||||
|
"installs": round(sum(row["installs"] for row in records), 2),
|
||||||
|
"revenue": round(sum(row["revenue"] for row in records), 4),
|
||||||
|
"budget_usd": total_budget,
|
||||||
|
"ffr_percent": monthly["ffr"].get("TOTAL"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for row in summary:
|
||||||
|
kpis[row["country"]] = {
|
||||||
|
"installs": row["installs"],
|
||||||
|
"revenue": row["revenue"],
|
||||||
|
"budget_usd": row["budget_usd"],
|
||||||
|
"ffr_percent": row["ffr_percent"],
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"title": "Ozon · 当月投放表现",
|
||||||
|
"generated_at": now.isoformat(timespec="seconds"),
|
||||||
|
"timezone": "Asia/Shanghai",
|
||||||
|
"period": {"start": start_date.isoformat(), "end": end_date.isoformat(), "days": days},
|
||||||
|
"countries": COUNTRIES,
|
||||||
|
"kpis": kpis,
|
||||||
|
"summary": summary,
|
||||||
|
"records": records,
|
||||||
|
"definitions": {
|
||||||
|
"period": "当月 1 号至昨天",
|
||||||
|
"installs": "各国家区块的 Install",
|
||||||
|
"revenue": "各国家区块的 Revenue(USD),取源表 Daily Spend 列",
|
||||||
|
"cac": "各国家区块的 CAC from client report 原始值;不改算、不标记为 USD",
|
||||||
|
"budget_usd": "源表 Current Month Budget,按 Total 与各国读取",
|
||||||
|
"ffr_percent": "源表 FFR,按 Total 与各国读取",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
raise SystemExit("用法: update_report_data.py <输出文件>")
|
||||||
|
|
||||||
|
now = datetime.now(TIMEZONE)
|
||||||
|
request = urllib.request.Request(CSV_URL, headers={"User-Agent": "AgentStage-Ozon-Report/2.0"})
|
||||||
|
with urllib.request.urlopen(request, timeout=90) as response:
|
||||||
|
text = response.read().decode("utf-8-sig")
|
||||||
|
rows = list(csv.reader(text.splitlines()))
|
||||||
|
payload = build_payload(rows, now)
|
||||||
|
with open(sys.argv[1], "w", encoding="utf-8") as handle:
|
||||||
|
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
||||||
|
period = payload["period"]
|
||||||
|
print(f"OK {period['start']}..{period['end']} records={len(payload['records'])}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user