Compare commits
6
Commits
82e64d1512
...
1b3faaf925
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b3faaf925 | ||
|
|
d2b6417e80 | ||
|
|
9960b516da | ||
|
|
15748d4ee5 | ||
|
|
f53fd78b2a | ||
|
|
03944f2aef |
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"title": "Ozon · 过去14天国家投放表现",
|
||||
"generated_at": "2026-08-24T15:42:16+08:00",
|
||||
"title": "Ozon · 当月投放表现",
|
||||
"generated_at": "2026-08-26T16:26:52+08:00",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"period": {
|
||||
"start": "2026-08-10",
|
||||
"end": "2026-08-23",
|
||||
"days": 14
|
||||
"start": "2026-08-01",
|
||||
"end": "2026-08-25",
|
||||
"days": 25
|
||||
},
|
||||
"countries": [
|
||||
"KZ",
|
||||
@@ -13,433 +13,784 @@
|
||||
"KG",
|
||||
"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": [
|
||||
{
|
||||
"country": "KZ",
|
||||
"installs": 12980.0,
|
||||
"spend": 9086.0,
|
||||
"avg_cac": 807.3846,
|
||||
"cac_days": 13
|
||||
"installs": 24033.0,
|
||||
"revenue": 16823.1,
|
||||
"avg_cac": 743.625,
|
||||
"cac_days": 24,
|
||||
"budget_usd": 23450.0,
|
||||
"ffr_percent": 71.74
|
||||
},
|
||||
{
|
||||
"country": "UZ",
|
||||
"installs": 5630.0,
|
||||
"spend": 2815.0,
|
||||
"avg_cac": 5723.6923,
|
||||
"cac_days": 13
|
||||
"installs": 10048.0,
|
||||
"revenue": 5024.0,
|
||||
"avg_cac": 4621.5833,
|
||||
"cac_days": 24,
|
||||
"budget_usd": 7400.0,
|
||||
"ffr_percent": 67.89
|
||||
},
|
||||
{
|
||||
"country": "KG",
|
||||
"installs": 1724.0,
|
||||
"spend": 862.0,
|
||||
"avg_cac": 5302.0,
|
||||
"cac_days": 10
|
||||
"installs": 3619.0,
|
||||
"revenue": 1809.5,
|
||||
"avg_cac": 5340.2917,
|
||||
"cac_days": 24,
|
||||
"budget_usd": 3080.0,
|
||||
"ffr_percent": 58.75
|
||||
},
|
||||
{
|
||||
"country": "AM",
|
||||
"installs": 971.0,
|
||||
"spend": 485.5,
|
||||
"avg_cac": 3127.553,
|
||||
"cac_days": 10
|
||||
"installs": 2328.0,
|
||||
"revenue": 1164.0,
|
||||
"avg_cac": 2888.4008,
|
||||
"cac_days": 24,
|
||||
"budget_usd": 1850.0,
|
||||
"ffr_percent": 62.92
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"country": "KZ",
|
||||
"installs": 1075.0,
|
||||
"spend": 752.5,
|
||||
"revenue": 752.5,
|
||||
"cac": 636.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-11",
|
||||
"country": "KZ",
|
||||
"installs": 1027.0,
|
||||
"spend": 718.9,
|
||||
"revenue": 718.9,
|
||||
"cac": 745.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-12",
|
||||
"country": "KZ",
|
||||
"installs": 1034.0,
|
||||
"spend": 723.8,
|
||||
"cac": 954.0
|
||||
"revenue": 723.8,
|
||||
"cac": 929.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-13",
|
||||
"country": "KZ",
|
||||
"installs": 1097.0,
|
||||
"spend": 767.9,
|
||||
"revenue": 767.9,
|
||||
"cac": 837.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-14",
|
||||
"country": "KZ",
|
||||
"installs": 1072.0,
|
||||
"spend": 750.4,
|
||||
"revenue": 750.4,
|
||||
"cac": 724.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-15",
|
||||
"country": "KZ",
|
||||
"installs": 718.0,
|
||||
"spend": 502.6,
|
||||
"cac": 837.0
|
||||
"revenue": 502.6,
|
||||
"cac": 788.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16",
|
||||
"country": "KZ",
|
||||
"installs": 675.0,
|
||||
"spend": 472.5,
|
||||
"revenue": 472.5,
|
||||
"cac": 1062.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-17",
|
||||
"country": "KZ",
|
||||
"installs": 715.0,
|
||||
"spend": 500.5,
|
||||
"cac": 702.0
|
||||
"revenue": 500.5,
|
||||
"cac": 683.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-18",
|
||||
"country": "KZ",
|
||||
"installs": 682.0,
|
||||
"spend": 477.4,
|
||||
"revenue": 477.4,
|
||||
"cac": 677.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-19",
|
||||
"country": "KZ",
|
||||
"installs": 612.0,
|
||||
"spend": 428.4,
|
||||
"cac": 597.0
|
||||
"revenue": 428.4,
|
||||
"cac": 582.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
"country": "KZ",
|
||||
"installs": 602.0,
|
||||
"spend": 421.4,
|
||||
"revenue": 421.4,
|
||||
"cac": 770.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-21",
|
||||
"country": "KZ",
|
||||
"installs": 1117.0,
|
||||
"spend": 781.9,
|
||||
"cac": 909.0
|
||||
"revenue": 781.9,
|
||||
"cac": 851.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-22",
|
||||
"country": "KZ",
|
||||
"installs": 1298.0,
|
||||
"spend": 908.6,
|
||||
"cac": 1046.0
|
||||
"revenue": 908.6,
|
||||
"cac": 856.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-23",
|
||||
"country": "KZ",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"country": "UZ",
|
||||
"installs": 550.0,
|
||||
"spend": 275.0,
|
||||
"revenue": 275.0,
|
||||
"cac": 4493.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-11",
|
||||
"country": "UZ",
|
||||
"installs": 614.0,
|
||||
"spend": 307.0,
|
||||
"cac": 5103.0
|
||||
"revenue": 307.0,
|
||||
"cac": 4536.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-12",
|
||||
"country": "UZ",
|
||||
"installs": 614.0,
|
||||
"spend": 307.0,
|
||||
"revenue": 307.0,
|
||||
"cac": 3349.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-13",
|
||||
"country": "UZ",
|
||||
"installs": 608.0,
|
||||
"spend": 304.0,
|
||||
"revenue": 304.0,
|
||||
"cac": 7011.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-14",
|
||||
"country": "UZ",
|
||||
"installs": 587.0,
|
||||
"spend": 293.5,
|
||||
"revenue": 293.5,
|
||||
"cac": 2545.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-15",
|
||||
"country": "UZ",
|
||||
"installs": 224.0,
|
||||
"spend": 112.0,
|
||||
"revenue": 112.0,
|
||||
"cac": 3768.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16",
|
||||
"country": "UZ",
|
||||
"installs": 176.0,
|
||||
"spend": 88.0,
|
||||
"revenue": 88.0,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-17",
|
||||
"country": "UZ",
|
||||
"installs": 211.0,
|
||||
"spend": 105.5,
|
||||
"revenue": 105.5,
|
||||
"cac": 8147.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-18",
|
||||
"country": "UZ",
|
||||
"installs": 170.0,
|
||||
"spend": 85.0,
|
||||
"revenue": 85.0,
|
||||
"cac": 20293.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-19",
|
||||
"country": "UZ",
|
||||
"installs": 141.0,
|
||||
"spend": 70.5,
|
||||
"revenue": 70.5,
|
||||
"cac": 8912.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
"country": "UZ",
|
||||
"installs": 124.0,
|
||||
"spend": 62.0,
|
||||
"revenue": 62.0,
|
||||
"cac": 3829.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-21",
|
||||
"country": "UZ",
|
||||
"installs": 509.0,
|
||||
"spend": 254.5,
|
||||
"cac": 3233.0
|
||||
"revenue": 254.5,
|
||||
"cac": 2985.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-22",
|
||||
"country": "UZ",
|
||||
"installs": 530.0,
|
||||
"spend": 265.0,
|
||||
"cac": 3725.0
|
||||
"revenue": 265.0,
|
||||
"cac": 3415.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-23",
|
||||
"country": "UZ",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"country": "KG",
|
||||
"installs": 197.0,
|
||||
"spend": 98.5,
|
||||
"cac": 2186.0
|
||||
"revenue": 98.5,
|
||||
"cac": 3278.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-11",
|
||||
"country": "KG",
|
||||
"installs": 196.0,
|
||||
"spend": 98.0,
|
||||
"cac": 2537.0
|
||||
"revenue": 98.0,
|
||||
"cac": 6765.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-12",
|
||||
"country": "KG",
|
||||
"installs": 186.0,
|
||||
"spend": 93.0,
|
||||
"cac": 3633.0
|
||||
"revenue": 93.0,
|
||||
"cac": 7265.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-13",
|
||||
"country": "KG",
|
||||
"installs": 179.0,
|
||||
"spend": 89.5,
|
||||
"cac": 20857.0
|
||||
"revenue": 89.5,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-14",
|
||||
"country": "KG",
|
||||
"installs": 167.0,
|
||||
"spend": 83.5,
|
||||
"cac": 3520.0
|
||||
"revenue": 83.5,
|
||||
"cac": 5280.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-15",
|
||||
"country": "KG",
|
||||
"installs": 31.0,
|
||||
"spend": 15.5,
|
||||
"cac": 2545.0
|
||||
"revenue": 15.5,
|
||||
"cac": 12724.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16",
|
||||
"country": "KG",
|
||||
"installs": 26.0,
|
||||
"spend": 13.0,
|
||||
"cac": 15506.0
|
||||
"revenue": 13.0,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-17",
|
||||
"country": "KG",
|
||||
"installs": 30.0,
|
||||
"spend": 15.0,
|
||||
"revenue": 15.0,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-18",
|
||||
"country": "KG",
|
||||
"installs": 31.0,
|
||||
"spend": 15.5,
|
||||
"revenue": 15.5,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-19",
|
||||
"country": "KG",
|
||||
"installs": 22.0,
|
||||
"spend": 11.0,
|
||||
"cac": 2236.0
|
||||
"revenue": 11.0,
|
||||
"cac": 4471.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
"country": "KG",
|
||||
"installs": 28.0,
|
||||
"spend": 14.0,
|
||||
"cac": null
|
||||
"revenue": 14.0,
|
||||
"cac": 6257.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-21",
|
||||
"country": "KG",
|
||||
"installs": 192.0,
|
||||
"spend": 96.0,
|
||||
"cac": null
|
||||
"revenue": 96.0,
|
||||
"cac": 15812.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-22",
|
||||
"country": "KG",
|
||||
"installs": 210.0,
|
||||
"spend": 105.0,
|
||||
"cac": null
|
||||
"revenue": 105.0,
|
||||
"cac": 9055.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-23",
|
||||
"country": "KG",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"country": "AM",
|
||||
"installs": 124.0,
|
||||
"spend": 62.0,
|
||||
"revenue": 62.0,
|
||||
"cac": 7132.05
|
||||
},
|
||||
{
|
||||
"date": "2026-08-11",
|
||||
"country": "AM",
|
||||
"installs": 136.0,
|
||||
"spend": 68.0,
|
||||
"revenue": 68.0,
|
||||
"cac": 7806.27
|
||||
},
|
||||
{
|
||||
"date": "2026-08-12",
|
||||
"country": "AM",
|
||||
"installs": 126.0,
|
||||
"spend": 63.0,
|
||||
"revenue": 63.0,
|
||||
"cac": 3517.38
|
||||
},
|
||||
{
|
||||
"date": "2026-08-13",
|
||||
"country": "AM",
|
||||
"installs": 124.0,
|
||||
"spend": 62.0,
|
||||
"revenue": 62.0,
|
||||
"cac": 7146.1
|
||||
},
|
||||
{
|
||||
"date": "2026-08-14",
|
||||
"country": "AM",
|
||||
"installs": 112.0,
|
||||
"spend": 56.0,
|
||||
"cac": 1686.59
|
||||
"revenue": 56.0,
|
||||
"cac": 2248.79
|
||||
},
|
||||
{
|
||||
"date": "2026-08-15",
|
||||
"country": "AM",
|
||||
"installs": 19.0,
|
||||
"spend": 9.5,
|
||||
"revenue": 9.5,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16",
|
||||
"country": "AM",
|
||||
"installs": 25.0,
|
||||
"spend": 12.5,
|
||||
"revenue": 12.5,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-17",
|
||||
"country": "AM",
|
||||
"installs": 30.0,
|
||||
"spend": 15.0,
|
||||
"revenue": 15.0,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-18",
|
||||
"country": "AM",
|
||||
"installs": 28.0,
|
||||
"spend": 14.0,
|
||||
"cac": 3987.14
|
||||
"revenue": 14.0,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-19",
|
||||
"country": "AM",
|
||||
"installs": 19.0,
|
||||
"spend": 9.5,
|
||||
"revenue": 9.5,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
"country": "AM",
|
||||
"installs": 15.0,
|
||||
"spend": 7.5,
|
||||
"cac": null
|
||||
"revenue": 7.5,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-21",
|
||||
"country": "AM",
|
||||
"installs": 75.0,
|
||||
"spend": 37.5,
|
||||
"cac": null
|
||||
"revenue": 37.5,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-22",
|
||||
"country": "AM",
|
||||
"installs": 67.0,
|
||||
"spend": 33.5,
|
||||
"cac": null
|
||||
"revenue": 33.5,
|
||||
"cac": 2283.09
|
||||
},
|
||||
{
|
||||
"date": "2026-08-23",
|
||||
"country": "AM",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"installs": "Google Sheet 各国家区块的 Install",
|
||||
"spend": "Google Sheet 各国家区块的 Daily Spend(USD)",
|
||||
"cac": "Google Sheet 各国家区块的 CAC from client report;汇总值为有数据日期的日均 CAC"
|
||||
"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 与各国读取"
|
||||
}
|
||||
}
|
||||
+247
-27
@@ -1,30 +1,250 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Ozon 国家投放表现</title>
|
||||
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Ozon 投放表现</title>
|
||||
<style>
|
||||
:root{--bg:#f4f7fb;--card:#fff;--text:#14213d;--muted:#6b7280;--line:#e6eaf0;--blue:#5577ee;--shadow:0 8px 30px rgba(24,39,75,.06)}
|
||||
*{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}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header class="hero"><div><h1>Ozon · 国家投放表现</h1><p id="period">正在加载过去 14 天数据…</p></div><div class="updated" id="updated">数据加载中</div></header>
|
||||
<div class="grid" id="kpis"><div class="card"><div class="label">总安装</div><div class="value skeleton">00000</div></div></div>
|
||||
<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>
|
||||
<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>
|
||||
<div class="footer">数据来源:Google Sheets · Ozon_IronSpace · 每 24 小时自动更新</div>
|
||||
</div>
|
||||
: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:26px}
|
||||
.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)}
|
||||
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}
|
||||
.scope{display:flex;align-items:center;gap:10px;margin:16px 0 0;font-size:13px;color:var(--muted)}
|
||||
select{border:1px solid var(--line);background:#fff;padding:8px 11px;border-radius:9px;color:var(--text);font:inherit}
|
||||
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:13px;margin:13px 0}
|
||||
.card,.panel{background:var(--card);border:1px solid var(--line);border-radius:15px;box-shadow:0 8px 28px rgba(24,39,75,.05)}
|
||||
.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}
|
||||
.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}
|
||||
.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>
|
||||
const colors={KZ:'#5577ee',UZ:'#32b67a',KG:'#f59e0b',AM:'#e4568b'};let report=null;let metric='installs';
|
||||
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]));
|
||||
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()}
|
||||
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('')}
|
||||
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('')}
|
||||
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;
|
||||
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>`});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'use strict';
|
||||
const $=id=>document.getElementById(id),COUNTRIES=['KZ','UZ','KG','AM'],
|
||||
colors={KZ:'#5577ee',UZ:'#23a673',KG:'#f59e0b',AM:'#e4568b'},
|
||||
esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])),
|
||||
ok=v=>v!==null&&v!==undefined&&v!==''&&Number.isFinite(Number(v)),
|
||||
n=v=>ok(v)?Number(v):0,
|
||||
num=(v,d=0)=>n(v).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d}),
|
||||
usd=v=>ok(v)?'$'+num(v,2):'—',
|
||||
pct=v=>ok(v)?num(v,2)+'%':'—';
|
||||
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>
|
||||
|
||||
@@ -5,135 +5,7 @@ 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
|
||||
python3 "$SCRIPT_DIR/update_report_data.py" "$TMP_FILE"
|
||||
|
||||
mv "$TMP_FILE" "$OUTPUT_FILE"
|
||||
echo "Updated $OUTPUT_FILE"
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
"""生成 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 <输出文件>")
|
||||
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, datetime.now(TIMEZONE))
|
||||
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