第一版
This commit is contained in:
@@ -1,67 +1,67 @@
|
||||
[
|
||||
{
|
||||
"name": "tfstk",
|
||||
"value": "gREthFDeE6fgrssCwhbhnyNsGkB3kw2ap5yWmjcMlWFLGSHDm-GmDSFYMf0f_cqY9SNxnn2bmxZxbk4MmffZD-NoD_flqgmaQ0ofZ_AYbUGtcxsmGw01D9Qpu_flqMb6dmCOZRq-YUhIeYijCfiXd9HoFAij5mMCpxMD1Et_GJ6KTxLXfngsAMMZHAijcSwCpxlIGmg_GJ6K3XGjZaCxOdGDDye4oACPRf-XckHt5w2sOnMUvA3tNRZ9cn9rBVh7CXCtMibER-uY0K-xuRazgYN61_0ahyFKBmIH50wLl7HUfgAtLP2QEXwJ3nyZXkELl2B1fJnt64qQtdLjXrNaDVz93TyIfWzraV_FTvEgqqhrJB1_KJhsyrFGTnGgyJNKrkAHqmPUlSMYOCIztuq--0RowxY1p9L2uVMETlR31IcEn7kKZOIvuEuFLvhlpT82uVM-pbX9eE8q895..",
|
||||
"value": "gEzShTThBLv5ZgfRpL52Cwej6DuBVs7NRBGLsWLy943-d2NQ6DLFq4oIR-H4zYPryvgQO5nLLvPyZo2a6UzPq28Ir20dQOSV7bVo-2LiE4rIESFm9sl84JEEQ20dQO-2J0bs-8Nbr_kJDjHmOUh-JJKxDbkxw3n8pxnx6fJ-J2edGxhE_bd-wDHxHfDxp23LpSCjtxh-J2eLMsGngcAsOgMuN6_dnQMJ_XV-heLLkwmSC7KeJeUjN0aLwxN4grGSVAnCBw0Qy-PLzzmcq3u3glwQvRB24YEs1-iaVOT7C8cLHjePTHcbAWUir0AH-Rg7dcU-cBL_64HoljUOThD4lYyLPmCw5DuY7c380iXrY4G_pz2WOeeTg5zomy66HvqrsqhQ-1KIdDIrVFksS-KBGcYIGA5fGHxnzkyVpsV-4un-ibiVGs98x0hmGxCfGHvj2jc7Us1Xekf..",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1786125734
|
||||
"expires": 1786155394
|
||||
},
|
||||
{
|
||||
"name": "arms_uid",
|
||||
"value": "27b17ae2-81c6-4d4f-9496-3e0582ff16d2",
|
||||
"value": "2b3df2db-a3c6-47ca-849c-a3cfa22bfe25",
|
||||
"domain": "cart.taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1786125734.323322
|
||||
"expires": 1786155394.629206
|
||||
},
|
||||
{
|
||||
"name": "ucn",
|
||||
"value": "center",
|
||||
"domain": "cart.taobao.com",
|
||||
"path": "/",
|
||||
"expires": 1789673650.898824
|
||||
"expires": 1789673650.373558
|
||||
},
|
||||
{
|
||||
"name": "ubn",
|
||||
"value": "p",
|
||||
"domain": "cart.taobao.com",
|
||||
"path": "/",
|
||||
"expires": 1805133733.898694
|
||||
"expires": 1805163393.37341
|
||||
},
|
||||
{
|
||||
"name": "havana_sdkSilent",
|
||||
"value": "1770602527511",
|
||||
"value": "1770632187990",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"expires": -1
|
||||
},
|
||||
{
|
||||
"name": "havana_lgc_exp",
|
||||
"value": "1801677727511",
|
||||
"value": "1801707387990",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1801706527.482485
|
||||
"expires": 1801736188.271188
|
||||
},
|
||||
{
|
||||
"name": "bxuab",
|
||||
"value": "0",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"expires": 1770574326.708635
|
||||
"expires": 1770603987.787739
|
||||
},
|
||||
{
|
||||
"name": "_m_h5_tk_enc",
|
||||
"value": "4c9af36e47eb95f02be3d950f7d55fe8",
|
||||
"value": "648302098481380669e8cb431a99c6f5",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1770579126.261152
|
||||
"expires": 1770608786.32924
|
||||
},
|
||||
{
|
||||
"name": "sdkSilent",
|
||||
"value": "1770602527511",
|
||||
"value": "1770632187990",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
@@ -69,43 +69,28 @@
|
||||
},
|
||||
{
|
||||
"name": "_cc_",
|
||||
"value": "Vq8l%2BKCLiw%3D%3D",
|
||||
"value": "V32FPkk%2Fhw%3D%3D",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1802138525.099279
|
||||
"expires": 1802168184.925658
|
||||
},
|
||||
{
|
||||
"name": "uc3",
|
||||
"value": "nk2=2W5q6SxD\u0026id2=UU8OeHEFYuIE%2Fw%3D%3D\u0026lg2=VFC%2FuZ9ayeYq2g%3D%3D\u0026vt3=F8dD29DnFvY6X0CtCDI%3D",
|
||||
"value": "nk2=2W5q6SxD\u0026lg2=U%2BGCWk%2F75gdr5Q%3D%3D\u0026vt3=F8dD29DnGZqiqJAND%2BU%3D\u0026id2=UU8OeHEFYuIE%2Fw%3D%3D",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1773165725.098812
|
||||
},
|
||||
{
|
||||
"name": "thw",
|
||||
"value": "cn",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"expires": 1802109726.659989
|
||||
},
|
||||
{
|
||||
"name": "sn",
|
||||
"value": "",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773165725.098801
|
||||
"expires": 1773195384.92465
|
||||
},
|
||||
{
|
||||
"name": "isg",
|
||||
"value": "BOfnyr79QnbK1cb6ZiUi-ICWdhuxbLtOBy1sRLlUA3adqAdqwTxLniW7yKg2QJPG",
|
||||
"value": "BM3NGKTJmCW4DzwJPINXoxvZ3OlHqgF8mbM21g9SCWTTBu241_oRTBuFcppgxhk0",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1786125729
|
||||
"expires": 1786155393
|
||||
},
|
||||
{
|
||||
"name": "cookie1",
|
||||
@@ -114,7 +99,7 @@
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1773165725.099435
|
||||
"expires": 1773195384.926001
|
||||
},
|
||||
{
|
||||
"name": "wk_unb",
|
||||
@@ -123,7 +108,7 @@
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1775786522.464568
|
||||
"expires": 1775816181.362139
|
||||
},
|
||||
{
|
||||
"name": "_l_g_",
|
||||
@@ -131,15 +116,15 @@
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773165725.099344
|
||||
"expires": 1773195384.925815
|
||||
},
|
||||
{
|
||||
"name": "cna",
|
||||
"value": "h70PIpIjgAECAa+8pHQPSpRy",
|
||||
"value": "RzEQIimlMgMBASQOA0EDvmjO",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1805133703.735092
|
||||
"expires": 1805163337.88989
|
||||
},
|
||||
{
|
||||
"name": "tracknick",
|
||||
@@ -147,28 +132,11 @@
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1802138525.099204
|
||||
},
|
||||
{
|
||||
"name": "_samesite_flag_",
|
||||
"value": "true",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": -1
|
||||
},
|
||||
{
|
||||
"name": "_nk_",
|
||||
"value": "%5Cu4F55%5Cu672A%5Cu5C39",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773165725.099402
|
||||
"expires": 1802168184.925502
|
||||
},
|
||||
{
|
||||
"name": "uc1",
|
||||
"value": "cookie14=UoYZaqFObbA9zg%3D%3D",
|
||||
"value": "cookie14=UoYZaqLDqbCA2w%3D%3D",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
@@ -176,12 +144,12 @@
|
||||
},
|
||||
{
|
||||
"name": "skt",
|
||||
"value": "3e7dedf3c5d7f59e",
|
||||
"value": "20ea6b27c857e4b6",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1773165725.099071
|
||||
"expires": 1773195384.925233
|
||||
},
|
||||
{
|
||||
"name": "dnk",
|
||||
@@ -189,7 +157,7 @@
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773165725.099038
|
||||
"expires": 1773195384.925163
|
||||
},
|
||||
{
|
||||
"name": "cookie17",
|
||||
@@ -198,15 +166,15 @@
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1773165725.099007
|
||||
"expires": 1773195384.925061
|
||||
},
|
||||
{
|
||||
"name": "existShop",
|
||||
"value": "MTc3MDU3MzcyNQ%3D%3D",
|
||||
"value": "MTc3MDYwMzM4NA%3D%3D",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773165725.099129
|
||||
"expires": 1773195384.925355
|
||||
},
|
||||
{
|
||||
"name": "cancelledSubSites",
|
||||
@@ -214,24 +182,58 @@
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773165725.098946
|
||||
"expires": 1773195384.924932
|
||||
},
|
||||
{
|
||||
"name": "csg",
|
||||
"value": "63a3b224",
|
||||
"value": "0264eb7d",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773165725.098847
|
||||
"expires": 1773195384.924719
|
||||
},
|
||||
{
|
||||
"name": "uc4",
|
||||
"value": "id4=0%40U22Jqe1yyAMca0PbJ175RuXiEyQm\u0026nk4=0%4024ipPGyqAyNcYskAXekPLbg%3D",
|
||||
"name": "wk_cookie2",
|
||||
"value": "162fccd75f81c2ab3adf6c46893d896e",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1773165725.099159
|
||||
"expires": 1775816181.362046
|
||||
},
|
||||
{
|
||||
"name": "lgc",
|
||||
"value": "%5Cu4F55%5Cu672A%5Cu5C39",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773195384.92485
|
||||
},
|
||||
{
|
||||
"name": "havana_lgc2_0",
|
||||
"value": "eyJoaWQiOjI3Njk3OTA4MjUsInNnIjoiZGY0MTk5Y2I5OGNmNDhkNWY2ZWEyMTc1OGUxNmNkMDIiLCJzaXRlIjowLCJ0b2tlbiI6IjEwUDI4UVlVcmQ0bmFLeDJxTW5jQnJnIn0",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1801736188.271261
|
||||
},
|
||||
{
|
||||
"name": "_tb_token_",
|
||||
"value": "b3b3916eee03",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773195384.926065
|
||||
},
|
||||
{
|
||||
"name": "uc4",
|
||||
"value": "nk4=0%4024ipPGyqAyNcYskD8y6O6Zo%3D\u0026id4=0%40U22Jqe1yyAMca0PbJ175RQwJKeTy",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1773195384.925414
|
||||
},
|
||||
{
|
||||
"name": "unb",
|
||||
@@ -240,36 +242,26 @@
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1773165725.098682
|
||||
"expires": 1773195384.924399
|
||||
},
|
||||
{
|
||||
"name": "mtop_partitioned_detect",
|
||||
"value": "1",
|
||||
"name": "sn",
|
||||
"value": "",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1770579126.261076
|
||||
"expires": 1773195384.924613
|
||||
},
|
||||
{
|
||||
"name": "3PcFlag",
|
||||
"value": "1770573702709",
|
||||
"name": "thw",
|
||||
"value": "cn",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1771466502.684566
|
||||
},
|
||||
{
|
||||
"name": "sgcookie",
|
||||
"value": "E100QYmZjz3WUI%2B6Yb1YO6UfaxPQ62sdAFFOvgkUiNEFnVLhaN%2FpQ9g6DjgxTOJNI9udV24oFQAPLP17peJOZks%2FOvhoACbilBAC1Hhe3vMAoa9jvUDa9%2F5D%2FVIoj40y%2Ffnx",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1802138522.464589
|
||||
"expires": 1802139363.365101
|
||||
},
|
||||
{
|
||||
"name": "ultraCookieBase",
|
||||
"value": "1k6S5%2BcxkgQpZA4Xfbyi8gTG%2By4cSksG3aXzy18EbaWBcFwmxQrEFaptGmtlgMhMyFkb9xXZw49oDDyhu36m6TasHWvnb5c0fdjGmLf0P4VfHzktxDxzIUgpJasHyc2AFlKN6wB8MGHOhvDSnHXMMA9Ak3KjaEUqLHbfZjokqSviRjqfKadUT%2BRD5bo15bmtMvqZRIj851tLvlG8tRKCZ5OwrvUSwimB4MvXB0EjsKp0nOQx3IDBO%2Bf2xICypyv1o13sbZyPDPfe756xwOhelodlzHk7hp%2FhysThoa8BCrcd65zMYzb5Tdio2bhkgVfFIya0JcQ%3D%3D",
|
||||
"value": "1k6S5%2BcxkgQpZBn6L87UFT1gB9dmSToMf4PxhORghtU%2FDagzMKO981v6AnsKpz%2BwDWaU87CeU7Sgb2N%2FPec%2FgpkEJ5kKklDVUZM3AmRyZ1l7uyhr9OreEmQsDaZ3f2MDNFZ8eblrhJsnqpN1oxZ4DYYGQsy6myUzIIK14RgAkbxEeNL%2BA4Izs76EUOpLCEPn%2FwSpHEyB6tiyfgJkngMhCgcOkn2lpq3PLd%2BtyrvoRQUy1oDnrjBCq%2FyEEMn5Ldi6IAMTiF5vBvSzQwsoeX88gnDa%2BTSrhB3UWCPdK4UD1xZU2fe4a7k5OzHib5zf9wlR7uJsP",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
@@ -282,41 +274,32 @@
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1801706527.482438
|
||||
"expires": 1801736188.27097
|
||||
},
|
||||
{
|
||||
"name": "wk_cookie2",
|
||||
"value": "182060f48332dfef01c11cf24af15454",
|
||||
"name": "mtop_partitioned_detect",
|
||||
"value": "1",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1770608786.329052
|
||||
},
|
||||
{
|
||||
"name": "3PcFlag",
|
||||
"value": "1770603366030",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1771496166.315193
|
||||
},
|
||||
{
|
||||
"name": "sgcookie",
|
||||
"value": "E100DaLni74mW2GGJkv7IBQE9jRlQNcxZe5nEjs4GYaV77r6LAZVU3a4Ee0FYbKjNVMzsBquzyxjwu27MnV%2B6WIq%2BU4M%2FvkKb%2FnDq2%2Bh4oModQpRJ1N8pm4BjLT%2BEPKB4NKs",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1775786522.464268
|
||||
},
|
||||
{
|
||||
"name": "lgc",
|
||||
"value": "%5Cu4F55%5Cu672A%5Cu5C39",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773165725.098914
|
||||
},
|
||||
{
|
||||
"name": "havana_lgc2_0",
|
||||
"value": "eyJoaWQiOjI3Njk3OTA4MjUsInNnIjoiNmIwZGE3OTVhOTNmNDE0MTQ5ZWQ2YmYzNjY0ZDAyM2QiLCJzaXRlIjowLCJ0b2tlbiI6IjFYNXFrLTlIeExkV2k5MUtnTXlVV0hnIn0",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1801706527.482518
|
||||
},
|
||||
{
|
||||
"name": "_tb_token_",
|
||||
"value": "e71eb736fe5ee",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773165725.099468
|
||||
"expires": 1802168181.362182
|
||||
},
|
||||
{
|
||||
"name": "sg",
|
||||
@@ -324,7 +307,24 @@
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773165725.099374
|
||||
"expires": 1773195384.925878
|
||||
},
|
||||
{
|
||||
"name": "_nk_",
|
||||
"value": "%5Cu4F55%5Cu672A%5Cu5C39",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773195384.925937
|
||||
},
|
||||
{
|
||||
"name": "_samesite_flag_",
|
||||
"value": "true",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": -1
|
||||
},
|
||||
{
|
||||
"name": "xlly_s",
|
||||
@@ -332,31 +332,31 @@
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1770832903
|
||||
"expires": 1770862538
|
||||
},
|
||||
{
|
||||
"name": "t",
|
||||
"value": "b19353ffe27e48a62571c666710e67aa",
|
||||
"value": "e4f280cfb52f5d89ece3d20bfa6b2567",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1773165725.098976
|
||||
"expires": 1773195384.924994
|
||||
},
|
||||
{
|
||||
"name": "_m_h5_tk",
|
||||
"value": "bbf86786a9fc68cbff2d3ea69228ef04_1770584166279",
|
||||
"value": "26d4b2054abadb735d63ee433ff5dd9a_1770611666043",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"expires": 1770579126.261124
|
||||
"expires": 1770608786.329178
|
||||
},
|
||||
{
|
||||
"name": "cookie2",
|
||||
"value": "1a0986ed49f3d18139c1d71113f6ce18",
|
||||
"value": "1d18a139e7c5fd502d3420930fddeed2",
|
||||
"domain": ".taobao.com",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 1773165725.0991
|
||||
"expires": 1773195384.925293
|
||||
}
|
||||
]
|
||||
315
cmd/main.go
315
cmd/main.go
@@ -1,201 +1,156 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go-sccchina/libs"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
"go-taobao/config"
|
||||
"go-taobao/libs"
|
||||
"go-taobao/services"
|
||||
"go-taobao/utils"
|
||||
)
|
||||
|
||||
var login = flag.Bool("login", false, "是否登录")
|
||||
var test = flag.Bool("time", false, "测试")
|
||||
var Browser libs.Browser
|
||||
var (
|
||||
mode = flag.String("mode", "", "运行模式: login(登录) | rush(抢购)")
|
||||
)
|
||||
|
||||
// var Chrome libs.Chrome
|
||||
func main() {
|
||||
flag.Parse()
|
||||
log.Println(*login)
|
||||
if *login {
|
||||
loginFun()
|
||||
}
|
||||
if *test {
|
||||
obj := Browser.GetBrowser(1, &libs.Fingerprint{})
|
||||
browser := obj.Browser
|
||||
page := obj.Page
|
||||
cookies, _ := loadCookiesFromFile("account_cookie.json")
|
||||
browser.SetCookies(cookies)
|
||||
page.MustNavigate("https://cart.taobao.com/cart.htm?spm=tbpc.mytb_index.leftnav.1.6db2782d0TVVip").MustWaitLoad()
|
||||
page.MustWaitNavigation()
|
||||
page.MustWaitLoad() // DOM 加载完成
|
||||
page.MustWaitIdle() // 基本静止(减少早期误判)
|
||||
page.MustActivate() // 前置以便扫码
|
||||
// // 获取当前课程列表
|
||||
// res := getApiResponse(page, "https://hnit.sccchina.net/student/student/coursestudy/getlist")
|
||||
// var dataBody map[string]any
|
||||
// json.Unmarshal([]byte(res), &dataBody)
|
||||
// list := dataBody["items"].([]interface{})
|
||||
// // pretty, _ := json.MarshalIndent(list, "", " ")
|
||||
// // 判断当前有多少个课程需要学习
|
||||
// for _, v := range list {
|
||||
// m := v.(map[string]interface{})
|
||||
// realCoursewarePlayNum := m["realCoursewarePlayNum"].(string)
|
||||
// realCoursewarePlayTime := m["realCoursewarePlayTime"].(string)
|
||||
// var showScore string = "0"
|
||||
// if m["showScore"] != nil {
|
||||
// if str, ok := m["showScore"].(string); ok {
|
||||
// showScore = str
|
||||
// }
|
||||
// }
|
||||
// courseVersionID := m["courseVersionID"].(string)
|
||||
// teachplanCourseVersionId := m["teachplanCourseVersionId"].(string)
|
||||
// sign := m["sign"].(string)
|
||||
// userAccountId := m["userAccountId"].(string)
|
||||
// versionName := m["versionName"].(string)
|
||||
// // 分割字符串 判断当前 课时 成绩
|
||||
// playTimeArr := strings.Split(realCoursewarePlayTime, " / ")
|
||||
// // 将第一个转为数字
|
||||
// activeTime, _ := strconv.ParseFloat(strings.TrimSpace(playTimeArr[0]), 64)
|
||||
// var endTime float64
|
||||
// if strings.TrimSpace(playTimeArr[1]) == "--" {
|
||||
// endTime = 360
|
||||
// } else {
|
||||
// TimeEnd, _ := strconv.ParseFloat(strings.TrimSpace(playTimeArr[1]), 64)
|
||||
// endTime = TimeEnd
|
||||
// }
|
||||
// // 判断有哪些课程需要观看
|
||||
// // var pages []*rod.Page
|
||||
// if showScore == "0" && activeTime < endTime {
|
||||
// log.Println(activeTime)
|
||||
// log.Println(endTime)
|
||||
// log.Println(realCoursewarePlayNum, realCoursewarePlayTime, showScore, courseVersionID, teachplanCourseVersionId, sign, userAccountId, versionName)
|
||||
// url := fmt.Sprintf(`https://hnit.sccchina.net/student/videolearning.html#Subpage/StudentVersionVideo?courseVersionId=%s&teachplanCourseVersionId=%s&sign=%s&userAccountId=%s&syncPracticePiwik=1&piwikSiteId=750&versionName=%s`, courseVersionID, teachplanCourseVersionId, sign, userAccountId, versionName)
|
||||
// log.Println(url)
|
||||
// time.Sleep(3 * time.Second)
|
||||
// p := browser.MustPage(url)
|
||||
// satrt := p.Timeout(5 * time.Second).MustElement(`#button-courseware`)
|
||||
// satrt.MustClick()
|
||||
// time.Sleep(2 * time.Second)
|
||||
// log.Println(1)
|
||||
// iframeEl := p.Timeout(10*time.Second).MustElementR(`iframe`, ".*")
|
||||
// log.Println(2)
|
||||
// log.Println(iframeEl)
|
||||
// if iframeEl != nil {
|
||||
// iframePage := iframeEl.MustFrame()
|
||||
// iframePage.MustWaitLoad()
|
||||
// // 再次进入内嵌的iframe
|
||||
// iframeEl2 := iframePage.Timeout(10 * time.Second).MustElement(`iframe#iframe`)
|
||||
// iframePage2 := iframeEl2.MustFrame()
|
||||
// iframePage2.MustWaitLoad()
|
||||
// cl := iframePage2.Timeout(5 * time.Second).MustElement(`em.pop_close`)
|
||||
// cl.MustClick()
|
||||
// }
|
||||
// time.Sleep(3 * time.Second)
|
||||
// // pages = append(pages, p)
|
||||
|
||||
// // go func() {
|
||||
// // browser.MustPage(url)
|
||||
// // }()
|
||||
// }
|
||||
// }
|
||||
// log.Println(string(pretty))
|
||||
cfg := config.DefaultConfig()
|
||||
|
||||
switch *mode {
|
||||
case "login":
|
||||
runLogin(cfg)
|
||||
case "rush":
|
||||
runRush(cfg)
|
||||
default:
|
||||
fmt.Println("淘宝抢购脚本")
|
||||
fmt.Println("用法:")
|
||||
fmt.Println(" -mode login 扫码登录并保存 Cookie")
|
||||
fmt.Println(" -mode rush 启动定时抢购")
|
||||
fmt.Printf("\n当前抢购目标时间: %s\n", cfg.TargetTime.Format("2006-01-02 15:04:05"))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// runLogin 登录模式:扫码登录并保存 Cookie
|
||||
func runLogin(cfg *config.RushConfig) {
|
||||
log.Println("========== 登录模式 ==========")
|
||||
auth := services.NewAuth(cfg)
|
||||
if err := auth.Login(); err != nil {
|
||||
log.Fatalf("[错误] 登录失败: %v", err)
|
||||
}
|
||||
log.Println("[完成] 登录成功,Cookie 已保存")
|
||||
}
|
||||
|
||||
// runRush 抢购模式:定时抢购主流程
|
||||
// 流程: 提前进购物车 → 全选 → 结算 → 进入订单确认页 → 到点刷新 → 检测可购买 → 提交
|
||||
func runRush(cfg *config.RushConfig) {
|
||||
log.Println("========== 抢购模式 ==========")
|
||||
log.Printf("[配置] 目标时间: %s", cfg.TargetTime.Format("2006-01-02 15:04:05"))
|
||||
log.Printf("[配置] 最大重试: %d 次", cfg.MaxRetry)
|
||||
log.Printf("[配置] 提前量: %v", cfg.SubmitAdvance)
|
||||
|
||||
// 1. 同步服务器时间
|
||||
log.Println("\n--- 第1步: 同步服务器时间 ---")
|
||||
timeSync, err := utils.NewTimeSync(5)
|
||||
if err != nil {
|
||||
log.Fatalf("[错误] 时间同步失败: %v", err)
|
||||
}
|
||||
log.Printf("[时间] 本地时间: %s", time.Now().Format("15:04:05.000"))
|
||||
log.Printf("[时间] 服务器时间: %s", timeSync.Now().Format("15:04:05.000"))
|
||||
log.Printf("[时间] 偏移量: %v", timeSync.Offset)
|
||||
|
||||
remaining := timeSync.UntilTarget(cfg.TargetTime)
|
||||
if remaining <= 0 {
|
||||
log.Println("[警告] 抢购时间已过!")
|
||||
return
|
||||
}
|
||||
log.Printf("[时间] 距离抢购还有: %v", remaining.Round(time.Second))
|
||||
|
||||
// 2. 加载 Cookie & 启动浏览器
|
||||
log.Println("\n--- 第2步: 启动浏览器 ---")
|
||||
auth := services.NewAuth(cfg)
|
||||
cookies, err := auth.LoadCookies()
|
||||
if err != nil {
|
||||
log.Fatalf("[错误] 加载 Cookie 失败: %v (请先运行 -mode login)", err)
|
||||
}
|
||||
|
||||
var browserMgr libs.Browser
|
||||
browserInfo := browserMgr.GetBrowser(cfg.AccountID, &libs.Fingerprint{})
|
||||
if err := auth.InjectCookies(browserInfo.Browser, cookies); err != nil {
|
||||
log.Fatalf("[错误] 注入 Cookie 失败: %v", err)
|
||||
}
|
||||
log.Println("[浏览器] Cookie 注入成功")
|
||||
|
||||
// 3. 提前打开购物车,全选商品,点击结算进入订单页
|
||||
log.Println("\n--- 第3步: 进入购物车并结算 ---")
|
||||
cart := services.NewCart(browserInfo.Page)
|
||||
cart.Open()
|
||||
cart.DisableAnimations()
|
||||
cart.SelectAll()
|
||||
|
||||
log.Println("[购物车] 点击结算,进入订单确认页...")
|
||||
if err := cart.ClickCheckout(); err != nil {
|
||||
log.Fatalf("[错误] 结算失败: %v", err)
|
||||
}
|
||||
|
||||
// 4. 等待进入订单确认页
|
||||
order := services.NewOrder(browserInfo.Page)
|
||||
if err := order.WaitForConfirmPage(15 * time.Second); err != nil {
|
||||
log.Fatalf("[错误] 进入订单确认页失败: %v", err)
|
||||
}
|
||||
log.Println("[订单] 已停留在订单确认页,等待抢购时间...")
|
||||
|
||||
// 5. 创建定时器 & 启动倒计时
|
||||
timer := services.NewTimer(cfg, timeSync)
|
||||
countdownDone := make(chan struct{})
|
||||
go timer.PrintCountdown(countdownDone)
|
||||
|
||||
// 6. 精确等待到抢购时间(目标时间 - 提前量)
|
||||
log.Println("\n--- 第4步: 等待抢购时间 ---")
|
||||
timer.WaitUntilRush()
|
||||
close(countdownDone)
|
||||
|
||||
// 7. 到点! 刷新订单页 → 检测是否可提交 → 提交
|
||||
log.Println("\n--- 第5步: 开始抢购! ---")
|
||||
|
||||
for attempt := 1; attempt <= cfg.MaxRetry; attempt++ {
|
||||
log.Printf("[抢购] 第 %d/%d 次尝试", attempt, cfg.MaxRetry)
|
||||
|
||||
// 刷新订单确认页
|
||||
order.Refresh()
|
||||
|
||||
// 检测是否可以提交
|
||||
if !order.CanSubmit() {
|
||||
log.Println("[抢购] 当前仍不可购买")
|
||||
if attempt < cfg.MaxRetry {
|
||||
time.Sleep(cfg.RetryInterval)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 可以购买,立即提交!
|
||||
result := order.Submit()
|
||||
if result.Success {
|
||||
log.Println("🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉")
|
||||
log.Printf("[成功] %s", result.Message)
|
||||
log.Println("🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉")
|
||||
log.Println("[提示] 浏览器保持打开,请尽快完成付款!")
|
||||
select {}
|
||||
}
|
||||
|
||||
log.Printf("[抢购] 提交失败: %s", result.Message)
|
||||
if attempt < cfg.MaxRetry {
|
||||
time.Sleep(cfg.RetryInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func loginFun() {
|
||||
obj := Browser.GetBrowser(1, &libs.Fingerprint{})
|
||||
browser := obj.Browser
|
||||
page := obj.Page
|
||||
page.MustNavigate(`https://login.taobao.com/havanaone/login/login.htm`).MustWaitLoad()
|
||||
page.MustWaitNavigation()
|
||||
page.MustWaitLoad() // DOM 加载完成
|
||||
page.MustWaitIdle() // 基本静止(减少早期误判)
|
||||
page.MustActivate() // 前置以便扫码
|
||||
|
||||
var usl string = "https://login.taobao.com/havanaone/login/login.htm"
|
||||
for usl == "https://login.taobao.com/havanaone/login/login.htm" {
|
||||
uri := page.MustEval("() => window.location.href").String()
|
||||
log.Println("获取当前网页URL:", uri)
|
||||
usl = uri
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
page.MustNavigate("https://cart.taobao.com/cart.htm?spm=tbpc.mytb_index.leftnav.1.6db2782d0TVVip").MustWaitLoad()
|
||||
page.MustWaitNavigation()
|
||||
page.MustWaitLoad() // DOM 加载完成
|
||||
page.MustWaitIdle() // 基本静止(减少早期误判)
|
||||
page.MustActivate() // 前置以便扫码
|
||||
cookieArr := getPageCookie(page, "https://cart.taobao.com/cart.htm?spm=tbpc.mytb_index.leftnav.1.6db2782d0TVVip")
|
||||
saveCookiesToFile(cookieArr, "account_cookie.json")
|
||||
browser.Close()
|
||||
}
|
||||
|
||||
func getPageCookie(page *rod.Page, url string) []*proto.NetworkCookieParam {
|
||||
cookieArr, _ := page.Cookies([]string{url})
|
||||
var cookies []*proto.NetworkCookieParam
|
||||
for _, cookie := range cookieArr {
|
||||
cookies = append(cookies, &proto.NetworkCookieParam{
|
||||
Name: cookie.Name,
|
||||
Value: cookie.Value,
|
||||
Domain: cookie.Domain,
|
||||
Path: cookie.Path,
|
||||
Expires: cookie.Expires, // 过期时间
|
||||
HTTPOnly: cookie.HTTPOnly, // 禁止 JS 访问
|
||||
Secure: cookie.Secure, // 仅 HTTPS 传输
|
||||
})
|
||||
}
|
||||
return cookies
|
||||
}
|
||||
|
||||
func saveCookiesToFile(cookies []*proto.NetworkCookieParam, filename string) error {
|
||||
data, err := json.MarshalIndent(cookies, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filename, data, 0644)
|
||||
}
|
||||
|
||||
func loadCookiesFromFile(filename string) ([]*proto.NetworkCookieParam, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cookies []*proto.NetworkCookieParam
|
||||
if err := json.Unmarshal(data, &cookies); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cookies, nil
|
||||
}
|
||||
|
||||
// 获取指定接口的响应参数
|
||||
func getApiResponse(page *rod.Page, url string) string {
|
||||
// 定义一个通道接收消息
|
||||
ch := make(chan string)
|
||||
go page.EachEvent(func(e *proto.PageLoadEventFired) {
|
||||
log.Println("1234567890")
|
||||
}, func(e *proto.NetworkResponseReceived) {
|
||||
if strings.Contains(e.Response.URL, url) {
|
||||
reply, err := (proto.NetworkGetResponseBody{RequestID: e.RequestID}).Call(page)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
ch <- reply.Body
|
||||
// fmt.Println(reply.Body)
|
||||
}
|
||||
})()
|
||||
|
||||
select {
|
||||
case res := <-ch:
|
||||
return res
|
||||
case <-time.After(30 * time.Second):
|
||||
log.Println("等待上传响应超时")
|
||||
return ""
|
||||
}
|
||||
log.Println("[结束] 已达最大重试次数")
|
||||
log.Println("[提示] 浏览器保持打开,你可以手动操作")
|
||||
select {}
|
||||
}
|
||||
|
||||
84
config/config.go
Normal file
84
config/config.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// RushConfig 抢购配置
|
||||
type RushConfig struct {
|
||||
// 抢购目标时间
|
||||
TargetTime time.Time
|
||||
// 提交失败最大重试次数
|
||||
MaxRetry int
|
||||
// 重试间隔
|
||||
RetryInterval time.Duration
|
||||
// 提前多久预加载购物车页面
|
||||
PreloadBefore time.Duration
|
||||
// 提前多少毫秒开始提交(网络延迟补偿)
|
||||
SubmitAdvance time.Duration
|
||||
// Cookie 文件路径
|
||||
CookieFile string
|
||||
// 浏览器用户数据目录 ID
|
||||
AccountID int
|
||||
}
|
||||
|
||||
// DefaultConfig 返回默认抢购配置
|
||||
func DefaultConfig() *RushConfig {
|
||||
// 默认目标: 2026-02-10 20:00:00 CST
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
target := time.Date(2026, 2, 10, 20, 0, 0, 0, loc)
|
||||
|
||||
return &RushConfig{
|
||||
TargetTime: target,
|
||||
MaxRetry: 5,
|
||||
RetryInterval: 200 * time.Millisecond,
|
||||
PreloadBefore: 30 * time.Second,
|
||||
SubmitAdvance: 500 * time.Millisecond,
|
||||
CookieFile: "account_cookie.json",
|
||||
AccountID: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// 淘宝关键页面 URL
|
||||
const (
|
||||
LoginURL = "https://login.taobao.com/havanaone/login/login.htm"
|
||||
CartURL = "https://cart.taobao.com/cart.htm"
|
||||
)
|
||||
|
||||
// TimeAPIs 时间同步 API 列表(按优先级排列,依次降级)
|
||||
var TimeAPIs = []TimeAPIConfig{
|
||||
{
|
||||
Name: "淘宝 H5",
|
||||
URL: "https://h5api.m.taobao.com/h5/mtop.common.getTimestamp/",
|
||||
Type: TimeAPITypeTaobaoH5,
|
||||
},
|
||||
{
|
||||
Name: "苏宁",
|
||||
URL: "https://quan.suning.com/getSysTime.do",
|
||||
Type: TimeAPITypeSuning,
|
||||
},
|
||||
{
|
||||
Name: "淘宝主站",
|
||||
URL: "https://www.taobao.com",
|
||||
Type: TimeAPITypeHTTPHeader,
|
||||
},
|
||||
{
|
||||
Name: "天猫",
|
||||
URL: "https://www.tmall.com",
|
||||
Type: TimeAPITypeHTTPHeader,
|
||||
},
|
||||
}
|
||||
|
||||
// TimeAPIType 时间 API 类型
|
||||
type TimeAPIType int
|
||||
|
||||
const (
|
||||
TimeAPITypeTaobaoH5 TimeAPIType = iota // 淘宝 H5 JSON 响应
|
||||
TimeAPITypeSuning // 苏宁 JSON 响应
|
||||
TimeAPITypeHTTPHeader // 使用 HTTP Date 响应头
|
||||
)
|
||||
|
||||
// TimeAPIConfig 时间 API 配置
|
||||
type TimeAPIConfig struct {
|
||||
Name string
|
||||
URL string
|
||||
Type TimeAPIType
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
package libs
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
)
|
||||
|
||||
// Browser 浏览器实例管理器
|
||||
type Browser struct{}
|
||||
|
||||
// BrowserInfo 浏览器实例信息
|
||||
type BrowserInfo struct {
|
||||
Id int
|
||||
Browser *rod.Browser
|
||||
@@ -15,7 +18,7 @@ type BrowserInfo struct {
|
||||
Page *rod.Page
|
||||
}
|
||||
|
||||
// 浏览器指纹数据
|
||||
// Fingerprint 浏览器指纹配置
|
||||
type Fingerprint struct {
|
||||
Viewport []struct {
|
||||
Height int
|
||||
@@ -26,58 +29,61 @@ type Fingerprint struct {
|
||||
}
|
||||
|
||||
var (
|
||||
BrowserPool = make(map[int]*BrowserInfo) // 初始化map
|
||||
poolMutex sync.RWMutex // 添加读写锁保证并发安全
|
||||
browserPool = make(map[int]*BrowserInfo)
|
||||
poolMutex sync.RWMutex
|
||||
)
|
||||
|
||||
var ChromeExample Chrome
|
||||
|
||||
// GetBrowser 获取或创建浏览器实例(线程安全,双重检查锁定)
|
||||
func (*Browser) GetBrowser(id int, params *Fingerprint) *BrowserInfo {
|
||||
poolMutex.RLock()
|
||||
value, ok := BrowserPool[id]
|
||||
value, ok := browserPool[id]
|
||||
poolMutex.RUnlock()
|
||||
|
||||
if ok {
|
||||
return value
|
||||
}
|
||||
|
||||
// 未找到时获取写锁(互斥)
|
||||
poolMutex.Lock()
|
||||
defer poolMutex.Unlock()
|
||||
|
||||
// 双检查:避免在等待锁期间其他goroutine已创建
|
||||
if value, ok := BrowserPool[id]; ok {
|
||||
// 双重检查
|
||||
if value, ok := browserPool[id]; ok {
|
||||
return value
|
||||
}
|
||||
|
||||
// 创建新实例(补充CancelFunc)
|
||||
var ChromeExample Chrome
|
||||
browser, url, page := ChromeExample.Create(id, params)
|
||||
var chrome Chrome
|
||||
browser, url, page := chrome.Create(id, params)
|
||||
browserInfo := &BrowserInfo{
|
||||
Id: id,
|
||||
Browser: browser,
|
||||
Url: url,
|
||||
Page: page,
|
||||
}
|
||||
BrowserPool[id] = browserInfo
|
||||
browserPool[id] = browserInfo
|
||||
log.Printf("[浏览器] 创建实例 #%d", id)
|
||||
return browserInfo
|
||||
}
|
||||
|
||||
func (*Browser) GetAllBrowser() map[int]*BrowserInfo {
|
||||
return BrowserPool
|
||||
}
|
||||
|
||||
func (*Browser) CancelAllBrowser() {
|
||||
for _, v := range BrowserPool {
|
||||
// CloseAll 关闭所有浏览器实例
|
||||
func (*Browser) CloseAll() {
|
||||
poolMutex.Lock()
|
||||
defer poolMutex.Unlock()
|
||||
for id, v := range browserPool {
|
||||
v.Browser.MustClose()
|
||||
delete(browserPool, id)
|
||||
}
|
||||
log.Println("[浏览器] 已关闭所有实例")
|
||||
}
|
||||
|
||||
func (*Browser) CancelBrowser(id int) {
|
||||
browserInfo := BrowserPool[id]
|
||||
if browserInfo == nil {
|
||||
// Close 关闭指定浏览器实例
|
||||
func (*Browser) Close(id int) {
|
||||
poolMutex.Lock()
|
||||
defer poolMutex.Unlock()
|
||||
info, ok := browserPool[id]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
browserInfo.Browser.MustClose()
|
||||
delete(BrowserPool, id)
|
||||
info.Browser.MustClose()
|
||||
delete(browserPool, id)
|
||||
log.Printf("[浏览器] 已关闭实例 #%d", id)
|
||||
}
|
||||
|
||||
@@ -11,70 +11,60 @@ import (
|
||||
"github.com/go-rod/stealth"
|
||||
)
|
||||
|
||||
// Chrome 浏览器创建器
|
||||
type Chrome struct{}
|
||||
|
||||
// Create 创建一个带反检测的 Chrome 浏览器实例
|
||||
func (c *Chrome) Create(accountId int, params *Fingerprint) (*rod.Browser, string, *rod.Page) {
|
||||
var accountIdStr = strconv.Itoa(accountId)
|
||||
userDataDir := "user-data/" + accountIdStr
|
||||
userDataDir := "user-data/" + strconv.Itoa(accountId)
|
||||
absPath, _ := filepath.Abs(userDataDir)
|
||||
log.Printf("用户数据目录: %s", absPath)
|
||||
path, _ := launcher.LookPath() // 获取当前系统的浏览器目录
|
||||
// 判断是否传递代理
|
||||
// var proxyHandle utils.ProxyHandle
|
||||
// protocol, username, password, ip, port, errProxy := proxyHandle.ParseProxy(params.Proxy)
|
||||
log.Printf("[Chrome] 用户数据目录: %s", absPath)
|
||||
|
||||
chromePath, _ := launcher.LookPath()
|
||||
|
||||
l := launcher.New().
|
||||
Bin(path).
|
||||
// Proxy("210.51.27.121:50003"). // 直接传入完整认证URL
|
||||
Delete("use-mock-keychain"). // delete flag "--use-mock-keychain"
|
||||
Set("disable-blink-features", "AutomationControlled"). // 绕过自动化检测
|
||||
// Set("incognito"). // 无痕模式
|
||||
Set("user-data-dir", absPath). // 数据持久化目录
|
||||
Set("window-size", "1920,1480"). // 窗口尺寸
|
||||
Set("disable-infobars", "true"). // 隐藏自动化提示栏
|
||||
Set("no-sandbox", "true").
|
||||
Bin(chromePath).
|
||||
Delete("use-mock-keychain").
|
||||
// 反自动化检测
|
||||
Set("disable-blink-features", "AutomationControlled").
|
||||
Set("excludeSwitches", "enable-automation").
|
||||
Set("enable-gpu"). // 启用 GPU 加速
|
||||
Set("ignore-certificate-errors"). // 忽略证书错误
|
||||
Set("use-fake-ui-for-media-stream"). // 允许媒体流
|
||||
Set("autoplay-policy", "no-user-gesture-required"). // 自动播放
|
||||
Set("disable-infobars", "true").
|
||||
// 数据持久化 & 窗口
|
||||
Set("user-data-dir", absPath).
|
||||
Set("window-size", "1920,1480").
|
||||
// 性能 & 安全
|
||||
Set("no-sandbox", "true").
|
||||
Set("enable-gpu").
|
||||
Set("ignore-certificate-errors").
|
||||
Set("disable-application-cache").
|
||||
Set("disable-dev-shm-usage").
|
||||
// 禁用webrtc
|
||||
Set("disable-webrtc", "true"). // 核心禁用参数
|
||||
Set("disable-features", "WebRtcHideLocalIpsWithMdns"). // 隐藏本地IP
|
||||
Set("webrtc-ip-handling-policy", "disable_non_proxied_udp"). // 禁用非代理UDP
|
||||
// 媒体
|
||||
Set("use-fake-ui-for-media-stream").
|
||||
Set("autoplay-policy", "no-user-gesture-required").
|
||||
// 隐私: 禁用 WebRTC 防止 IP 泄露
|
||||
Set("disable-webrtc", "true").
|
||||
Set("disable-features", "WebRtcHideLocalIpsWithMdns").
|
||||
Set("webrtc-ip-handling-policy", "disable_non_proxied_udp").
|
||||
Set("force-webrtc-ip-handling-policy").
|
||||
Headless(false) // 关闭无头模式
|
||||
|
||||
// 修改时区
|
||||
l.Set("disable-geolocation", "true") // 启用地理定位API
|
||||
l.Set("disable-web-security", "true") // 允许跨域(某些网站需要) 可以跨域访问iframe标签
|
||||
l.Set("disable-notifications", "true")
|
||||
l.Set("disable-site-isolation-trials", "true")
|
||||
// if errProxy == nil {
|
||||
// if strings.Contains(protocol, "http") {
|
||||
// log.Println(protocol + "://" + ip + ":" + port)
|
||||
// l = l.Proxy(ip + ":" + port)
|
||||
// }
|
||||
// }
|
||||
// 跨域 & 通知
|
||||
Set("disable-web-security", "true").
|
||||
Set("disable-notifications", "true").
|
||||
Set("disable-site-isolation-trials", "true").
|
||||
Set("disable-geolocation", "true").
|
||||
Headless(false)
|
||||
|
||||
uri := l.MustLaunch()
|
||||
|
||||
browser := rod.New().
|
||||
ControlURL(uri).
|
||||
MustConnect().NoDefaultDevice()
|
||||
MustConnect().
|
||||
NoDefaultDevice()
|
||||
browser.MustIgnoreCertErrors(true)
|
||||
// if errProxy == nil {
|
||||
// if strings.Contains(protocol, "http") {
|
||||
// log.Println(username, password)
|
||||
// go browser.MustHandleAuth(username, password)()
|
||||
// }
|
||||
// }
|
||||
page := stealth.MustPage(browser)
|
||||
// page := browser.MustPage()
|
||||
|
||||
// 使用 stealth 模式创建页面
|
||||
page := stealth.MustPage(browser)
|
||||
|
||||
// 为所有新打开的页面也注入 stealth 脚本
|
||||
go browser.EachEvent(func(e *proto.TargetTargetCreated) {
|
||||
if e.TargetInfo.Type != proto.TargetTargetInfoTypePage {
|
||||
return
|
||||
@@ -82,8 +72,8 @@ func (c *Chrome) Create(accountId int, params *Fingerprint) (*rod.Browser, strin
|
||||
browser.MustPageFromTargetID(e.TargetInfo.TargetID).MustEvalOnNewDocument(stealth.JS)
|
||||
})()
|
||||
|
||||
// 在页面加载前注入时区覆盖代码
|
||||
page.MustSetViewport(1920, 1480, 1.0, false)
|
||||
|
||||
log.Printf("[Chrome] 浏览器已启动 (账号 #%d)", accountId)
|
||||
return browser, uri, page
|
||||
}
|
||||
|
||||
113
services/auth.go
Normal file
113
services/auth.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go-taobao/config"
|
||||
"go-taobao/libs"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
)
|
||||
|
||||
// Auth 认证服务,管理登录和 Cookie
|
||||
type Auth struct {
|
||||
browser *libs.BrowserInfo
|
||||
cfg *config.RushConfig
|
||||
}
|
||||
|
||||
// NewAuth 创建认证服务
|
||||
func NewAuth(cfg *config.RushConfig) *Auth {
|
||||
return &Auth{cfg: cfg}
|
||||
}
|
||||
|
||||
// Login 打开登录页面,等待用户扫码,保存 Cookie
|
||||
func (a *Auth) Login() error {
|
||||
var mgr libs.Browser
|
||||
a.browser = mgr.GetBrowser(a.cfg.AccountID, &libs.Fingerprint{})
|
||||
|
||||
page := a.browser.Page
|
||||
page.MustNavigate(config.LoginURL).MustWaitLoad()
|
||||
page.MustWaitNavigation()
|
||||
page.MustWaitLoad()
|
||||
page.MustWaitIdle()
|
||||
page.MustActivate()
|
||||
|
||||
log.Println("[登录] 请扫码登录...")
|
||||
|
||||
// 轮询等待登录成功(URL 变化表示登录完成)
|
||||
for {
|
||||
uri := page.MustEval("() => window.location.href").String()
|
||||
log.Printf("[登录] 当前 URL: %s", uri)
|
||||
if uri != config.LoginURL {
|
||||
log.Println("[登录] 登录成功!")
|
||||
break
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
|
||||
// 导航到购物车页面获取完整 Cookie
|
||||
page.MustNavigate(config.CartURL).MustWaitLoad()
|
||||
page.MustWaitNavigation()
|
||||
page.MustWaitLoad()
|
||||
page.MustWaitIdle()
|
||||
|
||||
// 提取并保存 Cookie
|
||||
cookies := a.extractCookies(page, config.CartURL)
|
||||
if err := a.SaveCookies(cookies); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[登录] Cookie 已保存到 %s", a.cfg.CookieFile)
|
||||
a.browser.Browser.MustClose()
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadCookies 从文件加载 Cookie
|
||||
func (a *Auth) LoadCookies() ([]*proto.NetworkCookieParam, error) {
|
||||
data, err := os.ReadFile(a.cfg.CookieFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cookies []*proto.NetworkCookieParam
|
||||
if err := json.Unmarshal(data, &cookies); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("[认证] 已加载 %d 条 Cookie", len(cookies))
|
||||
return cookies, nil
|
||||
}
|
||||
|
||||
// SaveCookies 保存 Cookie 到文件
|
||||
func (a *Auth) SaveCookies(cookies []*proto.NetworkCookieParam) error {
|
||||
data, err := json.MarshalIndent(cookies, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(a.cfg.CookieFile, data, 0644)
|
||||
}
|
||||
|
||||
// InjectCookies 将 Cookie 注入浏览器
|
||||
func (a *Auth) InjectCookies(browser *rod.Browser, cookies []*proto.NetworkCookieParam) error {
|
||||
return browser.SetCookies(cookies)
|
||||
}
|
||||
|
||||
// extractCookies 从页面提取 Cookie
|
||||
func (a *Auth) extractCookies(page *rod.Page, url string) []*proto.NetworkCookieParam {
|
||||
cookieArr, _ := page.Cookies([]string{url})
|
||||
var cookies []*proto.NetworkCookieParam
|
||||
for _, cookie := range cookieArr {
|
||||
cookies = append(cookies, &proto.NetworkCookieParam{
|
||||
Name: cookie.Name,
|
||||
Value: cookie.Value,
|
||||
Domain: cookie.Domain,
|
||||
Path: cookie.Path,
|
||||
Expires: cookie.Expires,
|
||||
HTTPOnly: cookie.HTTPOnly,
|
||||
Secure: cookie.Secure,
|
||||
})
|
||||
}
|
||||
return cookies
|
||||
}
|
||||
132
services/cart.go
Normal file
132
services/cart.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
)
|
||||
|
||||
// Cart 购物车操作服务
|
||||
type Cart struct {
|
||||
page *rod.Page
|
||||
}
|
||||
|
||||
// NewCart 创建购物车服务
|
||||
func NewCart(page *rod.Page) *Cart {
|
||||
return &Cart{page: page}
|
||||
}
|
||||
|
||||
// Open 打开购物车页面并等待完全加载
|
||||
func (c *Cart) Open() error {
|
||||
log.Println("[购物车] 正在打开购物车页面...")
|
||||
c.page.MustNavigate("https://cart.taobao.com/cart.htm").MustWaitLoad()
|
||||
c.page.MustWaitIdle()
|
||||
log.Println("[购物车] 页面加载完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh 刷新购物车页面
|
||||
func (c *Cart) Refresh() error {
|
||||
log.Println("[购物车] 刷新购物车...")
|
||||
c.page.MustReload().MustWaitLoad()
|
||||
c.page.MustWaitIdle()
|
||||
log.Println("[购物车] 刷新完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelectAll 全选购物车商品
|
||||
func (c *Cart) SelectAll() error {
|
||||
log.Println("[购物车] 尝试全选商品...")
|
||||
|
||||
// 淘宝购物车全选按钮,尝试多种选择器兼容
|
||||
selectors := []string{
|
||||
// 常见的全选 checkbox 选择器
|
||||
`#cart-operation-fixed > div.cartOperationLeft--JnN5kBCf > label > span.ant-checkbox.ant-checkbox-checked > input[type="checkbox"]`,
|
||||
}
|
||||
|
||||
for _, sel := range selectors {
|
||||
el, err := c.page.Timeout(3 * time.Second).Element(sel)
|
||||
if err == nil && el != nil {
|
||||
el.MustClick()
|
||||
log.Printf("[购物车] 已点击全选按钮: %s", sel)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底方案:通过 JS 全选
|
||||
log.Println("[购物车] CSS 选择器未匹配,尝试 JS 全选...")
|
||||
c.page.MustEval(`() => {
|
||||
// 查找所有未选中的 checkbox 并点击
|
||||
const checkboxes = document.querySelectorAll('input[type="checkbox"]:not(:checked)');
|
||||
checkboxes.forEach(cb => cb.click());
|
||||
}`)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClickCheckout 点击"结算"按钮
|
||||
func (c *Cart) ClickCheckout() error {
|
||||
log.Println("[购物车] 查找结算按钮...")
|
||||
|
||||
// 按稳定性排序: class 选择器 > 容器内查找 > 文本匹配
|
||||
selectors := []string{
|
||||
"div.trade-cart-btn-submit div.btn--QDjHtErD", // 精确 class 路径
|
||||
"div.btnWrap--hJoXF9T5 div.btn--QDjHtErD", // 备用 wrapper class
|
||||
"#settlementContainer_1 div.btn--QDjHtErD", // 容器 ID + class
|
||||
"div[class*='trade-cart-btn-submit'] div[class*='btn--']", // 模糊匹配
|
||||
}
|
||||
|
||||
for _, sel := range selectors {
|
||||
el, err := c.page.Timeout(3 * time.Second).Element(sel)
|
||||
if err == nil && el != nil {
|
||||
el.MustClick()
|
||||
log.Printf("[购物车] 已点击结算按钮: %s", sel)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 兆底: 通过文本内容查找结算按钮
|
||||
log.Println("[购物车] 尝试通过文本内容查找结算按钮...")
|
||||
el, err := c.page.Timeout(3*time.Second).ElementR("div", "结算")
|
||||
if err == nil && el != nil {
|
||||
el.MustClick()
|
||||
log.Println("[购物车] 已通过文本匹配点击结算按钮")
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("未找到结算按钮")
|
||||
}
|
||||
|
||||
// HoverCheckout 将鼠标悬停在结算按钮上(预热,减少点击延迟)
|
||||
func (c *Cart) HoverCheckout() {
|
||||
log.Println("[购物车] 预热:悬停结算按钮...")
|
||||
|
||||
selectors := []string{
|
||||
"div.trade-cart-btn-submit div.btn--QDjHtErD",
|
||||
"div.btnWrap--hJoXF9T5 div.btn--QDjHtErD",
|
||||
"#settlementContainer_1 div.btn--QDjHtErD",
|
||||
}
|
||||
|
||||
for _, sel := range selectors {
|
||||
el, err := c.page.Timeout(2 * time.Second).Element(sel)
|
||||
if err == nil && el != nil {
|
||||
el.MustHover()
|
||||
log.Printf("[购物车] 已悬停结算按钮: %s", sel)
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Println("[购物车] 预热:未找到结算按钮")
|
||||
}
|
||||
|
||||
// DisableAnimations 注入 JS 禁用页面动画,加速交互
|
||||
func (c *Cart) DisableAnimations() {
|
||||
c.page.MustEval(`() => {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = '*, *::before, *::after { transition: none !important; animation: none !important; }';
|
||||
document.head.appendChild(style);
|
||||
}`)
|
||||
log.Println("[购物车] 已禁用页面动画")
|
||||
}
|
||||
164
services/order.go
Normal file
164
services/order.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
)
|
||||
|
||||
// OrderResult 下单结果
|
||||
type OrderResult struct {
|
||||
Success bool
|
||||
Message string
|
||||
}
|
||||
|
||||
// Order 订单提交服务
|
||||
type Order struct {
|
||||
page *rod.Page
|
||||
}
|
||||
|
||||
// NewOrder 创建订单服务
|
||||
func NewOrder(page *rod.Page) *Order {
|
||||
return &Order{page: page}
|
||||
}
|
||||
|
||||
// 提交订单按钮选择器列表
|
||||
var submitSelectors = []string{
|
||||
"#submitOrder > div > div.btnBox--p9CumEtE > div.btn--QDjHtErD", // 淘宝新版提交按钮
|
||||
}
|
||||
|
||||
// WaitForConfirmPage 等待订单确认页加载
|
||||
func (o *Order) WaitForConfirmPage(timeout time.Duration) error {
|
||||
log.Println("[订单] 等待订单确认页加载...")
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
url := o.page.MustEval("() => window.location.href").String()
|
||||
if o.isConfirmPage(url) {
|
||||
log.Printf("[订单] 已进入订单确认页: %s", url)
|
||||
o.page.MustWaitLoad()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
return fmt.Errorf("等待订单确认页超时")
|
||||
}
|
||||
|
||||
// Refresh 刷新当前订单确认页
|
||||
func (o *Order) Refresh() {
|
||||
log.Println("[订单] 刷新订单确认页...")
|
||||
o.page.MustReload().MustWaitLoad()
|
||||
o.page.MustWaitIdle()
|
||||
log.Println("[订单] 刷新完成")
|
||||
}
|
||||
|
||||
// CanSubmit 检测当前订单页是否可以提交(按钮存在且未被禁用)
|
||||
func (o *Order) CanSubmit() bool {
|
||||
for _, sel := range submitSelectors {
|
||||
el, err := o.page.Timeout(1 * time.Second).Element(sel)
|
||||
if err != nil || el == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查按钮是否被禁用(class 含 disabled / 属性 disabled 等)
|
||||
className, _ := el.Attribute("class")
|
||||
if className != nil && strings.Contains(*className, "disabled") {
|
||||
log.Printf("[订单] 按钮存在但被禁用: %s", sel)
|
||||
return false
|
||||
}
|
||||
|
||||
disabledAttr, _ := el.Attribute("disabled")
|
||||
if disabledAttr != nil {
|
||||
log.Printf("[订单] 按钮存在但 disabled 属性: %s", sel)
|
||||
return false
|
||||
}
|
||||
|
||||
log.Printf("[订单] ✅ 按钮可点击: %s", sel)
|
||||
return true
|
||||
}
|
||||
|
||||
// 还可以检查页面上是否有"该商品暂时无法购买"之类的提示
|
||||
pageText := o.page.MustEval(`() => document.body.innerText`).String()
|
||||
if strings.Contains(pageText, "无法购买") ||
|
||||
strings.Contains(pageText, "暂不可售") ||
|
||||
strings.Contains(pageText, "已下架") ||
|
||||
strings.Contains(pageText, "未开始") {
|
||||
log.Println("[订单] 页面提示商品暂时无法购买")
|
||||
return false
|
||||
}
|
||||
|
||||
log.Println("[订单] 未找到提交按钮")
|
||||
return false
|
||||
}
|
||||
|
||||
// RefreshUntilCanSubmit 循环刷新订单页,直到可以提交,返回时表示可以立即点击提交
|
||||
func (o *Order) RefreshUntilCanSubmit(interval time.Duration, maxAttempts int) bool {
|
||||
for i := 1; i <= maxAttempts; i++ {
|
||||
log.Printf("[订单] 第 %d/%d 次检测是否可购买...", i, maxAttempts)
|
||||
|
||||
if o.CanSubmit() {
|
||||
log.Println("[订单] ✅ 商品可购买!")
|
||||
return true
|
||||
}
|
||||
|
||||
if i < maxAttempts {
|
||||
time.Sleep(interval)
|
||||
o.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("[订单] 已达最大检测次数,商品仍不可购买")
|
||||
return false
|
||||
}
|
||||
|
||||
// Submit 点击"提交订单"按钮
|
||||
func (o *Order) Submit() *OrderResult {
|
||||
log.Println("[订单] 点击提交订单...")
|
||||
|
||||
for _, sel := range submitSelectors {
|
||||
el, err := o.page.Timeout(2 * time.Second).Element(sel)
|
||||
if err == nil && el != nil {
|
||||
el.MustClick()
|
||||
log.Printf("[订单] 已点击提交按钮: %s", sel)
|
||||
time.Sleep(1 * time.Second)
|
||||
return o.checkResult()
|
||||
}
|
||||
}
|
||||
|
||||
return &OrderResult{
|
||||
Success: false,
|
||||
Message: "未找到提交订单按钮",
|
||||
}
|
||||
}
|
||||
|
||||
// checkResult 检查下单结果
|
||||
func (o *Order) checkResult() *OrderResult {
|
||||
time.Sleep(2 * time.Second)
|
||||
url := o.page.MustEval("() => window.location.href").String()
|
||||
log.Printf("[订单] 提交后 URL: %s", url)
|
||||
|
||||
if strings.Contains(url, "cashier") ||
|
||||
strings.Contains(url, "pay") ||
|
||||
strings.Contains(url, "alipay") {
|
||||
return &OrderResult{
|
||||
Success: true,
|
||||
Message: "抢购成功!请尽快完成付款",
|
||||
}
|
||||
}
|
||||
|
||||
return &OrderResult{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("提交后仍在: %s", url),
|
||||
}
|
||||
}
|
||||
|
||||
// isConfirmPage 判断 URL 是否为订单确认页
|
||||
func (o *Order) isConfirmPage(url string) bool {
|
||||
return strings.Contains(url, "buy.taobao.com") ||
|
||||
strings.Contains(url, "buyertrade")
|
||||
}
|
||||
112
services/timer.go
Normal file
112
services/timer.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"go-taobao/config"
|
||||
"go-taobao/utils"
|
||||
)
|
||||
|
||||
// Timer 定时触发器,精确控制抢购时机
|
||||
type Timer struct {
|
||||
cfg *config.RushConfig
|
||||
timeSync *utils.TimeSync
|
||||
}
|
||||
|
||||
// NewTimer 创建定时器
|
||||
func NewTimer(cfg *config.RushConfig, ts *utils.TimeSync) *Timer {
|
||||
return &Timer{cfg: cfg, timeSync: ts}
|
||||
}
|
||||
|
||||
// WaitUntilPreload 等待到预加载时间点(目标时间 - PreloadBefore)
|
||||
// 返回后应立即开始预加载购物车
|
||||
func (t *Timer) WaitUntilPreload() {
|
||||
preloadTime := t.cfg.TargetTime.Add(-t.cfg.PreloadBefore)
|
||||
remaining := t.timeSync.UntilTarget(preloadTime)
|
||||
|
||||
if remaining <= 0 {
|
||||
log.Println("[定时器] 已过预加载时间,立即执行")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[定时器] 距离预加载还有 %v,等待中...", remaining.Round(time.Second))
|
||||
t.sleepUntil(preloadTime)
|
||||
log.Println("[定时器] 预加载时间到!")
|
||||
}
|
||||
|
||||
// WaitUntilRush 等待到抢购时间点(目标时间 - SubmitAdvance)
|
||||
// 使用 Sleep + 自旋忙等 的混合策略确保毫秒级精度
|
||||
func (t *Timer) WaitUntilRush() {
|
||||
// 实际触发时间 = 目标时间 - 提前量(补偿网络延迟)
|
||||
rushTime := t.cfg.TargetTime.Add(-t.cfg.SubmitAdvance)
|
||||
remaining := t.timeSync.UntilTarget(rushTime)
|
||||
|
||||
if remaining <= 0 {
|
||||
log.Println("[定时器] 已过抢购时间,立即执行")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[定时器] 距离抢购还有 %v (含 %v 提前量)",
|
||||
remaining.Round(time.Millisecond), t.cfg.SubmitAdvance)
|
||||
|
||||
t.sleepUntil(rushTime)
|
||||
|
||||
serverNow := t.timeSync.Now()
|
||||
diff := serverNow.Sub(t.cfg.TargetTime)
|
||||
log.Printf("[定时器] 触发! 服务器时间: %s (偏差: %v)",
|
||||
serverNow.Format("15:04:05.000"), diff)
|
||||
}
|
||||
|
||||
// sleepUntil 混合等待策略:先 Sleep 粗等,最后 3 秒自旋忙等
|
||||
func (t *Timer) sleepUntil(target time.Time) {
|
||||
// 阶段1: 距离目标 >5s 时,用 Sleep 粗等(省 CPU)
|
||||
for {
|
||||
remaining := t.timeSync.UntilTarget(target)
|
||||
if remaining <= 3*time.Second {
|
||||
break
|
||||
}
|
||||
// 每次 sleep 剩余时间的一半,逐步逼近
|
||||
sleepDur := remaining / 2
|
||||
if sleepDur > 1*time.Second {
|
||||
sleepDur = 1 * time.Second
|
||||
}
|
||||
time.Sleep(sleepDur)
|
||||
}
|
||||
|
||||
// 阶段2: 最后 3 秒,锁定 OS 线程 + 自旋忙等(最高精度)
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
log.Println("[定时器] 进入自旋忙等模式...")
|
||||
for t.timeSync.Now().Before(target) {
|
||||
// 自旋,不让出 CPU
|
||||
runtime.Gosched() // 偶尔让出一下,避免完全饿死其他 goroutine
|
||||
}
|
||||
}
|
||||
|
||||
// PrintCountdown 持续打印倒计时日志(在单独的 goroutine 中运行)
|
||||
func (t *Timer) PrintCountdown(done <-chan struct{}) {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
remaining := t.timeSync.UntilTarget(t.cfg.TargetTime)
|
||||
if remaining <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
hours := int(remaining.Hours())
|
||||
mins := int(remaining.Minutes()) % 60
|
||||
secs := int(remaining.Seconds()) % 60
|
||||
log.Printf("[倒计时] %02d:%02d:%02d (服务器时间: %s)",
|
||||
hours, mins, secs,
|
||||
t.timeSync.Now().Format("15:04:05"))
|
||||
}
|
||||
}
|
||||
}
|
||||
216
utils/timesync.go
Normal file
216
utils/timesync.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go-taobao/config"
|
||||
)
|
||||
|
||||
// TimeSync 时间同步器,计算本地时钟与服务器的时间偏移
|
||||
type TimeSync struct {
|
||||
// offset = 服务器时间 - 本地时间
|
||||
Offset time.Duration
|
||||
// 使用的时间源名称
|
||||
Source string
|
||||
}
|
||||
|
||||
// taobaoH5Resp 淘宝 H5 时间 API 响应
|
||||
type taobaoH5Resp struct {
|
||||
Data struct {
|
||||
T string `json:"t"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// suningTimeResp 苏宁时间 API 响应
|
||||
type suningTimeResp struct {
|
||||
SysTime2 string `json:"sysTime2"` // 格式: 2026-02-09 14:30:00
|
||||
}
|
||||
|
||||
// NewTimeSync 创建时间同步器,依次尝试多个时间源
|
||||
func NewTimeSync(samples int) (*TimeSync, error) {
|
||||
if samples < 1 {
|
||||
samples = 5
|
||||
}
|
||||
|
||||
// 依次尝试每个时间源
|
||||
for _, api := range config.TimeAPIs {
|
||||
log.Printf("[时间同步] 尝试时间源: %s (%s)", api.Name, api.URL)
|
||||
|
||||
offsets := make([]time.Duration, 0, samples)
|
||||
for i := 0; i < samples; i++ {
|
||||
offset, err := fetchOffset(api)
|
||||
if err != nil {
|
||||
log.Printf("[时间同步] %s 第 %d 次采样失败: %v", api.Name, i+1, err)
|
||||
continue
|
||||
}
|
||||
offsets = append(offsets, offset)
|
||||
log.Printf("[时间同步] %s 第 %d 次采样: 偏移 = %v", api.Name, i+1, offset)
|
||||
|
||||
if i < samples-1 {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
if len(offsets) == 0 {
|
||||
log.Printf("[时间同步] %s 所有采样失败,尝试下一个时间源", api.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// 取中位数
|
||||
sort.Slice(offsets, func(i, j int) bool {
|
||||
return offsets[i] < offsets[j]
|
||||
})
|
||||
median := offsets[len(offsets)/2]
|
||||
|
||||
ts := &TimeSync{Offset: median, Source: api.Name}
|
||||
log.Printf("[时间同步] ✅ 使用 %s, 偏移量: %v (%d 次有效采样)",
|
||||
api.Name, median, len(offsets))
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
// 所有时间源都失败,降级到本地时间
|
||||
log.Println("[时间同步] ⚠️ 所有时间源均失败,降级使用本地时间 (偏移=0)")
|
||||
return &TimeSync{Offset: 0, Source: "本地时间(降级)"}, nil
|
||||
}
|
||||
|
||||
// fetchOffset 根据 API 类型获取单次时间偏移
|
||||
func fetchOffset(api config.TimeAPIConfig) (time.Duration, error) {
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
localBefore := time.Now()
|
||||
|
||||
req, err := http.NewRequest("GET", api.URL, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
// 模拟浏览器请求头
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
localAfter := time.Now()
|
||||
rtt := localAfter.Sub(localBefore)
|
||||
localEstimate := localBefore.Add(rtt / 2)
|
||||
|
||||
var serverTime time.Time
|
||||
|
||||
switch api.Type {
|
||||
case config.TimeAPITypeTaobaoH5:
|
||||
serverTime, err = parseTaobaoH5(resp)
|
||||
case config.TimeAPITypeSuning:
|
||||
serverTime, err = parseSuning(resp)
|
||||
case config.TimeAPITypeHTTPHeader:
|
||||
serverTime, err = parseHTTPDate(resp)
|
||||
default:
|
||||
return 0, fmt.Errorf("未知 API 类型")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
offset := serverTime.Sub(localEstimate)
|
||||
log.Printf("[时间同步] RTT: %v, 服务器时间: %s", rtt, serverTime.Format("15:04:05.000"))
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
// parseTaobaoH5 解析淘宝 H5 API 响应
|
||||
func parseTaobaoH5(resp *http.Response) (time.Time, error) {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("读取响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 淘宝 H5 API 可能返回 JSONP,需要提取 JSON 部分
|
||||
bodyStr := string(body)
|
||||
jsonStr := bodyStr
|
||||
if idx := strings.Index(bodyStr, "("); idx != -1 {
|
||||
jsonStr = bodyStr[idx+1:]
|
||||
if endIdx := strings.LastIndex(jsonStr, ")"); endIdx != -1 {
|
||||
jsonStr = jsonStr[:endIdx]
|
||||
}
|
||||
}
|
||||
|
||||
var result taobaoH5Resp
|
||||
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
|
||||
return time.Time{}, fmt.Errorf("解析 JSON 失败: %w (body: %s)", err, bodyStr[:min(len(bodyStr), 200)])
|
||||
}
|
||||
|
||||
if result.Data.T == "" {
|
||||
return time.Time{}, fmt.Errorf("时间戳为空 (body: %s)", bodyStr[:min(len(bodyStr), 200)])
|
||||
}
|
||||
|
||||
ms, err := strconv.ParseInt(result.Data.T, 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("解析时间戳失败: %w", err)
|
||||
}
|
||||
|
||||
return time.UnixMilli(ms), nil
|
||||
}
|
||||
|
||||
// parseSuning 解析苏宁时间 API 响应
|
||||
func parseSuning(resp *http.Response) (time.Time, error) {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("读取响应失败: %w", err)
|
||||
}
|
||||
|
||||
var result suningTimeResp
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return time.Time{}, fmt.Errorf("解析 JSON 失败: %w", err)
|
||||
}
|
||||
|
||||
if result.SysTime2 == "" {
|
||||
return time.Time{}, fmt.Errorf("时间字段为空")
|
||||
}
|
||||
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
t, err := time.ParseInLocation("2006-01-02 15:04:05", result.SysTime2, loc)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("解析时间失败: %w", err)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// parseHTTPDate 从 HTTP 响应头 Date 字段解析服务器时间
|
||||
func parseHTTPDate(resp *http.Response) (time.Time, error) {
|
||||
dateStr := resp.Header.Get("Date")
|
||||
if dateStr == "" {
|
||||
return time.Time{}, fmt.Errorf("响应头无 Date 字段")
|
||||
}
|
||||
|
||||
// HTTP Date 格式: Thu, 09 Feb 2026 06:30:00 GMT
|
||||
t, err := time.Parse(time.RFC1123, dateStr)
|
||||
if err != nil {
|
||||
// 尝试备用格式
|
||||
t, err = time.Parse(time.RFC1123Z, dateStr)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("解析 Date 头失败: %w (值: %s)", err, dateStr)
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Now 返回校准后的当前时间
|
||||
func (ts *TimeSync) Now() time.Time {
|
||||
return time.Now().Add(ts.Offset)
|
||||
}
|
||||
|
||||
// UntilTarget 计算距离目标时间还有多久
|
||||
func (ts *TimeSync) UntilTarget(target time.Time) time.Duration {
|
||||
return target.Sub(ts.Now())
|
||||
}
|
||||
Reference in New Issue
Block a user