diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ee20e4..995db57 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,10 +3,10 @@ name: Build on: push: tags: - - 'v*' - + - "v*" + workflow_dispatch: - + pull_request: jobs: @@ -28,29 +28,32 @@ jobs: build_type: ${{ matrix.debug == 'release' && '正式版' || 'debug' }} CARGO_TERM_COLOR: always steps: - - uses: actions/checkout@v4 - - name: Print github.ref - run: echo "${{ github.ref }}" - - name: Build Release - if: matrix.debug == 'release' - run: cargo build --verbose --release - - name: Build Debug - if: matrix.debug == 'debug' - run: cargo build --verbose - - name: Copy artifacts on windows - if: matrix.os == 'windows-latest' - run: | - mkdir -p target/output - cp target/${{matrix.debug}}/frontend.exe target/output/${{ env.build_type }}-${{ matrix.build }}.exe - cp target/${{matrix.debug}}/frontend.pdb target/output/${{ env.build_type }}-${{ matrix.build }}.pdb - - name: Copy artifacts on other systems - if: matrix.os != 'windows-latest' - run: | - mkdir -p target/output - cp target/${{matrix.debug}}/frontend target/output/${{ env.build_type }}-${{ matrix.build }} - - name: Upload Artifacts - uses: actions/upload-artifact@v4 - with: + - uses: actions/checkout@v4 + - name: Print github.ref + run: echo "${{ github.ref }}" + - name: Install glibc2 + if: matrix.os == 'ubuntu-latest' + run: sudo apt-get update && sudo apt-get install -y libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev pkg-config + - name: Build Release + if: matrix.debug == 'release' + run: cargo build --verbose --release + - name: Build Debug + if: matrix.debug == 'debug' + run: cargo build --verbose + - name: Copy artifacts on windows + if: matrix.os == 'windows-latest' + run: | + mkdir -p target/output + cp target/${{matrix.debug}}/frontend.exe target/output/${{ env.build_type }}-${{ matrix.build }}.exe + cp target/${{matrix.debug}}/frontend.pdb target/output/${{ env.build_type }}-${{ matrix.build }}.pdb + - name: Copy artifacts on other systems + if: matrix.os != 'windows-latest' + run: | + mkdir -p target/output + cp target/${{matrix.debug}}/frontend target/output/${{ env.build_type }}-${{ matrix.build }} + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: name: ${{ env.build_type }}-${{ matrix.build }} path: target/output publish: @@ -66,7 +69,7 @@ jobs: uses: actions/download-artifact@v4 with: path: artifacts - + - name: List artifacts run: ls -la artifacts @@ -74,7 +77,7 @@ jobs: run: | mkdir release-assets find artifacts -type f -not -name "*.pdb" -exec cp {} release-assets/ \; - + - name: Create Release uses: softprops/action-gh-release@v2 with: @@ -84,4 +87,4 @@ jobs: prerelease: false generate_release_notes: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 096e371..ff6fb7e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ Thumbs.db *.tmp *.log /学习 -*.json *txt config.json config @@ -21,3 +20,4 @@ config /models/ /效果/ permissions +models diff --git a/Cargo.toml b/Cargo.toml index ea0a4bb..f2605aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,24 @@ [workspace] members = [ - "frontend", - "backend", - "common" + "crates/frontend", + "crates/backend", + "crates/common", + "crates/cli" ] +resolver = "2" + +[workspace.package] +version = "7.0.0" +edition = "2024" +authors = ["biliticket"] +license = "GPLv3" +description = "B站会员购自动抢票工具" +repository = "https://github.com/biliticket/bili_ticket_rush" +readme = "README.md" +keywords = ["bilibili", "ticket", "rush", "automation"] +categories = ["gui", "automation"] + [patch.crates-io] ort = { git="https://github.com/biliticket/ort" } ort-sys = { git = "https://github.com/biliticket/ort" } diff --git a/backend/src/show_orderlist.rs b/backend/src/show_orderlist.rs deleted file mode 100644 index af13e6f..0000000 --- a/backend/src/show_orderlist.rs +++ /dev/null @@ -1,50 +0,0 @@ -use common::{cookie_manager::CookieManager, http_utils::request_get}; -use serde_json; -use std::sync::Arc; -use common::show_orderlist::{*}; - - -pub async fn get_orderlist(cookie_manager :Arc) -> Result{ - match cookie_manager.get( - - "https://show.bilibili.com/api/ticket/ordercenter/ticketList?page=0&page_size=10" - ).await.send().await{ - Ok(resp) =>{ - if resp.status().is_success(){ - - - match tokio::task::block_in_place(||{ - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(resp.text()) - }){ - Ok(text) => { - log::debug!("获取全部订单:{}",text); - match serde_json::from_str::(&text){ - Ok(order_resp) => { - return Ok(order_resp); - } - Err(e) => {log::error!("获取全部订单json解析失败:{}",e); - return Err(format!("获取全部订单json解析失败:{}",e))} - - } - - - } - Err(e) => { - //log::error!("获取data失败: {}",e); - return Err(format!("获取data失败: {}",e)) - } - } - }else { - // log::error!("获取订单不期待响应:{}", resp.status()); - return Err(format!("获取订单不期待响应:{}", resp.status())) - } - } - Err(err) => { - //log::error!("请求失败: {}", err); - return Err(err.to_string()); - } - }; - - -} diff --git a/common/src/login.rs b/common/src/login.rs deleted file mode 100644 index d544813..0000000 --- a/common/src/login.rs +++ /dev/null @@ -1,197 +0,0 @@ -use crate::account::add_account; -use crate::account::Account; -use crate::captcha::LocalCaptcha; -use crate::http_utils::{request_get,request_post,request_get_sync}; -use serde_json::json; -use crate::utility::CustomConfig; -use crate::captcha::captcha; -use reqwest::Client; - -pub struct LoginInput{ - pub phone: String, - pub account: String, - pub password: String, - pub sms_code: String, - pub cookie: String, -} - -pub struct QrCodeLoginTask { - pub qrcode_key: String, - pub qrcode_url: String, - pub start_time: std::time::Instant, - pub status: QrCodeLoginStatus, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum QrCodeLoginStatus { - Pending, - Scanning, - Confirming, - Success(String), //成功时返回cookie信息 - Failed(String), //失败时返回错误信息 - Expired, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum SendLoginSmsStatus{ - Success(String), - Failed(String), -} - -pub fn qrcode_login(client: &Client) -> Result { - // 创建一个临时的运行时来执行异步代码 - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async { - let response = request_get( - client, - "https://passport.bilibili.com/x/passport-login/web/qrcode/generate", - - None, - ).await.map_err(|e| e.to_string())?; - - let json = response.json::() - .await.map_err(|e| e.to_string())?; - - - if let Some(qrcode_key) = json["data"]["qrcode_key"].as_str() { - Ok(qrcode_key.to_string()) - } else { - Err("无法获取二维码URL".to_string()) - } -}) -} -pub fn password_login(username: &str, password: &str) -> Result { - Err("暂不支持账号密码登录".to_string()) -} - -pub async fn send_loginsms(phone: &str, client: &Client, custom_config: CustomConfig,local_captcha: LocalCaptcha) -> Result { - - let response = request_get( - client, - "https://www.bilibili.com/", - - None, - ).await.map_err(|e| e.to_string())?; - - log::debug!("{:?}", response.cookies().collect::>()); - - - - // 发送请求 - let response = request_get( - client, - "https://passport.bilibili.com/x/passport-login/captcha", - - None, - ).await.map_err(|e| e.to_string())?; - log::info!("获取验证码: {:?}", response); - - let json = response.json::().await.map_err(|e| e.to_string())?; - let gt = json["data"]["geetest"]["gt"].as_str().unwrap_or(""); - let challenge = json["data"]["geetest"]["challenge"].as_str().unwrap_or(""); - let token = json["data"]["token"].as_str().unwrap_or(""); - let referer = "https://passport.bilibili.com/x/passport-login/captcha"; - match captcha(custom_config.clone(), gt, challenge, referer, 33,local_captcha).await { - Ok(result_str) => { - log::info!("验证码识别成功: {}", result_str); - let result: serde_json::Value = serde_json::from_str(&result_str).map_err(|e| e.to_string())?; - - let json_data = json!({ - "cid": 86, - "tel": phone.parse::().unwrap_or(0), - "token": token, - "source":"main_mini", - "challenge": result["challenge"], - "validate": result["validate"], - "seccode": result["seccode"], - }); - log::debug!("验证码数据: {:?}", json_data); - let send_sms = request_post( - client, - "https://passport.bilibili.com/x/passport-login/web/sms/send", - - None, - Some(&json_data), - ).await.map_err(|e| e.to_string())?; - - let json_response = send_sms.json::().await.map_err(|e| e.to_string())?; - log::debug!("验证码发送响应: {:?}", json_response); - if json_response["code"].as_i64() == Some(0) { - let captcha_key = json_response["data"]["captcha_key"].as_str().unwrap_or(""); - log::info!("验证码发送成功"); - log::debug!("captcha_key: {:?}", captcha_key); - Ok(captcha_key.to_string()) - } else { - log::error!("验证码发送失败: {}", json_response["message"].as_str().unwrap_or("未知错误")); - Err("验证码发送失败".to_string()) - } - } - Err(e) => { - log::error!("验证码识别失败: {}", e); - Err("验证码识别失败".to_string()) - } - - } - - -} - -pub async fn sms_login(phone: &str, sms_code: &str, captcha_key:&str, client: &Client) -> Result { - let data = serde_json::json!({ - "cid": 86, - "tel": phone.parse::().unwrap_or(0), - "code": sms_code.parse::().unwrap_or(0), - "source":"main_mini", - "captcha_key":captcha_key, - }); - log::debug!("短信登录数据: {:?}", data); - let login_response = request_post( - client, - "https://passport.bilibili.com/x/passport-login/web/login/sms", - - None, - Some(&data), - ).await.map_err(|e| e.to_string())?; - let mut all_cookies = Vec::new(); - let cookie_headers = login_response.headers().get_all(reqwest::header::SET_COOKIE); - log::debug!("headers返回:{:?}",cookie_headers); - for value in cookie_headers { - if let Ok(cookie_str) = value.to_str() { - - if let Some(end_pos) = cookie_str.find(';') { - all_cookies.push(cookie_str[0..end_pos].to_string()); - } else { - all_cookies.push(cookie_str.to_string()); - } - } - } - log::info!("获取cookie: {:?}", all_cookies); - let json_response = login_response.json::() - .await - .map_err(|e| format!("解析JSON失败: {}", e))?; - log::debug!("登录接口响应:{:?}",json_response); - if json_response["code"].as_i64() == Some(0) { - log::info!("短信登录成功!"); - log::info!("登录cookie:{:?}", all_cookies); - return Ok(all_cookies.to_vec().join(";")); - - } - Err("短信登录失败".to_string()) - - -} - -pub fn cookie_login(cookie: &str, client: &Client, ua: &str) -> Result { - match add_account(cookie,client,ua){ - Ok(account) => { - log::info!("ck登录成功"); - Ok(account) - }, - Err(e) => { - log::error!("ck登录失败: {}", e); - Err(e) - } - } -} - - diff --git a/common/src/utility.rs b/common/src/utility.rs deleted file mode 100644 index 42756bd..0000000 --- a/common/src/utility.rs +++ /dev/null @@ -1,27 +0,0 @@ -use serde::{Serialize, Deserialize}; - - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CustomConfig{ - pub open_custom_ua: bool, //是否开启自定义UA - pub custom_ua: String, //自定义UA - pub captcha_mode: usize, //验证码模式 //0:本地打码 1:ttocr - pub ttocr_key: String, //ttocr key - pub preinput_phone1: String, //预填账号1手机号 - pub preinput_phone2: String, //预填账号2手机号 - - -} - -impl CustomConfig{ - pub fn new() -> Self{ - Self{ - open_custom_ua: true, - custom_ua: String::from("Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Mobile Safari/537.36"), - captcha_mode: 0, - ttocr_key: String::new(), - preinput_phone1: String::new(), - preinput_phone2: String::new(), - } - } -} \ No newline at end of file diff --git a/backend/Cargo.toml b/crates/backend/Cargo.toml similarity index 86% rename from backend/Cargo.toml rename to crates/backend/Cargo.toml index 9926f24..b1aa605 100644 --- a/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "backend" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] common = { path = "../common" } @@ -9,19 +9,14 @@ tokio = { version = "1", features = ["full"] } uuid = { version = "1.3", features = ["v4"] } chrono = "0.4" -# log log = "0.4" env_logger = "0.9" - -#json serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -#reqwest reqwest = { version="0.11.22", features=["json", "blocking"]} -#rand rand = "0.8" -base64 = "0.22" \ No newline at end of file +base64 = "0.22" diff --git a/backend/src/api.rs b/crates/backend/src/api.rs similarity index 63% rename from backend/src/api.rs rename to crates/backend/src/api.rs index cb3c4f3..edfbfef 100644 --- a/backend/src/api.rs +++ b/crates/backend/src/api.rs @@ -1,26 +1,28 @@ use common::cookie_manager::CookieManager; -use common::http_utils::request_get; -use common::ticket::{*}; use common::gen_cp::CTokenGenerator; -use serde_json; +use common::http_utils::request_get; use common::login::QrCodeLoginStatus; +use common::ticket::*; +use rand::{Rng, thread_rng}; use reqwest::Client; +use serde_json; +use serde_json::{Value, json}; use std::collections::HashMap; use std::sync::Arc; use std::sync::Mutex; -use serde_json::{json, Value}; -use rand::{thread_rng, Rng}; use std::time::{SystemTime, UNIX_EPOCH}; - -pub async fn get_countdown(cookie_manager: Arc, info: Option) -> Result { +pub async fn get_countdown( + cookie_manager: Arc, + info: Option, +) -> Result { // 获取开始时间 (秒级) let sale_begin_sec = match info { - Some(info) => info.sale_begin , + Some(info) => info.sale_begin, None => return Err("获取开始时间失败".to_string()), }; log::debug!("获取开始时间(秒级):{}", sale_begin_sec); - + // 获取网络时间 (秒级) let url = "https://api.bilibili.com/x/click-interface/click/now"; let response = cookie_manager.get(url).await; @@ -28,17 +30,14 @@ pub async fn get_countdown(cookie_manager: Arc, info: Option { let text = data.text().await.unwrap_or_default(); log::debug!("API原始响应:{}", text); - - let json_data: serde_json::Value = serde_json::from_str(&text).unwrap_or( - json!({ - "code": 0, - "data": { - "now": 0 - } - }) - ); - - + + let json_data: serde_json::Value = serde_json::from_str(&text).unwrap_or(json!({ + "code": 0, + "data": { + "now": 0 + } + })); + let now_sec = json_data["data"]["now"].as_i64().unwrap_or(0); log::debug!("解析出的网络时间(秒级):{}", now_sec); now_sec @@ -48,7 +47,7 @@ pub async fn get_countdown(cookie_manager: Arc, info: Option, info: Option) -> Result{ - let req = cookie_manager.get("https://show.bilibili.com/api/ticket/buyer/list").await; +pub async fn get_buyer_info( + cookie_manager: Arc, +) -> Result { + let req = cookie_manager + .get("https://show.bilibili.com/api/ticket/buyer/list") + .await; let response = req.send().await; match response { - Ok(resp)=>{ - if resp.status().is_success(){ - match tokio::task::block_in_place(||{ + Ok(resp) => { + if resp.status().is_success() { + match tokio::task::block_in_place(|| { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(resp.text()) - }){ + }) { Ok(text) => { - log::debug!("获取购票人信息:{}",text); - match serde_json::from_str::(&text){ + log::debug!("获取购票人信息:{}", text); + match serde_json::from_str::(&text) { Ok(buyer_info) => { return Ok(buyer_info); } Err(e) => { - log::error!("获取购票人信息json解析失败:{}",e); - return Err(format!("获取购票人信息json解析失败:{}",e)) + log::error!("获取购票人信息json解析失败:{}", e); + return Err(format!("获取购票人信息json解析失败:{}", e)); } - } } Err(e) => { - log::error!("获取购票人信息失败:{}",e); - return Err(format!("获取购票人信息失败:{}",e)) + log::error!("获取购票人信息失败:{}", e); + return Err(format!("获取购票人信息失败:{}", e)); } - } - } - else{ - + } else { log::debug!("请求响应失败: {:?}", resp); return Err(format!("请求响应失败: {}", resp.status())); } } - Err(e) => { - Err(format!("请求失败: {}", e)) - } + Err(e) => Err(format!("请求失败: {}", e)), } } -pub async fn get_project(cookie_manager: Arc, project_id : &str) -> Result{ - let req = cookie_manager.get(format!("https://show.bilibili.com/api/ticket/project/getV2?id={}",project_id).as_str()).await; +pub async fn get_project( + cookie_manager: Arc, + project_id: &str, +) -> Result { + let req = cookie_manager + .get( + format!( + "https://show.bilibili.com/api/ticket/project/getV2?id={}", + project_id + ) + .as_str(), + ) + .await; let response = req.send().await; match response { - Ok(resp)=>{ - if resp.status().is_success(){ - match tokio::task::block_in_place(||{ + Ok(resp) => { + if resp.status().is_success() { + match tokio::task::block_in_place(|| { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(resp.text()) - }){ + }) { Ok(text) => { - log::debug!("获取项目详情:{}",text); + log::debug!("获取项目详情:{}", text); // 尝试常规解析 - match serde_json::from_str::(&text){ + match serde_json::from_str::(&text) { Ok(ticket_info) => { return Ok(ticket_info); } @@ -138,114 +149,118 @@ pub async fn get_project(cookie_manager: Arc, project_id : &str) return Err(format!("获取项目详情失败:{}", e)); } } - } - else{ + } else { log::debug!("请求响应失败: {:?}", resp); return Err(format!("请求响应失败: {}", resp.status())); } } - Err(e) => { - Err(format!("请求失败: {}", e)) - } + Err(e) => Err(format!("请求失败: {}", e)), } } - //轮询登录状态 -pub async fn poll_qrcode_login(qrcode_key: &str,user_agent: Option<&str>) ->QrCodeLoginStatus { - - +pub async fn poll_qrcode_login(qrcode_key: &str, user_agent: Option<&str>) -> QrCodeLoginStatus { let client_builder = Client::builder(); let client = if let Some(ua) = user_agent { client_builder.user_agent(ua) } else { - client_builder.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/110.0.0.0 Safari/537.36") - }.build() + client_builder + .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/110.0.0.0 Safari/537.36") + } + .build() .unwrap_or_default(); - + let max_attempts = 60; - - for attempt in 1..max_attempts{ - - - //轮询 - let response = match request_get( - &client, - &format!("https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={}", qrcode_key), - - None, - ).await { - Ok(resp) => resp, - Err(e) => return QrCodeLoginStatus::Failed(e.to_string()), - }; - let mut all_cookies = Vec::new(); - let cookie_headers = response.headers().get_all(reqwest::header::SET_COOKIE); - for value in cookie_headers { - if let Ok(cookie_str) = value.to_str() { - - if let Some(end_pos) = cookie_str.find(';') { - all_cookies.push(cookie_str[0..end_pos].to_string()); - } else { - all_cookies.push(cookie_str.to_string()); + for attempt in 1..max_attempts { + //轮询 + let response = match request_get( + &client, + &format!( + "https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={}", + qrcode_key + ), + None, + ) + .await + { + Ok(resp) => resp, + Err(e) => return QrCodeLoginStatus::Failed(e.to_string()), + }; + + let mut all_cookies = Vec::new(); + let cookie_headers = response.headers().get_all(reqwest::header::SET_COOKIE); + for value in cookie_headers { + if let Ok(cookie_str) = value.to_str() { + if let Some(end_pos) = cookie_str.find(';') { + all_cookies.push(cookie_str[0..end_pos].to_string()); + } else { + all_cookies.push(cookie_str.to_string()); + } + } } - } - } - - let json = match response.json::().await { - Ok(j) => j, - Err(e) => return QrCodeLoginStatus::Failed(e.to_string()), - }; - - - let code = json["data"]["code"].as_i64().unwrap_or(-1); - match code { - 0 => { - //json获取cookie - - if let Some(cookie_info) = json["data"]["cookie_info"].as_object() { - for (key, value) in cookie_info { - if let Some(val_str) = value["value"].as_str() { - all_cookies.push(format!("{}={}", key, val_str)); + + let json = match response.json::().await { + Ok(j) => j, + Err(e) => return QrCodeLoginStatus::Failed(e.to_string()), + }; + + let code = json["data"]["code"].as_i64().unwrap_or(-1); + match code { + 0 => { + //json获取cookie + + if let Some(cookie_info) = json["data"]["cookie_info"].as_object() { + for (key, value) in cookie_info { + if let Some(val_str) = value["value"].as_str() { + all_cookies.push(format!("{}={}", key, val_str)); + } } } + + if !all_cookies.is_empty() { + return QrCodeLoginStatus::Success(all_cookies.join("; ")); + } else { + return QrCodeLoginStatus::Failed("无法获取Cookie信息".to_string()); + } } - - - if !all_cookies.is_empty() { - return QrCodeLoginStatus::Success(all_cookies.join("; ")); - } else { - return QrCodeLoginStatus::Failed("无法获取Cookie信息".to_string()); + 86038 => return QrCodeLoginStatus::Expired, + 86090 => { + log::info!( + "二维码已扫描,等待确认 (尝试 {} / {} 次)", + attempt, + max_attempts + ); + //return QrCodeLoginStatus::Scanning; } - }, - 86038 => return QrCodeLoginStatus::Expired, - 86090 => { - log::info!("二维码已扫描,等待确认 (尝试 {} / {} 次)", attempt, max_attempts); - //return QrCodeLoginStatus::Scanning; - }, - 86101 => { - log::info!("二维码已生成,等待扫描 (尝试 {} / {} 次)", attempt, max_attempts); - //return QrCodeLoginStatus::Pending - }, - _ => { - let message = json["message"].as_str().unwrap_or("未知错误"); + 86101 => { + log::info!( + "二维码已生成,等待扫描 (尝试 {} / {} 次)", + attempt, + max_attempts + ); + //return QrCodeLoginStatus::Pending + } + _ => { + let message = json["message"].as_str().unwrap_or("未知错误"); - return QrCodeLoginStatus::Failed(message.to_string()) + return QrCodeLoginStatus::Failed(message.to_string()); + } } + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; -} -QrCodeLoginStatus::Expired + QrCodeLoginStatus::Expired } - -pub async fn get_ticket_token(cookie_manager:Arc, +pub async fn get_ticket_token( + cookie_manager: Arc, cpdd: Arc>, - project_id : &str , screen_id: &str, ticket_id: &str, count: i16,is_hot: bool) - -> Result<(String,String),TokenRiskParam>{ - - - + project_id: &str, + screen_id: &str, + ticket_id: &str, + count: i16, + is_hot: bool, +) -> Result<(String, String), TokenRiskParam> { let params = if is_hot { json!({ "project_id": project_id, @@ -270,26 +285,29 @@ pub async fn get_ticket_token(cookie_manager:Arc, }) }; log::debug!("获取票token参数:{:?}", params); - let url = format!("https://show.bilibili.com/api/ticket/order/prepare?project_id={}",project_id); - let response = cookie_manager - .post(&url).await - .json(¶ms) - .send() - .await; + let url = format!( + "https://show.bilibili.com/api/ticket/order/prepare?project_id={}", + project_id + ); + let response = cookie_manager.post(&url).await.json(¶ms).send().await; match response { Ok(resp) => { - if resp.status().is_success(){ - match tokio::task::block_in_place(||{ + if resp.status().is_success() { + match tokio::task::block_in_place(|| { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(resp.json::()) - }){ + }) { Ok(json) => { - log::debug!("获取票token:{}",json); + log::debug!("获取票token:{}", json); let errno_value = json.get("errno").and_then(|v| v.as_i64()).unwrap_or(-1); let code_value = json.get("code").and_then(|v| v.as_i64()).unwrap_or(-1); - let code = if errno_value != -1 { errno_value } else { code_value }; + let code = if errno_value != -1 { + errno_value + } else { + code_value + }; let msg = json["msg"].as_str().unwrap_or("未知错误"); - + match code { 0 => { let token = json["data"]["token"].as_str().unwrap_or(""); @@ -301,17 +319,32 @@ pub async fn get_ticket_token(cookie_manager:Arc, } -401 | 401 => { log::info!("需要进行人机验证"); - let mid = json["data"]["ga_data"]["riskParams"]["mid"].as_str().unwrap_or(""); - let decision_type = json["data"]["ga_data"]["riskParams"]["decision_type"].as_str().unwrap_or(""); - let buvid = json["data"]["ga_data"]["riskParams"]["buvid"].as_str().unwrap_or(""); - let ip = json["data"]["ga_data"]["riskParams"]["ip"].as_str().unwrap_or(""); - let scene = json["data"]["ga_data"]["riskParams"]["scene"].as_str().unwrap_or(""); - let ua = json["data"]["ga_data"]["riskParams"]["ua"].as_str().unwrap_or(""); - let v_voucher = json["data"]["ga_data"]["riskParams"]["v_voucher"].as_str().unwrap_or(""); + let mid = json["data"]["ga_data"]["riskParams"]["mid"] + .as_str() + .unwrap_or(""); + let decision_type = + json["data"]["ga_data"]["riskParams"]["decision_type"] + .as_str() + .unwrap_or(""); + let buvid = json["data"]["ga_data"]["riskParams"]["buvid"] + .as_str() + .unwrap_or(""); + let ip = json["data"]["ga_data"]["riskParams"]["ip"] + .as_str() + .unwrap_or(""); + let scene = json["data"]["ga_data"]["riskParams"]["scene"] + .as_str() + .unwrap_or(""); + let ua = json["data"]["ga_data"]["riskParams"]["ua"] + .as_str() + .unwrap_or(""); + let v_voucher = json["data"]["ga_data"]["riskParams"]["v_voucher"] + .as_str() + .unwrap_or(""); let risk_param = json["data"]["ga_data"]["riskParams"].clone(); let token_risk_param = TokenRiskParam { code: code as i32, - + message: msg.to_string(), mid: Some(mid.to_string()), decision_type: Some(decision_type.to_string()), @@ -326,11 +359,15 @@ pub async fn get_ticket_token(cookie_manager:Arc, return Err(token_risk_param); } _ => { - log::error!("获取token失败,未知错误码:{},错误信息:{},请提issue修复此问题", code, msg); + log::error!( + "获取token失败,未知错误码:{},错误信息:{},请提issue修复此问题", + code, + msg + ); log::error!("{:?}", json); return Err(TokenRiskParam { code: code as i32, - + message: msg.to_string(), mid: None, decision_type: None, @@ -343,33 +380,35 @@ pub async fn get_ticket_token(cookie_manager:Arc, }); } } - }, - Err(e) => { - log::error!("解析票务token响应失败: {}", e); - return Err(TokenRiskParam{ - - code: 999 as i32, - - message: e.to_string(), - - mid: None, - decision_type: None, - buvid: None, - ip: None, - scene: None, - ua: None, - v_voucher: None, - risk_param: None, - }) + } + Err(e) => { + log::error!("解析票务token响应失败: {}", e); + return Err(TokenRiskParam { + code: 999 as i32, + + message: e.to_string(), + + mid: None, + decision_type: None, + buvid: None, + ip: None, + scene: None, + ua: None, + v_voucher: None, + risk_param: None, + }); + } } - } - }else{ - log::error!("获取票token失败,服务器不期待响应,响应状态码:{}",resp.status()); - return Err(TokenRiskParam{ + } else { + log::error!( + "获取票token失败,服务器不期待响应,响应状态码:{}", + resp.status() + ); + return Err(TokenRiskParam { code: 999 as i32, - + message: resp.status().to_string(), - + mid: None, decision_type: None, buvid: None, @@ -382,12 +421,12 @@ pub async fn get_ticket_token(cookie_manager:Arc, } } Err(e) => { - log::error!("获取票token失败,错误信息:{}",e); - return Err(TokenRiskParam{ + log::error!("获取票token失败,错误信息:{}", e); + return Err(TokenRiskParam { code: 999 as i32, - + message: e.to_string(), - + mid: None, decision_type: None, buvid: None, @@ -399,28 +438,39 @@ pub async fn get_ticket_token(cookie_manager:Arc, }); } } - } -pub async fn confirm_ticket_order(cookie_manager:Arc,project_id : &str,token: &str) -> Result { - let url = format!("https://show.bilibili.com/api/ticket/order/confirmInfo?token={}&voucher=&project_id={}&requestSource=neul-next",token,project_id); - let response = cookie_manager.get(&url) +pub async fn confirm_ticket_order( + cookie_manager: Arc, + project_id: &str, + token: &str, +) -> Result { + let url = format!( + "https://show.bilibili.com/api/ticket/order/confirmInfo?token={}&voucher=&project_id={}&requestSource=neul-next", + token, project_id + ); + let response = cookie_manager + .get(&url) .await .send() .await .map_err(|e| format!("请求失败: {}", e))?; - + if !response.status().is_success() { return Err(format!("请求失败: {}", response.status())); } - let text = response.text() + let text = response + .text() .await .map_err(|e| format!("获取响应文本失败: {}", e))?; log::debug!("确认订单响应:{}", text); - let json: serde_json::Value = serde_json::from_str(&text) - .map_err(|e| format!("解析响应文本失败: {}", e))?; - if json["errno"]!=0 { - return Err(format!("确认订单失败: {}", json["msg"].as_str().unwrap_or("未知错误"))); + let json: serde_json::Value = + serde_json::from_str(&text).map_err(|e| format!("解析响应文本失败: {}", e))?; + if json["errno"] != 0 { + return Err(format!( + "确认订单失败: {}", + json["msg"].as_str().unwrap_or("未知错误") + )); } let confirm_result = serde_json::from_value(json["data"].clone()) .map_err(|e| format!("解析确认订单结果失败: {}", e))?; @@ -440,14 +490,21 @@ pub async fn create_order( is_mobile: bool, need_retry: bool, fast_mode: bool, - screen_size: Option<(u32, u32)> // 可选参数:(宽度,高度) + screen_size: Option<(u32, u32)>, // 可选参数:(宽度,高度) ) -> Result { let url = if !is_hot { - format!("https://show.bilibili.com/api/ticket/order/createV2?project_id={}", project_id) - }else{ - format!("https://show.bilibili.com/api/ticket/order/createV2?project_id={}&ptoken={}", project_id,ptoken.clone()) + format!( + "https://show.bilibili.com/api/ticket/order/createV2?project_id={}", + project_id + ) + } else { + format!( + "https://show.bilibili.com/api/ticket/order/createV2?project_id={}&ptoken={}", + project_id, + ptoken.clone() + ) }; - + // 选择适当的位置类型 let position_type = if need_retry && is_mobile { ClickPositionType::RetryButton @@ -456,25 +513,27 @@ pub async fn create_order( } else { ClickPositionType::PcConfirm }; - - let risk_header = format!("platform/{} uid/{} deviceId/{}" - ,"h5" - ,cookie_manager.get_cookie("DedeUserID").unwrap_or("".to_string()) - ,cookie_manager.get_cookie("buvid3").unwrap_or("".to_string()) + + let risk_header = format!( + "platform/{} uid/{} deviceId/{}", + "h5", + cookie_manager + .get_cookie("DedeUserID") + .unwrap_or("".to_string()), + cookie_manager + .get_cookie("buvid3") + .unwrap_or("".to_string()) ); let mut input_risk_header = HashMap::new(); input_risk_header.insert("X-Risk-Header", risk_header.as_str()); - + // 提取屏幕尺寸(如果提供) let (width, height) = screen_size.unwrap_or((1080, 2400)); - + // 生成点击位置 - let click_position = random_click_position( - position_type, - fast_mode, - Some(width), - Some(height) - ).await.to_string(); + let click_position = random_click_position(position_type, fast_mode, Some(width), Some(height)) + .await + .to_string(); let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -486,31 +545,29 @@ pub async fn create_order( let ticket_id = match biliticket.select_ticket_id.clone() { Some(id) => id, - None => return Err(999), + None => return Err(999), }; let ticket_id_int = ticket_id.parse::().map_err(|_| 999)?; - let data = match biliticket.id_bind { 0 => { - // 不实名制购票人信息 let no_bind_buyer_info = biliticket.no_bind_buyer_info.clone().unwrap(); - + let data = json!({ "project_id": project_id.parse::().unwrap_or(0), "screen_id": biliticket.screen_id.parse::().unwrap_or(0), - "sku_id": ticket_id_int, + "sku_id": ticket_id_int, "token": token, "buyer": no_bind_buyer_info.name, "tel": no_bind_buyer_info.tel, "clickPosition": click_position, "newRisk": true, - "requestSource": if is_mobile { "neul-next" } else { "pc-new" }, + "requestSource": if is_mobile { "neul-next" } else { "pc-new" }, "deviceId": cookie_manager.get_cookie("deviceFingerprint"), "pay_money": pay_money, "count": count, "timestamp": timestamp, - "order_type": 1, + "order_type": 1, }); data } @@ -519,35 +576,35 @@ pub async fn create_order( json!({ "project_id": project_id.parse::().unwrap_or(0), "screen_id": biliticket.screen_id.parse::().unwrap_or(0), - "sku_id": ticket_id_int, + "sku_id": ticket_id_int, "token": token, "ctoken": cpdd.lock().unwrap().generate_ctoken(true), "ptoken":ptoken, "buyer_info": serde_json::to_string(buyer_info).unwrap_or_default(), "clickPosition": click_position, "newRisk": true, - "requestSource": if is_mobile { "neul-next" } else { "pc-new" }, + "requestSource": if is_mobile { "neul-next" } else { "pc-new" }, "deviceId": cookie_manager.get_cookie("deviceFingerprint"), "pay_money": pay_money, "count": count, "timestamp": timestamp, - "order_type": 1, + "order_type": 1, }) - }else{ + } else { json!({ "project_id": project_id.parse::().unwrap_or(0), "screen_id": biliticket.screen_id.parse::().unwrap_or(0), - "sku_id": ticket_id_int, + "sku_id": ticket_id_int, "token": token, "buyer_info": serde_json::to_string(buyer_info).unwrap_or_default(), "clickPosition": click_position, "newRisk": true, - "requestSource": if is_mobile { "neul-next" } else { "pc-new" }, + "requestSource": if is_mobile { "neul-next" } else { "pc-new" }, "deviceId": cookie_manager.get_cookie("deviceFingerprint"), "pay_money": pay_money, "count": count, "timestamp": timestamp, - "order_type": 1, + "order_type": 1, }) }; data @@ -559,7 +616,9 @@ pub async fn create_order( }; log::debug!("抢票data :{:?}", data); - let response = cookie_manager.post_with_headers(&url,input_risk_header).await + let response = cookie_manager + .post_with_headers(&url, input_risk_header) + .await .json(&data) .send() .await @@ -571,46 +630,51 @@ pub async fn create_order( log::error!("请求失败: {}", response.status()); return Err(response.status().as_u16() as i32); }; - let text = response - .text() - .await - .map_err(|e| { - log::error!("获取响应文本失败: {}", e); - 412 - })?; - log::info!("{}",text); - let value: Value = serde_json::from_str(&text) - .map_err(|e| { - log::error!("解析响应文本失败: {}", e); - 412 - })?; - + let text = response.text().await.map_err(|e| { + log::error!("获取响应文本失败: {}", e); + 412 + })?; + log::info!("{}", text); + let value: Value = serde_json::from_str(&text).map_err(|e| { + log::error!("解析响应文本失败: {}", e); + 412 + })?; + let errno_value = value.get("errno").and_then(|v| v.as_i64()).unwrap_or(-1); let code_value = value.get("code").and_then(|v| v.as_i64()).unwrap_or(-1); - + // 只要有一个错误码不是0,就认为有错误 if errno_value != 0 || (errno_value == -1 && code_value != 0) { - return Err(if errno_value != -1 { - errno_value as i32 - } else { - code_value as i32 + return Err(if errno_value != -1 { + errno_value as i32 + } else { + code_value as i32 }); } - + Ok(value) -} +} -pub async fn check_fake_ticket(cookie_manager: Arc, project_id: &str, pay_token: &str, order_id: i64) -> Result{ +pub async fn check_fake_ticket( + cookie_manager: Arc, + project_id: &str, + pay_token: &str, + order_id: i64, +) -> Result { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() as u32; - let mut url = format!("https://show.bilibili.com/api/ticket/order/createstatus?project_id={}&token={}×tamp={}",project_id, pay_token, timestamp); - if order_id != 0{ - url = format!("{}&orderId={}",url, order_id); - } + let mut url = format!( + "https://show.bilibili.com/api/ticket/order/createstatus?project_id={}&token={}×tamp={}", + project_id, pay_token, timestamp + ); + if order_id != 0 { + url = format!("{}&orderId={}", url, order_id); + } log::debug!("check_fake_ticket_url: {}", url); - let response = cookie_manager.get(&url) + let response = cookie_manager + .get(&url) .await .send() .await @@ -632,71 +696,70 @@ pub enum ClickPositionType { RetryButton, } - /// 生成随机点击位置 -/// +/// /// # 参数 /// * `position_type` - 位置类型:PC/手机/重试按钮 /// * `fast_mode` - 时间间隔模式:true为快模式(0.8-4.6秒),false为慢模式(4-12秒) /// * `screen_width` - (可选)手机屏幕宽度,用于计算比例坐标,默认1080 /// * `screen_height` - (可选)手机屏幕高度,用于计算比例坐标,默认2400 -/// +/// pub async fn random_click_position( - position_type: ClickPositionType, + position_type: ClickPositionType, fast_mode: bool, screen_width: Option, - screen_height: Option + screen_height: Option, ) -> Value { let mut rng = thread_rng(); - + // 获取手机屏幕尺寸(默认使用常见尺寸1080x2400) let mobile_width = screen_width.unwrap_or(1080); let mobile_height = screen_height.unwrap_or(2400); - + // 根据不同设备/按钮类型确定基准坐标和偏移范围 let (base_x, base_y, offset_range) = match position_type { ClickPositionType::PcConfirm => { // PC端确认下单按钮位置(右侧中下部) (1131, 636, 10) - }, + } ClickPositionType::MobileConfirm => { // 手机端确认下单按钮位置(右下角) // 使用比例计算:x在屏幕宽度的0.55-0.9之间,y在屏幕底部附近 let x_ratio = rng.gen_range(0.55..0.9); let y_ratio = rng.gen_range(0.9..0.95); - + let x = (mobile_width as f32 * x_ratio) as i32; let y = (mobile_height as f32 * y_ratio) as i32; - + (x, y, mobile_width.min(20) as i32 / 4) // 偏移范围根据屏幕宽度按比例缩放 - }, + } ClickPositionType::RetryButton => { // 手机版"再试一次"按钮位置(屏幕中间靠下) // x坐标在屏幕宽度的1/3到2/3之间,y坐标在屏幕高度的2/3左右 let x_ratio = rng.gen_range(0.33..0.67); // 屏幕宽度的2/6到4/6之间 - let y_ratio = rng.gen_range(0.6..0.7); // 屏幕高度的2/3左右 - + let y_ratio = rng.gen_range(0.6..0.7); // 屏幕高度的2/3左右 + let x = (mobile_width as f32 * x_ratio) as i32; let y = (mobile_height as f32 * y_ratio) as i32; - + (x, y, mobile_width.min(30) as i32 / 4) // 偏移较大,因为这是个大按钮 } }; - + // 生成随机偏移 let offset_x = rng.gen_range(-offset_range..=offset_range); let offset_y = rng.gen_range(-offset_range..=offset_range); - + // 计算最终坐标 let final_x = base_x + offset_x; let final_y = base_y + offset_y; - + // 获取当前时间戳 let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_millis() as u64; - + // 根据模式生成不同的延迟时间 let random_delay = if fast_mode { // 快模式:0.8-4.6秒 @@ -705,10 +768,10 @@ pub async fn random_click_position( // 慢模式:4-12秒 rng.gen_range(4000..12000) }; - + // 计算起始时间 let origin = now - random_delay; - + // 构建JSON对象 json!({ "x": final_x, @@ -716,4 +779,4 @@ pub async fn random_click_position( "origin": origin, "now": now }) -} \ No newline at end of file +} diff --git a/backend/src/lib.rs b/crates/backend/src/lib.rs similarity index 58% rename from backend/src/lib.rs rename to crates/backend/src/lib.rs index fa161ae..d4afce0 100644 --- a/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -1,3 +1,3 @@ -pub mod taskmanager; pub mod api; -pub mod show_orderlist; \ No newline at end of file +pub mod show_orderlist; +pub mod taskmanager; diff --git a/crates/backend/src/show_orderlist.rs b/crates/backend/src/show_orderlist.rs new file mode 100644 index 0000000..6a949bb --- /dev/null +++ b/crates/backend/src/show_orderlist.rs @@ -0,0 +1,46 @@ +use common::show_orderlist::*; +use common::{cookie_manager::CookieManager, http_utils::request_get}; +use serde_json; +use std::sync::Arc; + +pub async fn get_orderlist(cookie_manager: Arc) -> Result { + match cookie_manager + .get("https://show.bilibili.com/api/ticket/ordercenter/ticketList?page=0&page_size=10") + .await + .send() + .await + { + Ok(resp) => { + if resp.status().is_success() { + match tokio::task::block_in_place(|| { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(resp.text()) + }) { + Ok(text) => { + log::debug!("获取全部订单:{}", text); + match serde_json::from_str::(&text) { + Ok(order_resp) => { + return Ok(order_resp); + } + Err(e) => { + log::error!("获取全部订单json解析失败:{}", e); + return Err(format!("获取全部订单json解析失败:{}", e)); + } + } + } + Err(e) => { + //log::error!("获取data失败: {}",e); + return Err(format!("获取data失败: {}", e)); + } + } + } else { + // log::error!("获取订单不期待响应:{}", resp.status()); + return Err(format!("获取订单不期待响应:{}", resp.status())); + } + } + Err(err) => { + //log::error!("请求失败: {}", err); + return Err(err.to_string()); + } + }; +} diff --git a/backend/src/taskmanager.rs b/crates/backend/src/taskmanager.rs similarity index 89% rename from backend/src/taskmanager.rs rename to crates/backend/src/taskmanager.rs index eb186a1..cb9e147 100644 --- a/backend/src/taskmanager.rs +++ b/crates/backend/src/taskmanager.rs @@ -9,20 +9,16 @@ use rand::{Rng, SeedableRng, rngs::StdRng}; use serde_json::json; - - - -use tokio::runtime::Runtime; -use tokio::sync::mpsc; -use common::taskmanager::{*}; +use crate::api::*; +use crate::show_orderlist::get_orderlist; use common::captcha::handle_risk_verification; -use common::login::{send_loginsms,sms_login}; -use common::ticket::ConfirmTicketResult; use common::gen_cp::CTokenGenerator; -use common::ticket::{*}; -use crate::show_orderlist::get_orderlist; -use crate::api::{*}; - +use common::login::{send_loginsms, sms_login}; +use common::taskmanager::*; +use common::ticket::ConfirmTicketResult; +use common::ticket::*; +use tokio::runtime::Runtime; +use tokio::sync::mpsc; pub struct TaskManagerImpl { task_sender: mpsc::Sender, @@ -43,11 +39,11 @@ impl TaskManager for TaskManagerImpl { // 创建通道 let (task_tx, mut task_rx) = mpsc::channel(100); let (result_tx, result_rx) = mpsc::channel(100); - + // 创建tokio运行时 let runtime = Arc::new(Runtime::new().unwrap()); let rt = runtime.clone(); - + // 启动工作线程 let worker = thread::spawn(move || { rt.block_on(async { @@ -56,23 +52,23 @@ impl TaskManager for TaskManagerImpl { TaskMessage::SubmitTask(request) => { let task_id = uuid::Uuid::new_v4().to_string(); let result_tx = result_tx.clone(); - + // 根据任务类型处理 match request { - + TaskRequest::QrCodeLoginRequest(qrcode_req) => { tokio::spawn(async move { // 二维码登录逻辑 let status = poll_qrcode_login(&qrcode_req.qrcode_key,qrcode_req.user_agent.as_deref()).await; - + let (cookie, error) = match &status { - common::login::QrCodeLoginStatus::Success(cookie) => + common::login::QrCodeLoginStatus::Success(cookie) => (Some(cookie.clone()), None), - common::login::QrCodeLoginStatus::Failed(err) => + common::login::QrCodeLoginStatus::Failed(err) => (None, Some(err.clone())), _ => (None, None) }; - + // 创建正确的结果类型 let task_result = TaskResult::QrCodeLoginResult(TaskQrCodeLoginResult { task_id, @@ -80,7 +76,7 @@ impl TaskManager for TaskManagerImpl { cookie, error, }); - + let _ = result_tx.send(task_result).await; }); } @@ -91,7 +87,7 @@ impl TaskManager for TaskManagerImpl { let custom_config = login_sms_req.custom_config.clone(); let result_tx = result_tx.clone(); let local_captcha = login_sms_req.local_captcha.clone(); - + /* let client = match reqwest::Client::builder() .user_agent(user_agent.clone()) @@ -101,28 +97,28 @@ impl TaskManager for TaskManagerImpl { Err(err) => { // 记录错误并发送错误结果 log::error!("创建请求客户端失败 ID: {}, 错误: {}", task_id, err); - + let task_result = TaskResult::LoginSmsResult(LoginSmsRequestResult { task_id, phone, success: false, message: format!("创建客户端失败: {}", err), }); - + let _ = result_tx.send(task_result).await; - return; + return; } }; */ - + tokio::spawn(async move{ log::info!("开始发送短信验证码 ID: {}", task_id); - + log::info!("开始发送短信验证码 ID: {}", task_id); let response = send_loginsms( - &phone, - &client, + &phone, + &client, custom_config, local_captcha, ).await; @@ -135,8 +131,8 @@ impl TaskManager for TaskManagerImpl { err.to_string() }, }; - log::info!("发送短信任务完成 ID: {}, 结果: {}", - task_id, + log::info!("发送短信任务完成 ID: {}, 结果: {}", + task_id, if success { "成功" } else { "失败" } ); @@ -149,12 +145,12 @@ impl TaskManager for TaskManagerImpl { let _ = result_tx.send(task_result).await; - - + + }); - + } TaskRequest::PushRequest(push_req) => { let task_id = uuid::Uuid::new_v4().to_string(); @@ -164,20 +160,20 @@ impl TaskManager for TaskManagerImpl { let jump_url = push_req.jump_url.clone(); let push_type = push_req.push_type.clone(); let result_tx = result_tx.clone(); - + // 启动异步任务处理推送 tokio::spawn(async move { log::info!("开始处理推送任务 ID: {}, 类型: {:?}", task_id, push_type); - + let (success, result_message) = match push_type { PushType::All => { push_config.push_all_async( &title, &message,&jump_url).await }, - + // 其他推送类型的处理... _ => (false, "未实现的推送类型".to_string()) }; - + // 创建任务结果 let task_result = TaskResult::PushResult(PushRequestResult { task_id: task_id.clone(), @@ -185,17 +181,17 @@ impl TaskManager for TaskManagerImpl { message: result_message, push_type: push_type.clone(), }); - + // 发送结果 if let Err(e) = result_tx.send(task_result).await { log::error!("发送推送任务结果失败: {}", e); } - - log::info!("推送任务 ID: {} 完成, 结果: {}", task_id, + + log::info!("推送任务 ID: {} 完成, 结果: {}", task_id, if success { "成功" } else { "失败" }); }); - - + + } TaskRequest::SubmitLoginSmsRequest(login_sms_req) => { let task_id = uuid::Uuid::new_v4().to_string(); @@ -207,7 +203,7 @@ impl TaskManager for TaskManagerImpl { tokio::spawn(async move{ log::info!("短信验证码登录进行中 ID: {}", task_id); - + let result = async{ let response = sms_login(&phone, &code,&captcha_key, &client).await; let success = response.is_ok(); @@ -222,8 +218,8 @@ impl TaskManager for TaskManagerImpl { Ok(msg) => Some(msg.clone()), Err(_) => None, }; - log::info!("提交短信任务完成 ID: {}, 结果: {}", - task_id, + log::info!("提交短信任务完成 ID: {}, 结果: {}", + task_id, if success { "成功" } else { "失败" } ); @@ -354,7 +350,7 @@ impl TaskManager for TaskManagerImpl { }); let _ = result_tx.send(task_result).await; - + }); } TaskRequest::GrabTicketRequest(grab_ticket_req)=>{ @@ -369,9 +365,9 @@ impl TaskManager for TaskManagerImpl { let mode = grab_ticket_req.grab_mode.clone(); let custon_config = grab_ticket_req.biliticket.config.clone(); let csrf = grab_ticket_req.biliticket.account.csrf.clone(); - let local_captcha = grab_ticket_req.local_captcha.clone(); - let count = grab_ticket_req.count.clone(); - let project_info = grab_ticket_req.biliticket.project_info.clone(); + let local_captcha = grab_ticket_req.local_captcha.clone(); + let count = grab_ticket_req.count.clone(); + let project_info = grab_ticket_req.biliticket.project_info.clone(); let skip_words= grab_ticket_req.skip_words.clone(); let mut rng = StdRng::from_entropy(); let mut is_hot = grab_ticket_req.is_hot.clone(); @@ -380,21 +376,21 @@ impl TaskManager for TaskManagerImpl { project_info.clone().unwrap().sale_begin as i64, 0, rng.gen_range(2000..10000) - + ))) }else{ Arc::new(Mutex::new(CTokenGenerator::new( SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() - .as_secs() as i64, + .as_secs() as i64, 0, rng.gen_range(2000..10000) ))) }; tokio::spawn(async move{ log::debug!("开始分析抢票任务:{}",task_id); - + match mode { 0 => { log::debug!("定时抢票模式"); @@ -406,7 +402,7 @@ impl TaskManager for TaskManagerImpl { return; } }; - + //log::debug!("获取倒计时成功:{}",countdown); if countdown > 0.0{ log::info!("距离抢票时间还有{}秒",countdown); @@ -417,7 +413,7 @@ impl TaskManager for TaskManagerImpl { countdown = countdown - 15.0; tokio::time::sleep(tokio::time::Duration::from_secs(15)).await; log::info!("距离抢票时间还有{}秒",countdown); - + } loop{ if countdown <= 1.3 { //按道理来说countdown是1秒,为了保险多设置几秒 @@ -447,18 +443,18 @@ impl TaskManager for TaskManagerImpl { log::info!("获取抢票token成功!:{} ptoken:{}",token,ptoken); let mut confirm_retry_count = 0; const MAX_CONFIRM_RETRY: i8 = 4; - + //尝试下单 loop { let (success, retry_limit) = handle_grab_ticket( - cookie_manager.clone(), + cookie_manager.clone(), cpdd.clone(), - &project_id, - &token, + &project_id, + &token, &ptoken, is_hot.clone(), - &task_id, - uid, + &task_id, + uid, &result_tx, &grab_ticket_req, &buyer_info @@ -467,8 +463,8 @@ impl TaskManager for TaskManagerImpl { log::info!("抢票流程结束,退出定时抢票模式"); break; //成功或致命错误,跳出循环 } - - + + confirm_retry_count += 1; if confirm_retry_count >= MAX_CONFIRM_RETRY { log::error!("确认订单失败,已达最大重试次数"); @@ -486,7 +482,7 @@ impl TaskManager for TaskManagerImpl { break; } } - + break; // 跳出token获取循环 }, Err(risk_param) => { @@ -495,7 +491,7 @@ impl TaskManager for TaskManagerImpl { //需要处理验证码 log::warn!("需要验证码,开始处理验证码..."); match handle_risk_verification( - cookie_manager.clone(), + cookie_manager.clone(), risk_param, &custon_config, &csrf, @@ -561,18 +557,18 @@ impl TaskManager for TaskManagerImpl { } - - } + + } 1 => { log::debug!("直接抢票模式"); let mut token_retry_count = 0; - const MAX_TOKEN_RETRY: i8 = 10; + const MAX_TOKEN_RETRY: i8 = 10; let mut confirm_order_retry_count = 0; const MAX_CONFIRM_ORDER_RETRY: i8 = 4; let mut order_retry_count = 0; let mut need_retry = false; - - + + //抢票主循环 loop{ @@ -583,29 +579,29 @@ impl TaskManager for TaskManagerImpl { log::info!("获取抢票token成功!:{} ptoken:{}",token,ptoken); let mut confirm_retry_count = 0; const MAX_CONFIRM_RETRY: i8 = 4; - + //尝试下单 loop { let (success, retry_limit) = handle_grab_ticket( - cookie_manager.clone(), + cookie_manager.clone(), cpdd.clone(), - &project_id, - &token, + &project_id, + &token, &ptoken, is_hot.clone(), - &task_id, - uid, + &task_id, + uid, &result_tx, &grab_ticket_req, &buyer_info ).await ; if success { log::info!("抢票流程结束,退出捡漏模式"); - + break; //成功或致命错误,跳出循环 } - - + + confirm_retry_count += 1; if confirm_retry_count >= MAX_CONFIRM_RETRY { log::error!("确认订单失败,已达最大重试次数"); @@ -623,7 +619,7 @@ impl TaskManager for TaskManagerImpl { break; } } - + break; // 跳出token获取循环 }, Err(risk_param) => { @@ -632,7 +628,7 @@ impl TaskManager for TaskManagerImpl { //需要处理验证码 log::warn!("需要验证码,开始处理验证码..."); match handle_risk_verification( - cookie_manager.clone(), + cookie_manager.clone(), risk_param, &custon_config, &csrf, @@ -706,7 +702,7 @@ impl TaskManager for TaskManagerImpl { // 外层循环,一旦抢票成功或遇到致命错误就退出 'main_loop: loop { log::debug!("project_id: {}, screen_id: {}, ticket_id: {}", project_id, screen_id, ticket_id); - + // 获取项目数据 let project_data = match get_project(cookie_manager.clone(), project_id.clone().as_str()).await { Ok(data) => data, @@ -717,27 +713,27 @@ impl TaskManager for TaskManagerImpl { } }; is_hot = project_data.data.hot_project; - + // 检查项目是否可售 /* if ![8,2].contains(&project_data.data.sale_flag_number){ log::error!("当前项目已停售,暂时不会放出回流票,请等等重新提交任务"); break 'main_loop; // 直接退出整个捡漏模式 } */ - + if ![1, 2].contains(&project_data.data.id_bind) { log::error!("暂不支持抢非实名票捡漏模式"); - break 'main_loop; - } + break 'main_loop; + } local_grab_request.biliticket.id_bind = project_data.data.id_bind.clone() as usize; 'screen_loop: for screen_data in project_data.data.screen_list { if !screen_data.clickable { - continue; + continue; } - + local_grab_request.screen_id = screen_data.id.clone().to_string(); local_grab_request.biliticket.screen_id = screen_data.id.clone().to_string(); log::info!("当前项目有可抢票场次,开始抢票!"); - + // 遍历票种 'ticket_loop: for ticket_data in screen_data.ticket_list { if !ticket_data.clickable { @@ -756,23 +752,23 @@ impl TaskManager for TaskManagerImpl { continue; // 跳过这个票种 } } - + log::info!("当前{} {}票种可售,开始抢票!", ticket_data.screen_name, ticket_data.desc); local_grab_request.ticket_id = ticket_data.id.clone().to_string(); local_grab_request.biliticket.select_ticket_id = Some(ticket_data.id.clone().to_string()); cpdd = Arc::new(Mutex::new(CTokenGenerator::new( - project_data.data.sale_begin as i64, - 0, + project_data.data.sale_begin as i64, + 0, rng.gen_range(2000..10000) ))); // 获取token let token_result = get_ticket_token( - cookie_manager.clone(), + cookie_manager.clone(), cpdd.clone(), - &project_id, + &project_id, - &local_grab_request.screen_id, - &local_grab_request.ticket_id, + &local_grab_request.screen_id, + &local_grab_request.ticket_id, count, is_hot.clone() ).await; @@ -782,37 +778,37 @@ impl TaskManager for TaskManagerImpl { log::info!("获取抢票token成功!:{} ptoken:{}",token,ptoken); let mut confirm_retry_count = 0; const MAX_CONFIRM_RETRY: i8 = 4; - + loop { let (success, retry_limit) = handle_grab_ticket( - cookie_manager.clone(), + cookie_manager.clone(), cpdd.clone(), - &project_id, - &token, + &project_id, + &token, &ptoken, is_hot.clone(), - &task_id, - uid, + &task_id, + uid, &result_tx, &local_grab_request, &buyer_info ).await ; if success { log::info!("抢票流程结束,退出捡漏模式"); - + break 'main_loop; } if retry_limit { log::info!("该票种已达到最大重试次数,恢复捡漏模式,尝试其他票种"); break 'screen_loop; } - + confirm_retry_count += 1; if confirm_retry_count >= MAX_CONFIRM_RETRY { log::error!("确认订单失败,已达最大重试次数,尝试其他票种"); break; // 只跳出当前票种的重试循环 } - + tokio::time::sleep(tokio::time::Duration::from_secs_f32(0.3)).await; } }, @@ -822,7 +818,7 @@ impl TaskManager for TaskManagerImpl { //需要处理验证码 log::warn!("需要验证码,开始处理验证码..."); match handle_risk_verification( - cookie_manager.clone(), + cookie_manager.clone(), risk_param, &custon_config, &csrf, @@ -884,16 +880,16 @@ impl TaskManager for TaskManagerImpl { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } - + } } } - + // 本轮所有场次和票种都检查完毕,休息一秒后继续下一轮 log::info!("所有场次和票种检查完毕,等待2秒后重新检查"); tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; } - + log::info!("捡漏模式任务已退出"); } _=> { @@ -912,7 +908,7 @@ impl TaskManager for TaskManagerImpl { } }); }); - + Self { task_sender: task_tx, result_receiver: result_rx, @@ -921,14 +917,31 @@ impl TaskManager for TaskManagerImpl { _worker_thread: Some(worker), } } - + fn submit_task(&mut self, request: TaskRequest) -> Result { - // 生成任务ID - let task_id = uuid::Uuid::new_v4().to_string(); - + // 根据请求类型获取或生成任务ID + let task_id = match &request { + TaskRequest::GetBuyerInfoRequest(req) => { + // 使用请求中已有的任务ID + if !req.task_id.is_empty() { + req.task_id.clone() + } else { + uuid::Uuid::new_v4().to_string() + } + } + TaskRequest::GetTicketInfoRequest(req) => { + // 使用请求中已有的任务ID + if !req.task_id.is_empty() { + req.task_id.clone() + } else { + uuid::Uuid::new_v4().to_string() + } + } + _ => uuid::Uuid::new_v4().to_string(), + }; + // 根据请求类型创建相应的任务 match &request { - TaskRequest::QrCodeLoginRequest(qrcode_req) => { log::info!("提交二维码登录任务 ID: {}", task_id); // 创建二维码登录任务 @@ -939,13 +952,18 @@ impl TaskManager for TaskManagerImpl { status: TaskStatus::Pending, start_time: Some(std::time::Instant::now()), }; - + // 保存任务 - self.running_tasks.insert(task_id.clone(), Task::QrCodeLoginTask(task)); + self.running_tasks + .insert(task_id.clone(), Task::QrCodeLoginTask(task)); } TaskRequest::LoginSmsRequest(login_sms_req) => { - log::info!("提交短信验证码任务 ID: {}, 手机号: {}", task_id, login_sms_req.phone); - + log::info!( + "提交短信验证码任务 ID: {}, 手机号: {}", + task_id, + login_sms_req.phone + ); + // 创建短信任务 let task = LoginSmsRequestTask { task_id: task_id.clone(), @@ -953,29 +971,35 @@ impl TaskManager for TaskManagerImpl { status: TaskStatus::Pending, start_time: Some(std::time::Instant::now()), }; - + // 保存任务 - self.running_tasks.insert(task_id.clone(), Task::LoginSmsRequestTask(task)); + self.running_tasks + .insert(task_id.clone(), Task::LoginSmsRequestTask(task)); } TaskRequest::PushRequest(push_req) => { log::info!("提交推送任务 ID: {}", task_id); // 创建推送任务 let task = PushTask { task_id: task_id.clone(), - push_type: push_req.push_type.clone(), // 使用push_type + push_type: push_req.push_type.clone(), // 使用push_type title: push_req.title.clone(), message: push_req.message.clone(), status: TaskStatus::Pending, start_time: Some(std::time::Instant::now()), }; - + // 保存任务 - self.running_tasks.insert(task_id.clone(), Task::PushTask(task)); + self.running_tasks + .insert(task_id.clone(), Task::PushTask(task)); } TaskRequest::SubmitLoginSmsRequest(login_sms_req) => { - log::info!("提交短信验证码登录任务 ID: {}, 手机号: {}", task_id, login_sms_req.phone); - + log::info!( + "提交短信验证码登录任务 ID: {}, 手机号: {}", + task_id, + login_sms_req.phone + ); + // 创建短信验证码登录任务 let task = SubmitLoginSmsRequestTask { task_id: task_id.clone(), @@ -985,13 +1009,14 @@ impl TaskManager for TaskManagerImpl { status: TaskStatus::Pending, start_time: Some(std::time::Instant::now()), }; - + // 保存任务 - self.running_tasks.insert(task_id.clone(), Task::SubmitLoginSmsRequestTask(task)); + self.running_tasks + .insert(task_id.clone(), Task::SubmitLoginSmsRequestTask(task)); } TaskRequest::GetAllorderRequest(get_order_req) => { log::info!("提交获取全部订单任务 ID: {}", task_id); - + // 创建获取全部订单任务 let task = GetAllorderRequest { task_id: task_id.clone(), @@ -1001,24 +1026,26 @@ impl TaskManager for TaskManagerImpl { account_id: get_order_req.account_id.clone(), start_time: Some(std::time::Instant::now()), }; - + // 保存任务 - self.running_tasks.insert(task_id.clone(), Task::GetAllorderRequestTask(task)); + self.running_tasks + .insert(task_id.clone(), Task::GetAllorderRequestTask(task)); } TaskRequest::GetTicketInfoRequest(get_ticketinfo_req) => { - log::info!("{}",task_id); - let task = GetTicketInfoTask{ - task_id : task_id.clone(), + log::info!("提交获取票务信息任务 ID: {}", task_id); + let task = GetTicketInfoTask { + task_id: task_id.clone(), project_id: get_ticketinfo_req.project_id.clone(), status: TaskStatus::Running, start_time: Some(std::time::Instant::now()), - cookie_manager: get_ticketinfo_req.cookie_manager.clone(), + cookie_manager: get_ticketinfo_req.cookie_manager.clone(), }; - self.running_tasks.insert(task_id.clone(),Task::GetTicketInfoTask(task)); + self.running_tasks + .insert(task_id.clone(), Task::GetTicketInfoTask(task)); } TaskRequest::GetBuyerInfoRequest(get_buyerinfo_req) => { log::info!("提交获取购票人信息任务 ID: {}", task_id); - + //创建任务 let task = GetBuyerInfoTask { uid: get_buyerinfo_req.uid.clone(), @@ -1026,16 +1053,16 @@ impl TaskManager for TaskManagerImpl { cookie_manager: get_buyerinfo_req.cookie_manager.clone(), status: TaskStatus::Pending, start_time: Some(std::time::Instant::now()), - }; - + // 保存任务 - self.running_tasks.insert(task_id.clone(), Task::GetBuyerInfoTask(task)); + self.running_tasks + .insert(task_id.clone(), Task::GetBuyerInfoTask(task)); } TaskRequest::GrabTicketRequest(grab_ticket_req) => { log::info!("提交抢票任务 ID: {}", task_id); - - /* // 创建抢票任务 + + /* // 创建抢票任务 let task = GrabTicketTask { task_id: task_id.clone(), project_id: grab_ticket_req.project_id.clone(), @@ -1048,48 +1075,52 @@ impl TaskManager for TaskManagerImpl { uid: grab_ticket_req.uid.clone(), grab_mode: grab_ticket_req.grab_mode.clone(), }; - + // 保存任务 self.running_tasks.insert(task_id.clone(), Task::GrabTicketTask(task)); */ } - } - + // 发送任务 - if let Err(e) = self.task_sender.blocking_send(TaskMessage::SubmitTask(request)) { + if let Err(e) = self + .task_sender + .blocking_send(TaskMessage::SubmitTask(request)) + { return Err(format!("无法提交任务: {}", e)); } - + Ok(task_id) } - + fn get_results(&mut self) -> Vec { let mut results = Vec::new(); - + // 非阻塞方式获取所有可用结果 while let Ok(result) = self.result_receiver.try_recv() { results.push(result); } - + results } - + fn cancel_task(&mut self, task_id: &str) -> Result<(), String> { if !self.running_tasks.contains_key(task_id) { return Err("任务不存在".to_string()); } - - if let Err(e) = self.task_sender.blocking_send(TaskMessage::CancelTask(task_id.to_owned())) { + + if let Err(e) = self + .task_sender + .blocking_send(TaskMessage::CancelTask(task_id.to_owned())) + { return Err(format!("无法取消任务: {}", e)); } - + Ok(()) } - + fn get_task_status(&self, task_id: &str) -> Option { if let Some(task) = self.running_tasks.get(task_id) { match task { - Task::QrCodeLoginTask(t) => Some(t.status.clone()), Task::LoginSmsRequestTask(t) => Some(t.status.clone()), Task::PushTask(t) => Some(t.status.clone()), @@ -1103,7 +1134,7 @@ impl TaskManager for TaskManagerImpl { None } } - + fn shutdown(&mut self) { let _ = self.task_sender.blocking_send(TaskMessage::Shutdown); if let Some(handle) = self._worker_thread.take() { @@ -1112,8 +1143,6 @@ impl TaskManager for TaskManagerImpl { } } - - async fn handle_grab_ticket( cookie_manager: Arc, cpdd: Arc>, @@ -1131,9 +1160,8 @@ async fn handle_grab_ticket( match confirm_ticket_order(cookie_manager.clone(), project_id, token).await { Ok(confirm_result) => { log::info!("确认订单成功!准备下单"); - - - if let Some((success,retry_limit)) = try_create_order( + + if let Some((success, retry_limit)) = try_create_order( cookie_manager.clone(), cpdd.clone(), project_id, @@ -1146,11 +1174,12 @@ async fn handle_grab_ticket( task_id, uid, result_tx, - ).await { - - return (success,retry_limit); + ) + .await + { + return (success, retry_limit); } - + (true, false) // 订单流程已完成 } Err(e) => { @@ -1176,21 +1205,21 @@ async fn try_create_order( result_tx: &mpsc::Sender, ) -> Option<( bool, - bool // 第二个参数标记是因为达到重试上限 - )> { + bool, // 第二个参数标记是因为达到重试上限 +)> { let mut order_retry_count = 0; let mut need_retry = false; - + // 下单循环 loop { if order_retry_count >= 3 { need_retry = true; } - + match create_order( - cookie_manager.clone(), + cookie_manager.clone(), cpdd.clone(), - project_id, + project_id, token, ptoken, confirm_result, @@ -1200,42 +1229,63 @@ async fn try_create_order( true, need_retry, false, - None - ).await { + None, + ) + .await + { Ok(order_result) => { log::info!("下单成功!订单信息{:?}", order_result); let empty_json = json!({}); let order_data = order_result.get("data").unwrap_or(&empty_json); - + let zero_json = json!(0); - let order_id = order_data.get("orderId").unwrap_or(&zero_json).as_i64().unwrap_or(0); - + let order_id = order_data + .get("orderId") + .unwrap_or(&zero_json) + .as_i64() + .unwrap_or(0); + let empty_string_json = json!(""); - let pay_token = order_data.get("token").unwrap_or(&empty_string_json).as_str().unwrap_or(""); - + let pay_token = order_data + .get("token") + .unwrap_or(&empty_string_json) + .as_str() + .unwrap_or(""); + log::info!("下单成功!正在检测是否假票!"); // 检测假票 - let check_result = match check_fake_ticket(cookie_manager.clone(), project_id, pay_token, order_id).await{ + let check_result = match check_fake_ticket( + cookie_manager.clone(), + project_id, + pay_token, + order_id, + ) + .await + { Ok(result) => result, Err(e) => { log::error!("检测假票失败,原因:{},请前往订单列表查看是否下单成功", e); continue; // 继续重试 } }; - let errno = check_result.get("errno").unwrap_or(&zero_json).as_i64().unwrap_or(0); + let errno = check_result + .get("errno") + .unwrap_or(&zero_json) + .as_i64() + .unwrap_or(0); if errno != 0 { log::error!("假票,继续抢票"); continue; } - let analyze_result = match serde_json::from_value::(check_result.clone()){ - Ok(result) => result, - Err(e) => { - log::error!("解析假票结果失败,原因:{}", e); - continue; // 继续重试 - } - }; - - + let analyze_result = + match serde_json::from_value::(check_result.clone()) { + Ok(result) => result, + Err(e) => { + log::error!("解析假票结果失败,原因:{}", e); + continue; // 继续重试 + } + }; + let pay_result = analyze_result.data.pay_param; // 通知成功 let task_result = TaskResult::GrabTicketResult(GrabTicketResult { @@ -1243,89 +1293,107 @@ async fn try_create_order( uid, success: true, message: "抢票成功".to_string(), - order_id: Some(order_id.clone().to_string()), + order_id: Some(order_id.clone().to_string()), pay_token: Some(pay_token.to_string()), confirm_result: Some(confirm_result.clone()), - pay_result : Some(pay_result.clone()), - + pay_result: Some(pay_result.clone()), }); let _ = result_tx.send(task_result.clone()).await; - + //修复由于挂在后台egui不运行导致任务管理器不加载导致不推送 - let jump_url = Some(format!("bilibili://mall/web?url=https://mall.bilibili.com/neul-next/ticket/orderDetail.html?order_id={}", order_id.to_string())); + let jump_url = Some(format!( + "bilibili://mall/web?url=https://mall.bilibili.com/neul-next/ticket/orderDetail.html?order_id={}", + order_id.to_string() + )); let pay_url = pay_result.code_url.clone(); let title = format!("恭喜{}抢票成功!", confirm_result.project_name); - let message = format!("抢票成功!\n项目:{}\n场次:{}\n票类型:{}\n支付链接:{}\n请尽快支付{}元,以免支付超时导致票丢失\n如果觉得本项目好用,可前往https://github.com/biliticket/bili_ticket_rush 帮我们点个小星星star收藏本项目以防走丢\n本项目完全免费开源,仅供学习使用,开发组不承担使用本软件造成的一切后果",confirm_result.project_name, confirm_result.screen_name, confirm_result.ticket_info.name, pay_url ,confirm_result.ticket_info.price * confirm_result.count as i64/ 100); - - let _ = &grab_ticket_req.biliticket.push_self.push_all_async( &title, &message,&jump_url).await; - return Some((true,false)); // 成功,不需要继续重试 + let message = format!( + "抢票成功!\n项目:{}\n场次:{}\n票类型:{}\n支付链接:{}\n请尽快支付{}元,以免支付超时导致票丢失\n如果觉得本项目好用,可前往https://github.com/biliticket/bili_ticket_rush 帮我们点个小星星star收藏本项目以防走丢\n本项目完全免费开源,仅供学习使用,开发组不承担使用本软件造成的一切后果", + confirm_result.project_name, + confirm_result.screen_name, + confirm_result.ticket_info.name, + pay_url, + confirm_result.ticket_info.price * confirm_result.count as i64 / 100 + ); + + let _ = &grab_ticket_req + .biliticket + .push_self + .push_all_async(&title, &message, &jump_url) + .await; + return Some((true, false)); // 成功,不需要继续重试 //有个问题:取的是缓存里的pushconfig,动态修改的新的推不了 } - + Err(e) => { // 处理错误情况 match e { //需要继续重试的临时错误 - 100001 | 429 | 900001=> log::info!("b站限速,正常现象"), - 100009 => { + 100001 | 429 | 900001 => log::info!("b站限速,正常现象"), + 100009 => { log::info!("当前票种库存不足"); //再次降速,不给b站服务器带来压力 - tokio::time::sleep(tokio::time::Duration::from_secs_f32(0.6)).await; - - }, + tokio::time::sleep(tokio::time::Duration::from_secs_f32(0.6)).await; + } 211 => { log::info!("很遗憾,差一点点抢到票,继续加油吧!"); } - + //需要暂停的情况 3 => { log::info!("抢票速度过快,即将被硬控5秒"); log::info!("暂停4.8秒"); tokio::time::sleep(tokio::time::Duration::from_secs_f32(4.8)).await; - }, - + } + //需要重新获取token的情况 - 100041 | 100050 | 900002=> { + 100041 | 100050 | 900002 => { log::info!("token失效,即将重新获取token"); - return Some((true,true)); // 需要重新获取token - }, - + return Some((true, true)); // 需要重新获取token + } + //需要终止抢票的致命错误 100017 | 100016 => { log::info!("当前项目/类型/场次已停售"); - return Some((true,false)); - }, + return Some((true, false)); + } 1 => { - log::error!("超人 请慢一点,这是仅限1人抢票的项目,或抢票格式有误,请重新提交任务"); - return Some((true,false)); + log::error!( + "超人 请慢一点,这是仅限1人抢票的项目,或抢票格式有误,请重新提交任务" + ); + return Some((true, false)); } 83000004 => { log::error!("没有配置购票人信息!请重新配置"); - return Some((true,false)); - }, - 100079 | 100003 => { + return Some((true, false)); + } + 100079 | 100003 => { log::error!("购票人存在待付款订单,请前往支付或取消后重新下单"); - return Some((true,false)); - }, + return Some((true, false)); + } 100039 => { log::error!("活动收摊啦,下次要快点哦"); - return Some((true,false)); + return Some((true, false)); } - + 209001 => { log::error!("当前项目只能选择一个购票人!不支持多选,请重新提交任务"); - return Some((true,false)); + return Some((true, false)); } 737 => { - log::error!("B站传了一个NUll回来,请看一下上一行的message提示信息,自行决定是否继续,如果取消请关闭重新打开该应用"); + log::error!( + "B站传了一个NUll回来,请看一下上一行的message提示信息,自行决定是否继续,如果取消请关闭重新打开该应用" + ); } - + 999 => { log::error!("程序内部错误!传参错误") } 919 => { - log::error!("程序内部错误!该项目区分绑定非绑定项目错误,传入意外值,请尝试重新下单以及提出issue"); - return Some((true,false)); + log::error!( + "程序内部错误!该项目区分绑定非绑定项目错误,传入意外值,请尝试重新下单以及提出issue" + ); + return Some((true, false)); } //未知错误 @@ -1333,14 +1401,16 @@ async fn try_create_order( } } } - + // 增加重试计数并等待 order_retry_count += 1; if grab_ticket_req.grab_mode == 2 && order_retry_count >= 30 { - log::error!("捡漏模式下单失败,已达最大重试次数,放弃该票种抢票,准备检测其他票种继续捡漏"); - return Some((false,true)); // 捡漏模式下单失败,放弃该票种抢票 + log::error!( + "捡漏模式下单失败,已达最大重试次数,放弃该票种抢票,准备检测其他票种继续捡漏" + ); + return Some((false, true)); // 捡漏模式下单失败,放弃该票种抢票 } tokio::time::sleep(tokio::time::Duration::from_secs_f32(0.4)).await; //降低速度,不带来b站服务器压力 } -} \ No newline at end of file +} diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml new file mode 100644 index 0000000..578131b --- /dev/null +++ b/crates/cli/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "cli" +version = "0.1.0" +edition = "2024" + +[dependencies] +common = { path = "../common" } +tokio = { version = "1", features = ["full"] } +uuid = { version = "1.3", features = ["v4"] } +chrono = "0.4" + +log = "0.4" +env_logger = "0.9" + +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +reqwest = { version="0.11.22", features=["json", "blocking"]} + +rand = "0.8" + +base64 = "0.22" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/crates/cli/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/common/Cargo.toml b/crates/common/Cargo.toml similarity index 53% rename from common/Cargo.toml rename to crates/common/Cargo.toml index 928bc7f..cf6e93c 100644 --- a/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -1,49 +1,34 @@ [package] name = "common" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] tokio = { version = "1", features = ["full"] } -rand = "0.8" - -eframe = { version = "0.23.0", features = ["default"] } -egui = "0.31.0" - -# log -log = "0.4.27" -env_logger = "0.9" -chrono = "0.4" -once_cell = "1.8" - -#json serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" - -reqwest = { version="0.11.22", features=["json", "blocking", "cookies"]} - +log = "0.4" +chrono = "0.4" +rand = "0.8" base64 = "0.21" + +once_cell = "1.8" aes = "0.7.5" block-modes = "0.8.1" machine-uid = "0.5.3" lazy_static = "1.4" image = "0.25" - -whoami = "1.5.1" -ctrlc = "3.4.0" - -#captcha -bili_ticket_gt = { git = "https://github.com/Amorter/biliTicker_gt",branch = "rust"} - +whoami = "1.5.1" +ctrlc = "3.4.0" md5 = "0.7.0" - -uuid = { version = "1.0", features = ["v4", "fast-rng"] } - hmac = "0.12.1" sha2 = "0.10.7" hex = "0.4.3" cookie = "0.16" -fs2 = "0.4.3" # 添加对fs2的依赖 +fs2 = "0.4.3" +single-instance = "0.3.3" + +bili_ticket_gt = { git = "https://github.com/Amorter/biliTicker_gt", branch = "rust" } -#Singleton -single-instance = "0.3.3" \ No newline at end of file +reqwest = { version = "0.11.22", features = ["json", "blocking", "cookies"], override = true } +uuid = { version = "1.0", features = ["v4", "fast-rng"], override = true } diff --git a/common/src/account.rs b/crates/common/src/account.rs similarity index 67% rename from common/src/account.rs rename to crates/common/src/account.rs index b993eed..db493e5 100644 --- a/common/src/account.rs +++ b/crates/common/src/account.rs @@ -1,27 +1,28 @@ -use serde::{Serialize, Deserialize}; +use crate::cookie_manager::CookieManager; +use crate::{ + cookie_manager, + http_utils::{request_get_sync, request_post_sync}, +}; use reqwest::Client; -use crate::{cookie_manager, http_utils::{request_get_sync,request_post_sync}}; +use serde::{Deserialize, Serialize}; use serde_json; use std::sync::Arc; -use crate::cookie_manager::CookieManager; #[derive(Clone, Serialize, Deserialize)] -pub struct Account{ - pub uid: i64, //UID - pub name: String, //昵称 +pub struct Account { + pub uid: i64, //UID + pub name: String, //昵称 pub level: String, - pub cookie: String, //cookie - pub csrf : String, //csrf - pub is_login: bool, //是否登录 - pub account_status: String, //账号状态 - pub vip_label: String, //大会员,对应/nav请求中data['vip_label']['text'] - pub is_active: bool, //该账号是否启动抢票 + pub cookie: String, //cookie + pub csrf: String, //csrf + pub is_login: bool, //是否登录 + pub account_status: String, //账号状态 + pub vip_label: String, //大会员,对应/nav请求中data['vip_label']['text'] + pub is_active: bool, //该账号是否启动抢票 pub avatar_url: Option, //头像地址 #[serde(skip)] - pub avatar_texture: Option, //头像地址 - #[serde(skip)] pub cookie_manager: Option>, //cookie管理器 } -impl std::fmt::Debug for Account{ +impl std::fmt::Debug for Account { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Account") .field("uid", &self.uid) @@ -34,29 +35,29 @@ impl std::fmt::Debug for Account{ .field("vip_label", &self.vip_label) .field("is_active", &self.is_active) .field("avatar_url", &self.avatar_url) - .field("avatar_texture", &"SKipped") .field("client", &self.cookie_manager) .finish() } } -pub fn add_account(cookie: &str ,client: &Client, ua: &str) -> Result{ +pub fn add_account(cookie: &str, client: &Client, ua: &str) -> Result { log::info!("添加账号: {}", cookie); let response = request_get_sync( client, "https://api.bilibili.com/x/web-interface/nav", Some(ua.to_string()), Some(cookie), - ).map_err(|e| e.to_string())?; - + ) + .map_err(|e| e.to_string())?; + // 创建一个临时的运行时来执行异步代码 let rt = tokio::runtime::Runtime::new().unwrap(); - let json = rt.block_on(async { - response.json::().await - }).map_err(|e| e.to_string())?; - let cookie_manager = Arc::new(rt.block_on(async{ - cookie_manager::CookieManager::new(cookie, Some(ua), 0).await - })); + let json = rt + .block_on(async { response.json::().await }) + .map_err(|e| e.to_string())?; + let cookie_manager = Arc::new( + rt.block_on(async { cookie_manager::CookieManager::new(cookie, Some(ua), 0).await }), + ); log::debug!("获取账号信息: {:?}", json); match json.get("code") { Some(code) if code.as_i64() == Some(0) => {} // 成功 @@ -65,8 +66,14 @@ pub fn add_account(cookie: &str ,client: &Client, ua: &str) -> Result Result Result { }); let rt = tokio::runtime::Runtime::new().unwrap(); - let response = rt.block_on(async{ - account.cookie_manager.clone().unwrap().post("https://passport.bilibili.com/login/exit/v2") - .await - .json(&data) - .send() - .await + let response = rt.block_on(async { + account + .cookie_manager + .clone() + .unwrap() + .post("https://passport.bilibili.com/login/exit/v2") + .await + .json(&data) + .send() + .await }); - + let resp = match response { Ok(res) => res, Err(e) => return Err(format!("请求失败: {}", e)), }; - log::debug!("退出登录响应: {:?}",resp); + log::debug!("退出登录响应: {:?}", resp); Ok(resp.status().is_success()) - } - //提取 csrf fn extract_csrf(cookie: &str) -> String { // 打印原始cookie用于调试 log::debug!("提取CSRF的原始cookie: {}", cookie); - + for part in cookie.split(';') { let part = part.trim(); // 检查是否以bili_jct开头(不区分大小写) @@ -127,7 +135,7 @@ fn extract_csrf(cookie: &str) -> String { } } } - + // 没找到,记录并返回空字符串 log::warn!("无法从cookie中提取CSRF值"); String::new() @@ -137,45 +145,44 @@ impl Account { pub fn ensure_client(&mut self) { let rt = tokio::runtime::Runtime::new().unwrap(); if self.cookie_manager.is_none() { - rt.block_on(async{ - self.cookie_manager = Some(Arc::new(CookieManager::new( - &self.cookie, - None, - 0, - ).await)) - }); + rt.block_on(async { + self.cookie_manager = + Some(Arc::new(CookieManager::new(&self.cookie, None, 0).await)) + }); } } - - } // 创建client fn create_client_for_account(cookie: &str) -> reqwest::Client { use reqwest::header; - - - let random_id = format!("{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().subsec_nanos()); - - + + let random_id = format!( + "{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .subsec_nanos() + ); + let user_agent = format!( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 {}", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 {}", random_id ); - + let mut headers = header::HeaderMap::new(); headers.insert( header::USER_AGENT, header::HeaderValue::from_str(&user_agent).unwrap_or_else(|_| { // 提供一个替代值,而不是使用 unwrap_or_default() header::HeaderValue::from_static("Mozilla/5.0") - }) + }), ); - + // 创建 client reqwest::Client::builder() .default_headers(headers) .cookie_store(true) .build() .unwrap_or_else(|_| reqwest::Client::new()) -} \ No newline at end of file +} diff --git a/common/src/captcha.rs b/crates/common/src/captcha.rs similarity index 61% rename from common/src/captcha.rs rename to crates/common/src/captcha.rs index 929ba46..b55d95f 100644 --- a/common/src/captcha.rs +++ b/crates/common/src/captcha.rs @@ -1,13 +1,13 @@ -use std::fmt; -use serde_json::json; -use std::sync::{Arc, Mutex}; +use crate::cookie_manager::CookieManager; +use crate::{ticket::TokenRiskParam, utility::CustomConfig}; use bili_ticket_gt::click::Click; use bili_ticket_gt::slide::Slide; -use crate::cookie_manager::CookieManager; -use crate::{ ticket::TokenRiskParam, utility::CustomConfig}; +use serde_json::json; +use std::fmt; +use std::sync::{Arc, Mutex}; -#[derive(Clone)] -pub struct LocalCaptcha{ +#[derive(Clone)] +pub struct LocalCaptcha { click: Option>>, slide: Option>>, } @@ -16,8 +16,22 @@ pub struct LocalCaptcha{ impl fmt::Debug for LocalCaptcha { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("LocalCaptcha") - .field("click", &if self.click.is_some() { "Some(Click)" } else { "None" }) - .field("slide", &if self.slide.is_some() { "Some(Slide)" } else { "None" }) + .field( + "click", + &if self.click.is_some() { + "Some(Click)" + } else { + "None" + }, + ) + .field( + "slide", + &if self.slide.is_some() { + "Some(Slide)" + } else { + "None" + }, + ) .finish() } } @@ -31,30 +45,30 @@ impl LocalCaptcha { } } pub async fn captcha( - custom_config: CustomConfig, - gt: &str, - challenge: &str, - referer: &str, // referer(ttocr打码使用) - captcha_type:usize, // 33对应三代点字 32对应三代滑块 - local_captcha: LocalCaptcha, //本地打码需要传入实例结构体 -) - -> Result { + custom_config: CustomConfig, + gt: &str, + challenge: &str, + referer: &str, // referer(ttocr打码使用) + captcha_type: usize, // 33对应三代点字 32对应三代滑块 + local_captcha: LocalCaptcha, //本地打码需要传入实例结构体 +) -> Result { // 0:本地打码 1:ttocr match custom_config.captcha_mode { 0 => { - match captcha_type{ + match captcha_type { 32 => { let mut slide = match local_captcha.slide { - Some(c) =>c, + Some(c) => c, None => { return Err("本地打码需要传入slide对象".to_string()); } }; Err("本地打码暂不支持三代滑块".to_string()) } - 33 => { //三代点字 + 33 => { + //三代点字 let click_mutex = match &local_captcha.click { - Some(c) =>Arc::clone(c), + Some(c) => Arc::clone(c), None => { return Err("本地打码需要传入click对象".to_string()); } @@ -64,30 +78,28 @@ pub async fn captcha( let validate = tokio::task::spawn_blocking(move || { let mut click = click_mutex.lock().unwrap(); click.simple_match_retry(>_clone, &challenge_clone) - }).await - .map_err(|e| format!("任务执行出错:{}",e))? - .map_err(|e| format!("验证码模块出错:{}",e))?; - - - + }) + .await + .map_err(|e| format!("任务执行出错:{}", e))? + .map_err(|e| format!("验证码模块出错:{}", e))?; + log::info!("验证码识别结果: {:?}", validate); Ok(serde_json::to_string(&json!({ "challenge": challenge, "validate": validate, "seccode": format!("{}|jordan", validate), - })).map_err(|e| format!("序列化JSON失败: {}", e))?) - - + })) + .map_err(|e| format!("序列化JSON失败: {}", e))?) } _ => { return Err("无效的验证码类型".to_string()); } } - }, + } 1 => { // ttocr let client = reqwest::Client::builder() - .danger_accept_invalid_certs(true) // 禁用证书验证 + .danger_accept_invalid_certs(true) // 禁用证书验证 .build() .map_err(|e| format!("创建HTTP客户端失败: {}", e))?; let form_data = json!({ @@ -98,69 +110,71 @@ pub async fn captcha( "referer": referer, }); log::info!("验证码请求参数: {:?}", form_data); - let response = client.post("http://api.ttocr.com/api/recognize") - .json(&form_data) - .send() - .await - .map_err(|e| format!("发送请求失败: {}", e))?; + let response = client + .post("http://api.ttocr.com/api/recognize") + .json(&form_data) + .send() + .await + .map_err(|e| format!("发送请求失败: {}", e))?; log::info!("验证码请求响应: {:?}", response); - let text = response.text() - .await - .map_err(|e| format!("读取响应内容失败: {}", e))?; - + let text = response + .text() + .await + .map_err(|e| format!("读取响应内容失败: {}", e))?; + // 打印文本内容 log::info!("响应内容: {}", text); - let json_response: serde_json::Value = serde_json::from_str(&text) - .map_err(|e| format!("解析JSON失败: {}", e))?; + let json_response: serde_json::Value = + serde_json::from_str(&text).map_err(|e| format!("解析JSON失败: {}", e))?; - if json_response["status"].as_i64() == Some(1){ + if json_response["status"].as_i64() == Some(1) { log::info!("验证码提交识别成功"); - } - else{ - log::error!("验证码提交识别失败: {}", json_response["msg"].as_str().unwrap_or("未知错误")); + } else { + log::error!( + "验证码提交识别失败: {}", + json_response["msg"].as_str().unwrap_or("未知错误") + ); return Err("验证码提交识别失败".to_string()); } let result_id = json_response["resultid"].as_str().unwrap_or(""); - for _ in 0..20{ - let response = client.post("http://api.ttocr.com/api/results") - .json(&json!({ - "appkey": custom_config.ttocr_key, - "resultid": result_id, - })) - .send() - .await - .map_err(|e| format!("发送请求失败: {}", e))?; - let text = response.text() - .await - .map_err(|e| format!("读取响应内容失败: {}", e))?; - - // 打印文本内容 - log::info!("响应内容: {}", text); - let json_response: serde_json::Value = serde_json::from_str(&text) - .map_err(|e| format!("解析JSON失败: {}", e))?; + for _ in 0..20 { + let response = client + .post("http://api.ttocr.com/api/results") + .json(&json!({ + "appkey": custom_config.ttocr_key, + "resultid": result_id, + })) + .send() + .await + .map_err(|e| format!("发送请求失败: {}", e))?; + let text = response + .text() + .await + .map_err(|e| format!("读取响应内容失败: {}", e))?; + // 打印文本内容 + log::info!("响应内容: {}", text); + let json_response: serde_json::Value = + serde_json::from_str(&text).map_err(|e| format!("解析JSON失败: {}", e))?; - if json_response["status"].as_i64() == Some(1){ + if json_response["status"].as_i64() == Some(1) { log::info!("验证码识别成功"); return Ok(serde_json::to_string(&json!({ "challenge": json_response["data"]["challenge"], "validate": json_response["data"]["validate"], "seccode": json_response["data"]["seccode"], - })).map_err(|e| format!("序列化JSON失败: {}", e))?); - + })) + .map_err(|e| format!("序列化JSON失败: {}", e))?); } tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } Err("验证码识别超时".to_string()) - }, + } _ => Err("无效的验证码模式".to_string()), } } - - - pub async fn handle_risk_verification( cookie_manager: Arc, risk_param: TokenRiskParam, @@ -174,62 +188,67 @@ pub async fn handle_risk_verification( }; log::debug!("风控参数: {:?}", risk_params_value); let url = "https://api.bilibili.com/x/gaia-vgate/v1/register"; - let response = cookie_manager.post(url).await + let response = cookie_manager + .post(url) + .await .json(&json!(risk_params_value)) .send() .await - .map_err(|e| format!("发送风控请求失败: {}", e))?; + .map_err(|e| format!("发送风控请求失败: {}", e))?; if !response.status().is_success() { return Err(format!("风控请求返回错误状态码: {}", response.status())); } - - let text = response.text().await + + let text = response + .text() + .await .map_err(|e| format!("读取响应内容失败: {}", e))?; log::debug!("验证码请求响应: {}", text); - - let json_response: serde_json::Value = serde_json::from_str(&text) - .map_err(|e| format!("解析JSON失败: {}", e))?; - - + + let json_response: serde_json::Value = + serde_json::from_str(&text).map_err(|e| format!("解析JSON失败: {}", e))?; + if json_response["code"].as_i64() != Some(0) { let message = json_response["message"].as_str().unwrap_or("未知错误"); - return Err(format!("风控请求失败: {} (code: {})", message, json_response["code"])); + return Err(format!( + "风控请求失败: {} (code: {})", + message, json_response["code"] + )); } - - + let captcha_type = json_response["data"]["type"].as_str().unwrap_or(""); - + match captcha_type { "geetest" => { log::info!("验证码类型: geetest"); - - - let gt = json_response["data"]["geetest"]["gt"].as_str().unwrap_or(""); - let challenge = json_response["data"]["geetest"]["challenge"].as_str().unwrap_or(""); - let token = json_response["data"]["geetest"]["token"].as_str().unwrap_or(""); - + + let gt = json_response["data"]["geetest"]["gt"] + .as_str() + .unwrap_or(""); + let challenge = json_response["data"]["geetest"]["challenge"] + .as_str() + .unwrap_or(""); + let token = json_response["data"]["geetest"]["token"] + .as_str() + .unwrap_or(""); + if gt.is_empty() || challenge.is_empty() || token.is_empty() { return Err("获取验证码参数失败".to_string()); } - - + let captcha_result = captcha( - custom_config.clone(), - gt, - challenge, - "https://api.bilibili.com/x/gaia-vgate/v1/validate", - 33 ,// 点选类型 + custom_config.clone(), + gt, + challenge, + "https://api.bilibili.com/x/gaia-vgate/v1/validate", + 33, // 点选类型 local_captcha, + ) + .await?; - ).await?; - - let captcha_data: serde_json::Value = serde_json::from_str(&captcha_result) .map_err(|e| format!("解析验证码结果失败: {}", e))?; - - - - + let params = json!({ "buvid": risk_param.buvid.unwrap_or_default(), "csrf": csrf, @@ -238,39 +257,45 @@ pub async fn handle_risk_verification( "geetest_validate": captcha_data["validate"], "token": token }); - - + log::debug!("发送验证请求: {:?}", params); let validate_url = "https://api.bilibili.com/x/gaia-vgate/v1/validate"; - let validate_response = cookie_manager.post(validate_url).await + let validate_response = cookie_manager + .post(validate_url) + .await .json(¶ms) .send() .await .map_err(|e| format!("发送验证请求失败: {}", e))?; - + if !validate_response.status().is_success() { - return Err(format!("验证请求返回错误状态码: {}", validate_response.status())); + return Err(format!( + "验证请求返回错误状态码: {}", + validate_response.status() + )); } - - let validate_json = validate_response.json::().await + + let validate_json = validate_response + .json::() + .await .map_err(|e| format!("解析验证响应失败: {}", e))?; - - + if validate_json["code"].as_i64() != Some(0) { let message = validate_json["message"].as_str().unwrap_or("未知错误"); - return Err(format!("验证失败: {} (code: {})", message, validate_json["code"])); + return Err(format!( + "验证失败: {} (code: {})", + message, validate_json["code"] + )); } - + let is_valid = validate_json["data"]["is_valid"].as_bool().unwrap_or(false); if !is_valid { return Err("验证未通过".to_string()); } - - - + log::info!("验证码验证成功"); Ok(()) - }, - _ => Err(format!("不支持的验证码类型: {}", captcha_type)) + } + _ => Err(format!("不支持的验证码类型: {}", captcha_type)), } -} \ No newline at end of file +} diff --git a/common/src/cookie_manager.rs b/crates/common/src/cookie_manager.rs similarity index 70% rename from common/src/cookie_manager.rs rename to crates/common/src/cookie_manager.rs index 44d9911..82802be 100644 --- a/common/src/cookie_manager.rs +++ b/crates/common/src/cookie_manager.rs @@ -1,13 +1,11 @@ - -use reqwest::cookie::Jar; +use crate::web_ck_obfuscated::*; use cookie::Cookie; +use rand::seq::SliceRandom; +use reqwest::cookie::Jar; use std::collections::HashMap; use std::sync::{Arc, Mutex}; //?有用到吗 -use rand::seq::SliceRandom; use std::time::SystemTime; use std::time::UNIX_EPOCH; -use crate::web_ck_obfuscated::{*}; - #[derive(Debug, Clone)] pub struct AppData { @@ -20,7 +18,7 @@ pub struct AppData { pub struct WebData { pub browser_version: String, pub os_version: String, - pub ua: String, + pub ua: String, pub buvid3: String, pub buvid4: String, pub b_nut: String, @@ -28,12 +26,9 @@ pub struct WebData { pub _uuid: String, pub bili_ticket: String, pub bili_ticket_expires: String, - pub msource : String, - - + pub msource: String, } - #[derive(Debug, Clone)] pub struct CookieManager { pub client: Arc, @@ -41,23 +36,28 @@ pub struct CookieManager { app_data: Option, pub web_data: Option, pub cookies: CookiesData, - } #[derive(Debug, Clone)] pub struct CookiesData { pub cookies_map: Arc>>, - pub cookie_jar : Arc>, + pub cookie_jar: Arc>, } impl CookiesData { pub fn insert(&self, key: String, value: String) { - self.cookies_map.lock().unwrap().insert(key.clone(), value.clone()); + self.cookies_map + .lock() + .unwrap() + .insert(key.clone(), value.clone()); let cookie = Cookie::build(&key, value) .domain(".bilibili.com") .path("/") .finish(); - self.cookie_jar.lock().unwrap().add_cookie_str(&cookie.to_string(), &"https://bilibili.com".parse().unwrap()); + self.cookie_jar.lock().unwrap().add_cookie_str( + &cookie.to_string(), + &"https://bilibili.com".parse().unwrap(), + ); } pub fn clear(&self) { @@ -68,96 +68,102 @@ impl CookiesData { impl CookieManager { pub async fn new( - original_cookie : &str , + original_cookie: &str, user_agent: Option<&str>, create_type: usize, //0:默认网页浏览器 1:app - ) -> Self { - let mut cookies = Self::parse_cookie_string(original_cookie); - + match create_type { 0 => { - - //UA部分 + //UA部分 //浏览器 let browser_version_list = vec![ //chrome - "Chrome/126.0.6478.55", "Chrome/127.0.6520.0", "Chrome/125.0.6422.61", "Chrome/124.0.6367.60", "Chrome/135.0.0.0", + "Chrome/126.0.6478.55", + "Chrome/127.0.6520.0", + "Chrome/125.0.6422.61", + "Chrome/124.0.6367.60", + "Chrome/135.0.0.0", //edge - "Chrome/126.0.6478.55 Edg/126.0.2578.55", "Chrome/127.0.6520.0 Edg/127.0.2610.0", "Chrome/125.0.6422.61 Edg/125.0.2535.51", "Chrome/124.0.6367.60 Edg/124.0.2478.67", "Chrome/135.0.0.0, Edg/135.0.0.0", - - ]; - let os_version_list = vec![ - - "(Windows NT 10.0; Win64; x64)" - + "Chrome/126.0.6478.55 Edg/126.0.2578.55", + "Chrome/127.0.6520.0 Edg/127.0.2610.0", + "Chrome/125.0.6422.61 Edg/125.0.2535.51", + "Chrome/124.0.6367.60 Edg/124.0.2478.67", + "Chrome/135.0.0.0, Edg/135.0.0.0", ]; - let browser_version = browser_version_list.choose(&mut rand::thread_rng()) - .map(|&s| s.to_string()) - .unwrap_or_else(|| "Chrome/135.0.0.0".to_string()); - let os_version = os_version_list.choose(&mut rand::thread_rng()) - .map(|&s| s.to_string()) - .unwrap_or_else(|| "(Windows NT 10.0; Win64; x64)".to_string()); + let os_version_list = vec!["(Windows NT 10.0; Win64; x64)"]; + let browser_version = browser_version_list + .choose(&mut rand::thread_rng()) + .map(|&s| s.to_string()) + .unwrap_or_else(|| "Chrome/135.0.0.0".to_string()); + let os_version = os_version_list + .choose(&mut rand::thread_rng()) + .map(|&s| s.to_string()) + .unwrap_or_else(|| "(Windows NT 10.0; Win64; x64)".to_string()); let ua = match user_agent { Some(ua) => ua.to_string(), None => { - format!("Mozilla/5.0 {} AppleWebKit/537.36 (KHTML, like Gecko) {} Safari/537.36", os_version, browser_version) + format!( + "Mozilla/5.0 {} AppleWebKit/537.36 (KHTML, like Gecko) {} Safari/537.36", + os_version, browser_version + ) } }; - + log::debug!("UA: {}", ua); - - let client_builder = reqwest::Client::builder() - .cookie_store(true); - let client = client_builder.user_agent(ua.clone()).build().unwrap_or_default() - ; + let client_builder = reqwest::Client::builder().cookie_store(true); + let client = client_builder + .user_agent(ua.clone()) + .build() + .unwrap_or_default(); - //ck部分 + //ck部分 let (buvid3, buvid4, b_nut) = { - - let cookies_map = cookies.cookies_map.lock().unwrap(); - let existing_buvid3 = cookies_map.get("buvid3").cloned(); - let existing_buvid4 = cookies_map.get("buvid4").cloned(); - let existing_b_nut = cookies_map.get("b_nut").cloned(); - drop(cookies_map); - - - if existing_buvid3.is_some() && existing_buvid4.is_some() && existing_b_nut.is_some() { - - (existing_buvid3.unwrap(), existing_buvid4.unwrap(), existing_b_nut.unwrap()) - } else { - - gen_buvid3and4(client.clone()).await.unwrap_or_else(|err| { - - ("".to_string(), "".to_string(), "".to_string()) - }) - } + let cookies_map = cookies.cookies_map.lock().unwrap(); + let existing_buvid3 = cookies_map.get("buvid3").cloned(); + let existing_buvid4 = cookies_map.get("buvid4").cloned(); + let existing_b_nut = cookies_map.get("b_nut").cloned(); + drop(cookies_map); + + if existing_buvid3.is_some() + && existing_buvid4.is_some() + && existing_b_nut.is_some() + { + ( + existing_buvid3.unwrap(), + existing_buvid4.unwrap(), + existing_b_nut.unwrap(), + ) + } else { + gen_buvid3and4(client.clone()) + .await + .unwrap_or_else(|err| ("".to_string(), "".to_string(), "".to_string())) + } }; log::debug!("buvid3: {}, buvid4: {}, b_nut: {}", buvid3, buvid4, b_nut); let fp = { let cookies_map = cookies.cookies_map.lock().unwrap(); let existing_fp = cookies_map.get("buvid_fp").cloned(); drop(cookies_map); - + if let Some(fp_value) = existing_fp { - fp_value } else { let new_fp = gen_fp(); - + new_fp } }; - + log::debug!("fp: {}", fp); let _uuid = { let cookies_map = cookies.cookies_map.lock().unwrap(); let existing_uuid = cookies_map.get("_uuid").cloned(); drop(cookies_map); - + if let Some(uuid_value) = existing_uuid { log::debug!("使用现有 _uuid: {}", uuid_value); uuid_value @@ -173,7 +179,7 @@ impl CookieManager { let existing_ticket = cookies_map.get("bili_ticket").cloned(); let existing_expires = cookies_map.get("bili_ticket_expires").cloned(); drop(cookies_map); - + if existing_ticket.is_some() && existing_expires.is_some() { //验证过期时间 if let Some(expires_str) = &existing_expires { @@ -181,11 +187,15 @@ impl CookieManager { let current_time = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() - .as_secs() as i64; - + .as_secs() + as i64; + // 未过期,使用已有的 if current_time < expires_time { - log::debug!("使用现有 bili_ticket (有效期至: {})", expires_time); + log::debug!( + "使用现有 bili_ticket (有效期至: {})", + expires_time + ); (existing_ticket.unwrap(), existing_expires.unwrap()) // 删除了return关键字 } else { log::debug!("bili_ticket 已过期,重新生成"); @@ -216,7 +226,6 @@ impl CookieManager { }) } } else { - log::debug!("生成新的 bili_ticket"); gen_ckbili_ticket(client.clone()) .await @@ -230,13 +239,12 @@ impl CookieManager { let cookies_map = cookies.cookies_map.lock().unwrap(); let existing_msource = cookies_map.get("msource").cloned(); drop(cookies_map); - + if let Some(msource_value) = existing_msource { - msource_value } else { let new_msource = "bilibiliapp".to_string(); - + new_msource } }; @@ -244,7 +252,7 @@ impl CookieManager { let cookies_map = cookies.cookies_map.lock().unwrap(); let existing_01x96 = cookies_map.get("deviceFingerprint").cloned(); drop(cookies_map); - + if let Some(_01x25) = existing_01x96 { log::debug!("使用现有 01x96: {}", _01x25); _01x25 @@ -255,8 +263,8 @@ impl CookieManager { }; let _obf_key = unsafe { std::str::from_utf8_unchecked(&[ - 100, 101, 118, 105, 99, 101, 70, 105, 110, 103, - 101, 114, 112, 114, 105, 110, 116 + 100, 101, 118, 105, 99, 101, 70, 105, 110, 103, 101, 114, 112, 114, 105, + 110, 116, ]) }; cookies.insert("buvid3".to_string(), buvid3.clone()); @@ -265,13 +273,25 @@ impl CookieManager { cookies.insert("buvid_fp".to_string(), fp.clone()); cookies.insert("_uuid".to_string(), _uuid.clone()); cookies.insert("bili_ticket".to_string(), bili_ticket.clone()); - cookies.insert("bili_ticket_expires".to_string(), bili_ticket_expires.clone()); + cookies.insert( + "bili_ticket_expires".to_string(), + bili_ticket_expires.clone(), + ); cookies.insert("header_theme_version".to_string(), "CLOSE".to_string()); cookies.insert("enable_web_push".to_string(), "DISABLE".to_string()); cookies.insert("enable_feed_channel".to_string(), "ENABLE".to_string()); cookies.insert("msource".to_string(), msourse.clone()); cookies.insert(_obf_key.to_string(), _01x96.clone()); - log::debug!("buvid3: {}, buvid4: {}, b_nut: {}, fp: {}, _uuid: {}, bili_ticket: {}, bili_ticket_expires: {}", buvid3, buvid4, b_nut, fp, _uuid, bili_ticket, bili_ticket_expires); + log::debug!( + "buvid3: {}, buvid4: {}, b_nut: {}, fp: {}, _uuid: {}, bili_ticket: {}, bili_ticket_expires: {}", + buvid3, + buvid4, + b_nut, + fp, + _uuid, + bili_ticket, + bili_ticket_expires + ); let web_data = WebData { browser_version: browser_version, @@ -294,17 +314,17 @@ impl CookieManager { cookies: cookies, } } - - - + _ => { //默认浏览器 log::warn!("创建类型错误"); - Self{ - client: Arc::new(reqwest::Client::builder() - .cookie_store(true) - .build() - .unwrap_or_default()), + Self { + client: Arc::new( + reqwest::Client::builder() + .cookie_store(true) + .build() + .unwrap_or_default(), + ), create_type: 0, app_data: None, web_data: None, @@ -312,10 +332,8 @@ impl CookieManager { } } } - } - pub fn get_all_cookies(&self) -> String { let cookies_map = self.cookies.cookies_map.lock().unwrap(); let mut cookie_str = String::new(); @@ -324,7 +342,7 @@ impl CookieManager { } cookie_str } - //解析cookie字符串 + //解析cookie字符串 //TODO:(ck登录待去多余字符) fn parse_cookie_string(cookie_str: &str) -> CookiesData { let mut map = HashMap::new(); @@ -332,26 +350,29 @@ impl CookieManager { for cookie in cookie_str.split(';') { let cookie = cookie.trim(); if let Some(index) = cookie.find("=") { - let (key , value) = cookie.split_at(index); - if value.len() >1 { + let (key, value) = cookie.split_at(index); + if value.len() > 1 { map.insert(key.to_string(), value[1..].to_string()); let cookie = Cookie::build(key, value[1..].to_string()) .domain(".bilibili.com") .path("/") .finish(); - cookie_jar.lock().unwrap().add_cookie_str(&cookie.to_string() , &"https://bilibili.com".parse().unwrap()); + cookie_jar.lock().unwrap().add_cookie_str( + &cookie.to_string(), + &"https://bilibili.com".parse().unwrap(), + ); } } } - - CookiesData{ + + CookiesData { cookies_map: Arc::new(Mutex::new(map)), cookie_jar: cookie_jar, } } //现有client创建ck管理器 (已封进client的ck无法读取) - pub fn from_client(client: Arc, original_cookie : &str) -> Self { + pub fn from_client(client: Arc, original_cookie: &str) -> Self { let cookies = Self::parse_cookie_string(original_cookie); Self { client: client, @@ -363,23 +384,30 @@ impl CookieManager { } //更新单个字段 - pub fn update_cookie(&self, key:&str, value:&str){ - + pub fn update_cookie(&self, key: &str, value: &str) { self.cookies.insert(key.to_string(), value.to_string()); log::debug!("更新Cookie: {}={}", key, value); } //移除某个键对应的值 - pub fn remove_cookie(&self, key:&str) -> bool { - - let existed = self.cookies.cookies_map.lock().unwrap().remove(key).is_some(); + pub fn remove_cookie(&self, key: &str) -> bool { + let existed = self + .cookies + .cookies_map + .lock() + .unwrap() + .remove(key) + .is_some(); if existed { let expire_cookie = Cookie::build(key, "") .domain(".bilibili.com") .path("/") .max_age(cookie::time::Duration::seconds(-1)) .finish(); - self.cookies.cookie_jar.lock().unwrap().add_cookie_str(&expire_cookie.to_string(), &"https://bilibili.com".parse().unwrap()); + self.cookies.cookie_jar.lock().unwrap().add_cookie_str( + &expire_cookie.to_string(), + &"https://bilibili.com".parse().unwrap(), + ); log::debug!("删除Cookie: {}", key); } else { log::debug!("Cookie不存在: {}", key); @@ -390,8 +418,8 @@ impl CookieManager { //更新大量ck pub fn update_cookies(&self, cookies_str: &str) { let new_cookies = Self::parse_cookie_string(cookies_str); - - for(key,value) in new_cookies.cookies_map.lock().unwrap().iter() { + + for (key, value) in new_cookies.cookies_map.lock().unwrap().iter() { self.cookies.insert(key.clone(), value.clone()); } log::debug!("批量更新Cookie: {}", cookies_str); @@ -414,7 +442,7 @@ impl CookieManager { let builder = self.client.get(url); self.prepare_request(builder) } - + // 发送 POST 请求 pub async fn post(&self, url: &str) -> reqwest::RequestBuilder { let builder = self.client.post(url); @@ -422,40 +450,53 @@ impl CookieManager { } // 创建具有自定义标头的请求 - pub async fn get_with_headers(&self, url: &str, headers: HashMap<&str, &str>) -> reqwest::RequestBuilder { + pub async fn get_with_headers( + &self, + url: &str, + headers: HashMap<&str, &str>, + ) -> reqwest::RequestBuilder { let mut builder = self.get(url).await; for (key, value) in headers { builder = builder.header(key, value); } builder } - - pub async fn post_with_headers(&self, url: &str, headers: HashMap<&str, &str>) -> reqwest::RequestBuilder { + + pub async fn post_with_headers( + &self, + url: &str, + headers: HashMap<&str, &str>, + ) -> reqwest::RequestBuilder { let mut builder = self.post(url).await; for (key, value) in headers { builder = builder.header(key, value); } builder } - + // 临时覆盖 UA - pub async fn with_custom_ua(&self, builder: reqwest::RequestBuilder, ua: &str) -> reqwest::RequestBuilder { + pub async fn with_custom_ua( + &self, + builder: reqwest::RequestBuilder, + ua: &str, + ) -> reqwest::RequestBuilder { builder.header(reqwest::header::USER_AGENT, ua) } fn prepare_request(&self, mut builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { // 1. 先添加所有 cookie - 覆盖内部 cookie_store 的值 let cookies = self.cookies.cookies_map.lock().unwrap(); - + if !cookies.is_empty() { - let cookie_header = cookies.iter() + let cookie_header = cookies + .iter() .map(|(k, v)| format!("{}={}", k, v)) .collect::>() .join("; "); - + builder = builder.header(reqwest::header::COOKIE, cookie_header); } - + // 2. 根据创建类型添加其他头 let builder = match self.create_type { 0 => { @@ -468,39 +509,38 @@ impl CookieManager { } else { builder } - }, + } 1 => { // App 请求头 if let Some(app_data) = &self.app_data { - builder - .header("x-bili-aurora-zone", "sh") - // 其他 app 相关头 + builder.header("x-bili-aurora-zone", "sh") + // 其他 app 相关头 } else { builder } - }, - _ => builder + } + _ => builder, }; - + builder } - + //处理响应中的 cookie - pub async fn execute(&self, request: reqwest::RequestBuilder) -> Result { + pub async fn execute( + &self, + request: reqwest::RequestBuilder, + ) -> Result { let response = request.send().await?; - + // 从响应中提取并更新 cookies - let cookies = response.headers().get_all(reqwest::header::SET_COOKIE) ; - for cookie_header in cookies { - if let Ok(cookie_str) = cookie_header.to_str() { - log::debug!("从响应中获取到 cookie: {}", cookie_str); - self.update_cookies(cookie_str); - } + let cookies = response.headers().get_all(reqwest::header::SET_COOKIE); + for cookie_header in cookies { + if let Ok(cookie_str) = cookie_header.to_str() { + log::debug!("从响应中获取到 cookie: {}", cookie_str); + self.update_cookies(cookie_str); } - - + } + Ok(response) } } - - diff --git a/common/src/gen_cp.rs b/crates/common/src/gen_cp.rs similarity index 99% rename from common/src/gen_cp.rs rename to crates/common/src/gen_cp.rs index 14ab3ca..ece7585 100644 --- a/common/src/gen_cp.rs +++ b/crates/common/src/gen_cp.rs @@ -131,7 +131,7 @@ impl CTokenGenerator { pub fn generate_ctoken(&mut self, is_create_v2: bool) -> String { let mut rng = thread_rng(); - + self.touch_event = 255; // 触摸事件数: 手机端抓包数据 self.isibility_change = 2; // 可见性变化数: 手机端抓包数据 self.inner_width = 255; // 窗口内部宽度: 手机端抓包数据 @@ -161,4 +161,4 @@ impl CTokenGenerator { self.encode() } -} \ No newline at end of file +} diff --git a/common/src/http_utils.rs b/crates/common/src/http_utils.rs similarity index 80% rename from common/src/http_utils.rs rename to crates/common/src/http_utils.rs index 6658759..1710649 100644 --- a/common/src/http_utils.rs +++ b/crates/common/src/http_utils.rs @@ -1,8 +1,8 @@ -use reqwest::{Client, header, Response, Error}; use rand::seq::SliceRandom; use rand::thread_rng; -use std::collections::HashMap; +use reqwest::{Client, Error, Response, header}; use serde_json; +use std::collections::HashMap; // 随机UA生成 @@ -13,42 +13,40 @@ pub fn get_random_ua() -> String { "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/99.0.4844.51", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/605.1.15", "Mozilla/5.0 (Linux; Android 12; SM-S908B) Chrome/101.0.4951.41", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/102.0.0.0" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/102.0.0.0", ]; - + let mut rng = thread_rng(); ua_list.choose(&mut rng).unwrap_or(&ua_list[0]).to_string() } - -pub async fn request_get(client: &Client, url: &str, cookie: Option<&str>) -> Result { - - +pub async fn request_get( + client: &Client, + url: &str, + cookie: Option<&str>, +) -> Result { let mut req = client.get(url); - - + if let Some(cookie_str) = cookie { req = req.header(header::COOKIE, cookie_str); } - + req.send().await } pub async fn request_post( - client: &Client, - url: &str, - + client: &Client, + url: &str, + cookie: Option<&str>, - json_data: Option<&T> + json_data: Option<&T>, ) -> Result { - - let mut req = client.post(url); - + if let Some(cookie_str) = cookie { req = req.header(header::COOKIE, cookie_str); } - + if let Some(data) = json_data { if let Ok(json_value) = serde_json::to_value(data) { if let Some(obj) = json_value.as_object() { @@ -61,35 +59,43 @@ pub async fn request_post( } } } - + req.send().await } - -pub fn request_get_sync(client: &Client, url: &str, ua: Option, cookie: Option<&str>) -> Result { +pub fn request_get_sync( + client: &Client, + url: &str, + ua: Option, + cookie: Option<&str>, +) -> Result { let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(request_get(client, url, cookie)) - + rt.block_on(request_get(client, url, cookie)) } -pub fn request_post_sync(client: &Client, url: &str, ua: Option, cookie: Option<&str>, json_data: Option<&serde_json::Value>) -> Result { +pub fn request_post_sync( + client: &Client, + url: &str, + ua: Option, + cookie: Option<&str>, + json_data: Option<&serde_json::Value>, +) -> Result { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(request_post(client, url, cookie, json_data)) - } pub fn request_form_sync( - client: &Client, + client: &Client, url: &str, ua: Option, cookie: Option<&str>, - form_data: &HashMap + form_data: &HashMap, ) -> Result { let rt = tokio::runtime::Runtime::new().unwrap(); - + rt.block_on(async { let mut req = client.post(url); - + if let Some(cookie_str) = cookie { req = req.header(header::COOKIE, cookie_str); } @@ -97,26 +103,25 @@ pub fn request_form_sync( if let Some(ua_str) = ua { req = req.header(header::USER_AGENT, ua_str); } - + req = req.form(&form_data); req.send().await }) } - pub fn request_json_form_sync( - client: &Client, + client: &Client, url: &str, ua: Option, referer: Option, cookie: Option<&str>, - json_form: &serde_json::Map + json_form: &serde_json::Map, ) -> Result { let rt = tokio::runtime::Runtime::new().unwrap(); - + rt.block_on(async { let mut req = client.post(url); - + if let Some(cookie_str) = cookie { req = req.header(header::COOKIE, cookie_str); } @@ -124,7 +129,7 @@ pub fn request_json_form_sync( if let Some(ua_str) = ua { req = req.header(header::USER_AGENT, ua_str); } - + if let Some(referer_str) = referer { req = req.header(header::REFERER, referer_str); } @@ -135,23 +140,23 @@ pub fn request_json_form_sync( // 字符串类型 serde_json::Value::String(s) => { form.insert(key.clone(), s.clone()); - }, + } // 数字类型 - 直接转为字符串但不加引号 serde_json::Value::Number(n) => { form.insert(key.clone(), n.to_string()); - }, + } // 布尔类型 serde_json::Value::Bool(b) => { form.insert(key.clone(), b.to_string()); - }, + } // 其他类型 _ => { form.insert(key.clone(), value.to_string().trim_matches('"').to_string()); } } } - + req = req.form(&form); req.send().await }) -} \ No newline at end of file +} diff --git a/common/src/lib.rs b/crates/common/src/lib.rs similarity index 71% rename from common/src/lib.rs rename to crates/common/src/lib.rs index 93d49a2..6e5316b 100644 --- a/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -1,21 +1,22 @@ -pub mod taskmanager; -pub mod record_log; pub mod account; -pub mod utils; -pub mod push; -pub mod utility; -pub mod login; -pub mod http_utils; pub mod captcha; +pub mod http_utils; +pub mod login; +pub mod push; +pub mod record_log; pub mod show_orderlist; +pub mod taskmanager; pub mod ticket; +pub mod utility; +pub mod utils; pub mod cookie_manager; -pub mod web_ck_obfuscated; -pub mod machine_id; pub mod gen_cp; +pub mod machine_id; +pub mod web_ck_obfuscated; // 重导出日志收集器 -pub use record_log::LOG_COLLECTOR; pub use record_log::init as init_logger; +pub use record_log::{GRAB_LOG_COLLECTOR, LOG_COLLECTOR}; - +// 重导出任务管理器相关类型 +pub use taskmanager::{PushRequest, PushType}; diff --git a/crates/common/src/login.rs b/crates/common/src/login.rs new file mode 100644 index 0000000..1d5f93a --- /dev/null +++ b/crates/common/src/login.rs @@ -0,0 +1,220 @@ +use crate::account::Account; +use crate::account::add_account; +use crate::captcha::LocalCaptcha; +use crate::captcha::captcha; +use crate::http_utils::{request_get, request_get_sync, request_post}; +use crate::utility::CustomConfig; +use reqwest::Client; +use serde_json::json; + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct LoginInput { + pub phone: String, + pub account: String, + pub password: String, + pub sms_code: String, + pub cookie: String, +} + +pub struct QrCodeLoginTask { + pub qrcode_key: String, + pub qrcode_url: String, + pub start_time: std::time::Instant, + pub status: QrCodeLoginStatus, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum QrCodeLoginStatus { + Pending, + Scanning, + Confirming, + Success(String), //成功时返回cookie信息 + Failed(String), //失败时返回错误信息 + Expired, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum SendLoginSmsStatus { + Success(String), + Failed(String), +} + +pub fn qrcode_login(client: &Client) -> Result { + // 创建一个临时的运行时来执行异步代码 + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let response = request_get( + client, + "https://passport.bilibili.com/x/passport-login/web/qrcode/generate", + None, + ) + .await + .map_err(|e| e.to_string())?; + + let json = response + .json::() + .await + .map_err(|e| e.to_string())?; + + if let Some(qrcode_key) = json["data"]["qrcode_key"].as_str() { + Ok(qrcode_key.to_string()) + } else { + Err("无法获取二维码URL".to_string()) + } + }) +} +pub fn password_login(username: &str, password: &str) -> Result { + Err("暂不支持账号密码登录".to_string()) +} + +pub async fn send_loginsms( + phone: &str, + client: &Client, + custom_config: CustomConfig, + local_captcha: LocalCaptcha, +) -> Result { + let response = request_get(client, "https://www.bilibili.com/", None) + .await + .map_err(|e| e.to_string())?; + + log::debug!("{:?}", response.cookies().collect::>()); + + // 发送请求 + let response = request_get( + client, + "https://passport.bilibili.com/x/passport-login/captcha", + None, + ) + .await + .map_err(|e| e.to_string())?; + log::info!("获取验证码: {:?}", response); + + let json = response + .json::() + .await + .map_err(|e| e.to_string())?; + let gt = json["data"]["geetest"]["gt"].as_str().unwrap_or(""); + let challenge = json["data"]["geetest"]["challenge"].as_str().unwrap_or(""); + let token = json["data"]["token"].as_str().unwrap_or(""); + let referer = "https://passport.bilibili.com/x/passport-login/captcha"; + match captcha( + custom_config.clone(), + gt, + challenge, + referer, + 33, + local_captcha, + ) + .await + { + Ok(result_str) => { + log::info!("验证码识别成功: {}", result_str); + let result: serde_json::Value = + serde_json::from_str(&result_str).map_err(|e| e.to_string())?; + + let json_data = json!({ + "cid": 86, + "tel": phone.parse::().unwrap_or(0), + "token": token, + "source":"main_mini", + "challenge": result["challenge"], + "validate": result["validate"], + "seccode": result["seccode"], + }); + log::debug!("验证码数据: {:?}", json_data); + let send_sms = request_post( + client, + "https://passport.bilibili.com/x/passport-login/web/sms/send", + None, + Some(&json_data), + ) + .await + .map_err(|e| e.to_string())?; + + let json_response = send_sms + .json::() + .await + .map_err(|e| e.to_string())?; + log::debug!("验证码发送响应: {:?}", json_response); + if json_response["code"].as_i64() == Some(0) { + let captcha_key = json_response["data"]["captcha_key"].as_str().unwrap_or(""); + log::info!("验证码发送成功"); + log::debug!("captcha_key: {:?}", captcha_key); + Ok(captcha_key.to_string()) + } else { + log::error!( + "验证码发送失败: {}", + json_response["message"].as_str().unwrap_or("未知错误") + ); + Err("验证码发送失败".to_string()) + } + } + Err(e) => { + log::error!("验证码识别失败: {}", e); + Err("验证码识别失败".to_string()) + } + } +} + +pub async fn sms_login( + phone: &str, + sms_code: &str, + captcha_key: &str, + client: &Client, +) -> Result { + let data = serde_json::json!({ + "cid": 86, + "tel": phone.parse::().unwrap_or(0), + "code": sms_code.parse::().unwrap_or(0), + "source":"main_mini", + "captcha_key":captcha_key, + }); + log::debug!("短信登录数据: {:?}", data); + let login_response = request_post( + client, + "https://passport.bilibili.com/x/passport-login/web/login/sms", + None, + Some(&data), + ) + .await + .map_err(|e| e.to_string())?; + let mut all_cookies = Vec::new(); + let cookie_headers = login_response + .headers() + .get_all(reqwest::header::SET_COOKIE); + log::debug!("headers返回:{:?}", cookie_headers); + for value in cookie_headers { + if let Ok(cookie_str) = value.to_str() { + if let Some(end_pos) = cookie_str.find(';') { + all_cookies.push(cookie_str[0..end_pos].to_string()); + } else { + all_cookies.push(cookie_str.to_string()); + } + } + } + log::info!("获取cookie: {:?}", all_cookies); + let json_response = login_response + .json::() + .await + .map_err(|e| format!("解析JSON失败: {}", e))?; + log::debug!("登录接口响应:{:?}", json_response); + if json_response["code"].as_i64() == Some(0) { + log::info!("短信登录成功!"); + log::info!("登录cookie:{:?}", all_cookies); + return Ok(all_cookies.to_vec().join(";")); + } + Err("短信登录失败".to_string()) +} + +pub fn cookie_login(cookie: &str, client: &Client, ua: &str) -> Result { + match add_account(cookie, client, ua) { + Ok(account) => { + log::info!("ck登录成功"); + Ok(account) + } + Err(e) => { + log::error!("ck登录失败: {}", e); + Err(e) + } + } +} diff --git a/common/src/machine_id.rs b/crates/common/src/machine_id.rs similarity index 73% rename from common/src/machine_id.rs rename to crates/common/src/machine_id.rs index f36c0de..928e873 100644 --- a/common/src/machine_id.rs +++ b/crates/common/src/machine_id.rs @@ -1,22 +1,23 @@ -use sha2::{Sha256, Digest}; -use std::process::Command; -use std::fs::File; -use std::io:: Read; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use std::collections::BTreeMap; -use serde::{Serialize, Deserialize}; +use std::fs::File; +use std::io::Read; +use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; - fn _d(s: &[u8]) -> String { - s.iter().map(|&x| ((x as u16 ^ 0x37) as u8) as char).collect() + s.iter() + .map(|&x| ((x as u16 ^ 0x37) as u8) as char) + .collect() } - -const _X1: [u8; 15] = [54, 88, 91, 83, 66, 58, 90, 3, 73, 91, 87, 82, 3, 78, 83]; -const _X2: [u8; 14] = [65, 75, 80, 95, 93, 22, 61, 70, 70, 81, 66, 72, 84, 72]; -const _X3: [u8; 17] = [78, 89, 80, 86, 90, 93, 90, 84, 81, 19, 64, 91, 82, 83, 86, 84, 88]; -const _X4: [u8; 9] = [84, 86, 95, 81, 92, 67, 19, 84, 90]; - +const _X1: [u8; 15] = [54, 88, 91, 83, 66, 58, 90, 3, 73, 91, 87, 82, 3, 78, 83]; +const _X2: [u8; 14] = [65, 75, 80, 95, 93, 22, 61, 70, 70, 81, 66, 72, 84, 72]; +const _X3: [u8; 17] = [ + 78, 89, 80, 86, 90, 93, 90, 84, 81, 19, 64, 91, 82, 83, 86, 84, 88, +]; +const _X4: [u8; 9] = [84, 86, 95, 81, 92, 67, 19, 84, 90]; #[derive(Debug, Serialize, Deserialize)] struct _R4d10 { @@ -26,33 +27,35 @@ struct _R4d10 { _cpu_d47a: String, } - fn _rotate(s: &str, n: u8) -> String { - s.chars().map(|c| { - if c.is_ascii_alphabetic() { - let base = if c.is_ascii_uppercase() { b'A' } else { b'a' }; - ((((c as u8 - base) as u16 + n as u16) % 26) as u8 + base) as char - } else { - c - } - }).collect() + s.chars() + .map(|c| { + if c.is_ascii_alphabetic() { + let base = if c.is_ascii_uppercase() { b'A' } else { b'a' }; + ((((c as u8 - base) as u16 + n as u16) % 26) as u8 + base) as char + } else { + c + } + }) + .collect() } - fn _apply_transform(data: &[u8]) -> Vec { let mut hasher = Sha256::new(); hasher.update(data); hasher.finalize().to_vec() } - pub fn get_machine_id_ob() -> String { - - let _t0 = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos(); + let _t0 = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); let _r1 = (_t0 % 1000) as u32; - let _x = [0x7F, 0x45, 0x4C, 0x46, 0x01].iter().fold(0u32, |a, &b| a ^ (b as u32)); - - + let _x = [0x7F, 0x45, 0x4C, 0x46, 0x01] + .iter() + .fold(0u32, |a, &b| a ^ (b as u32)); + let _data_src = if _r1 % 17 == 0 { _get_machine_data_alt() } else if _r1 % 7 == 0 { @@ -60,8 +63,7 @@ pub fn get_machine_id_ob() -> String { } else { _get_machine_data() }; - - + let _hex_result = if _r1 % 13 == 0 { let _intermediate = _apply_transform(_data_src.as_bytes()); hex::encode(_intermediate) @@ -77,21 +79,15 @@ pub fn get_machine_id_ob() -> String { } format!("{:x}", hasher.finalize()) }; - - - _hex_result } - fn _get_machine_data() -> String { let _q1 = _fetch_attributes(); - - + let _q2 = _get_obfuscated_platform(); - - + let _q3 = { let _base = std::env::consts::ARCH; match _base { @@ -100,19 +96,16 @@ fn _get_machine_data() -> String { other => other, } }; - - + let _q4 = _extract_processor_signature(); - - + let _composed_data = _R4d10 { _attr_s3t: _q1.clone(), _os_n4me: _q2.clone(), _arch_7ype: _q3.to_string(), _cpu_d47a: _q4.clone(), }; - - + let _final_str = format!( "{{'hardware': {{'cpu': '{}', 'baseboard': '{}', 'disk': '{}'}}, 'platform': '{}', 'machine': '{}', 'processor': '{}'}}", _q1.get("cpu").unwrap_or(&"".to_string()), @@ -122,11 +115,10 @@ fn _get_machine_data() -> String { _q3, _q4 ); - + _final_str } - fn _get_machine_data_alt() -> String { let _hw = _fetch_attributes(); let _sys = std::env::consts::OS.to_uppercase(); @@ -134,50 +126,54 @@ fn _get_machine_data_alt() -> String { "WINDOWS" => "Windows", "LINUX" => "Linux", "MACOS" => "Darwin", - _ => _sys.as_str() + _ => _sys.as_str(), }; - + let _arc = match std::env::consts::ARCH { "x86_64" => "AMD64", other => other, }; - + let _prc = _extract_processor_signature(); - + format!( "{{'hardware': {{'cpu': '{}', 'baseboard': '{}', 'disk': '{}'}}, 'platform': '{}', 'machine': '{}', 'processor': '{}'}}", _hw.get("cpu").unwrap_or(&String::new()), _hw.get("baseboard").unwrap_or(&String::new()), _hw.get("disk").unwrap_or(&String::new()), - _plt, _arc, _prc + _plt, + _arc, + _prc ) } - fn _get_machine_data_dummy() -> (String, Vec) { let real_data = _get_machine_data(); - let dummy_bytes = real_data.as_bytes().iter() + let dummy_bytes = real_data + .as_bytes() + .iter() .enumerate() .map(|(i, &b)| b ^ ((i % 255) as u8)) .collect(); (real_data, dummy_bytes) } - fn _get_obfuscated_platform() -> String { let _raw_os = unsafe { std::mem::transmute::<&str, &[u8]>(std::env::consts::OS) }; - let _encoded = _raw_os.iter().enumerate() + let _encoded = _raw_os + .iter() + .enumerate() .map(|(i, &b)| b ^ (0x20 + (i % 7) as u8)) .collect::>(); - - - let _decoded = _encoded.iter().enumerate() + + let _decoded = _encoded + .iter() + .enumerate() .map(|(i, &b)| b ^ (0x20 + (i % 7) as u8)) .collect::>(); - + let _os_str = unsafe { std::str::from_utf8_unchecked(&_decoded) }; - - + match _os_str { "windows" => "Windows".to_string(), "linux" => "Linux".to_string(), @@ -186,53 +182,59 @@ fn _get_obfuscated_platform() -> String { } } - fn _extract_processor_signature() -> String { - let _path = rand::random::() % 3; - + if _path == 0 || _path == 1 || _path == 2 { if cfg!(target_os = "windows") { - let _cmd_parts = [ - (_d(&[54, 78, 91]), vec!["c", "m", "d"]), - (_d(&[28, 78]), vec!["/", "c"]), - (_d(&[56, 89, 81, 83, 78]), vec!["w", "m", "i", "c"]), - (_d(&[65, 80, 93, 87]), vec!["p", "a", "t", "h"]), - (_d(&[24, 83, 91, 16, 15, 35, 21, 49, 88, 82, 78, 84, 92, 92, 82, 87]), - vec!["W", "i", "n", "3", "2", "_", "P", "r", "o", "c", "e", "s", "s", "o", "r"]), - (_d(&[72, 84, 93]), vec!["g", "e", "t"]), - (_d(&[44, 80, 65, 93, 83, 82, 91, 15, 46, 80, 91, 94, 85, 80, 78, 93, 94, 87, 84, 87]), - vec!["C", "a", "p", "t", "i", "o", "n", ",", "M", "a", "n", "u", "f", "a", "c", "t", "u", "r", "e", "r"]) + (_d(&[54, 78, 91]), vec!["c", "m", "d"]), + (_d(&[28, 78]), vec!["/", "c"]), + (_d(&[56, 89, 81, 83, 78]), vec!["w", "m", "i", "c"]), + (_d(&[65, 80, 93, 87]), vec!["p", "a", "t", "h"]), + ( + _d(&[ + 24, 83, 91, 16, 15, 35, 21, 49, 88, 82, 78, 84, 92, 92, 82, 87, + ]), + vec![ + "W", "i", "n", "3", "2", "_", "P", "r", "o", "c", "e", "s", "s", "o", "r", + ], + ), + (_d(&[72, 84, 93]), vec!["g", "e", "t"]), + ( + _d(&[ + 44, 80, 65, 93, 83, 82, 91, 15, 46, 80, 91, 94, 85, 80, 78, 93, 94, 87, 84, + 87, + ]), + vec![ + "C", "a", "p", "t", "i", "o", "n", ",", "M", "a", "n", "u", "f", "a", "c", + "t", "u", "r", "e", "r", + ], + ), ]; - - - + let _arg1 = _cmd_parts[0].1.join(""); let _arg2 = _cmd_parts[1].1.join(""); - let _cmd_str = format!("{} {} {} {} {}", - _cmd_parts[2].1.join(""), - _cmd_parts[3].1.join(""), - _cmd_parts[4].1.join(""), - _cmd_parts[5].1.join(""), - _cmd_parts[6].1.join("")); - - if let Ok(output) = Command::new(&_arg1) - .arg(&_arg2) - .arg(&_cmd_str) - .output() - { + let _cmd_str = format!( + "{} {} {} {} {}", + _cmd_parts[2].1.join(""), + _cmd_parts[3].1.join(""), + _cmd_parts[4].1.join(""), + _cmd_parts[5].1.join(""), + _cmd_parts[6].1.join("") + ); + + if let Ok(output) = Command::new(&_arg1).arg(&_arg2).arg(&_cmd_str).output() { let stdout = String::from_utf8_lossy(&output.stdout); log::debug!("Processor info: {}", stdout); - + let lines: Vec<&str> = stdout.lines().collect(); if lines.len() > 1 { let processor_info = lines[1].trim(); if !processor_info.is_empty() { - if let Some(idx) = processor_info.find(" ") { let mut result = processor_info.to_string(); - result.replace_range(idx..idx+2, ", "); + result.replace_range(idx..idx + 2, ", "); log::debug!("Final processor info: {}", result); return result; } @@ -241,21 +243,15 @@ fn _extract_processor_signature() -> String { } } } else if cfg!(target_os = "linux") { - - let _file_paths = [ - "/proc/cpuinfo", - "/proc/cpuinfo.bak", - "/etc/proc/cpu.info" - ]; - + let _file_paths = ["/proc/cpuinfo", "/proc/cpuinfo.bak", "/etc/proc/cpu.info"]; + for &path in _file_paths.iter().filter(|&&p| p == "/proc/cpuinfo") { if let Ok(mut file) = File::open(path) { let mut content = String::new(); if file.read_to_string(&mut content).is_ok() { - let _size = content.len(); let _is_valid = _size > 0 && (_size % 2 == 0 || _size % 2 == 1); - + if _is_valid || true { return content.trim().to_string(); } @@ -263,31 +259,25 @@ fn _extract_processor_signature() -> String { } } } else if cfg!(target_os = "macos") { - let _cmds = [ (vec!["sysctl", "-n", "machdep.cpu.brand_string"], true), (vec!["system_profiler", "SPHardwareDataType"], false), - (vec!["uname", "-a"], false) + (vec!["uname", "-a"], false), ]; - + for (cmd, use_it) in _cmds.iter().filter(|&&(_, use_it)| use_it) { - if let Ok(output) = Command::new(&cmd[0]) - .args(&cmd[1..]) - .output() - { + if let Ok(output) = Command::new(&cmd[0]).args(&cmd[1..]).output() { let stdout = String::from_utf8_lossy(&output.stdout); return stdout.trim().to_string(); } } } } - - "".to_string() -} + "".to_string() +} fn _fetch_attributes() -> BTreeMap { - fn _transform_cmd_output(cmd: &str, args: &[&str], filter_idx: usize) -> Option { if let Ok(output) = Command::new(cmd).args(args).output() { let stdout = String::from_utf8_lossy(&output.stdout); @@ -303,34 +293,28 @@ fn _fetch_attributes() -> BTreeMap { } let mut _metadata = BTreeMap::new(); - - + if cfg!(target_os = "windows") { - if let Some(cpu_id) = _transform_cmd_output("wmic", &["cpu", "get", "ProcessorId"], 1) { _metadata.insert("cpu".to_string(), cpu_id); } - - - if let Some(board_serial) = _transform_cmd_output("wmic", &["baseboard", "get", "serialnumber"], 1) { + + if let Some(board_serial) = + _transform_cmd_output("wmic", &["baseboard", "get", "serialnumber"], 1) + { if !board_serial.to_lowercase().contains("default string") { _metadata.insert("baseboard".to_string(), board_serial); } } - - - if let Some(disk_serial) = _transform_cmd_output("wmic", &["diskdrive", "get", "serialnumber"], 1) { + + if let Some(disk_serial) = + _transform_cmd_output("wmic", &["diskdrive", "get", "serialnumber"], 1) + { _metadata.insert("disk".to_string(), disk_serial); } } else if cfg!(target_os = "linux") { - - - - let _cpu_paths = [ - "/proc/cpuinfo", - "/dev/null" - ]; - + let _cpu_paths = ["/proc/cpuinfo", "/dev/null"]; + for &path in _cpu_paths.iter().filter(|&&p| p.contains("cpu")) { if let Ok(mut file) = File::open(path) { let mut content = String::new(); @@ -346,13 +330,9 @@ fn _fetch_attributes() -> BTreeMap { } } } - - - let _board_paths = [ - "/sys/class/dmi/id/board_serial", - "/tmp/fake_board" - ]; - + + let _board_paths = ["/sys/class/dmi/id/board_serial", "/tmp/fake_board"]; + for &path in _board_paths.iter().filter(|&&p| p.contains("board")) { if let Ok(mut file) = File::open(path) { let mut content = String::new(); @@ -365,44 +345,45 @@ fn _fetch_attributes() -> BTreeMap { } } } - - + if let Ok(output) = Command::new("df").args(&["--output=source", "/"]).output() { let stdout = String::from_utf8_lossy(&output.stdout); - - + let mut found = false; for (i, line) in stdout.lines().enumerate() { if i > 0 && !found { let dev_path = line.trim(); if dev_path.starts_with("/dev/") { let disk_id = &dev_path[5..]; - - + let is_nvme = disk_id.starts_with("nvme"); let is_sd = disk_id.starts_with("sd"); let is_hd = disk_id.starts_with("hd"); - - + if is_nvme || is_sd || is_hd { let cmd_str = if is_nvme { - format!("udevadm info --query=property --name=/dev/{} | grep ID_SERIAL_SHORT", disk_id) + format!( + "udevadm info --query=property --name=/dev/{} | grep ID_SERIAL_SHORT", + disk_id + ) } else { format!("hdparm -i /dev/{} | grep SerialNo", disk_id) }; - - if let Ok(cmd_out) = Command::new("sh").arg("-c").arg(&cmd_str).output() { + + if let Ok(cmd_out) = Command::new("sh").arg("-c").arg(&cmd_str).output() + { let cmd_stdout = String::from_utf8_lossy(&cmd_out.stdout); - - + let serial = if is_nvme { cmd_stdout.split('=').nth(1).map(|s| s.trim().to_string()) } else { - cmd_stdout.split('=').nth(1).and_then(|s| - s.split_whitespace().next().map(|s| s.trim_matches('"').to_string()) - ) + cmd_stdout.split('=').nth(1).and_then(|s| { + s.split_whitespace() + .next() + .map(|s| s.trim_matches('"').to_string()) + }) }; - + if let Some(s) = serial { _metadata.insert("disk".to_string(), s); found = true; @@ -415,12 +396,11 @@ fn _fetch_attributes() -> BTreeMap { } } } else if cfg!(target_os = "macos") { - let _cpu_cmds = [ (vec!["sysctl", "-n", "machdep.cpu.brand_string"], true), - (vec!["system_profiler", "SPHardwareDataType"], false) + (vec!["system_profiler", "SPHardwareDataType"], false), ]; - + for (cmd, use_it) in _cpu_cmds.iter() { if *use_it { if let Ok(output) = Command::new(&cmd[0]).args(&cmd[1..]).output() { @@ -433,9 +413,7 @@ fn _fetch_attributes() -> BTreeMap { } } } - - } - + _metadata -} \ No newline at end of file +} diff --git a/common/src/push.rs b/crates/common/src/push.rs similarity index 57% rename from common/src/push.rs rename to crates/common/src/push.rs index cbdd751..38ab88d 100644 --- a/common/src/push.rs +++ b/crates/common/src/push.rs @@ -1,13 +1,12 @@ -use std::default; - -use serde::{Serialize, Deserialize}; -use crate::taskmanager::{TaskManager, PushRequest, PushType, TaskRequest}; +use crate::taskmanager::{PushRequest, PushType, TaskManager, TaskRequest}; use reqwest::Client; +use serde::{Deserialize, Serialize}; //推送token #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct PushConfig{ +pub struct PushConfig { pub enabled: bool, + pub enabled_methods: Vec, pub bark_token: String, pub pushplus_token: String, pub fangtang_token: String, @@ -15,39 +14,46 @@ pub struct PushConfig{ pub wechat_token: String, pub gotify_config: GotifyConfig, pub smtp_config: SmtpConfig, - } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct GotifyConfig{ +pub struct GotifyConfig { pub gotify_url: String, pub gotify_token: String, } impl GotifyConfig { - pub fn new() -> Self{ - Self { + pub fn new() -> Self { + Self { gotify_url: String::new(), gotify_token: String::new(), - } - + } } } //邮箱配置(属于pushconfig) #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SmtpConfig{ +pub struct SmtpConfig { pub smtp_server: String, pub smtp_port: String, pub smtp_username: String, pub smtp_password: String, pub smtp_from: String, pub smtp_to: String, - } +} -impl PushConfig{ - pub fn new() -> Self{ - Self{ +impl PushConfig { + pub fn new() -> Self { + Self { enabled: false, + enabled_methods: vec![ + "bark".to_string(), + "pushplus".to_string(), + "fangtang".to_string(), + "dingtalk".to_string(), + "wechat".to_string(), + "smtp".to_string(), + "gotify".to_string(), + ], bark_token: String::new(), pushplus_token: String::new(), fangtang_token: String::new(), @@ -58,122 +64,172 @@ impl PushConfig{ } } - pub fn push_all(&self,title:&str,message:&str,jump_url:&Option, task_manager: &mut dyn TaskManager){ - if !self.enabled{ + pub fn push_all( + &self, + title: &str, + message: &str, + jump_url: &Option, + task_manager: &mut dyn TaskManager, + ) { + if !self.enabled { return; } - let push_request =TaskRequest::PushRequest( PushRequest{ + let push_request = TaskRequest::PushRequest(PushRequest { title: title.to_string(), message: message.to_string(), jump_url: jump_url.clone(), push_config: self.clone(), push_type: PushType::All, }); - match task_manager.submit_task(push_request){ + match task_manager.submit_task(push_request) { Ok(task_id) => { log::debug!("提交全渠道推送任务成功,任务ID: {}", task_id); - }, + } Err(e) => { log::error!("提交推送任务失败: {}", e); } } - - } - pub async fn push_all_async(&self, title:&str, message: &str, jump_url:&Option) -> (bool,String){ + pub async fn push_all_async( + &self, + title: &str, + message: &str, + jump_url: &Option, + ) -> (bool, String) { let mut success_count = 0; let mut failure_count = 0; let mut failures = Vec::new(); - if !self.bark_token.is_empty(){ + // 检查是否启用了Bark推送 + if self.enabled_methods.contains(&"bark".to_string()) && !self.bark_token.is_empty() { let (success, msg) = self.push_bark(title, message).await; - if success{ + if success { success_count += 1; - }else{ + } else { failure_count += 1; failures.push(format!("Bark推送出错: {}", msg)); } } - if !self.pushplus_token.is_empty(){ + // 检查是否启用了PushPlus推送 + if self.enabled_methods.contains(&"pushplus".to_string()) && !self.pushplus_token.is_empty() + { let (success, msg) = self.push_pushplus(title, message).await; - if success{ + if success { success_count += 1; - }else{ + } else { failure_count += 1; failures.push(format!("PushPlus推送出错: {}", msg)); } } - if !self.fangtang_token.is_empty(){ + + // 检查是否启用了方糖推送 + if self.enabled_methods.contains(&"fangtang".to_string()) && !self.fangtang_token.is_empty() + { let (success, msg) = self.push_fangtang(title, message).await; - if success{ + if success { success_count += 1; - }else{ + } else { failure_count += 1; failures.push(format!("Fangtang推送出错: {}", msg)); } } - if !self.dingtalk_token.is_empty(){ + + // 检查是否启用了钉钉推送 + if self.enabled_methods.contains(&"dingtalk".to_string()) && !self.dingtalk_token.is_empty() + { let (success, msg) = self.push_dingtalk(title, message).await; - if success{ + if success { success_count += 1; - }else{ + } else { failure_count += 1; failures.push(format!("Dingtalk推送出错: {}", msg)); } } - if !self.wechat_token.is_empty(){ + + // 检查是否启用了微信推送 + if self.enabled_methods.contains(&"wechat".to_string()) && !self.wechat_token.is_empty() { let (success, msg) = self.push_wechat(title, message).await; - if success{ + if success { success_count += 1; - }else{ + } else { failure_count += 1; failures.push(format!("WeChat推送出错: {}", msg)); } } - if !self.smtp_config.smtp_server.is_empty(){ + + // 检查是否启用了SMTP推送 + if self.enabled_methods.contains(&"smtp".to_string()) + && !self.smtp_config.smtp_server.is_empty() + { let (success, msg) = self.push_smtp(title, message).await; - if success{ + if success { success_count += 1; - }else{ + } else { failure_count += 1; failures.push(format!("SMTP推送出错: {}", msg)); } } - if !self.gotify_config.gotify_token.is_empty(){ - let (success,msg) = self.push_gotify(title, message, jump_url).await; - if success{ + + // 检查是否启用了Gotify推送 + if self.enabled_methods.contains(&"gotify".to_string()) + && !self.gotify_config.gotify_token.is_empty() + { + let (success, msg) = self.push_gotify(title, message, jump_url).await; + if success { success_count += 1; - - }else{ + } else { failure_count += 1; failures.push(format!("Gotify推送出错: {}", msg)); } } - if success_count == 0{ - return (false, format!("{} 成功 / {} 失败。失败详情: {}", - success_count, failure_count, failures.join("; "))) - }else{ - return (true, format!("全部 {} 个渠道推送成功", success_count)) + + if success_count == 0 { + return ( + false, + format!( + "{} 成功 / {} 失败。失败详情: {}", + success_count, + failure_count, + failures.join("; ") + ), + ); + } else { + return (true, format!("{} 个渠道推送成功", success_count)); + } } -} - pub async fn push_gotify(&self, title:&str, message: &str, jump_url:&Option) -> (bool, String){ + pub async fn push_gotify( + &self, + title: &str, + message: &str, + jump_url: &Option, + ) -> (bool, String) { let mut default_headers = reqwest::header::HeaderMap::new(); let jump_url_real = match jump_url { Some(url) => url, None => &"bilibili://mall/web?url=https://www.bilibili.com".to_string(), }; - let push_target_url = if self.gotify_config.clone().gotify_url.contains("http"){ + let push_target_url = if self.gotify_config.clone().gotify_url.contains("http") { self.gotify_config.clone().gotify_url - }else{ + } else { format!("http://{}", self.gotify_config.clone().gotify_url) }; - default_headers.insert("Content-Type", reqwest::header::HeaderValue::from_static("application/json")); - default_headers.insert("Authorization", reqwest::header::HeaderValue::from_str(&format!("Bearer {}", self.gotify_config.gotify_token)).unwrap()); + default_headers.insert( + "Content-Type", + reqwest::header::HeaderValue::from_static("application/json"), + ); + default_headers.insert( + "Authorization", + reqwest::header::HeaderValue::from_str(&format!( + "Bearer {}", + self.gotify_config.gotify_token + )) + .unwrap(), + ); let client_builder = Client::builder() .default_headers(default_headers) - .timeout(std::time::Duration::from_secs(20)); + .timeout(std::time::Duration::from_secs(20)); let data = serde_json::json!({ "message": message, "title": title, @@ -187,61 +243,50 @@ impl PushConfig{ } } }); - let client = match client_builder.build(){ + let client = match client_builder.build() { Ok(client) => client, Err(e) => return (false, format!("创建HTTP客户端失败: {}", e)), }; - let url = format!("{}/message",push_target_url); + let url = format!("{}/message", push_target_url); - match client.post(&url) - .json(&data) - .send() - .await { - Ok(resp) => { - let status = resp.status(); - match resp.text().await { - Ok(text) => { - log::debug!("Gotify 推送响应: 状态码 {}, 内容: {}", status, text); - if status.is_success() { - (true, "推送成功".to_string()) - } else { - (false, format!("推送失败,状态码: {}", status)) - } - }, - Err(e) => (false, format!("读取响应失败: {}", e)) + match client.post(&url).json(&data).send().await { + Ok(resp) => { + let status = resp.status(); + match resp.text().await { + Ok(text) => { + log::debug!("Gotify 推送响应: 状态码 {}, 内容: {}", status, text); + if status.is_success() { + (true, "推送成功".to_string()) + } else { + (false, format!("推送失败,状态码: {}", status)) + } } - }, - Err(e) => { - (false, format!("推送失败: {}", e)) + Err(e) => (false, format!("读取响应失败: {}", e)), } - } - - + Err(e) => (false, format!("推送失败: {}", e)), + } } - pub async fn push_bark(&self, title:&str ,message: &str) -> (bool, String){ + pub async fn push_bark(&self, title: &str, message: &str) -> (bool, String) { let client = Client::new(); let data = serde_json::json!({ "title":title, "body":message, "level":"timeSensitive", - /* #推送中断级别。 + /* #推送中断级别。 #active:默认值,系统会立即亮屏显示通知 #timeSensitive:时效性通知,可在专注状态下显示通知。 - #passive:仅将通知添加到通知列表,不会亮屏提醒。 */ + #passive:仅将通知添加到通知列表,不会亮屏提醒。 */ "badge":1, "icon":"https://sr.mihoyo.com/favicon-mi.ico", - "group":"biliticket", + "group":"biliticket", "isArchive":1, }); let url = format!("https://api.day.app/{}/", self.bark_token); - match client.post(&url) - .json(&data) - .send() - .await{ - Ok(resp) => { - let status = resp.status(); + match client.post(&url).json(&data).send().await { + Ok(resp) => { + let status = resp.status(); match resp.text().await { Ok(text) => { log::debug!("Bark 推送响应: 状态码 {}, 内容: {}", status, text); @@ -250,17 +295,15 @@ impl PushConfig{ } else { (false, format!("推送失败,状态码: {}", status)) } - }, - Err(e) => (false, format!("读取响应失败: {}", e)) - } - }, - Err(e) => { - (false, format!("推送失败: {}", e)) + } + Err(e) => (false, format!("读取响应失败: {}", e)), } + } + Err(e) => (false, format!("推送失败: {}", e)), } } - pub async fn push_pushplus(&self, title:&str, message: &str) -> (bool, String){ + pub async fn push_pushplus(&self, title: &str, message: &str) -> (bool, String) { let client = Client::new(); let url = "http://www.pushplus.plus/send"; let data = serde_json::json!({ @@ -268,12 +311,9 @@ impl PushConfig{ "title":title, "content":message, }); - match client.post(url) - .json(&data) - .send() - .await{ - Ok(resp) => { - let status = resp.status(); + match client.post(url).json(&data).send().await { + Ok(resp) => { + let status = resp.status(); match resp.text().await { Ok(text) => { log::debug!("PushPlus 推送响应: 状态码 {}, 内容: {}", status, text); @@ -282,30 +322,25 @@ impl PushConfig{ } else { (false, format!("推送失败,状态码: {}", status)) } - }, - Err(e) => (false, format!("读取响应失败: {}", e)) - } - }, - Err(e) => { - (false, format!("推送失败: {}", e)) + } + Err(e) => (false, format!("读取响应失败: {}", e)), } + } + Err(e) => (false, format!("推送失败: {}", e)), } } - pub async fn push_fangtang(&self, title:&str, message: &str) -> (bool, String){ + pub async fn push_fangtang(&self, title: &str, message: &str) -> (bool, String) { let client = Client::new(); - let url = format!("https://sctapi.ftqq.com/{}.send",self.fangtang_token); + let url = format!("https://sctapi.ftqq.com/{}.send", self.fangtang_token); let data = serde_json::json!({ "title":title, "desp":message, "noip":1 }); - match client.post(url) - .json(&data) - .send() - .await{ - Ok(resp) => { - let status = resp.status(); + match client.post(url).json(&data).send().await { + Ok(resp) => { + let status = resp.status(); match resp.text().await { Ok(text) => { log::debug!("Fangtang 推送响应: 状态码 {}, 内容: {}", status, text); @@ -314,33 +349,36 @@ impl PushConfig{ } else { (false, format!("推送失败,状态码: {}", status)) } - }, - Err(e) => (false, format!("读取响应失败: {}", e)) - } - }, - Err(e) => { - (false, format!("推送失败: {}", e)) + } + Err(e) => (false, format!("读取响应失败: {}", e)), } + } + Err(e) => (false, format!("推送失败: {}", e)), } } - pub async fn push_dingtalk(&self, title:&str, message: &str) -> (bool, String){ + pub async fn push_dingtalk(&self, title: &str, message: &str) -> (bool, String) { let client = Client::new(); - let url = format!("https://oapi.dingtalk.com/robot/send?access_token={}",self.dingtalk_token); + let url = format!( + "https://oapi.dingtalk.com/robot/send?access_token={}", + self.dingtalk_token + ); let data = serde_json::json!({ "msgtype":"text", "text":{ "content":format!("{} \n {}", title, message) } }); - match client.post(url) + match client + .post(url) .json(&data) .header("Content-Type", "application/json") .header("Charset", "UTF-8") .send() - .await{ - Ok(resp) => { - let status = resp.status(); + .await + { + Ok(resp) => { + let status = resp.status(); match resp.text().await { Ok(text) => { log::debug!("钉钉推送响应: 状态码 {}, 内容: {}", status, text); @@ -349,33 +387,36 @@ impl PushConfig{ } else { (false, format!("推送失败,状态码: {}", status)) } - }, - Err(e) => (false, format!("读取响应失败: {}", e)) - } - }, - Err(e) => { - (false, format!("推送失败: {}", e)) + } + Err(e) => (false, format!("读取响应失败: {}", e)), } + } + Err(e) => (false, format!("推送失败: {}", e)), } } - pub async fn push_wechat(&self, title:&str, message: &str) -> (bool, String){ + pub async fn push_wechat(&self, title: &str, message: &str) -> (bool, String) { let client = Client::new(); - let url = format!("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={}",self.wechat_token); + let url = format!( + "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={}", + self.wechat_token + ); let data = serde_json::json!({ "msgtype":"text", "text":{ "content":format!("{} \n {}", title, message) } }); - match client.post(url) + match client + .post(url) .json(&data) .header("Content-Type", "application/json") .header("Charset", "UTF-8") .send() - .await{ - Ok(resp) => { - let status = resp.status(); + .await + { + Ok(resp) => { + let status = resp.status(); match resp.text().await { Ok(text) => { log::debug!("微信推送响应: 状态码 {}, 内容: {}", status, text); @@ -384,27 +425,22 @@ impl PushConfig{ } else { (false, format!("推送失败,状态码: {}", status)) } - }, - Err(e) => (false, format!("读取响应失败: {}", e)) - } - }, - Err(e) => { - (false, format!("推送失败: {}", e)) + } + Err(e) => (false, format!("读取响应失败: {}", e)), } + } + Err(e) => (false, format!("推送失败: {}", e)), } } - pub async fn push_smtp(&self, title: &str, message: &str) -> (bool, String){ - return (false,"SMTP推送功能未实现".to_string()) + pub async fn push_smtp(&self, title: &str, message: &str) -> (bool, String) { + return (false, "SMTP推送功能未实现".to_string()); } - - - } -impl SmtpConfig{ - pub fn new() -> Self{ - Self{ +impl SmtpConfig { + pub fn new() -> Self { + Self { smtp_server: String::new(), smtp_port: String::new(), smtp_username: String::new(), @@ -413,5 +449,4 @@ impl SmtpConfig{ smtp_to: String::new(), } } - -} \ No newline at end of file +} diff --git a/common/src/record_log.rs b/crates/common/src/record_log.rs similarity index 51% rename from common/src/record_log.rs rename to crates/common/src/record_log.rs index 2bda8a4..cc017ef 100644 --- a/common/src/record_log.rs +++ b/crates/common/src/record_log.rs @@ -1,10 +1,9 @@ -use std::sync::{Arc, Mutex}; -use log::{Record, Level, Metadata, LevelFilter, SetLoggerError}; +use log::{Level, LevelFilter, Metadata, Record, SetLoggerError}; use once_cell::sync::Lazy; use std::fs::{self, File, OpenOptions}; use std::io::Write; use std::path::Path; - +use std::sync::{Arc, Mutex}; // 日志文件处理相关内容 lazy_static::lazy_static! { @@ -19,17 +18,17 @@ fn create_log_file() -> Option<(String, File)> { eprintln!("无法创建日志目录: {}", e); return None; } - + // 创建带有时间戳的文件名 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S"); let filename = format!("Log/log_{}.log", timestamp); - + // 打开文件 match OpenOptions::new() .write(true) .create(true) .append(true) - .open(&filename) + .open(&filename) { Ok(file) => Some((filename.clone(), file)), Err(e) => { @@ -41,22 +40,22 @@ fn create_log_file() -> Option<(String, File)> { fn write_to_log_file(message: &str) -> bool { let mut file_guard = LOG_FILE.lock().unwrap(); - + // 检查是否需要创建新的日志文件 let create_new_file = match &*file_guard { Some((filename, _)) => { let current_date = chrono::Local::now().format("%Y%m%d").to_string(); !filename.contains(¤t_date) - }, - None => true + } + None => true, }; - + if create_new_file { if let Some(new_file) = create_log_file() { *file_guard = Some(new_file); } } - + // 写入日志 if let Some((_, file)) = file_guard.as_mut() { if let Err(_) = writeln!(file, "{}", message) { @@ -67,63 +66,125 @@ fn write_to_log_file(message: &str) -> bool { } return true; } - + false } -//日志记录器 -pub struct LogCollector{ +// 系统日志记录器 +pub struct LogCollector { pub logs: Vec, } -impl LogCollector{ - pub fn new() -> Self{ +impl LogCollector { + pub fn new() -> Self { Self { logs: Vec::new() } } //添加日志 - pub fn add(&mut self, message: String){ + pub fn add(&mut self, message: String) { self.logs.push(message); } //获取日志 - pub fn get_logs(&mut self) -> Option>{ - if self.logs.is_empty(){ + pub fn get_logs(&mut self) -> Option> { + if self.logs.is_empty() { return None; } let logs = self.logs.clone(); - + self.clear_logs(); Some(logs) } //清空日志 - pub fn clear_logs(&mut self){ + pub fn clear_logs(&mut self) { self.logs.clear(); } } -pub static LOG_COLLECTOR: Lazy>> = //? +// 抢票日志记录器 +pub struct GrabLogCollector { + pub logs: Vec, +} + +impl GrabLogCollector { + pub fn new() -> Self { + Self { logs: Vec::new() } + } + + //添加抢票日志 + pub fn add(&mut self, message: String) { + self.logs.push(message); + } + + //获取抢票日志 + pub fn get_logs(&mut self) -> Option> { + if self.logs.is_empty() { + return None; + } + let logs = self.logs.clone(); + + self.clear_logs(); + Some(logs) + } + + //清空抢票日志 + pub fn clear_logs(&mut self) { + self.logs.clear(); + } +} + +pub static LOG_COLLECTOR: Lazy>> = //? Lazy::new(|| Arc::new(Mutex::new(LogCollector::new()))); +pub static GRAB_LOG_COLLECTOR: Lazy>> = //? + Lazy::new(|| Arc::new(Mutex::new(GrabLogCollector::new()))); struct CollectorLogger; -impl log::Log for CollectorLogger{ - fn enabled(&self, metadata: &Metadata) -> bool{ +impl log::Log for CollectorLogger { + fn enabled(&self, metadata: &Metadata) -> bool { metadata.level() <= Level::Debug } - - fn log(&self,record: &Record){ - if self.enabled(record.metadata()){ + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S:%3f"); - let log_message = format!("[{}] {}: {}", - timestamp, record.level(), record.args()); + let log_message = format!("[{}] {}: {}", timestamp, record.level(), record.args()); - { - if let Ok(mut collector) = LOG_COLLECTOR.try_lock() { // 使用 try_lock 避免长时间等待 - collector.add(log_message.clone()); - } + { + if let Ok(mut collector) = LOG_COLLECTOR.try_lock() { + // 使用 try_lock 避免长时间等待 + collector.add(log_message.clone()); } - + } + + // 检查是否为抢票相关日志 + let args_str = record.args().to_string(); + if args_str.contains("抢票") + || args_str.contains("token") + || args_str.contains("订单") + || args_str.contains("验证码") + || args_str.contains("倒计时") + || args_str.contains("项目") + || args_str.contains("场次") + || args_str.contains("购票人") + || args_str.contains("开始抢票") + || args_str.contains("获取token") + || args_str.contains("确认订单") + || args_str.contains("下单") + || args_str.contains("重试") + || args_str.contains("失败") + || args_str.contains("成功") + || args_str.contains("距离抢票时间") + || args_str.contains("获取购票人信息") + || args_str.contains("获取项目详情") + || args_str.contains("二维码") + || args_str.contains("短信") + || args_str.contains("登录") + { + if let Ok(mut grab_collector) = GRAB_LOG_COLLECTOR.try_lock() { + grab_collector.add(log_message.clone()); + } + } println!("{}", log_message); @@ -139,7 +200,6 @@ impl log::Log for CollectorLogger{ let _ = file.flush(); } } - } // 静态日志记录器 @@ -147,13 +207,12 @@ static LOGGER: CollectorLogger = CollectorLogger; // 初始化日志系统 pub fn init() -> Result<(), SetLoggerError> { - if cfg!(debug_assertions) { println!("调试模式启动"); } else { println!("正式版"); } - + // 根据构建模式设置不同的日志级别 log::set_logger(&LOGGER).map(|()| { if cfg!(debug_assertions) { @@ -162,4 +221,4 @@ pub fn init() -> Result<(), SetLoggerError> { log::set_max_level(LevelFilter::Info) } }) -} \ No newline at end of file +} diff --git a/common/src/show_orderlist.rs b/crates/common/src/show_orderlist.rs similarity index 91% rename from common/src/show_orderlist.rs rename to crates/common/src/show_orderlist.rs index 46cbab7..8bb93e2 100644 --- a/common/src/show_orderlist.rs +++ b/crates/common/src/show_orderlist.rs @@ -1,4 +1,3 @@ - use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct OrderResponse { @@ -9,13 +8,13 @@ pub struct OrderResponse { } #[derive(Debug, Deserialize, Serialize, Clone)] -pub struct OrderData{ +pub struct OrderData { pub total: i32, pub list: Vec, } #[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Order{ +pub struct Order { pub order_id: String, pub order_type: i32, pub item_id: i64, @@ -32,7 +31,7 @@ pub struct Order{ } #[derive(Debug, Deserialize, Serialize, Clone)] -pub struct ItemInfo{ +pub struct ItemInfo { pub name: String, pub image: Option, pub screen_id: String, @@ -42,7 +41,6 @@ pub struct ItemInfo{ } #[derive(Debug, Deserialize, Serialize, Clone)] -pub struct ImageInfo{ +pub struct ImageInfo { pub url: String, - } diff --git a/common/src/taskmanager.rs b/crates/common/src/taskmanager.rs similarity index 85% rename from common/src/taskmanager.rs rename to crates/common/src/taskmanager.rs index eb6e1a1..6d07b77 100644 --- a/common/src/taskmanager.rs +++ b/crates/common/src/taskmanager.rs @@ -1,18 +1,15 @@ -use std::time::Instant; -use reqwest::Client; -use std::sync::Arc; -use crate::cookie_manager::CookieManager; -use crate::ticket::{*}; use crate::captcha::LocalCaptcha; +use crate::cookie_manager::CookieManager; use crate::push::PushConfig; -use crate::utility::CustomConfig; use crate::show_orderlist::OrderResponse; - - - +use crate::ticket::*; +use crate::utility::CustomConfig; +use reqwest::Client; +use std::sync::Arc; +use std::time::Instant; // 任务状态枚举 -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub enum TaskStatus { Pending, Running, @@ -32,7 +29,6 @@ pub struct TicketResult { // 任务信息 pub enum Task { - QrCodeLoginTask(QrCodeLoginTask), LoginSmsRequestTask(LoginSmsRequestTask), PushTask(PushTask), @@ -40,12 +36,11 @@ pub enum Task { GetAllorderRequestTask(GetAllorderRequest), GetTicketInfoTask(GetTicketInfoTask), GetBuyerInfoTask(GetBuyerInfoTask), - GrabTicketTask(GrabTicketTask), + GrabTicketTask(GrabTicketTask), } // 任务请求枚举 pub enum TaskRequest { - QrCodeLoginRequest(QrCodeLoginRequest), LoginSmsRequest(LoginSmsRequest), PushRequest(PushRequest), @@ -59,7 +54,6 @@ pub enum TaskRequest { // 任务结果枚举 #[derive(Clone)] pub enum TaskResult { - QrCodeLoginResult(TaskQrCodeLoginResult), LoginSmsResult(LoginSmsRequestResult), PushResult(PushRequestResult), @@ -70,12 +64,12 @@ pub enum TaskResult { GrabTicketResult(GrabTicketResult), } //抢票请求 -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct GrabTicketRequest { pub task_id: String, pub uid: i64, - pub project_id : String, - pub screen_id : String, + pub project_id: String, + pub screen_id: String, pub ticket_id: String, pub count: i16, pub buyer_info: Vec, @@ -87,93 +81,88 @@ pub struct GrabTicketRequest { pub is_hot: bool, pub local_captcha: LocalCaptcha, pub skip_words: Option>, - } -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct GrabTicketTask { pub task_id: String, pub biliticket: BilibiliTicket, pub status: TaskStatus, pub client: Arc, pub start_time: Option, - - } -#[derive(Clone,Debug)] -pub struct GrabTicketResult { +#[derive(Clone, Debug)] +pub struct GrabTicketResult { pub task_id: String, - pub uid : i64, + pub uid: i64, pub success: bool, - pub message:String, + pub message: String, pub order_id: Option, pub pay_token: Option, pub confirm_result: Option, pub pay_result: Option, } //获取购票人信息 -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct GetBuyerInfoRequest { - pub uid : i64, - pub task_id : String, + pub uid: i64, + pub task_id: String, pub cookie_manager: Arc, } -#[derive(Clone,Debug)] -pub struct GetBuyerInfoResult{ - pub task_id : String, - pub uid : i64, +#[derive(Clone, Debug)] +pub struct GetBuyerInfoResult { + pub task_id: String, + pub uid: i64, pub buyer_info: Option, pub success: bool, - pub message : String, + pub message: String, } -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct GetBuyerInfoTask { - pub uid : i64, - pub task_id : String, + pub uid: i64, + pub task_id: String, pub status: TaskStatus, - pub start_time : Option, + pub start_time: Option, pub cookie_manager: Arc, } //请求project_id票详情 -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct GetTicketInfoRequest { - pub uid : i64, - pub task_id : String, - pub project_id : String, + pub uid: i64, + pub task_id: String, + pub project_id: String, pub cookie_manager: Arc, - } -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct GetTicketInfoResult { - pub task_id : String, - pub uid : i64, + pub task_id: String, + pub uid: i64, pub ticket_info: Option, pub success: bool, - pub message : String, + pub message: String, } -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct GetTicketInfoTask { - pub task_id : String, - pub project_id : String, + pub task_id: String, + pub project_id: String, pub status: TaskStatus, - pub start_time : Option, + pub start_time: Option, pub cookie_manager: Arc, } - #[derive(Clone)] -pub struct PushRequest{ +pub struct PushRequest { pub title: String, pub message: String, pub jump_url: Option, pub push_config: PushConfig, - pub push_type : PushType, + pub push_type: PushType, } //推送类型 -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub enum PushType { All, Bark, @@ -193,15 +182,14 @@ pub struct PushRequestResult { pub push_type: PushType, } - #[derive(Clone)] pub struct PushTask { pub task_id: String, pub title: String, - pub message:String, + pub message: String, pub push_type: PushType, pub status: TaskStatus, - pub start_time: Option, + pub start_time: Option, } pub struct TicketTask { @@ -219,20 +207,18 @@ pub struct QrCodeLoginTask { pub qrcode_url: String, pub status: TaskStatus, pub start_time: Option, - } pub struct LoginSmsRequestTask { pub task_id: String, - pub phone : String, + pub phone: String, pub status: TaskStatus, pub start_time: Option, - } pub struct SubmitLoginSmsRequestTask { pub task_id: String, - pub phone : String, + pub phone: String, pub code: String, pub captcha_key: String, pub status: TaskStatus, @@ -247,7 +233,6 @@ pub struct GetAllorderRequest { pub cookies: String, pub account_id: String, pub start_time: Option, - } #[derive(Clone)] @@ -267,7 +252,6 @@ pub struct GetAllorderTask { pub start_time: Option, } - pub struct TicketRequest { pub ticket_id: String, pub account_id: String, @@ -288,15 +272,12 @@ pub struct LoginSmsRequest { } pub struct SubmitLoginSmsRequest { - pub phone : String, + pub phone: String, pub code: String, pub captcha_key: String, pub client: Client, - } - - #[derive(Clone)] pub struct TaskTicketResult { pub task_id: String, @@ -331,21 +312,23 @@ pub struct SubmitSmsLoginResult { // 更新 TaskManager trait pub trait TaskManager: Send + 'static { // 创建新的任务管理器 - fn new() -> Self where Self: Sized; - + fn new() -> Self + where + Self: Sized; + // 提交任务 fn submit_task(&mut self, request: TaskRequest) -> Result; - + // 获取可用结果,返回 TaskResult 枚举 fn get_results(&mut self) -> Vec; - + // 取消任务 fn cancel_task(&mut self, task_id: &str) -> Result<(), String>; // 获取任务状态 fn get_task_status(&self, task_id: &str) -> Option; - - // 关闭任务管理器 + + // 关闭任务管理器 fn shutdown(&mut self); } @@ -354,4 +337,4 @@ pub const DISCLAIMER_TEXT_ENCODED: &str = "4p2X5pys6aG555uu5a6M5YWo5YWN6LS55byA5 pub fn TaskManager_debug() -> String { let bytes = base64::decode(DISCLAIMER_TEXT_ENCODED).unwrap_or_default(); String::from_utf8(bytes).unwrap_or_else(|_| "本项目免费开源".to_string()) -} \ No newline at end of file +} diff --git a/common/src/ticket.rs b/crates/common/src/ticket.rs similarity index 59% rename from common/src/ticket.rs rename to crates/common/src/ticket.rs index 3f4d74b..245e984 100644 --- a/common/src/ticket.rs +++ b/crates/common/src/ticket.rs @@ -1,18 +1,18 @@ use std::sync::Arc; use reqwest::header::HeaderValue; -use reqwest::{header, Client}; +use reqwest::{Client, header}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::cookie_manager::CookieManager; use crate::account::Account; +use crate::cookie_manager::CookieManager; use crate::push::PushConfig; use crate::utility::CustomConfig; //成功下单结构体 -#[derive(Clone,Debug,Deserialize,Serialize)] -pub struct SubmitOrderResult{ +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SubmitOrderResult { #[serde(rename = "orderId")] pub order_id: i128, #[serde(rename = "orderCreateTime")] @@ -21,8 +21,8 @@ pub struct SubmitOrderResult{ pub order_token: String, } -#[derive(Clone,Debug,Deserialize,Serialize)] -pub struct CheckFakeResult{ +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CheckFakeResult { #[serde(default)] pub errno: i32, #[serde(default)] @@ -35,33 +35,32 @@ pub struct CheckFakeResult{ pub message: String, pub data: CheckFakeResultParam, } -#[derive(Clone,Debug,Deserialize,Serialize)] -pub struct CheckFakeResultParam{ +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CheckFakeResultParam { #[serde(rename = "payParam")] pub pay_param: CheckFakeResultData, } -#[derive(Clone,Debug,Deserialize,Serialize)] -pub struct CheckFakeResultData{ - pub sign : String, - pub code_url : String, +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CheckFakeResultData { + pub sign: String, + pub code_url: String, } -#[derive(Clone,Debug,Deserialize,Serialize)] -pub struct ConfirmTicketInfo{ +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ConfirmTicketInfo { pub name: String, pub count: i32, pub price: i64, } //确认订单结构体 -#[derive(Clone,Debug,Deserialize,Serialize)] -pub struct ConfirmTicketResult{ - pub count : i32, +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ConfirmTicketResult { + pub count: i32, pub pay_money: i64, pub project_name: String, pub screen_name: String, pub ticket_info: ConfirmTicketInfo, - } //获取token响应结构体 @@ -69,31 +68,31 @@ pub struct ConfirmTicketResult{ #[derive(Clone, Debug, Deserialize, Serialize)] pub struct TokenRiskParam { #[serde(default)] - pub code : i32, - + pub code: i32, + #[serde(default)] pub message: String, - - pub mid : Option, - pub decision_type : Option, - pub buvid : Option, - pub ip : Option, + + pub mid: Option, + pub decision_type: Option, + pub buvid: Option, + pub ip: Option, pub scene: Option, pub ua: Option, pub v_voucher: Option, pub risk_param: Option, } -#[derive(Clone,Debug)] -pub struct BilibiliTicket{ - pub uid : i64, //UID - pub method : u8, - pub ua : String, +#[derive(Clone, Debug)] +pub struct BilibiliTicket { + pub uid: i64, //UID + pub method: u8, + pub ua: String, pub config: CustomConfig, pub account: Account, - pub push_self : PushConfig, - pub status_delay : usize, - pub captcha_use_type: usize, //选择的验证码方式 + pub push_self: PushConfig, + pub status_delay: usize, + pub captcha_use_type: usize, //选择的验证码方式 pub cookie_manager: Option>, //抢票相关 @@ -101,79 +100,70 @@ pub struct BilibiliTicket{ pub screen_id: String, pub id_bind: usize, //是否绑定 - pub project_info : Option, //项目详情 + pub project_info: Option, //项目详情 pub all_buyer_info: Option, //所有购票人信息 - pub buyer_info: Option>, //购买人信息(实名票) + pub buyer_info: Option>, //购买人信息(实名票) pub no_bind_buyer_info: Option, //不实名制购票人信息 - pub select_ticket_id : Option, + pub select_ticket_id: Option, pub pay_money: Option, //支付金额 - pub count: Option, //购买数量 - pub device_id: String, //设备id - + pub count: Option, //购买数量 + pub device_id: String, //设备id } -impl BilibiliTicket{ +impl BilibiliTicket { pub fn new( - method: &u8, ua: &String, config: &CustomConfig, account: &Account, push_self: &PushConfig, status_delay: &usize, - project_id : &str, - - - ) -> Self{ + project_id: &str, + ) -> Self { let mut finally_ua = String::new(); if config.custom_ua != "" { - log::info!("使用自定义UA:{}",config.custom_ua); + log::info!("使用自定义UA:{}", config.custom_ua); finally_ua.push_str(&config.custom_ua); - }else{ - log::info!("使用默认UA:{}",ua); + } else { + log::info!("使用默认UA:{}", ua); finally_ua.push_str(ua); } let mut headers = header::HeaderMap::new(); - match HeaderValue::from_str(&account.cookie){ + match HeaderValue::from_str(&account.cookie) { Ok(ck_value) => { headers.insert(header::COOKIE, ck_value); - match HeaderValue::from_str(&finally_ua){ + match HeaderValue::from_str(&finally_ua) { Ok(ua_value) => { - headers.insert(header::USER_AGENT,ua_value); + headers.insert(header::USER_AGENT, ua_value); } Err(e) => { - log::error!("client插入ua失败!原因:{}",e); + log::error!("client插入ua失败!原因:{}", e); } } - } Err(e) => { - log::error!("cookie设置失败!原因:{:?}",e); + log::error!("cookie设置失败!原因:{:?}", e); } - } - let client = match Client::builder() - .cookie_store(true) - .user_agent(ua) - .default_headers(headers) - - .build(){ - Ok(client) => client, - Err(e) => { - log::error!("初始化client失败!,原因:{:?}",e); - Client::new() - } - }; - let captcha_type = config.captcha_mode; - - - - let new = Self{ + .cookie_store(true) + .user_agent(ua) + .default_headers(headers) + .build() + { + Ok(client) => client, + Err(e) => { + log::error!("初始化client失败!,原因:{:?}", e); + Client::new() + } + }; + let captcha_type = config.captcha_mode; + + let new = Self { uid: account.uid.clone(), method: method.clone(), ua: ua.clone(), @@ -194,44 +184,37 @@ impl BilibiliTicket{ count: None, device_id: "".to_string(), id_bind: 999, - }; - log::debug!("新建抢票对象:{:?}",new); + log::debug!("新建抢票对象:{:?}", new); new - } - } -#[derive(Clone,Debug,Deserialize,Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct TicketInfo { pub id: i32, pub name: String, pub is_sale: usize, pub start_time: i64, pub end_time: i64, - pub pick_seat: usize, //0:不选座 1:选座 - pub project_type: usize, //未知作用,bw2024是type1 - pub express_fee: usize, //快递费 - pub sale_begin: i64, //开售时间 - pub sale_end: i64, //截止时间 - pub count_down: i64, //倒计时(可能有负数) + pub pick_seat: usize, //0:不选座 1:选座 + pub project_type: usize, //未知作用,bw2024是type1 + pub express_fee: usize, //快递费 + pub sale_begin: i64, //开售时间 + pub sale_end: i64, //截止时间 + pub count_down: i64, //倒计时(可能有负数) pub screen_list: Vec, //场次列表 - pub sale_flag_number: usize, //售票标志位 + pub sale_flag_number: usize, //售票标志位 #[serde(default)] pub sale_flag: String, //售票状态 pub is_free: bool, pub performance_desc: Option, //基础信息 - pub id_bind: usize, //是否绑定 + pub id_bind: usize, //是否绑定 #[serde(rename = "hotProject")] pub hot_project: bool, //是否热门项目 - - - - } -#[derive(Clone,Debug,Deserialize,Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ScreenInfo { #[serde(default)] pub sale_flag: SaleFlag, @@ -243,15 +226,14 @@ pub struct ScreenInfo { pub delivery_type: usize, pub pick_seat: usize, pub ticket_list: Vec, //当日票种类列表 - pub clickable: bool, //是否可点(可售) - pub sale_end: usize, //截止时间 - pub sale_start: usize, //开售时间 - pub sale_flag_number: usize, //售票标志位 - pub show_date: String, //展示信息 - + pub clickable: bool, //是否可点(可售) + pub sale_end: usize, //截止时间 + pub sale_start: usize, //开售时间 + pub sale_flag_number: usize, //售票标志位 + pub show_date: String, //展示信息 } -#[derive(Clone,Debug,Deserialize,Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct SaleFlag { #[serde(default)] pub number: usize, @@ -268,42 +250,40 @@ impl Default for SaleFlag { } } -#[derive(Clone,Debug,Deserialize,Serialize)] -pub struct ScreenTicketInfo{ - pub saleStart : usize, //开售时间(时间戳) eg:1720260000 - pub saleEnd : usize, //截止时间(时间戳) - pub id: usize, //票种id - pub project_id: usize, //项目id - pub price: usize, //票价(分) - pub desc: String, //票种描述 - pub sale_start: String, //开售时间(字符串) eg:2024-07-06 18:00:00 - pub sale_end: String, //截止时间(字符串) - pub r#type: usize, //类型 关键词替换,对应”type“ - pub sale_type: usize, //销售状态 - pub is_sale: usize, //是否销售?0是1否 - pub num: usize, //数量 - pub sale_flag: SaleFlag, //售票状态 - pub clickable: bool, //是否可点(可售) +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ScreenTicketInfo { + pub saleStart: usize, //开售时间(时间戳) eg:1720260000 + pub saleEnd: usize, //截止时间(时间戳) + pub id: usize, //票种id + pub project_id: usize, //项目id + pub price: usize, //票价(分) + pub desc: String, //票种描述 + pub sale_start: String, //开售时间(字符串) eg:2024-07-06 18:00:00 + pub sale_end: String, //截止时间(字符串) + pub r#type: usize, //类型 关键词替换,对应”type“ + pub sale_type: usize, //销售状态 + pub is_sale: usize, //是否销售?0是1否 + pub num: usize, //数量 + pub sale_flag: SaleFlag, //售票状态 + pub clickable: bool, //是否可点(可售) pub sale_flag_number: usize, //售票标志位 - pub screen_name: String, //场次名称 - - + pub screen_name: String, //场次名称 } -#[derive(Clone,Debug,Deserialize,Serialize)] -pub struct DescribeList{ - pub r#type: u8, // 使用 r# 前缀处理 Rust 关键字 +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DescribeList { + pub r#type: u8, // 使用 r# 前缀处理 Rust 关键字 pub list: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ModuleItem { pub module: String, - + // details 可能是字符串或数组,使用 serde_json::Value 处理多态 #[serde(default)] pub details: Value, - + // 可选字段 #[serde(default)] pub module_name: Option, @@ -317,7 +297,7 @@ pub struct BaseInfoItem { } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct InfoResponse{ +pub struct InfoResponse { #[serde(default)] pub errno: i32, #[serde(default)] @@ -332,7 +312,7 @@ pub struct InfoResponse{ } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct BuyerInfo{ +pub struct BuyerInfo { pub id: i64, pub uid: i64, pub personal_id: String, @@ -350,12 +330,10 @@ pub struct BuyerInfo{ pub isBuyerInfoVerified: bool, #[serde(default)] pub isBuyerValid: bool, - - } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct BuyerInfoResponse{ +pub struct BuyerInfoResponse { #[serde(default)] pub errno: i32, #[serde(default)] @@ -370,9 +348,8 @@ pub struct BuyerInfoResponse{ } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct BuyerInfoData{ +pub struct BuyerInfoData { pub list: Vec, - } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -380,4 +357,4 @@ pub struct NoBindBuyerInfo { pub name: String, pub tel: String, pub uid: i64, -} \ No newline at end of file +} diff --git a/crates/common/src/utility.rs b/crates/common/src/utility.rs new file mode 100644 index 0000000..faa477f --- /dev/null +++ b/crates/common/src/utility.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CustomConfig { + pub open_custom_ua: bool, //是否开启自定义UA + pub custom_ua: String, //自定义UA + pub captcha_mode: usize, //验证码模式 //0:本地打码 1:ttocr + pub ttocr_key: String, //ttocr key + pub preinput_phone1: String, //预填账号1手机号 + pub preinput_phone2: String, //预填账号2手机号 +} + +impl CustomConfig { + pub fn new() -> Self { + Self { + open_custom_ua: true, + custom_ua: String::from( + "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Mobile Safari/537.36", + ), + captcha_mode: 0, + ttocr_key: String::new(), + preinput_phone1: String::new(), + preinput_phone2: String::new(), + } + } +} diff --git a/common/src/utils.rs b/crates/common/src/utils.rs similarity index 56% rename from common/src/utils.rs rename to crates/common/src/utils.rs index 38963b9..9e67f0a 100644 --- a/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -1,26 +1,22 @@ -use std::{fs, process}; -use std::fs::File; -use std::io; -use std::io::Write; -use std::ops::{Index, IndexMut}; -use std::sync::Arc; -use serde_json::{Value, json, Map}; use crate::account::Account; -use crate::cookie_manager::CookieManager; use crate::push::PushConfig; use crate::utility::CustomConfig; +use aes::Aes128; use base64::Engine as _; use base64::engine::general_purpose::STANDARD as BASE64; -use block_modes::{BlockMode, Cbc}; use block_modes::block_padding::Pkcs7; -use aes::Aes128; +use block_modes::{BlockMode, Cbc}; +use serde_json::{Map, Value, json}; +use std::fs; +use std::io; +use std::ops::{Index, IndexMut}; use rand::Rng; -use std::path::Path; use reqwest::Client; +use std::path::Path; -#[derive(Clone,Debug)] -pub struct Config{ +#[derive(Clone, Debug)] +pub struct Config { data: Value, } @@ -30,55 +26,56 @@ impl Config { } } -impl Config{ - pub fn load_config() -> io::Result{ +impl Config { + pub fn load_config() -> io::Result { let raw_context = fs::read_to_string("./config")?; let content = raw_context.split("%").collect::>(); // base64解码后解密 - let iv = BASE64.decode(content[0].trim()) + let iv = BASE64 + .decode(content[0].trim()) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - let decoded = BASE64.decode(content[1].trim()) + let decoded = BASE64 + .decode(content[1].trim()) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; let decrypted = decrypt_data(iv, &decoded) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; let plain_text = String::from_utf8(decrypted) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; let data = serde_json::from_str(&plain_text)?; - Ok(Self{data}) - + Ok(Self { data }) } - pub fn load_json_config() -> io::Result{ + pub fn load_json_config() -> io::Result { let content = fs::read_to_string("./config.json")?; let data = serde_json::from_str(&content)?; - Ok(Self{data}) - + Ok(Self { data }) } - pub fn new() -> Self{ + pub fn new() -> Self { let data = json!({}); - Self{data} + Self { data } } - pub fn save_config(&self) -> io::Result<()> { //后续上加密 + pub fn save_config(&self) -> io::Result<()> { + //后续上加密 let json_str = serde_json::to_string_pretty(&self.data)?; // 加密后base64编码 - let (iv,encrypted) = encrypt_data(json_str.as_bytes()) + let (iv, encrypted) = encrypt_data(json_str.as_bytes()) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - let encoded_iv = BASE64.encode(&iv); + let encoded_iv = BASE64.encode(&iv); let encoded_encrypted = BASE64.encode(&encrypted); - fs::write("./config", encoded_iv+"%" + &*encoded_encrypted) + fs::write("./config", encoded_iv + "%" + &*encoded_encrypted) } - //添加账号 - pub fn add_account(&mut self, account: &Account) -> io::Result<()>{ - if !self["accounts"].is_array(){ //不存在则创建 - self["accounts"]= json!([]); + pub fn add_account(&mut self, account: &Account) -> io::Result<()> { + if !self["accounts"].is_array() { + //不存在则创建 + self["accounts"] = json!([]); } let account_json = serde_json::to_value(account)?; - if let Value::Array(ref mut accounts)= self["accounts"]{ + if let Value::Array(ref mut accounts) = self["accounts"] { accounts.push(account_json); } @@ -86,59 +83,57 @@ impl Config{ } //加载账号 - pub fn load_accounts(&self) -> Result,serde_json::Error>{ - if self["accounts"].is_array(){ + pub fn load_accounts(&self) -> Result, serde_json::Error> { + if self["accounts"].is_array() { let accounts_json = &self["accounts"]; serde_json::from_value(accounts_json.clone()) - } - else{ + } else { Ok(Vec::new()) } } //账号更新(Account更新后调用这个保存,uid唯一寻找标识) - pub fn update_account(&mut self, account: &Account) ->io::Result{ - if !self["accounts"].is_array(){ + pub fn update_account(&mut self, account: &Account) -> io::Result { + if !self["accounts"].is_array() { return Ok(false); } let account_json = serde_json::to_value(account)?; - if let Value::Array(ref mut accounts) = self["accounts"]{ + if let Value::Array(ref mut accounts) = self["accounts"] { for (index, acc) in accounts.iter_mut().enumerate() { - if let Some(uid) = acc["uid"].as_i64(){ - if uid == account.uid{ + if let Some(uid) = acc["uid"].as_i64() { + if uid == account.uid { accounts[index] = account_json; return Ok(true); } - } } + } + } } Ok(false) - } //删除账号,传uid - pub fn delete_account(&mut self, uid: i64) ->bool{ - if !self["accounts"].is_array(){ + pub fn delete_account(&mut self, uid: i64) -> bool { + if !self["accounts"].is_array() { return false; } let mut remove_flag = false; - if let Value::Array(ref mut accounts )= self["accounts"]{ + if let Value::Array(ref mut accounts) = self["accounts"] { let old_len = accounts.len(); - accounts.retain(|acc|{ - if let Some(account_uid) = acc["uid"].as_i64(){ + accounts.retain(|acc| { + if let Some(account_uid) = acc["uid"].as_i64() { account_uid != uid - } - else{ + } else { true } }); remove_flag = accounts.len() != old_len; } - match save_config(self, None, None, None){ + match save_config(self, None, None, None) { Ok(_) => { log::info!("删除账号成功"); - }, + } Err(e) => { log::error!("删除账号失败: {}", e); } @@ -148,13 +143,11 @@ impl Config{ pub fn load_all_accounts() -> Vec { match Self::load_config() { - Ok(config) => { - match config.load_accounts() { - Ok(accounts) => accounts, - Err(e) => { - log::error!("加载账号失败: {}", e); - Vec::new() - } + Ok(config) => match config.load_accounts() { + Ok(accounts) => accounts, + Err(e) => { + log::error!("加载账号失败: {}", e); + Vec::new() } }, Err(e) => { @@ -163,19 +156,16 @@ impl Config{ } } } - } -impl Index<&str> for Config{ +impl Index<&str> for Config { type Output = Value; - fn index(&self, key: &str) -> &Self::Output{ - - match self.data.get(key){ + fn index(&self, key: &str) -> &Self::Output { + match self.data.get(key) { Some(value) => value, None => &Value::Null, } - } } @@ -199,7 +189,12 @@ impl IndexMut<&str> for Config { } } -pub fn save_config(config: &mut Config, push_config: Option<&PushConfig>, custon_config: Option<&CustomConfig>, account: Option) -> Result { +pub fn save_config( + config: &mut Config, + push_config: Option<&PushConfig>, + custon_config: Option<&CustomConfig>, + account: Option, +) -> Result { if let Some(push_config) = push_config { config["push_config"] = serde_json::to_value(push_config).unwrap(); } @@ -210,128 +205,35 @@ pub fn save_config(config: &mut Config, push_config: Option<&PushConfig>, custon config.add_account(&account).unwrap(); } - - match config.save_config(){ + match config.save_config() { Ok(_) => { log::info!("配置文件保存成功"); Ok(true) - }, + } Err(e) => { log::error!("配置文件保存失败: {}", e); Err(e.to_string()) } } - -} -pub fn load_texture_from_path(ctx: &eframe::egui::Context, path: &str, name: &str) -> Option { - use std::io::Read; - - - match File::open(path) { - - Ok(mut file) => { - let mut bytes = Vec::new(); - if file.read_to_end(&mut bytes).is_ok() { - match image::load_from_memory(&bytes) { - Ok(image) => { - let size = [image.width() as usize, image.height() as usize]; - let image_buffer = image.to_rgba8(); - let pixels = image_buffer.as_flat_samples(); - - Some(ctx.load_texture( - name, - eframe::egui::ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()), - Default::default() - )) - } - Err(_) => None, - } - } else { - None - } - } - Err(_) => None, - } } - -fn write_bytes_to_file(file_path: &str, bytes: &[u8]) -> io::Result<()> { - let mut file = File::create(file_path)?; // 创建文件 - file.write_all(bytes)?; // 写入字节流 - file.flush()?; // 确保数据写入磁盘 - Ok(()) -} - -pub fn load_texture_from_url(ctx: &eframe::egui::Context, cookie_manager: Arc, url: &String, name: &str) -> Option { - let rt = tokio::runtime::Runtime::new().unwrap(); - - - let bytes = rt.block_on(async { - // 发送请求 - let resp = match cookie_manager.get(url).await.send().await { - Ok(resp) => resp, - Err(err) => { - log::error!("HTTP请求失败: {}", err); - return None; - } - }; - - // 读取响应体 - match resp.bytes().await { - Ok(bytes) => Some(bytes), - Err(err) => { - log::error!("读取响应体失败: {}", err); - None - } - } - }); - - - let bytes = match bytes { - Some(b) => b, - None => return None, - }; - - // 处理图像数据 - match image::load_from_memory(&bytes) { - Ok(image) => { - let size = [image.width() as usize, image.height() as usize]; - let image_buffer = image.to_rgba8(); - let pixels = image_buffer.as_flat_samples(); - - Some(ctx.load_texture( - name, - eframe::egui::ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()), - Default::default() - )) - } - Err(err) => { - log::warn!("加载图片至内存失败: {},url:{}", err, url); - None - } - } -} - - -fn gen_machine_id_bytes_128b()->Vec { +fn gen_machine_id_bytes_128b() -> Vec { let id: String = machine_uid::get().unwrap(); println!("{}", id); id[..16].as_bytes().to_vec() - } // 加密函数 -fn encrypt_data(data: &[u8]) -> Result<(Vec,Vec), block_modes::BlockModeError> { +fn encrypt_data(data: &[u8]) -> Result<(Vec, Vec), block_modes::BlockModeError> { type Aes128Cbc = Cbc; let mut iv = [0u8; 16]; - rand::thread_rng() - .fill(&mut iv[..]); // 填充 16 字节的随机数据 + rand::thread_rng().fill(&mut iv[..]); // 填充 16 字节的随机数据 let cipher = Aes128Cbc::new_from_slices(&gen_machine_id_bytes_128b(), &iv) .map_err(|_| block_modes::BlockModeError)?; // 将 InvalidKeyIvLength 转换为 BlockModeError Ok((iv.to_vec(), cipher.encrypt_vec(data))) } -fn decrypt_data(iv:Vec,encrypted: &[u8]) -> Result, block_modes::BlockModeError> { +fn decrypt_data(iv: Vec, encrypted: &[u8]) -> Result, block_modes::BlockModeError> { type Aes128Cbc = Cbc; let cipher = Aes128Cbc::new_from_slices(&gen_machine_id_bytes_128b(), &iv) .map_err(|_| block_modes::BlockModeError)?; // 将 InvalidKeyIvLength 转换为 BlockModeError @@ -347,7 +249,7 @@ pub fn ensure_single_instance() -> bool { // 使用应用程序唯一标识 let app_id = "bili_ticket_rush_6BA7B79C-0E4F-4FCC-B7A2-4DA5E8D7E0F6"; // GUID 保证唯一性 let instance = SingleInstance::new(app_id).unwrap(); - + if !instance.is_single() { log::error!("程序已经在运行中,请勿重复启动!"); eprintln!("程序已经在运行中,请勿重复启动!"); @@ -369,19 +271,19 @@ fn is_process_running(_pid: u32) -> bool { #[cfg(target_os = "windows")] fn is_process_running(pid: u32) -> bool { use std::process::Command; - + // 使用 tasklist 命令检查进程 let output = Command::new("tasklist") .args(&["/NH", "/FI", &format!("PID eq {}", pid)]) .output(); - + match output { Ok(out) => { let stdout = String::from_utf8_lossy(&out.stdout); - !stdout.contains("信息: 没有运行的任务匹配指定标准") && - !stdout.contains("No tasks") && - stdout.contains(&format!("{}", pid)) - }, + !stdout.contains("信息: 没有运行的任务匹配指定标准") + && !stdout.contains("No tasks") + && stdout.contains(&format!("{}", pid)) + } Err(_) => false, // 执行命令失败,假设进程不存在 } } @@ -394,17 +296,17 @@ fn is_process_running(pid: u32) -> bool { #[cfg(target_os = "macos")] fn is_process_running(pid: u32) -> bool { use std::process::Command; - + // 使用 ps 命令检查进程 let output = Command::new("ps") .args(&["-p", &format!("{}", pid)]) .output(); - + match output { Ok(out) => { let stdout = String::from_utf8_lossy(&out.stdout); stdout.contains(&format!("{}", pid)) - }, + } Err(_) => false, // 执行命令失败,假设进程不存在 } } @@ -412,30 +314,26 @@ fn is_process_running(pid: u32) -> bool { pub async fn get_now_time(client: &Client) -> i64 { // 获取网络时间 (秒级) let url = "https://api.bilibili.com/x/click-interface/click/now"; - + let now_sec = match client.get(url).send().await { - Ok(response) => { - match response.text().await { - Ok(text) => { - log::debug!("API原始响应:{}", text); - - let json_data: serde_json::Value = serde_json::from_str(&text).unwrap_or( - json!({ - "code": 0, - "data": { - "now": 0 - } - }) - ); - - let now_sec = json_data["data"]["now"].as_i64().unwrap_or(0); - log::debug!("解析出的网络时间(秒级):{}", now_sec); - now_sec - }, - Err(e) => { - log::debug!("解析网络时间响应失败:{}", e); - 0 - } + Ok(response) => match response.text().await { + Ok(text) => { + log::debug!("API原始响应:{}", text); + + let json_data: serde_json::Value = serde_json::from_str(&text).unwrap_or(json!({ + "code": 0, + "data": { + "now": 0 + } + })); + + let now_sec = json_data["data"]["now"].as_i64().unwrap_or(0); + log::debug!("解析出的网络时间(秒级):{}", now_sec); + now_sec + } + Err(e) => { + log::debug!("解析网络时间响应失败:{}", e); + 0 } }, Err(e) => { @@ -443,7 +341,7 @@ pub async fn get_now_time(client: &Client) -> i64 { 0 } }; - + // 如果网络时间获取失败,使用本地时间 (转换为秒) if now_sec == 0 { log::debug!("使用本地时间"); diff --git a/common/src/web_ck_obfuscated.rs b/crates/common/src/web_ck_obfuscated.rs similarity index 78% rename from common/src/web_ck_obfuscated.rs rename to crates/common/src/web_ck_obfuscated.rs index f2110a5..3ebfe03 100644 --- a/common/src/web_ck_obfuscated.rs +++ b/crates/common/src/web_ck_obfuscated.rs @@ -1,17 +1,26 @@ -use reqwest::Client as _c1; -use serde_json::Value as _v1; -use std::time::{Duration as _d1, SystemTime as _st, UNIX_EPOCH as _ue}; -use rand::{Rng as _r1, seq::SliceRandom as _sr}; -use md5; use chrono::{Local as _l1, Utc as _u1}; -use uuid::Uuid as _uid; use hmac::{Hmac as _h1, Mac as _m1}; +use md5; +use rand::Rng as _r1; +use reqwest::Client as _c1; +use serde_json::Value as _v1; use sha2::Sha256 as _s256; use std::collections::HashMap as _hm; +use std::time::{Duration as _d1, SystemTime as _st, UNIX_EPOCH as _ue}; +use uuid::Uuid as _uid; -const _A1: &[u8] = &[97, 112, 105, 46, 98, 105, 108, 105, 98, 105, 108, 105, 46, 99, 111, 109]; -const _X1: &[u8] = &[120, 47, 102, 114, 111, 110, 116, 101, 110, 100, 47, 102, 105, 110, 103, 101, 114, 47, 115, 112, 105]; -const _X2: &[u8] = &[98, 97, 112, 105, 115, 47, 98, 105, 108, 105, 98, 105, 108, 105, 46, 97, 112, 105, 46, 116, 105, 99, 107, 101, 116, 46, 118, 49, 46, 84, 105, 99, 107, 101, 116, 47, 71, 101, 110, 87, 101, 98, 84, 105, 99, 107, 101, 116]; +const _A1: &[u8] = &[ + 97, 112, 105, 46, 98, 105, 108, 105, 98, 105, 108, 105, 46, 99, 111, 109, +]; +const _X1: &[u8] = &[ + 120, 47, 102, 114, 111, 110, 116, 101, 110, 100, 47, 102, 105, 110, 103, 101, 114, 47, 115, + 112, 105, +]; +const _X2: &[u8] = &[ + 98, 97, 112, 105, 115, 47, 98, 105, 108, 105, 98, 105, 108, 105, 46, 97, 112, 105, 46, 116, + 105, 99, 107, 101, 116, 46, 118, 49, 46, 84, 105, 99, 107, 101, 116, 47, 71, 101, 110, 87, 101, + 98, 84, 105, 99, 107, 101, 116, +]; macro_rules! _cx { ($e:expr) => { @@ -33,27 +42,28 @@ fn _x2>(data: T) -> String { fn _dx2>(data: T) -> String { let _s = data.as_ref().as_bytes(); let _l = _s.len(); - let _r: Vec = (0.._l).map(|i| { - let _x = _x1(_s[i], 0x44); - (_x + (i % 13) as u8) & 0xff - }).collect(); + let _r: Vec = (0.._l) + .map(|i| { + let _x = _x1(_s[i], 0x44); + (_x + (i % 13) as u8) & 0xff + }) + .collect(); _cx!(&_r).to_string() } - pub async fn gen_buvid3and4(client: _c1) -> Result<(String, String, String), String> { let _k1 = 5; let _k2 = 500; let _k3 = format!("https://{}/{}", _cx!(_A1), _cx!(_X1)); - + let mut _i1 = 0; - + loop { _i1 += 1; if _i1 > _k1 { break; } - + let _r1 = _z1(&client, &_k3, _i1 > 3).await; match _r1 { Ok((_a1, _a2, _a3)) => return Ok((_a1, _a2, _a3)), @@ -61,86 +71,94 @@ pub async fn gen_buvid3and4(client: _c1) -> Result<(String, String, String), Str if _i1 == _k1 { return Err(_x2(format!("获取 buvid 失败: {}", _e1))); } - + let _msg = format!("第{}次获取 buvid 失败: {},稍后重试", _i1, _e1); log::warn!("{}", _dx2(&_msg)); - + let _f = (_i1 * 100) as u64; std::thread::sleep(_d1::from_millis(_k2 + _f)); - + if _rand_bool(0.3) { _obfuscated_delay(); } } } } - + Err(format!("获取 buvid 重试次数已达上限")) } -async fn _z1(client: &_c1, url: &str, _add_params: bool) -> Result<(String, String, String), String> { +async fn _z1( + client: &_c1, + url: &str, + _add_params: bool, +) -> Result<(String, String, String), String> { let mut _req = client.get(url); - - + if _add_params { _req = _req.query(&[("_", _u1::now().timestamp_millis())]); } - - let _res = _req.send().await - .map_err(|e| format!("请求失败: {}", e))?; - + + let _res = _req.send().await.map_err(|e| format!("请求失败: {}", e))?; + if !_res.status().is_success() { return Err(format!("请求失败,状态码: {}", _res.status())); } - - let _j: _v1 = _res.json().await + + let _j: _v1 = _res + .json() + .await .map_err(|e| format!("解析 JSON 失败: {}", e))?; - - + _extract_buv_data(_j).await } async fn _extract_buv_data(_json: _v1) -> Result<(String, String, String), String> { - let _data = if let Some(d) = _json.get("data") { d } else { + let _data = if let Some(d) = _json.get("data") { + d + } else { return Err("返回 JSON 中缺少 data 字段".to_string()); }; - - + let _fields = ["b_3", "b_4"]; let mut _values = Vec::with_capacity(2); - + for &_f in &_fields { let _v = match _data.get(_f).and_then(|v| v.as_str()) { Some(s) => s.to_string(), - None => return Err(format!("返回 JSON 中缺少 {} 字段", _f)) + None => return Err(format!("返回 JSON 中缺少 {} 字段", _f)), }; _values.push(_v); } - - - let _t1 = _st::now().duration_since(_ue) + + let _t1 = _st::now() + .duration_since(_ue) .map_err(|e| format!("获取系统时间失败: {}", e))?; - + let _t2 = if _rand_bool(0.5) { _t1.as_secs() } else { _u1::now().timestamp() as u64 }; - + let _b_nut = _t2.to_string(); - + if _rand_bool(0.7) { - log::debug!("b_3: {}, b_4: {}, b_nut: {}", _values[0], _values[1], _b_nut); + log::debug!( + "b_3: {}, b_4: {}, b_nut: {}", + _values[0], + _values[1], + _b_nut + ); } - + Ok((_values[0].clone(), _values[1].clone(), _b_nut)) } - fn random_md5() -> String { let mut _rng = rand::thread_rng(); let _complexity = _rng.gen_range(0..3); - + let _val = match _complexity { 0 => _rng.r#gen::(), 1 => _rng.r#gen::() as f64 / 1000.0, @@ -150,16 +168,16 @@ fn random_md5() -> String { _base + _factor } }; - + let _data = _val.to_string(); let _digest = md5::compute(_data.as_bytes()); - + let _hex = format!("{:x}", _digest); - + if _rand_bool(0.2) { - let _len = _hex.len(); - _hex.chars().enumerate() + _hex.chars() + .enumerate() .map(|(i, c)| if i % 7 == 3 { c } else { c }) .collect() } else { @@ -167,26 +185,20 @@ fn random_md5() -> String { } } - pub fn gen_fp() -> String { _generate_fingerprint() } fn _generate_fingerprint() -> String { - let _md5_val = random_md5(); let _time_str = _l1::now().format("%Y%m%d%H%M%S").to_string(); - - + let _hex_str = _gen_random_hex(16); - - + let _raw_fp = format!("{}{}{}", _md5_val, _time_str, _hex_str); - - + let _check_val = _calculate_checksum(&_raw_fp); - - + format!("{}{}", _raw_fp, _check_val) } @@ -194,63 +206,57 @@ fn _gen_random_hex(_len: usize) -> String { let _chars = "0123456789abcdef"; let _char_vec: Vec = _chars.chars().collect(); let mut _rng = rand::thread_rng(); - + let mut _result = String::with_capacity(_len); for _ in 0.._len { let _idx = if _rand_bool(0.95) { - _rng.gen_range(0.._char_vec.len()) } else { - (_rng.r#gen::() as usize) % _char_vec.len() }; - + _result.push(_char_vec[_idx]); } - + _result } fn _calculate_checksum(_input: &str) -> String { - let _chunks: Vec<&str> = _input .as_bytes() .chunks(2) .map(|chunk| std::str::from_utf8(chunk).unwrap_or("")) .collect(); - + let mut _sum = 0u32; let mut _i = 0; - - + while _i < _chunks.len() { if _i % 2 == 0 { - _sum = _sum.wrapping_add( - u32::from_str_radix(_chunks[_i], 16).unwrap_or(0) - ); + _sum = _sum.wrapping_add(u32::from_str_radix(_chunks[_i], 16).unwrap_or(0)); } else { - let _val = u32::from_str_radix(_chunks[_i], 16).unwrap_or(0); _sum = _sum + _val; } _i += 2; } - + format!("{:x}", _sum % 256) } - pub fn gen_uuid_infoc() -> String { let _now = if _rand_bool(0.5) { _u1::now().timestamp_millis() } else { - _st::now().duration_since(_ue).unwrap_or_default().as_millis() as i64 + _st::now() + .duration_since(_ue) + .unwrap_or_default() + .as_millis() as i64 }; - + let _t = (_now % 100_000) as u32; let _t_str = format!("{:0<5}", _t); - - + let _uuid = if _rand_bool(0.6) { _uid::new_v4().to_string() } else { @@ -258,62 +264,57 @@ pub fn gen_uuid_infoc() -> String { rand::thread_rng().fill(&mut _u); _uid::from_bytes(_u).to_string() }; - + format!("{}{}infoc", _uuid, _t_str) } - pub async fn gen_ckbili_ticket(client: _c1) -> Result<(String, String), String> { const _MAX: u32 = 5; const _DELAY: u64 = 500; - + let _url = format!("https://{}/{}", _cx!(_A1), _cx!(_X2)); - + for _i in 1..=_MAX { let _res = _get_ticket(&client, &_url).await; - + match _res { Ok((_t1, _t2)) => return Ok((_t1, _t2)), Err(_e) => { if _i == _MAX { return Err(format!("获取 ckbili_ticket 失败: {}", _e)); } - + log::warn!("第{}次获取 ckbili_ticket 失败: {},稍后重试", _i, _e); std::thread::sleep(_d1::from_millis(_DELAY + (_i as u64 * 50))); } } } - + Err("获取 ckbili_ticket 重试次数已达上限".to_string()) } - async fn _get_ticket(client: &_c1, _url: &str) -> Result<(String, String), String> { let (_ts, _hex) = _prepare_ticket_params().await?; - - + let mut _params = _hm::new(); _params.insert("key_id".to_string(), "ec02".to_string()); - - + _add_ticket_params(&mut _params, _ts, _hex); - + // 发送请求并处理结果 _send_ticket_request(client, _url, _params).await } async fn _prepare_ticket_params() -> Result<(u64, String), String> { - - let _ts = _st::now().duration_since(_ue) + let _ts = _st::now() + .duration_since(_ue) .map_err(|e| format!("获取系统时间失败: {}", e))? .as_secs(); - - + let _key = "XgwSnGZ1p"; let _msg = format!("ts{}", _ts); let _hex = _calc_hmac(_key, &_msg)?; - + Ok((_ts, _hex)) } @@ -324,106 +325,107 @@ fn _add_ticket_params(_params: &mut _hm, _ts: u64, _hex: String) } async fn _send_ticket_request( - client: &_c1, - url: &str, - params: _hm + client: &_c1, + url: &str, + params: _hm, ) -> Result<(String, String), String> { - - let _resp = client.post(url) + let _resp = client + .post(url) .query(¶ms) .send() .await .map_err(|e| format!("请求失败: {}", e))?; - + if !_resp.status().is_success() { return Err(format!("请求失败,状态码: {}", _resp.status())); } - - - let _json: _v1 = _resp.json() + + let _json: _v1 = _resp + .json() .await .map_err(|e| format!("解析 JSON 失败: {}", e))?; - - + _extract_ticket_data(_json).await } async fn _extract_ticket_data(_json: _v1) -> Result<(String, String), String> { - let _data = _json.get("data") + let _data = _json + .get("data") .ok_or_else(|| "返回 JSON 中缺少 data 字段".to_string())?; - - - let _ticket = _data.get("ticket") + + let _ticket = _data + .get("ticket") .and_then(|v| v.as_str()) .ok_or_else(|| "返回 JSON 中缺少 ticket 字段".to_string())? .to_string(); - - - let _created = _data.get("created_at") + + let _created = _data + .get("created_at") .and_then(|v| v.as_i64()) .ok_or_else(|| "返回 JSON 中缺少 created_at 字段".to_string())?; - - let _ttl = _data.get("ttl") + + let _ttl = _data + .get("ttl") .and_then(|v| v.as_i64()) .ok_or_else(|| "返回 JSON 中缺少 ttl 字段".to_string())?; - + let _expires = (_created + _ttl).to_string(); - - + if let (Some(_img), Some(_sub)) = ( - _data.get("nav").and_then(|n| n.get("img")).and_then(|v| v.as_str()), - _data.get("nav").and_then(|n| n.get("sub")).and_then(|v| v.as_str()) + _data + .get("nav") + .and_then(|n| n.get("img")) + .and_then(|v| v.as_str()), + _data + .get("nav") + .and_then(|n| n.get("sub")) + .and_then(|v| v.as_str()), ) { log::debug!("获取到图片URL: {}, 子URL: {}", _img, _sub); } - + log::debug!("bili_ticket: {}, expires: {}", _ticket, _expires); Ok((_ticket, _expires)) } - fn _calc_hmac(key: &str, message: &str) -> Result { type _H = _h1<_s256>; - - let mut _mac = _H::new_from_slice(key.as_bytes()) - .map_err(|e| format!("HMAC 初始化失败: {}", e))?; - + + let mut _mac = + _H::new_from_slice(key.as_bytes()).map_err(|e| format!("HMAC 初始化失败: {}", e))?; + _mac.update(message.as_bytes()); - + let _result = _mac.finalize(); let _bytes = _result.into_bytes(); - + Ok(hex::encode(_bytes)) } pub fn gen_01x88() -> String { - let _x1 = |_n: u8| -> bool { (_n & 0x2D) == 0x2D }; - + let _x1 = |_n: u8| -> bool { (_n & 0x2D) == 0x2D }; + let _t0 = std::time::SystemTime::now(); - let _r1 = rand::thread_rng().r#gen::() % 4 > 0; - + let _r1 = rand::thread_rng().r#gen::() % 4 > 0; + let _id_src = if _r1 { - let _uuid_raw = uuid::Uuid::new_v4(); - _uuid_raw.as_bytes().to_vec() + _uuid_raw.as_bytes().to_vec() } else { - let mut _bytes = [0u8; 16]; rand::thread_rng().fill(&mut _bytes); let _u = uuid::Uuid::from_bytes(_bytes); _u.as_bytes().to_vec() }; - - + let mut _result = String::with_capacity(32); let _hex = "0123456789abcdef".as_bytes(); - + for &_b in _id_src.iter() { _result.push(_hex[(_b >> 4) as usize] as char); _result.push(_hex[(_b & 0xf) as usize] as char); } - - + let _elapsed = _t0.elapsed().unwrap_or_default(); if _elapsed.as_nanos() % 2 == 0 { _result.chars().filter(|&c| c != '-').collect() @@ -441,7 +443,6 @@ fn _obfuscated_delay() { std::thread::sleep(_d1::from_millis(_delay)); } - fn hmac_sha256(key: &str, message: &str) -> Result> { _calc_hmac(key, message).map_err(|e| e.into()) -} \ No newline at end of file +} diff --git a/crates/frontend/.gitignore b/crates/frontend/.gitignore new file mode 100644 index 0000000..2c41ac3 --- /dev/null +++ b/crates/frontend/.gitignore @@ -0,0 +1 @@ +/gen/ diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml new file mode 100644 index 0000000..9d6a428 --- /dev/null +++ b/crates/frontend/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "frontend" +version = "0.1.0" +edition = "2024" +build = "build.rs" + +[dependencies] +tauri = { version = "2", features = [] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +tokio = { version = "1", features = ["full"] } +reqwest = { version = "0.11.22", features = ["json", "blocking", "cookies"] } +chrono = "0.4" +rand = "0.8" +uuid = { version = "1", features = ["v4"] } +base64 = "0.21" +jsonwebtoken = "9" +qrcode = "0.14.1" +image = "0.25" +futures-util = "0.3" + +log = "0.4" +env_logger = "0.9" + +common = { path = "../common" } +backend = { path = "../backend" } + +[build-dependencies] +tauri-build = { version = "2", features = [] } diff --git a/crates/frontend/build.rs b/crates/frontend/build.rs new file mode 100644 index 0000000..261851f --- /dev/null +++ b/crates/frontend/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build(); +} diff --git a/crates/frontend/icons/128x128.png b/crates/frontend/icons/128x128.png new file mode 100644 index 0000000..447f892 Binary files /dev/null and b/crates/frontend/icons/128x128.png differ diff --git a/crates/frontend/icons/128x128@2x.png b/crates/frontend/icons/128x128@2x.png new file mode 100644 index 0000000..fd19cf4 Binary files /dev/null and b/crates/frontend/icons/128x128@2x.png differ diff --git a/crates/frontend/icons/32x32.png b/crates/frontend/icons/32x32.png new file mode 100644 index 0000000..3b11f11 Binary files /dev/null and b/crates/frontend/icons/32x32.png differ diff --git a/crates/frontend/icons/64x64.png b/crates/frontend/icons/64x64.png new file mode 100644 index 0000000..ed9aebf Binary files /dev/null and b/crates/frontend/icons/64x64.png differ diff --git a/crates/frontend/icons/Square107x107Logo.png b/crates/frontend/icons/Square107x107Logo.png new file mode 100644 index 0000000..bb7a74b Binary files /dev/null and b/crates/frontend/icons/Square107x107Logo.png differ diff --git a/crates/frontend/icons/Square142x142Logo.png b/crates/frontend/icons/Square142x142Logo.png new file mode 100644 index 0000000..a6647cd Binary files /dev/null and b/crates/frontend/icons/Square142x142Logo.png differ diff --git a/crates/frontend/icons/Square150x150Logo.png b/crates/frontend/icons/Square150x150Logo.png new file mode 100644 index 0000000..6da443f Binary files /dev/null and b/crates/frontend/icons/Square150x150Logo.png differ diff --git a/crates/frontend/icons/Square284x284Logo.png b/crates/frontend/icons/Square284x284Logo.png new file mode 100644 index 0000000..fb4ea4d Binary files /dev/null and b/crates/frontend/icons/Square284x284Logo.png differ diff --git a/crates/frontend/icons/Square30x30Logo.png b/crates/frontend/icons/Square30x30Logo.png new file mode 100644 index 0000000..a1d96f7 Binary files /dev/null and b/crates/frontend/icons/Square30x30Logo.png differ diff --git a/crates/frontend/icons/Square310x310Logo.png b/crates/frontend/icons/Square310x310Logo.png new file mode 100644 index 0000000..76eb1db Binary files /dev/null and b/crates/frontend/icons/Square310x310Logo.png differ diff --git a/crates/frontend/icons/Square44x44Logo.png b/crates/frontend/icons/Square44x44Logo.png new file mode 100644 index 0000000..b269c74 Binary files /dev/null and b/crates/frontend/icons/Square44x44Logo.png differ diff --git a/crates/frontend/icons/Square71x71Logo.png b/crates/frontend/icons/Square71x71Logo.png new file mode 100644 index 0000000..0856021 Binary files /dev/null and b/crates/frontend/icons/Square71x71Logo.png differ diff --git a/crates/frontend/icons/Square89x89Logo.png b/crates/frontend/icons/Square89x89Logo.png new file mode 100644 index 0000000..1657048 Binary files /dev/null and b/crates/frontend/icons/Square89x89Logo.png differ diff --git a/crates/frontend/icons/StoreLogo.png b/crates/frontend/icons/StoreLogo.png new file mode 100644 index 0000000..53fd0d6 Binary files /dev/null and b/crates/frontend/icons/StoreLogo.png differ diff --git a/crates/frontend/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/crates/frontend/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/crates/frontend/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/crates/frontend/icons/android/mipmap-hdpi/ic_launcher.png b/crates/frontend/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..c6cfc55 Binary files /dev/null and b/crates/frontend/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/crates/frontend/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/crates/frontend/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..a13abbb Binary files /dev/null and b/crates/frontend/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/crates/frontend/icons/android/mipmap-hdpi/ic_launcher_round.png b/crates/frontend/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..45635c8 Binary files /dev/null and b/crates/frontend/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/crates/frontend/icons/android/mipmap-mdpi/ic_launcher.png b/crates/frontend/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..2222a62 Binary files /dev/null and b/crates/frontend/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/crates/frontend/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/crates/frontend/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..4a05f8f Binary files /dev/null and b/crates/frontend/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/crates/frontend/icons/android/mipmap-mdpi/ic_launcher_round.png b/crates/frontend/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..af241da Binary files /dev/null and b/crates/frontend/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/crates/frontend/icons/android/mipmap-xhdpi/ic_launcher.png b/crates/frontend/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..f8ad411 Binary files /dev/null and b/crates/frontend/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/crates/frontend/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/crates/frontend/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..ed1f451 Binary files /dev/null and b/crates/frontend/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/crates/frontend/icons/android/mipmap-xhdpi/ic_launcher_round.png b/crates/frontend/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..cbc7bca Binary files /dev/null and b/crates/frontend/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/crates/frontend/icons/android/mipmap-xxhdpi/ic_launcher.png b/crates/frontend/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..598cf0e Binary files /dev/null and b/crates/frontend/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/crates/frontend/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/crates/frontend/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..9dcc438 Binary files /dev/null and b/crates/frontend/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/crates/frontend/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/crates/frontend/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..f736634 Binary files /dev/null and b/crates/frontend/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/crates/frontend/icons/android/mipmap-xxxhdpi/ic_launcher.png b/crates/frontend/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..0a3a955 Binary files /dev/null and b/crates/frontend/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/crates/frontend/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/crates/frontend/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..804435a Binary files /dev/null and b/crates/frontend/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/crates/frontend/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/crates/frontend/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..01ebca2 Binary files /dev/null and b/crates/frontend/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/crates/frontend/icons/android/values/ic_launcher_background.xml b/crates/frontend/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..ea9c223 --- /dev/null +++ b/crates/frontend/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/crates/frontend/icons/icon.icns b/crates/frontend/icons/icon.icns new file mode 100644 index 0000000..0533e67 Binary files /dev/null and b/crates/frontend/icons/icon.icns differ diff --git a/crates/frontend/icons/icon.ico b/crates/frontend/icons/icon.ico new file mode 100644 index 0000000..5b3bc0e Binary files /dev/null and b/crates/frontend/icons/icon.ico differ diff --git a/crates/frontend/icons/icon.png b/crates/frontend/icons/icon.png new file mode 100644 index 0000000..3805981 Binary files /dev/null and b/crates/frontend/icons/icon.png differ diff --git a/crates/frontend/icons/ios/AppIcon-20x20@1x.png b/crates/frontend/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..67975d8 Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-20x20@1x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-20x20@2x-1.png b/crates/frontend/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..c8ed2a1 Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/crates/frontend/icons/ios/AppIcon-20x20@2x.png b/crates/frontend/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..c8ed2a1 Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-20x20@2x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-20x20@3x.png b/crates/frontend/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..7c6c86e Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-20x20@3x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-29x29@1x.png b/crates/frontend/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..e9cf2f9 Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-29x29@1x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-29x29@2x-1.png b/crates/frontend/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..b80f3d5 Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/crates/frontend/icons/ios/AppIcon-29x29@2x.png b/crates/frontend/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..b80f3d5 Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-29x29@2x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-29x29@3x.png b/crates/frontend/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..1f3a4e4 Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-29x29@3x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-40x40@1x.png b/crates/frontend/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..c8ed2a1 Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-40x40@1x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-40x40@2x-1.png b/crates/frontend/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..dc3b17f Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/crates/frontend/icons/ios/AppIcon-40x40@2x.png b/crates/frontend/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..dc3b17f Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-40x40@2x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-40x40@3x.png b/crates/frontend/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..740b02d Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-40x40@3x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-512@2x.png b/crates/frontend/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..fb51997 Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-512@2x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-60x60@2x.png b/crates/frontend/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..740b02d Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-60x60@2x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-60x60@3x.png b/crates/frontend/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..0a6214c Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-60x60@3x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-76x76@1x.png b/crates/frontend/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..3df5438 Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-76x76@1x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-76x76@2x.png b/crates/frontend/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..d9bdbf3 Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-76x76@2x.png differ diff --git a/crates/frontend/icons/ios/AppIcon-83.5x83.5@2x.png b/crates/frontend/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..38a8858 Binary files /dev/null and b/crates/frontend/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/crates/frontend/src/main.rs b/crates/frontend/src/main.rs new file mode 100644 index 0000000..f0785e6 --- /dev/null +++ b/crates/frontend/src/main.rs @@ -0,0 +1,1682 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::sync::{Arc, Mutex}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use common::GRAB_LOG_COLLECTOR; + +use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode}; +use rand::{Rng, distributions::Alphanumeric, thread_rng}; +use reqwest::{Client, header}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; + +use tauri::State; + +use backend::taskmanager::TaskManagerImpl; +use common::PushType; +use common::account::{Account, add_account}; +use common::captcha::LocalCaptcha; +use common::login::LoginInput; +use common::push::PushConfig; + +use common::taskmanager::{ + GetAllorderRequest, GetBuyerInfoRequest, GetTicketInfoRequest, TaskManager, TaskRequest, + TaskStatus, +}; +use common::ticket::{BilibiliTicket, TicketInfo}; +use common::utility::CustomConfig; +use common::utils::{Config, save_config}; + +const APP_NAME: &str = "BTR"; +const APP_VERSION: &str = "6.6.2-indev"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Project { + id: String, + name: String, + url: String, + created_at: u64, + updated_at: u64, +} + +#[derive(Clone)] +struct AppState { + inner: Arc>, +} + +#[derive(Clone)] +struct AppStateInner { + // App info + app: String, + version: String, + policy: Option, + public_key: String, + machine_id: String, + + // UI state + selected_tab: usize, + is_loading: bool, + running_status: String, + + // Logs + logs: Vec, + show_log_window: bool, + + // Login + show_login_window: bool, + login_method: String, + client: Client, + default_ua: String, + login_qrcode_url: Option, + qrcode_polling_task_id: Option, + login_input: LoginInput, + pending_sms_task_id: Option, + sms_captcha_key: String, + cookie_login: Option, + + // Account management + accounts: Vec, + delete_account: Option, + account_switch: Option, + + // Task management + task_manager: Arc>>, + + // Config + config: Config, + push_config: PushConfig, + custom_config: CustomConfig, + + // Ticket grabbing + ticket_id: String, + status_delay: usize, + grab_mode: u8, + selected_account_uid: Option, + bilibiliticket_list: Vec, + ticket_info: Option, + show_screen_info: Option, + selected_screen_index: Option, + selected_screen_id: Option, + selected_ticket_id: Option, + ticket_info_last_request_time: Option, + confirm_ticket_info: Option, + selected_buyer_list: Option>, + selected_no_bind_buyer_info: Option, + buyer_type: u8, // 0: 非实名购票人, 1: 实名购票人 + + // Buyer management + show_add_buyer_window: Option, + show_orderlist_window: Option, + total_order_data: Option, + orderlist_need_reload: bool, + orderlist_last_request_time: Option, + orderlist_requesting: bool, + + // QR code payment + show_qr_windows: Option, + + // Announcements + announce1: Option, + announce2: Option, + announce3: Option, + announce4: Option, + + // Other + skip_words: Option>, + skip_words_input: String, +} + +#[derive(Clone)] +struct OrderData { + account_id: String, + data: Option, +} + +#[derive(Clone)] +struct AccountSwitch { + uid: String, + switch: bool, +} + +impl AppState { + pub fn new() -> Self { + let config = Config::load_config().unwrap_or_else(|_| Config::new()); + + let mut state = AppStateInner { + app: APP_NAME.to_string(), + version: APP_VERSION.to_string(), + policy: None, + public_key: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApTAS0RElXIs4Kr0bO4n8\nJB+eBFF/TwXUlvtOM9FNgHjK8m13EdwXaLy9zjGTSQr8tshSRr0dQ6iaCG19Zo2Y\nXfvJrwQLqdezMN+ayMKFy58/S9EGG3Np2eGgKHUPnCOAlRicqWvBdQ/cxzTDNCxa\nORMZdJRoBvya7JijLLIC3CoqmMc6Fxe5i8eIP0zwlyZ0L0C1PQ82BcWn58y7tlPY\nTCz12cWnuKwiQ9LSOfJ4odJJQK0k7rXxwBBsYxULRno0CJ3rKfApssW4cfITYVax\nFtdbu0IUsgEeXs3EzNw8yIYnsaoZlFwLS8SMVsiAFOy2y14lR9043PYAQHm1Cjaf\noQIDAQAB\n-----END PUBLIC KEY-----".to_string(), + machine_id: common::machine_id::get_machine_id_ob(), + selected_tab: 0, + is_loading: false, + running_status: "空闲".to_string(), + logs: Vec::new(), + show_log_window: false, + show_login_window: false, + login_method: "扫码登录".to_string(), + client: Client::new(), + default_ua: default_user_agent(), + login_qrcode_url: None, + qrcode_polling_task_id: None, + login_input: LoginInput { + phone: String::new(), + account: String::new(), + password: String::new(), + cookie: String::new(), + sms_code: String::new(), + }, + pending_sms_task_id: None, + sms_captcha_key: String::new(), + cookie_login: None, + accounts: Config::load_all_accounts(), + delete_account: None, + account_switch: None, + task_manager: Arc::new(Mutex::new(Box::new(TaskManagerImpl::new()))), + config: config.clone(), + push_config: serde_json::from_value::(config["push_config"].clone()) + .unwrap_or_else(|_| PushConfig::new()), + custom_config: serde_json::from_value::(config["custom_config"].clone()) + .unwrap_or_else(|_| CustomConfig::new()), + ticket_id: String::new(), + status_delay: 2, + grab_mode: 0, + selected_account_uid: None, + bilibiliticket_list: Vec::new(), + ticket_info: None, + show_screen_info: None, + selected_screen_index: None, + selected_screen_id: None, + selected_ticket_id: None, + ticket_info_last_request_time: None, + confirm_ticket_info: None, + selected_buyer_list: None, + selected_no_bind_buyer_info: None, + buyer_type: 1, // 默认使用实名购票人 + show_add_buyer_window: None, + show_orderlist_window: None, + total_order_data: None, + orderlist_need_reload: false, + orderlist_last_request_time: None, + orderlist_requesting: false, + show_qr_windows: None, + announce1: None, + announce2: None, + announce3: None, + announce4: None, + skip_words: None, + skip_words_input: String::new(), + }; + + // Initialize client with custom UA + let random_value = generate_random_string(8); + state.default_ua = format!( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0 {}", + random_value + ); + + if state.custom_config.open_custom_ua && !state.custom_config.custom_ua.is_empty() { + state.default_ua = state.custom_config.custom_ua.clone(); + } + + let new_client = create_client(state.default_ua.clone()); + state.client = new_client; + + // Initialize accounts + for account in &mut state.accounts { + account.ensure_client(); + } + + Self { + inner: Arc::new(Mutex::new(state)), + } + } +} + +#[tauri::command] +fn get_accounts(state: State<'_, AppState>) -> Result, String> { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + Ok(state.accounts.clone()) +} + +#[tauri::command] +fn reload_accounts(state: State<'_, AppState>) -> Result, String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.accounts = Config::load_all_accounts(); + Ok(state.accounts.clone()) +} + +#[tauri::command] +fn add_account_by_cookie(state: State<'_, AppState>, cookie: String) -> Result { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + let account = add_account(&cookie, &state.client, &state.default_ua)?; + save_config(&mut state.config, None, None, Some(account.clone())) + .map_err(|e| format!("save config failed: {}", e))?; + state.accounts.push(account.clone()); + Ok(account) +} + +#[tauri::command] +fn delete_account_by_uid(state: State<'_, AppState>, uid: i64) -> Result { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + let before = state.accounts.len(); + state.accounts.retain(|account| account.uid != uid); + state.config.delete_account(uid); + Ok(before != state.accounts.len()) +} + +#[tauri::command] +fn set_account_active(state: State<'_, AppState>, uid: i64, active: bool) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + if let Some(account) = state.accounts.iter_mut().find(|a| a.uid == uid) { + account.is_active = active; + let account_clone = account.clone(); + drop(state); + + let mut config = Config::load_config().map_err(|e| format!("load config failed: {}", e))?; + save_config(&mut config, None, None, Some(account_clone)) + .map_err(|e| format!("save config failed: {}", e))?; + return Ok(()); + } + Err("account not found".to_string()) +} + +#[tauri::command] +fn qrcode_login(state: State<'_, AppState>) -> Result { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + let qrcode_key = + common::login::qrcode_login(&state.client).map_err(|e| format!("生成二维码失败: {}", e))?; + + let qrcode_url = format!( + "https://passport.bilibili.com/h5-app/passport/login/scan?qrcode_key={}", + qrcode_key + ); + + use image::Luma; + use qrcode::QrCode; + + let code = QrCode::new(qrcode_url.as_bytes()).map_err(|e| format!("生成二维码失败: {}", e))?; + + let image = code + .render::>() + .min_dimensions(200, 200) + .max_dimensions(400, 400) + .build(); + + let mut png_data: Vec = Vec::new(); + image + .write_to( + &mut std::io::Cursor::new(&mut png_data), + image::ImageFormat::Png, + ) + .map_err(|e| format!("转换图片失败: {}", e))?; + + let base64_image = + base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &png_data); + let data_url = format!("data:image/png;base64,{}", base64_image); + + let request = TaskRequest::QrCodeLoginRequest(common::taskmanager::QrCodeLoginRequest { + qrcode_key: qrcode_key.clone(), + qrcode_url: qrcode_url.clone(), + user_agent: Some(state.default_ua.clone()), + }); + + let task_id = state + .task_manager + .lock() + .map_err(|_| "Failed to lock task manager".to_string())? + .submit_task(request) + .map_err(|e| format!("提交二维码登录任务失败: {}", e))?; + + Ok(json!({ + "key": qrcode_key, + "url": data_url, + "task_id": task_id, + "message": "二维码生成成功,请使用B站APP扫描" + })) +} + +#[tauri::command] +fn sms_login(state: State<'_, AppState>, phone: String) -> Result { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + let request = TaskRequest::LoginSmsRequest(common::taskmanager::LoginSmsRequest { + phone: phone.clone(), + client: state.client.clone(), + custom_config: state.custom_config.clone(), + local_captcha: common::captcha::LocalCaptcha::new(), + }); + + let result = state + .task_manager + .lock() + .map_err(|_| "Failed to lock task manager".to_string())? + .submit_task(request) + .map_err(|e| format!("submit sms login failed: {}", e)); + + result +} + +#[tauri::command] +fn submit_sms_code( + state: State<'_, AppState>, + captcha_key: String, + sms_code: String, +) -> Result { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + let request = TaskRequest::SubmitLoginSmsRequest(common::taskmanager::SubmitLoginSmsRequest { + phone: "".to_string(), + code: sms_code, + captcha_key, + client: state.client.clone(), + }); + + let result = state + .task_manager + .lock() + .map_err(|_| "Failed to lock task manager".to_string())? + .submit_task(request) + .map_err(|e| format!("submit sms code failed: {}", e)); + + result +} + +#[tauri::command] +fn get_ticket_info( + state: State<'_, AppState>, + uid: i64, + project_id: String, +) -> Result { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + let account = state + .accounts + .iter() + .find(|a| a.uid == uid) + .ok_or_else(|| "account not found".to_string())?; + + let cookie_manager = account + .cookie_manager + .clone() + .ok_or_else(|| "cookie manager not initialized".to_string())?; + + let task_id = uuid::Uuid::new_v4().to_string(); + let request = TaskRequest::GetTicketInfoRequest(GetTicketInfoRequest { + uid, + task_id: task_id.clone(), + project_id, + cookie_manager, + }); + + let result = state + .task_manager + .lock() + .map_err(|_| "Failed to lock task manager".to_string())? + .submit_task(request) + .map_err(|e| format!("submit ticket info request failed: {}", e)); + + result +} + +#[tauri::command] +fn get_buyer_info(state: State<'_, AppState>, uid: i64) -> Result { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + let account = state + .accounts + .iter() + .find(|a| a.uid == uid) + .ok_or_else(|| "account not found".to_string())?; + + let cookie_manager = account + .cookie_manager + .clone() + .ok_or_else(|| "cookie manager not initialized".to_string())?; + + let task_id = uuid::Uuid::new_v4().to_string(); + let request = TaskRequest::GetBuyerInfoRequest(GetBuyerInfoRequest { + uid, + task_id: task_id.clone(), + cookie_manager, + }); + + let result = state + .task_manager + .lock() + .map_err(|_| "Failed to lock task manager".to_string())? + .submit_task(request) + .map_err(|e| format!("submit buyer info request failed: {}", e)); + + result +} + +#[tauri::command] +fn get_order_list(state: State<'_, AppState>, uid: i64) -> Result { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + let account = state + .accounts + .iter() + .find(|a| a.uid == uid) + .ok_or_else(|| "account not found".to_string())?; + + let cookie_manager = account + .cookie_manager + .clone() + .ok_or_else(|| "cookie manager not initialized".to_string())?; + + let request = TaskRequest::GetAllorderRequest(GetAllorderRequest { + task_id: "".to_string(), + cookie_manager, + status: TaskStatus::Pending, + cookies: account.cookie.clone(), + account_id: uid.to_string(), + start_time: None, + }); + + let result = state + .task_manager + .lock() + .map_err(|_| "Failed to lock task manager".to_string())? + .submit_task(request) + .map_err(|e| format!("submit order list request failed: {}", e)); + + result +} + +#[tauri::command] +fn poll_task_results(state: State<'_, AppState>) -> Result { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + let results = state + .task_manager + .lock() + .map_err(|_| "Failed to lock task manager".to_string())? + .get_results(); + let json_results: Vec = results + .into_iter() + .map(|result| match result { + common::taskmanager::TaskResult::QrCodeLoginResult(r) => json!({ + "type": "QrCodeLoginResult", + "task_id": r.task_id, + "status": format!("{:?}", r.status), + "cookie": r.cookie, + "error": r.error + }), + common::taskmanager::TaskResult::LoginSmsResult(r) => json!({ + "type": "LoginSmsResult", + "success": r.success, + "message": r.message + }), + common::taskmanager::TaskResult::SubmitSmsLoginResult(r) => json!({ + "type": "SubmitSmsLoginResult", + "success": r.success, + "message": r.message, + "cookie": r.cookie + }), + common::taskmanager::TaskResult::PushResult(r) => json!({ + "type": "PushResult", + "success": r.success, + "message": r.message + }), + common::taskmanager::TaskResult::GetAllorderRequestResult(r) => json!({ + "type": "GetAllorderRequestResult", + "task_id": r.task_id, + "success": r.success, + "account_id": r.account_id, + "message": r.message, + "order_info": r.order_info + }), + common::taskmanager::TaskResult::GetTicketInfoResult(r) => json!({ + "type": "GetTicketInfoResult", + "task_id": r.task_id, + "success": r.success, + "uid": r.uid, + "message": r.message, + "ticket_info": r.ticket_info + }), + common::taskmanager::TaskResult::GetBuyerInfoResult(r) => json!({ + "type": "GetBuyerInfoResult", + "task_id": r.task_id, + "success": r.success, + "uid": r.uid, + "message": r.message, + "buyer_info": r.buyer_info + }), + common::taskmanager::TaskResult::GrabTicketResult(r) => json!({ + "type": "GrabTicketResult", + "success": r.success, + "order_id": r.order_id, + "message": r.message, + "pay_result": r.pay_result, + "confirm_result": r.confirm_result + }), + }) + .collect(); + + Ok(json!(json_results)) +} + +#[tauri::command] +fn push_test(state: State<'_, AppState>, title: String, message: String) -> Result<(), String> { + let push_config = { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + if !state.push_config.enabled { + return Err("push is disabled".to_string()); + } + state.push_config.clone() + }; + + let request = TaskRequest::PushRequest(common::taskmanager::PushRequest { + title, + message, + jump_url: None, + push_config, + push_type: PushType::All, + }); + + { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + let result = state + .task_manager + .lock() + .map_err(|_| "Failed to lock task manager".to_string())? + .submit_task(request) + .map(|_| ()) + .map_err(|e| format!("submit push failed: {}", e)); + result + } +} + +#[tauri::command] +async fn get_policy(state: State<'_, AppState>) -> Result { + let (machine_id, app, version, client) = { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + ( + state.machine_id.clone(), + state.app.clone(), + state.version.clone(), + state.client.clone(), + ) + }; + + let data = json!({ + "ts": current_timestamp(), + "machine_id": machine_id + }); + + let url = format!( + "https://policy.nexaorion.cn/api/client/{}/{}/dispatch.json", + app, version + ); + + let resp = client + .post(&url) + .json(&data) + .send() + .await + .map_err(|e| format!("request failed: {}", e))?; + + let value: Value = resp + .json() + .await + .map_err(|e| format!("parse failed: {}", e))?; + + if value["code"].as_i64().unwrap_or(-1) != 0 { + return Ok(json!({ "allow_run": true })); + } + + let policy_token = value["data"]["data"].as_str().unwrap_or(""); + let public_key = { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.public_key.clone() + }; + let policy = decode_policy(policy_token, &public_key)?; + if let Some(permission_token) = value["data"]["permission"].as_str() { + let permissions = decode_permissions(permission_token, &public_key)?; + save_permissions(permission_token); + { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + if let Value::Object(obj) = &mut state.config["permissions"] { + *obj = permissions.as_object().cloned().unwrap_or_default(); + } + } + } + Ok(policy) +} + +#[tauri::command] +fn get_logs(state: State<'_, AppState>) -> Result, String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + if let Some(logs) = common::LOG_COLLECTOR + .lock() + .ok() + .and_then(|mut c| c.get_logs()) + { + for log in logs { + state.logs.push(log); + } + } + if state.logs.len() > 5000 { + state.logs.drain(0..2500); + } + Ok(state.logs.clone()) +} + +#[tauri::command] +fn get_grab_logs(state: State<'_, AppState>) -> Result, String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + // 从抢票日志收集器获取日志 + if let Some(logs) = GRAB_LOG_COLLECTOR.lock().ok().and_then( + |mut c: std::sync::MutexGuard<'_, common::record_log::GrabLogCollector>| c.get_logs(), + ) { + for log in logs { + state.logs.push(log); + } + } + + // 过滤出抢票相关的日志 + let grab_logs: Vec = state + .logs + .iter() + .filter(|log| { + let log_str = log.to_lowercase(); + log_str.contains("抢票") + || log_str.contains("token") + || log_str.contains("订单") + || log_str.contains("验证码") + || log_str.contains("倒计时") + || log_str.contains("项目") + || log_str.contains("场次") + || log_str.contains("购票人") + || log_str.contains("开始抢票") + || log_str.contains("获取token") + || log_str.contains("确认订单") + || log_str.contains("下单") + || log_str.contains("重试") + || log_str.contains("失败") + || log_str.contains("成功") + || log_str.contains("距离抢票时间") + || log_str.contains("获取购票人信息") + || log_str.contains("获取项目详情") + || log_str.contains("二维码") + || log_str.contains("短信") + || log_str.contains("登录") + }) + .cloned() + .collect(); + + if grab_logs.len() > 5000 { + let skip_count = grab_logs.len() - 5000; + Ok(grab_logs.into_iter().skip(skip_count).collect()) + } else { + Ok(grab_logs) + } +} + +#[tauri::command] +fn add_log(state: State<'_, AppState>, message: String) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.logs.push(message); + if state.logs.len() > 5000 { + state.logs.drain(0..2500); + } + Ok(()) +} + +#[tauri::command] +fn get_app_info(state: State<'_, AppState>) -> Result { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + Ok(json!({ + "app": state.app, + "version": state.version, + "running_status": state.running_status, + "machine_id": state.machine_id, + "announce1": state.announce1, + "announce2": state.announce2, + "announce3": state.announce3, + "announce4": state.announce4 + })) +} + +#[tauri::command] +fn clear_grab_logs() -> Result<(), String> { + if let Ok(mut collector) = GRAB_LOG_COLLECTOR.lock() { + collector.clear_logs(); + } + Ok(()) +} + +#[tauri::command] +fn set_ticket_id(state: State<'_, AppState>, ticket_id: String) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.ticket_id = ticket_id; + Ok(()) +} + +#[tauri::command] +fn set_grab_mode(state: State<'_, AppState>, mode: u8) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.grab_mode = mode; + Ok(()) +} + +#[tauri::command] +fn cancel_task(state: State<'_, AppState>, task_id: String) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + let mut task_manager = state + .task_manager + .lock() + .map_err(|_| "Failed to lock task manager".to_string())?; + + task_manager.cancel_task(&task_id)?; + + log::info!("已取消任务: {}", task_id); + Ok(()) +} + +#[tauri::command] +fn start_grab_ticket(state: State<'_, AppState>) -> Result { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + // 验证必要信息 + if state.ticket_id.is_empty() { + return Err("请先选择项目".to_string()); + } + + if state.accounts.is_empty() { + return Err("请先添加账号".to_string()); + } + + // 获取选中的账号或使用第一个活跃账号 + let selected_account = if let Some(uid) = state.selected_account_uid { + state.accounts.iter().find(|acc| acc.uid == uid) + } else { + state.accounts.iter().find(|acc| acc.is_active) + }; + + let account = selected_account + .ok_or_else(|| "没有可用的账号,请确保至少有一个账号是激活状态".to_string())?; + + // 验证账号有 cookie_manager + let cookie_manager = account + .cookie_manager + .clone() + .ok_or_else(|| "账号未初始化,请重新添加账号".to_string())?; + + let (id_bind, buyer_info, no_bind_buyer_info) = match state.buyer_type { + 0 => { + // 非实名购票人信息 + if state.selected_no_bind_buyer_info.is_none() { + return Err("请先设置非实名购票人信息".to_string()); + } + (0, None, state.selected_no_bind_buyer_info.clone()) + } + 1 => { + // 实名购票人信息 + if state.selected_buyer_list.is_none() { + return Err("请先选择实名购票人信息".to_string()); + } + (1, state.selected_buyer_list.clone(), None) + } + 2 => { + // 实名购票人信息(备用模式) + if state.selected_buyer_list.is_none() { + return Err("请先选择实名购票人信息".to_string()); + } + (2, state.selected_buyer_list.clone(), None) + } + _ => { + return Err("无效的购票人类型".to_string()); + } + }; + + let biliticket = BilibiliTicket { + uid: account.uid, + method: 0, + ua: state.default_ua.clone(), + config: state.custom_config.clone(), + account: account.clone(), + push_self: state.push_config.clone(), + status_delay: state.status_delay, + captcha_use_type: 0, + cookie_manager: account.cookie_manager.clone(), + project_id: state.ticket_id.clone(), + screen_id: state + .selected_screen_id + .map(|id| id.to_string()) + .unwrap_or_default(), + id_bind, + project_info: state.ticket_info.clone(), + all_buyer_info: None, + buyer_info, + no_bind_buyer_info, + select_ticket_id: state.selected_ticket_id.map(|id| id.to_string()), + pay_money: None, + count: Some(1), + device_id: String::new(), + }; + + // 生成任务ID + let task_id = format!( + "{}-{}", + account.uid, + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() + ); + + // 创建抢票请求 + let grab_request = TaskRequest::GrabTicketRequest(common::taskmanager::GrabTicketRequest { + task_id: task_id.clone(), + uid: account.uid, + project_id: state.ticket_id.clone(), + screen_id: state + .selected_screen_id + .map(|id| id.to_string()) + .unwrap_or_default(), + ticket_id: state + .selected_ticket_id + .map(|id| id.to_string()) + .unwrap_or_default(), + count: 1, + buyer_info: vec![], + cookie_manager, + biliticket, + grab_mode: state.grab_mode, + status: TaskStatus::Pending, + start_time: None, + is_hot: false, + local_captcha: LocalCaptcha::new(), + skip_words: None, + }); + + // 提交任务 + state + .task_manager + .lock() + .map_err(|_| "Failed to lock task manager".to_string())? + .submit_task(grab_request) + .map_err(|e| format!("提交抢票任务失败: {}", e))?; + + Ok(task_id) +} + +#[tauri::command] +fn set_selected_account(state: State<'_, AppState>, uid: Option) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.selected_account_uid = uid; + Ok(()) +} + +#[tauri::command] +fn set_show_screen_info(state: State<'_, AppState>, uid: Option) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.show_screen_info = uid; + Ok(()) +} + +#[tauri::command] +fn set_confirm_ticket_info(state: State<'_, AppState>, uid: Option) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.confirm_ticket_info = uid; + Ok(()) +} + +#[tauri::command] +fn set_show_add_buyer_window( + state: State<'_, AppState>, + uid: Option, +) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.show_add_buyer_window = uid; + Ok(()) +} + +#[tauri::command] +fn set_show_orderlist_window( + state: State<'_, AppState>, + uid: Option, +) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.show_orderlist_window = uid; + Ok(()) +} + +#[tauri::command] +fn set_show_qr_windows(state: State<'_, AppState>, qr_data: Option) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.show_qr_windows = qr_data; + Ok(()) +} + +#[tauri::command] +fn set_login_method(state: State<'_, AppState>, method: String) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.login_method = method; + Ok(()) +} + +#[tauri::command] +fn set_show_login_window(state: State<'_, AppState>, show: bool) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.show_login_window = show; + Ok(()) +} + +#[tauri::command] +fn set_login_input(state: State<'_, AppState>, input: LoginInput) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.login_input = input; + Ok(()) +} + +#[tauri::command] +fn set_cookie_login(state: State<'_, AppState>, cookie: Option) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.cookie_login = cookie; + Ok(()) +} + +#[tauri::command] +fn set_delete_account(state: State<'_, AppState>, uid: Option) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.delete_account = uid; + Ok(()) +} + +#[tauri::command] +fn set_account_switch(state: State<'_, AppState>, uid: String, switch: bool) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.account_switch = Some(AccountSwitch { uid, switch }); + Ok(()) +} + +#[tauri::command] +fn set_selected_screen( + state: State<'_, AppState>, + index: Option, + id: Option, +) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.selected_screen_index = index; + state.selected_screen_id = id; + Ok(()) +} + +#[tauri::command] +fn set_selected_ticket(state: State<'_, AppState>, id: Option) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.selected_ticket_id = id; + Ok(()) +} + +#[tauri::command] +fn set_selected_buyer_list( + state: State<'_, AppState>, + buyer_list: Option>, +) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.selected_buyer_list = buyer_list; + Ok(()) +} + +#[tauri::command] +fn set_buyer_type(state: State<'_, AppState>, buyer_type: u8) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.buyer_type = buyer_type; + Ok(()) +} + +#[tauri::command] +fn set_no_bind_buyer_info( + state: State<'_, AppState>, + name: String, + tel: String, +) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + let no_bind_buyer_info = common::ticket::NoBindBuyerInfo { + name, + tel, + uid: 0, // 非实名购票人没有uid + }; + + state.selected_no_bind_buyer_info = Some(no_bind_buyer_info); + Ok(()) +} + +#[tauri::command] +fn clear_no_bind_buyer_info(state: State<'_, AppState>) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.selected_no_bind_buyer_info = None; + Ok(()) +} + +#[tauri::command] +fn set_skip_words(state: State<'_, AppState>, words: Option>) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.skip_words = words; + Ok(()) +} + +#[tauri::command] +fn set_skip_words_input(state: State<'_, AppState>, input: String) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + state.skip_words_input = input; + Ok(()) +} + +#[tauri::command] +fn get_state(state: State<'_, AppState>) -> Result { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + Ok(json!({ + "selected_tab": state.selected_tab, + "is_loading": state.is_loading, + "running_status": state.running_status, + "show_log_window": state.show_log_window, + "show_login_window": state.show_login_window, + "login_method": state.login_method, + "ticket_id": state.ticket_id, + "status_delay": state.status_delay, + "grab_mode": state.grab_mode, + "selected_account_uid": state.selected_account_uid, + "show_screen_info": state.show_screen_info, + "selected_screen_index": state.selected_screen_index, + "selected_screen_id": state.selected_screen_id, + "selected_ticket_id": state.selected_ticket_id, + "confirm_ticket_info": state.confirm_ticket_info, + "show_add_buyer_window": state.show_add_buyer_window, + "show_orderlist_window": state.show_orderlist_window, + "show_qr_windows": state.show_qr_windows, + "skip_words_input": state.skip_words_input, + "login_input": { + "phone": state.login_input.phone, + "account": state.login_input.account, + "password": state.login_input.password, + "cookie": state.login_input.cookie, + "sms_code": state.login_input.sms_code + } + })) +} + +// ========== Helper Functions ========== + +fn create_client(user_agent: String) -> Client { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::USER_AGENT, + header::HeaderValue::from_str(&user_agent).unwrap_or_else(|_| { + header::HeaderValue::from_static( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + ) + }), + ); + + Client::builder() + .default_headers(headers) + .cookie_store(true) + .build() + .unwrap_or_default() +} + +fn default_user_agent() -> String { + let random_value = generate_random_string(8); + format!( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0 {}", + random_value + ) +} + +fn generate_random_string(length: usize) -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(|c| c as char) + .collect() +} + +fn current_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +#[derive(Debug, Serialize, Deserialize)] +struct PolicyPayload { + policy: Value, +} + +#[derive(Debug, Serialize, Deserialize)] +struct PermissionsPayload { + permissions: Value, +} + +fn decode_policy(token: &str, public_key: &str) -> Result { + let decoding_key = DecodingKey::from_rsa_pem(public_key.as_bytes()) + .map_err(|e| format!("invalid public key: {}", e))?; + let mut validation = Validation::new(Algorithm::RS256); + validation.validate_exp = false; + validation.required_spec_claims.clear(); + + decode::(token, &decoding_key, &validation) + .map(|data| data.claims.policy) + .map_err(|e| format!("decode policy failed: {}", e)) +} + +fn decode_permissions(token: &str, public_key: &str) -> Result { + let decoding_key = DecodingKey::from_rsa_pem(public_key.as_bytes()) + .map_err(|e| format!("invalid public key: {}", e))?; + let mut validation = Validation::new(Algorithm::RS256); + validation.validate_exp = false; + validation.required_spec_claims.clear(); + + decode::(token, &decoding_key, &validation) + .map(|data| data.claims.permissions) + .map_err(|e| format!("decode permissions failed: {}", e)) +} + +fn save_permissions(token: &str) { + if let Ok(mut file) = std::fs::File::create("permissions") { + let _ = std::io::Write::write_all(&mut file, token.as_bytes()); + } +} + +// ========== 项目管理函数 ========== + +#[tauri::command] +fn add_project( + state: State<'_, AppState>, + id: String, + name: String, + url: String, +) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + // 创建新项目 + let project = Project { + id: id.clone(), + name: name.clone(), + url: url.clone(), + created_at: current_timestamp(), + updated_at: current_timestamp(), + }; + + // 添加到配置中 + if !state.config["projects"].is_array() { + state.config["projects"] = json!([]); + } + + if let Value::Array(ref mut projects) = state.config["projects"] { + // 检查是否已存在相同ID的项目 + for existing_project in projects.iter() { + if existing_project["id"].as_str() == Some(&id) { + return Err("项目ID已存在".to_string()); + } + } + + let project_json = + serde_json::to_value(&project).map_err(|e| format!("序列化项目失败: {}", e))?; + projects.push(project_json); + } + + // 保存配置 + if let Err(e) = state.config.save_config() { + log::error!("保存项目失败: {}", e); + return Err(format!("保存项目失败: {}", e)); + } + + log::info!("项目添加成功: ID={}, 名称={}", id, name); + Ok(()) +} + +#[tauri::command] +fn get_projects(state: State<'_, AppState>) -> Result, String> { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + // 从配置中获取项目列表 + let projects = if state.config["projects"].is_array() { + let projects_json = &state.config["projects"]; + serde_json::from_value(projects_json.clone()) + .map_err(|e| format!("解析项目列表失败: {}", e))? + } else { + Vec::new() + }; + + Ok(projects) +} + +#[tauri::command] +fn delete_project(state: State<'_, AppState>, id: String) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + if !state.config["projects"].is_array() { + return Err("项目列表不存在".to_string()); + } + + if let Value::Array(ref mut projects) = state.config["projects"] { + let original_len = projects.len(); + projects.retain(|project| project["id"].as_str() != Some(&id)); + + if projects.len() == original_len { + return Err("未找到指定ID的项目".to_string()); + } + + // 保存配置 + if let Err(e) = state.config.save_config() { + log::error!("删除项目后保存失败: {}", e); + return Err(format!("删除项目后保存失败: {}", e)); + } + + log::info!("项目删除成功: ID={}", id); + Ok(()) + } else { + Err("项目列表格式错误".to_string()) + } +} + +// ========== 监控统计函数 ========== + +#[tauri::command] +fn get_monitor_stats(state: State<'_, AppState>) -> Result { + let state = state + .inner + .lock() + .map_err(|_| "state lock失败".to_string())?; + + // 简化实现:返回基本统计信息 + // TODO: 未来从任务管理器获取实时统计 + Ok(json!({ + "attempts": 0, + "success": 0, + "failures": 0, + "running": state.running_status.contains("运行") || state.running_status.contains("抢票"), + "active_tasks": 0, + "completed_tasks": 0 + })) +} + +#[tauri::command] +fn get_recent_logs(state: State<'_, AppState>, count: usize) -> Result, String> { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + let logs: Vec = state.logs.iter().rev().take(count).cloned().collect(); + + Ok(logs) +} + +#[tauri::command] +fn save_settings( + state: State<'_, AppState>, + grab_mode: u8, + delay_time: usize, + max_attempts: i32, + enable_push: bool, + enabled_methods: Vec, + bark_token: String, + pushplus_token: String, + fangtang_token: String, + dingtalk_token: String, + wechat_token: String, + gotify_url: String, + gotify_token: String, + smtp_server: String, + smtp_port: String, + smtp_username: String, + smtp_password: String, + smtp_from: String, + smtp_to: String, + custom_ua: bool, + user_agent: String, +) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + state.grab_mode = grab_mode; + + state.status_delay = delay_time; + + state.config["max_attempts"] = json!(max_attempts); + log::info!("最大尝试次数设置: {}", max_attempts); + + state.push_config.enabled = enable_push; + state.push_config.enabled_methods = enabled_methods; + state.push_config.bark_token = bark_token; + state.push_config.pushplus_token = pushplus_token; + state.push_config.fangtang_token = fangtang_token; + state.push_config.dingtalk_token = dingtalk_token; + state.push_config.wechat_token = wechat_token; + + state.push_config.gotify_config.gotify_url = gotify_url; + state.push_config.gotify_config.gotify_token = gotify_token; + + state.push_config.smtp_config.smtp_server = smtp_server; + state.push_config.smtp_config.smtp_port = smtp_port; + state.push_config.smtp_config.smtp_username = smtp_username; + state.push_config.smtp_config.smtp_password = smtp_password; + state.push_config.smtp_config.smtp_from = smtp_from; + state.push_config.smtp_config.smtp_to = smtp_to; + + state.custom_config.open_custom_ua = custom_ua; + state.custom_config.custom_ua = user_agent.clone(); + + if custom_ua && !user_agent.is_empty() { + state.default_ua = user_agent.clone(); + let new_client = create_client(user_agent.clone()); + state.client = new_client; + } + + if let Err(e) = state.config.save_config() { + log::error!("保存配置失败: {}", e); + return Err(format!("保存配置失败: {}", e)); + } + + log::info!("设置已保存!"); + Ok(()) +} + +#[tauri::command] +fn clear_logs(state: State<'_, AppState>) -> Result<(), String> { + let mut state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + state.logs.clear(); + + log::info!("日志已清空"); + Ok(()) +} + +#[tauri::command] +fn poll_qrcode_status( + state: State<'_, AppState>, + key: String, +) -> Result { + let state = state + .inner + .lock() + .map_err(|_| "state lock failed".to_string())?; + + let rt = tokio::runtime::Runtime::new().map_err(|e| format!("创建运行时失败: {}", e))?; + + let status = + rt.block_on(async { backend::api::poll_qrcode_login(&key, Some(&state.default_ua)).await }); + + match status { + common::login::QrCodeLoginStatus::Pending => Ok(json!({ + "status": "pending", + "message": "二维码已生成,等待扫描", + "key": key + })), + common::login::QrCodeLoginStatus::Scanning => Ok(json!({ + "status": "scanning", + "message": "二维码已扫描,等待确认", + "key": key + })), + common::login::QrCodeLoginStatus::Confirming => Ok(json!({ + "status": "confirming", + "message": "二维码已确认,正在登录", + "key": key + })), + common::login::QrCodeLoginStatus::Success(cookie) => Ok(json!({ + "status": "success", + "message": "登录成功", + "key": key, + "cookie": cookie + })), + common::login::QrCodeLoginStatus::Failed(error) => Ok(json!({ + "status": "error", + "message": format!("登录失败: {}", error), + "key": key + })), + common::login::QrCodeLoginStatus::Expired => Ok(json!({ + "status": "expired", + "message": "二维码已过期", + "key": key + })), + } +} + +fn main() { + if let Err(e) = common::init_logger() { + eprintln!("初始化日志失败,原因: {}", e); + } + log::info!("日志初始化成功"); + + std::panic::set_hook(Box::new(|panic_info| { + if let Some(s) = panic_info.payload().downcast_ref::<&str>() { + if s.contains("swap") || s.contains("vsync") { + log::warn!("图形渲染非致命错误: {}", s); + } else { + log::error!("程序panic: {}", panic_info); + } + } else { + log::error!("程序panic: {}", panic_info); + } + })); + + if !common::utils::ensure_single_instance() { + eprintln!("程序已经在运行中,请勿重复启动!"); + std::thread::sleep(std::time::Duration::from_secs(5)); + std::process::exit(1); + } + + tauri::Builder::default() + .manage(AppState::new()) + .invoke_handler(tauri::generate_handler![ + get_accounts, + reload_accounts, + add_account_by_cookie, + delete_account_by_uid, + set_account_active, + qrcode_login, + sms_login, + submit_sms_code, + get_ticket_info, + get_buyer_info, + get_order_list, + poll_task_results, + push_test, + get_policy, + get_logs, + get_grab_logs, + add_log, + get_app_info, + clear_grab_logs, + cancel_task, + set_ticket_id, + set_grab_mode, + set_selected_account, + set_show_screen_info, + set_confirm_ticket_info, + set_show_add_buyer_window, + set_show_orderlist_window, + set_show_qr_windows, + set_login_method, + set_show_login_window, + set_login_input, + set_cookie_login, + start_grab_ticket, + set_delete_account, + set_account_switch, + set_selected_screen, + set_selected_ticket, + set_selected_buyer_list, + set_skip_words, + set_skip_words_input, + get_state, + add_project, + get_projects, + delete_project, + get_monitor_stats, + get_recent_logs, + save_settings, + clear_logs, + poll_qrcode_status, + set_buyer_type, + set_no_bind_buyer_info, + clear_no_bind_buyer_info + ]) + .run(tauri::generate_context!()) + .expect("tauri run failed"); +} diff --git a/crates/frontend/tauri.conf.json b/crates/frontend/tauri.conf.json new file mode 100644 index 0000000..8b50e55 --- /dev/null +++ b/crates/frontend/tauri.conf.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "BTR", + "version": "7.0.0", + "identifier": "com.biliticket.btr", + "build": { + "frontendDist": "web", + "beforeDevCommand": "", + "beforeBuildCommand": "" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "Bilibili Ticket Rush", + "width": 1200, + "height": 600, + "minWidth": 800, + "minHeight": 600, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": null + } + } +} diff --git a/crates/frontend/web/app.js b/crates/frontend/web/app.js new file mode 100644 index 0000000..8140ef0 --- /dev/null +++ b/crates/frontend/web/app.js @@ -0,0 +1,1859 @@ +let invoke = null; + +let qrcodePollingInterval = null; +let currentTaskId = null; +let monitorStats = { + attempts: 0, + success: 0, + failures: 0, +}; + +function showNotification(message, type = "info", duration = 5000) { + const container = document.getElementById("notification-container"); + if (!container) return; + + const notification = document.createElement("div"); + notification.className = `notification ${type}`; + + const content = document.createElement("div"); + content.className = "notification-content"; + content.textContent = message; + + const closeBtn = document.createElement("button"); + closeBtn.className = "notification-close"; + closeBtn.innerHTML = "×"; + closeBtn.onclick = () => { + notification.classList.add("hiding"); + setTimeout(() => notification.remove(), 300); + }; + + notification.appendChild(content); + notification.appendChild(closeBtn); + container.appendChild(notification); + + if (duration > 0) { + setTimeout(() => { + if (notification.parentNode) { + notification.classList.add("hiding"); + setTimeout(() => notification.remove(), 300); + } + }, duration); + } + + return notification; +} + +function showAlert(message, type = "info") { + showNotification(message, type, 5000); +} + +function showSuccess(message) { + showNotification(message, "success", 3000); +} + +function showError(message) { + showNotification(message, "error", 7000); +} + +function showWarning(message) { + showNotification(message, "warning", 5000); +} + +function initializeEventListeners() { + document.querySelectorAll(".nav-tab").forEach((tab) => { + const tabName = tab.getAttribute("data-tab"); + if (tabName) { + tab.addEventListener("click", (e) => { + e.preventDefault(); + switchTab(tabName); + return false; + }); + } + }); + + const buttonIds = { + "add-account-btn": showAddAccountModal, + "reload-accounts-btn": reloadAccounts, + "qrcode-login-btn": showQrcodeLoginModal, + + "start-grab-btn": startGrab, + "stop-grab-btn": stopGrab, + "refresh-monitor-btn": refreshMonitor, + + "load-logs-btn": loadLogs, + "clear-logs-btn": clearLogs, + "export-logs-btn": exportLogs, + }; + + Object.keys(buttonIds).forEach((id) => { + const element = document.getElementById(id); + const handler = buttonIds[id]; + if (element && handler) { + element.addEventListener("click", (e) => { + e.preventDefault(); + handler(); + }); + } + }); + + document.querySelectorAll("form").forEach((form) => { + form.addEventListener("submit", (e) => { + e.preventDefault(); + }); + }); +} + +document.addEventListener("DOMContentLoaded", function () { + console.log("DOM loaded, initializing application..."); + + initializeTabSwitching(); + initializeEventListeners(); + + let attempts = 0; + const maxAttempts = 20; + const checkInterval = 100; + + function checkTauriAvailability() { + attempts++; + console.log( + `Checking for Tauri API (attempt ${attempts}/${maxAttempts})...`, + ); + + if ( + window.__TAURI__ && + window.__TAURI__.core && + window.__TAURI__.core.invoke + ) { + console.log("Tauri API found!"); + initializeApp(); + } else if (attempts < maxAttempts) { + setTimeout(checkTauriAvailability, checkInterval); + } else { + console.log( + "Tauri API not available after " + + maxAttempts * checkInterval + + "ms, using basic UI", + ); + initializeBasicUI(); + } + } + + checkTauriAvailability(); +}); + +function initializeApp() { + console.log("Tauri API available, initializing application..."); + + try { + invoke = window.__TAURI__.core.invoke; + console.log("invoke function loaded successfully"); + + init(); + } catch (error) { + console.error("Failed to initialize Tauri API:", error); + initializeBasicUI(); + } +} + +function initializeBasicUI() { + console.log("Initializing basic UI without Tauri functions..."); + + const warning = document.createElement("div"); + warning.style.cssText = ` + position: fixed; + top: 10px; + left: 50%; + transform: translateX(-50%); + background: #ff6b6b; + color: white; + padding: 10px 20px; + border-radius: 4px; + z-index: 9999; + font-size: 14px; + text-align: center; + `; + warning.textContent = "Tauri API not available - some features may not work"; + document.body.appendChild(warning); + + updateUptime(); + + setTimeout(function () { + loadAccounts = function () { + console.log("loadAccounts called (mock)"); + document.getElementById("accounts-loading").style.display = "none"; + document.getElementById("accounts-list").innerHTML = + '
  • Tauri API not available
  • '; + document.getElementById("accounts-list").style.display = "block"; + }; + + loadLogs = function () { + console.log("loadLogs called (mock)"); + document.getElementById("logs-container").innerHTML = + '
    Tauri API not available - cannot load logs
    '; + }; + + loadAccounts(); + }, 100); +} + +function initializeTabSwitching() { + const navTabs = document.querySelectorAll(".nav-tab"); + console.log(`Found ${navTabs.length} nav tabs`); + + navTabs.forEach((tab) => { + tab.style.cursor = "pointer"; + + tab.onclick = function (e) { + e.preventDefault(); + const tabName = this.dataset.tab; + console.log("Tab clicked:", tabName); + switchTab(tabName); + return false; + }; + }); + + const homeTab = document.querySelector('[data-tab="home"]'); + if (homeTab && !homeTab.classList.contains("active")) { + homeTab.classList.add("active"); + } +} + +function showAddAccountModal() { + const modal = document.getElementById("add-account-modal"); + if (modal) { + modal.classList.add("active"); + } else { + console.error("Add account modal not found"); + } +} + +function closeAddProjectModal() { + const modal = document.getElementById("add-project-modal"); + modal.classList.remove("active"); + + document.getElementById("project-id").value = ""; + document.getElementById("project-name").value = ""; + document.getElementById("project-url").value = ""; +} + +function closeAddAccountModal() { + const modal = document.getElementById("add-account-modal"); + if (modal) { + modal.classList.remove("active"); + } + + const cookieInput = document.getElementById("account-cookie"); + if (cookieInput) { + cookieInput.value = ""; + } +} + +function showQrcodeLoginModal() { + document.getElementById("qrcode-login-modal").classList.add("active"); + refreshQrcode(); +} + +function closeQrcodeModal() { + document.getElementById("qrcode-login-modal").classList.remove("active"); + + if (qrcodePollingInterval) { + clearInterval(qrcodePollingInterval); + qrcodePollingInterval = null; + } +} + +async function refreshQrcode() { + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + const qrcodeData = await invoke("qrcode_login"); + + if (qrcodeData && qrcodeData.url) { + document.getElementById("qrcode-img").src = qrcodeData.url; + + startQrcodePolling(qrcodeData.key); + } else { + throw new Error("无法生成二维码"); + } + } catch (error) { + console.error("刷新二维码失败:", error); + showError("生成二维码失败: " + error.message); + } +} + +function startQrcodePolling(qrcodeKey) { + if (qrcodePollingInterval) { + clearInterval(qrcodePollingInterval); + } + + qrcodePollingInterval = setInterval(async () => { + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + const result = await invoke("poll_qrcode_status", { key: qrcodeKey }); + + if (result.status === "success") { + clearInterval(qrcodePollingInterval); + qrcodePollingInterval = null; + + if (result.cookie) { + try { + await invoke("add_account_by_cookie", { cookie: result.cookie }); + showSuccess("登录成功!账号已添加"); + } catch (error) { + console.error("添加账号失败:", error); + showError("登录成功但添加账号失败: " + error); + } + } else { + showSuccess("登录成功!"); + } + + closeQrcodeModal(); + await reloadAccounts(); + } else if (result.status === "expired") { + clearInterval(qrcodePollingInterval); + qrcodePollingInterval = null; + showWarning("二维码已过期,请刷新二维码"); + } else if (result.status === "error") { + clearInterval(qrcodePollingInterval); + qrcodePollingInterval = null; + showError("登录失败: " + result.message); + } + } catch (error) { + console.error("轮询二维码状态失败:", error); + } + }, 3000); +} + +function showAddProjectModal() { + document.getElementById("add-project-modal").classList.add("active"); +} + +async function submitAddAccount() { + const cookie = document.getElementById("account-cookie").value; + if (!cookie) { + showWarning("请输入Cookie"); + return; + } + + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + await invoke("add_account_by_cookie", { cookie }); + showSuccess("添加成功!"); + closeAddAccountModal(); + await reloadAccounts(); + } catch (error) { + showError("添加失败: " + error); + } +} + +async function submitAddProject() { + const projectId = document.getElementById("project-id").value; + const projectName = document.getElementById("project-name").value; + const projectUrl = document.getElementById("project-url").value; + + if (!projectId || !projectName || !projectUrl) { + showWarning("请填写所有字段"); + return; + } + + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + if (!/^\d+$/.test(projectId)) { + showWarning("项目ID必须为数字"); + return; + } + + if ( + !projectUrl.startsWith("http://") && + !projectUrl.startsWith("https://") + ) { + showWarning("请输入有效的URL(以http://或https://开头)"); + return; + } + + await invoke("add_project", { + id: projectId, + name: projectName, + url: projectUrl, + }); + + showSuccess("添加项目成功!"); + closeAddProjectModal(); + await loadProjects(); + } catch (error) { + showError("添加失败: " + error); + } +} + +async function loadAccounts() { + const loading = document.getElementById("accounts-loading"); + const list = document.getElementById("accounts-list"); + + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + const accounts = await invoke("get_accounts"); + + loading.style.display = "none"; + list.style.display = "block"; + list.innerHTML = ""; + + if (accounts.length === 0) { + list.innerHTML = + '
  • 暂无账号
  • '; + return; + } + + accounts.forEach((account) => { + const li = document.createElement("li"); + li.className = "account-item"; + li.innerHTML = ` + +
    + + +
    + `; + list.appendChild(li); + }); + + document.getElementById("account-count").textContent = accounts.length; + } catch (error) { + console.error("Failed to load accounts:", error); + loading.style.display = "none"; + list.style.display = "block"; + list.innerHTML = `
    加载失败: ${error.message}
    `; + } +} + +async function toggleAccountActive(uid, active) { + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + await invoke("set_account_active", { uid, active }); + } catch (error) { + showError("更新账号状态失败: " + error); + await reloadAccounts(); + } +} + +async function reloadAccounts() { + document.getElementById("accounts-loading").style.display = "block"; + document.getElementById("accounts-list").style.display = "none"; + await loadAccounts(); +} + +async function deleteAccount(uid) { + if (!confirm("确定要删除此账号吗?")) return; + + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + await invoke("delete_account_by_uid", { uid }); + await loadAccounts(); + } catch (error) { + showError("删除失败: " + error); + } +} + +async function loadProjects() { + const loading = document.getElementById("projects-loading"); + const list = document.getElementById("projects-list"); + + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + const projects = await invoke("get_projects"); + + loading.style.display = "none"; + list.style.display = "grid"; + list.innerHTML = ""; + + if (!projects || projects.length === 0) { + list.innerHTML = + '
    暂无项目
    '; + return; + } + + projects.forEach((project) => { + const div = document.createElement("div"); + div.className = "project-card"; + div.innerHTML = ` +
    ${project.name || "未命名项目"}
    +
    ID: ${project.id}
    +
    ${project.url || "无URL"}
    +
    + + +
    + `; + list.appendChild(div); + }); + } catch (error) { + console.error("加载项目失败:", error); + loading.style.display = "none"; + list.style.display = "grid"; + list.innerHTML = `
    加载失败: ${error.message}
    `; + } +} + +function addProject() { + showAddProjectModal(); +} + +async function refreshProjects() { + document.getElementById("projects-loading").style.display = "block"; + document.getElementById("projects-list").style.display = "none"; + await loadProjects(); +} + +async function selectProject(projectId) { + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + await invoke("set_ticket_id", { ticketId: projectId }); + + const accounts = await invoke("get_accounts"); + const activeAccount = accounts.find((a) => a.is_active); + + if (!activeAccount) { + showWarning("请先激活一个账号"); + return; + } + + showScreenTicketModal(); + + const taskId = await invoke("get_ticket_info", { + uid: activeAccount.uid, + projectId: projectId, + }); + + console.log("获取项目详情任务ID:", taskId); + + const ticketInfo = await pollForTicketInfo(taskId); + + showScreenTicketSelector(ticketInfo); + } catch (error) { + showError("选择项目失败: " + error); + closeScreenTicketModal(); + } +} + +async function pollForTicketInfo(taskId) { + const maxAttempts = 30; + + for (let i = 0; i < maxAttempts; i++) { + try { + const results = await invoke("poll_task_results"); + + const result = results.find((r) => r.type === "GetTicketInfoResult"); + + if (result) { + if (result.success && result.ticket_info) { + console.log("项目详情获取成功:", result.ticket_info); + return result.ticket_info.data; + } else if (!result.success) { + throw new Error(result.message || "获取项目详情失败"); + } + } + + await new Promise((resolve) => setTimeout(resolve, 500)); + } catch (error) { + console.error("轮询错误:", error); + throw error; + } + } + + throw new Error("获取项目详情超时"); +} + +function showScreenTicketModal() { + const modal = document.getElementById("screen-ticket-modal"); + const loading = document.getElementById("screen-ticket-loading"); + const selector = document.getElementById("screen-ticket-selector"); + + modal.classList.add("active"); + loading.style.display = "block"; + selector.style.display = "none"; + + // 隐藏购票人类型选择框 + const buyerTypeGroup = document.querySelector( + ".form-group:has(#buyer-type-select)", + ); + if (buyerTypeGroup) { + buyerTypeGroup.style.display = "none"; + } +} + +function closeScreenTicketModal() { + const modal = document.getElementById("screen-ticket-modal"); + const loading = document.getElementById("screen-ticket-loading"); + const selector = document.getElementById("screen-ticket-selector"); + + modal.classList.remove("active"); + + loading.style.display = "block"; + + selector.style.display = "none"; + document.getElementById("screen-select").innerHTML = ""; + document.getElementById("ticket-select").innerHTML = ""; + + document.getElementById("buyer-list").innerHTML = ""; + document.getElementById("buyer-list").style.display = "none"; + document.getElementById("buyer-loading").style.display = "none"; + document.getElementById("buyer-error").style.display = "none"; + + document.getElementById("no-bind-name").value = ""; + document.getElementById("no-bind-tel").value = ""; + + // 显示购票人类型选择框(下次打开时重新隐藏) + const buyerTypeGroup = document.querySelector( + ".form-group:has(#buyer-type-select)", + ); + if (buyerTypeGroup) { + buyerTypeGroup.style.display = "block"; + } +} + +async function showScreenTicketSelector(ticketInfo) { + const loading = document.getElementById("screen-ticket-loading"); + const selector = document.getElementById("screen-ticket-selector"); + const screenSelect = document.getElementById("screen-select"); + const ticketSelect = document.getElementById("ticket-select"); + + loading.style.display = "none"; + selector.style.display = "block"; + + const availableScreens = ticketInfo.screen_list.filter( + (s) => s.clickable !== false, + ); + + if (availableScreens.length === 0) { + showWarning("暂无可选场次"); + closeScreenTicketModal(); + return; + } + + screenSelect.innerHTML = availableScreens + .map( + (s) => + ``, + ) + .join(""); + + window.currentTicketInfo = ticketInfo; + + screenSelect.onchange = function () { + updateTicketList(parseInt(this.value)); + }; + + updateTicketList(availableScreens[0].id); + + const idBind = ticketInfo.id_bind; + console.log("项目实名制类型 id_bind:", idBind); + + const realNameSection = document.getElementById("real-name-buyer-section"); + const nonRealNameSection = document.getElementById( + "non-real-name-buyer-section", + ); + + if (idBind === 0) { + realNameSection.style.display = "none"; + nonRealNameSection.style.display = "block"; + showNotification("当前项目为非强实名制,请填写姓名和手机号", "info", 5000); + } else if (idBind === 1 || idBind === 2) { + realNameSection.style.display = "block"; + nonRealNameSection.style.display = "none"; + showNotification("当前项目为强实名制,请从购票人列表中选择", "info", 5000); + } else { + realNameSection.style.display = "block"; + nonRealNameSection.style.display = "none"; + console.warn("未知的实名制类型 id_bind:", idBind); + } + + await loadBuyerInfo(); +} + +function updateTicketList(screenId) { + const ticketSelect = document.getElementById("ticket-select"); + const ticketInfo = window.currentTicketInfo; + + if (!ticketInfo) return; + + const selectedScreen = ticketInfo.screen_list.find((s) => s.id === screenId); + + if (!selectedScreen || !selectedScreen.ticket_list) { + ticketSelect.innerHTML = ''; + return; + } + + ticketSelect.innerHTML = selectedScreen.ticket_list + .map((t) => { + const price = (t.price / 100).toFixed(2); + const status = + t.sale_type === 1 ? "可售" : t.sale_type === 2 ? "售罄" : "未开售"; + return ``; + }) + .join(""); +} + +async function confirmScreenTicketSelection() { + try { + const screenId = document.getElementById("screen-select").value; + const ticketId = document.getElementById("ticket-select").value; + + if (!screenId || !ticketId) { + showWarning("请选择场次和票种"); + return; + } + + // 根据当前显示的购票人部分确定购票人类型 + const realNameSection = document.getElementById("real-name-buyer-section"); + const nonRealNameSection = document.getElementById( + "non-real-name-buyer-section", + ); + + let buyerType = ""; + if (realNameSection.style.display !== "none") { + buyerType = "1"; + } else if (nonRealNameSection.style.display !== "none") { + buyerType = "0"; + } else { + showWarning("无法确定购票人类型"); + return; + } + + if (buyerType === "1") { + const selectedBuyers = getSelectedBuyers(); + console.log("选中的购票人数据:", JSON.stringify(selectedBuyers, null, 2)); + console.log("购票人数量:", selectedBuyers.length); + + if (selectedBuyers.length === 0) { + showWarning("请至少选择一个购票人"); + return; + } + } else if (buyerType === "0") { + const name = document.getElementById("no-bind-name").value.trim(); + const tel = document.getElementById("no-bind-tel").value.trim(); + + if (!name || !tel) { + showWarning("请填写非实名购票人的姓名和手机号"); + return; + } + + // 验证手机号格式 + const phoneRegex = /^1[3-9]\d{9}$/; + if (!phoneRegex.test(tel)) { + showWarning("请输入有效的手机号"); + return; + } + } + + await invoke("set_selected_screen", { + index: null, + id: parseInt(screenId), + }); + await invoke("set_selected_ticket", { + id: parseInt(ticketId), + }); + + await invoke("set_buyer_type", { buyerType: parseInt(buyerType) }); + + if (buyerType === "1") { + const selectedBuyers = getSelectedBuyers(); + const validatedBuyers = selectedBuyers.map((buyer) => ({ + id: Number(buyer.id), + uid: Number(buyer.uid) || 0, + personal_id: String(buyer.personal_id || ""), + name: String(buyer.name || ""), + tel: String(buyer.tel || ""), + id_type: Number(buyer.id_type) || 1, + is_default: Number(buyer.is_default) || 0, + id_card_front: String(buyer.id_card_front || ""), + id_card_back: String(buyer.id_card_back || ""), + })); + + console.log( + "验证后的实名购票人数据:", + JSON.stringify(validatedBuyers, null, 2), + ); + + await invoke("set_selected_buyer_list", { buyerList: validatedBuyers }); + + await invoke("clear_no_bind_buyer_info"); + + console.log( + "已设置场次:", + screenId, + "票种:", + ticketId, + "实名购票人数量:", + validatedBuyers.length, + ); + } else if (buyerType === "0") { + const name = document.getElementById("no-bind-name").value.trim(); + const tel = document.getElementById("no-bind-tel").value.trim(); + + console.log("非实名购票人信息:", { name, tel }); + + await invoke("set_no_bind_buyer_info", { name, tel }); + + await invoke("set_selected_buyer_list", { buyerList: null }); + + console.log( + "已设置场次:", + screenId, + "票种:", + ticketId, + "非实名购票人:", + name, + ); + } + + showSuccess("设置成功"); + closeScreenTicketModal(); + } catch (error) { + console.error("确认选择失败:", error); + showError("设置失败: " + error); + } +} + +function onBuyerTypeChange() { + // 此函数现在不再需要,因为购票人类型选择框已隐藏 + // 保留函数定义以避免调用错误 +} + +async function saveNoBindBuyerInfo() { + try { + const name = document.getElementById("no-bind-name").value.trim(); + const tel = document.getElementById("no-bind-tel").value.trim(); + + if (!name || !tel) { + showError("请填写姓名和手机号"); + return; + } + + const phoneRegex = /^1[3-9]\d{9}$/; + if (!phoneRegex.test(tel)) { + showError("请输入有效的手机号"); + return; + } + + await invoke("set_no_bind_buyer_info", { name, tel }); + + showSuccess("非实名购票人信息保存成功!"); + } catch (error) { + console.error("保存非实名购票人信息失败:", error); + showError("保存失败: " + error); + } +} + +async function loadBuyerInfo() { + const buyerLoading = document.getElementById("buyer-loading"); + const buyerList = document.getElementById("buyer-list"); + const buyerError = document.getElementById("buyer-error"); + + try { + buyerLoading.style.display = "block"; + buyerList.style.display = "none"; + buyerError.style.display = "none"; + + const accounts = await invoke("get_accounts"); + const activeAccount = accounts.find((a) => a.is_active); + + if (!activeAccount) { + throw new Error("请先激活一个账号"); + } + + const taskId = await invoke("get_buyer_info", { + uid: activeAccount.uid, + }); + + if (!taskId || taskId.trim() === "") { + throw new Error("获取任务ID失败,返回空值"); + } + + const buyerInfo = await pollForBuyerInfo(taskId); + + displayBuyerList(buyerInfo); + + buyerLoading.style.display = "none"; + buyerList.style.display = "block"; + } catch (error) { + console.error("加载购票人信息失败:", error); + console.error("错误堆栈:", error.stack); + buyerLoading.style.display = "none"; + buyerError.style.display = "block"; + buyerError.textContent = "加载购票人失败: " + error.message; + } +} + +async function pollForBuyerInfo(taskId) { + const maxAttempts = 30; + for (let i = 0; i < maxAttempts; i++) { + await new Promise((resolve) => setTimeout(resolve, 500)); + + const results = await invoke("poll_task_results"); + + const result = results.find((r) => r.task_id === taskId); + + if (result) { + if (!result.success) { + throw new Error(result.message || "获取购票人信息失败"); + } + return result.buyer_info; + } + } + throw new Error("获取购票人信息超时"); +} + +function displayBuyerList(buyerInfo) { + const buyerList = document.getElementById("buyer-list"); + + if ( + !buyerInfo || + !buyerInfo.data || + !buyerInfo.data.list || + buyerInfo.data.list.length === 0 + ) { + buyerList.innerHTML = + '

    暂无购票人,请先在账号页面添加

    '; + return; + } + + const buyers = buyerInfo.data.list; + + buyerList.innerHTML = buyers + .map( + (buyer) => ` +
    + + +
    + `, + ) + .join(""); + + if (buyers.length > 0) { + const firstCheckbox = document.getElementById(`buyer-${buyers[0].id}`); + if (firstCheckbox) { + firstCheckbox.checked = true; + } + } +} + +function getSelectedBuyers() { + const checkboxes = document.querySelectorAll( + '#buyer-list input[type="checkbox"]:checked', + ); + const selectedBuyers = []; + + console.log("找到选中的复选框数量:", checkboxes.length); + + checkboxes.forEach((checkbox, index) => { + try { + const buyerJson = checkbox.getAttribute("data-buyer"); + console.log(`复选框 ${index + 1} 的原始数据:`, buyerJson); + + if (!buyerJson) { + console.warn(`复选框 ${index + 1} 没有 data-buyer 属性`); + return; + } + + const decodedJson = decodeURIComponent(buyerJson); + const buyerData = JSON.parse(decodedJson); + console.log(`解析后的购票人 ${index + 1}:`, buyerData); + + if (!buyerData.id || !buyerData.name || !buyerData.tel) { + console.warn(`购票人 ${index + 1} 缺少必需字段:`, buyerData); + } + + selectedBuyers.push(buyerData); + } catch (e) { + console.error("解析购票人数据失败:", e); + console.error("原始数据:", buyerJson); + console.error("解码后数据:", decodedJson || "undefined"); + } + }); + + console.log("最终选中的购票人数组:", selectedBuyers); + return selectedBuyers; +} + +async function deleteProject(projectId) { + if (!confirm("确定要删除此项目吗?")) return; + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + await invoke("delete_project", { id: projectId }); + showSuccess("删除项目成功"); + await loadProjects(); + } catch (error) { + showError("删除失败: " + error); + } +} + +async function startGrab() { + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + await invoke("set_grab_mode", { mode: 1 }); + const taskId = await invoke("start_grab_ticket"); + + currentTaskId = taskId; + + document.getElementById("monitor-status").textContent = "运行中"; + document.getElementById("monitor-status").style.color = + "var(--success-color)"; + + showSuccess("开始抢票!任务ID: " + taskId); + await refreshMonitor(); + } catch (error) { + console.error("启动抢票失败:", error); + showError("启动失败: " + error); + } +} + +async function stopGrab() { + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + if (currentTaskId) { + try { + await invoke("cancel_task", { taskId: currentTaskId }); + showSuccess("已取消抢票任务: " + currentTaskId); + } catch (cancelError) { + console.warn("取消任务失败:", cancelError); + showWarning("取消任务失败,但已停止抢票模式: " + cancelError); + } + currentTaskId = null; + } + + await invoke("set_grab_mode", { mode: 0 }); + document.getElementById("monitor-status").textContent = "已停止"; + document.getElementById("monitor-status").style.color = + "var(--error-color)"; + + showInfo("停止抢票"); + } catch (error) { + showError("停止失败: " + error); + } +} + +async function refreshMonitor() { + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + const state = await invoke("get_state"); + document.getElementById("monitor-status").textContent = + state.running_status; + + const statusElem = document.getElementById("monitor-status"); + if ( + state.running_status.includes("运行") || + state.running_status.includes("抢票") + ) { + statusElem.style.color = "var(--success-color)"; + statusElem.style.fontWeight = "bold"; + } else if (state.running_status.includes("停止")) { + statusElem.style.color = "var(--error-color)"; + statusElem.style.fontWeight = "normal"; + } else { + statusElem.style.color = "var(--text-primary)"; + statusElem.style.fontWeight = "normal"; + } + + const stats = await invoke("get_monitor_stats"); + if (stats) { + monitorStats.attempts = stats.attempts || 0; + monitorStats.success = stats.success || 0; + monitorStats.failures = stats.failures || 0; + updateMonitorStats(); + } + } catch (error) { + console.error("刷新监控失败:", error); + } +} + +async function loadSettings() { + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + const state = await invoke("get_state"); + document.getElementById("grab-mode").value = state.grab_mode || "0"; + document.getElementById("delay-time").value = state.status_delay || "2"; + document.getElementById("max-attempts").value = + state.config?.max_attempts || "100"; + + if (state.custom_config && state.custom_config.open_custom_ua) { + document.getElementById("custom-ua").checked = true; + document.getElementById("user-agent").value = + state.custom_config.custom_ua || ""; + } + + if (state.push_config) { + document.getElementById("enable-push").checked = + state.push_config.enabled || false; + document.getElementById("bark-token").value = + state.push_config.bark_token || ""; + document.getElementById("pushplus-token").value = + state.push_config.pushplus_token || ""; + document.getElementById("fangtang-token").value = + state.push_config.fangtang_token || ""; + document.getElementById("dingtalk-token").value = + state.push_config.dingtalk_token || ""; + document.getElementById("wechat-token").value = + state.push_config.wechat_token || ""; + + // 设置多选框状态 + if (state.push_config.enabled_methods) { + document.getElementById("push-method-bark").checked = + state.push_config.enabled_methods.includes("bark"); + document.getElementById("push-method-pushplus").checked = + state.push_config.enabled_methods.includes("pushplus"); + document.getElementById("push-method-fangtang").checked = + state.push_config.enabled_methods.includes("fangtang"); + document.getElementById("push-method-dingtalk").checked = + state.push_config.enabled_methods.includes("dingtalk"); + document.getElementById("push-method-wechat").checked = + state.push_config.enabled_methods.includes("wechat"); + document.getElementById("push-method-smtp").checked = + state.push_config.enabled_methods.includes("smtp"); + document.getElementById("push-method-gotify").checked = + state.push_config.enabled_methods.includes("gotify"); + } else { + // 如果没有enabled_methods,默认全选 + document.getElementById("push-method-bark").checked = true; + document.getElementById("push-method-pushplus").checked = true; + document.getElementById("push-method-fangtang").checked = true; + document.getElementById("push-method-dingtalk").checked = true; + document.getElementById("push-method-wechat").checked = true; + document.getElementById("push-method-smtp").checked = true; + document.getElementById("push-method-gotify").checked = true; + } + + // 更新推送设置的可见性 + updatePushSettingsVisibility(); + + if (state.push_config.gotify_config) { + document.getElementById("gotify-url").value = + state.push_config.gotify_config.gotify_url || ""; + document.getElementById("gotify-token").value = + state.push_config.gotify_config.gotify_token || ""; + } + + if (state.push_config.smtp_config) { + document.getElementById("smtp-server").value = + state.push_config.smtp_config.smtp_server || ""; + document.getElementById("smtp-port").value = + state.push_config.smtp_config.smtp_port || ""; + document.getElementById("smtp-username").value = + state.push_config.smtp_config.smtp_username || ""; + document.getElementById("smtp-password").value = + state.push_config.smtp_config.smtp_password || ""; + document.getElementById("smtp-from").value = + state.push_config.smtp_config.smtp_from || ""; + document.getElementById("smtp-to").value = + state.push_config.smtp_config.smtp_to || ""; + } + } + } catch (error) { + console.error("加载设置失败:", error); + } +} + +async function saveSettings() { + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + const grabMode = document.getElementById("grab-mode").value; + const delayTime = document.getElementById("delay-time").value; + const maxAttempts = document.getElementById("max-attempts").value; + const enablePush = document.getElementById("enable-push").checked; + const barkToken = document.getElementById("bark-token").value; + const pushplusToken = document.getElementById("pushplus-token").value; + const fangtangToken = document.getElementById("fangtang-token").value; + const dingtalkToken = document.getElementById("dingtalk-token").value; + const wechatToken = document.getElementById("wechat-token").value; + const gotifyUrl = document.getElementById("gotify-url").value; + const gotifyToken = document.getElementById("gotify-token").value; + const smtpServer = document.getElementById("smtp-server").value; + const smtpPort = document.getElementById("smtp-port").value; + const smtpUsername = document.getElementById("smtp-username").value; + const smtpPassword = document.getElementById("smtp-password").value; + const smtpFrom = document.getElementById("smtp-from").value; + const smtpTo = document.getElementById("smtp-to").value; + const customUa = document.getElementById("custom-ua").checked; + const userAgent = document.getElementById("user-agent").value; + + const enabledMethods = []; + if (document.getElementById("push-method-bark").checked) { + enabledMethods.push("bark"); + } + if (document.getElementById("push-method-pushplus").checked) { + enabledMethods.push("pushplus"); + } + if (document.getElementById("push-method-fangtang").checked) { + enabledMethods.push("fangtang"); + } + if (document.getElementById("push-method-dingtalk").checked) { + enabledMethods.push("dingtalk"); + } + if (document.getElementById("push-method-wechat").checked) { + enabledMethods.push("wechat"); + } + if (document.getElementById("push-method-smtp").checked) { + enabledMethods.push("smtp"); + } + if (document.getElementById("push-method-gotify").checked) { + enabledMethods.push("gotify"); + } + + if (enablePush && enabledMethods.length === 0) { + showError("启用推送时,必须至少选择一个推送渠道"); + return; + } + + if (delayTime < 1 || delayTime > 10) { + showError("延迟时间必须在1-10秒之间"); + return; + } + + if (maxAttempts < 1 || maxAttempts > 1000) { + showError("最大尝试次数必须在1-1000之间"); + return; + } + + if (gotifyUrl && !gotifyUrl.startsWith("http")) { + showError("Gotify URL必须以http://或https://开头"); + return; + } + + if (smtpServer && !smtpPort) { + showError("请填写SMTP端口"); + return; + } + + if (customUa && !userAgent.trim()) { + showError("启用自定义User-Agent时,必须填写User-Agent"); + return; + } + + await invoke("save_settings", { + grabMode: parseInt(grabMode), + delayTime: parseInt(delayTime), + maxAttempts: parseInt(maxAttempts), + enablePush: enablePush, + enabledMethods: enabledMethods, + barkToken: barkToken, + pushplusToken: pushplusToken, + fangtangToken: fangtangToken, + dingtalkToken: dingtalkToken, + wechatToken: wechatToken, + gotifyUrl: gotifyUrl, + gotifyToken: gotifyToken, + smtpServer: smtpServer, + smtpPort: smtpPort, + smtpUsername: smtpUsername, + smtpPassword: smtpPassword, + smtpFrom: smtpFrom, + smtpTo: smtpTo, + customUa: customUa, + userAgent: userAgent, + }); + + showSuccess("设置保存成功"); + await loadSettings(); + } catch (error) { + showError("设置保存失败: " + error); + } +} + +function resetSettings() { + if (confirm("确定要恢复默认设置吗?")) { + document.getElementById("grab-mode").value = "0"; + document.getElementById("delay-time").value = "2"; + document.getElementById("max-attempts").value = "100"; + document.getElementById("enable-push").checked = false; + document.getElementById("bark-token").value = ""; + document.getElementById("pushplus-token").value = ""; + document.getElementById("fangtang-token").value = ""; + document.getElementById("dingtalk-token").value = ""; + document.getElementById("wechat-token").value = ""; + document.getElementById("gotify-url").value = ""; + document.getElementById("gotify-token").value = ""; + document.getElementById("smtp-server").value = ""; + document.getElementById("smtp-port").value = ""; + document.getElementById("smtp-username").value = ""; + document.getElementById("smtp-password").value = ""; + document.getElementById("smtp-from").value = ""; + document.getElementById("smtp-to").value = ""; + document.getElementById("custom-ua").checked = false; + document.getElementById("user-agent").value = ""; + showSuccess("设置已恢复默认"); + } +} + +async function loadLogs() { + const container = document.getElementById("logs-container"); + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + const logs = await invoke("get_logs"); + + if (logs && logs.length > 0) { + container.innerHTML = logs + .map((log) => `
    ${log}
    `) + .join(""); + container.scrollTop = container.scrollHeight; + + document.getElementById("log-count").textContent = logs.length; + } else { + container.innerHTML = '
    暂无日志
    '; + document.getElementById("log-count").textContent = "0"; + } + } catch (error) { + console.error("Failed to load logs:", error); + container.innerHTML = `
    加载日志失败: ${error.message}
    `; + document.getElementById("log-count").textContent = "0"; + } +} + +async function clearLogs() { + if (!confirm("确定要清空所有日志吗?此操作不可撤销!")) return; + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + + await invoke("clear_logs"); + showSuccess("日志已清空"); + await loadLogs(); + } catch (error) { + showError("设置失败: " + error); + } +} + +async function exportLogs() { + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + const logs = await invoke("get_logs"); + const logText = logs.join("\n"); + const blob = new Blob([logText], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `btr_logs_${new Date().toISOString().slice(0, 10)}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (error) { + showError("设置失败: " + error); + } +} + +function updateUptime() { + const startTime = Date.now(); + setInterval(() => { + const elapsed = Date.now() - startTime; + const minutes = Math.floor(elapsed / 60000); + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + + let uptimeText = ""; + if (hours > 0) { + uptimeText = `${hours} 小时 ${remainingMinutes} 分钟`; + } else { + uptimeText = `${minutes} 分钟`; + } + + document.getElementById("uptime").textContent = uptimeText; + }, 60000); +} + +function updateMonitorStats() { + document.getElementById("monitor-attempts").textContent = + monitorStats.attempts; + document.getElementById("monitor-success").textContent = monitorStats.success; + document.getElementById("monitor-failures").textContent = + monitorStats.failures; +} + +function resetMonitorStats() { + monitorStats = { + attempts: 0, + success: 0, + failures: 0, + }; + updateMonitorStats(); +} + +// Initialization +async function init() { + console.log("Starting application initialization..."); + + try { + updateUptime(); + await updateSystemInfo(); + + await loadAccounts(); + await loadSettings(); + setupPushSettingsEventListeners(); + + resetMonitorStats(); + + // 初始化抢票日志 + await initGrabLogs(); + + setInterval(() => { + const logsTab = document.getElementById("tab-logs"); + if (logsTab && logsTab.classList.contains("active")) { + loadLogs(); + } + }, 5000); + + setInterval(() => { + const monitorTab = document.getElementById("tab-monitor"); + if (monitorTab && monitorTab.classList.contains("active")) { + refreshMonitor(); + } + }, 3000); + + setInterval(async () => { + const accountsTab = document.getElementById("tab-accounts"); + if (accountsTab && accountsTab.classList.contains("active")) { + await reloadAccounts(); + } + }, 30000); + + console.log("Application initialization complete"); + } catch (error) { + console.error("Initialization error:", error); + } +} + +async function updateSystemInfo() { + try { + if (!invoke) { + return; + } + + const appInfo = await invoke("get_app_info"); + if (appInfo) { + const versionElement = document.querySelector(".app-version"); + if (versionElement) { + versionElement.textContent = `v${appInfo.version}`; + } + + if (appInfo.machine_id) { + console.log("Machine ID:", appInfo.machine_id); + } + } + } catch (error) { + console.error("更新系统信息失败:", error); + } +} + +function switchTab(tabName) { + console.log("switchTab called with:", tabName); + + if (!tabName) { + console.error("No tab name provided"); + return; + } + + document.querySelectorAll(".nav-tab").forEach((tab) => { + tab.classList.remove("active"); + }); + + document.querySelectorAll(".tab-content").forEach((content) => { + content.classList.remove("active"); + }); + + const clickedTab = document.querySelector(`[data-tab="${tabName}"]`); + if (clickedTab) { + clickedTab.classList.add("active"); + } + + const targetContent = document.getElementById(`tab-${tabName}`); + if (targetContent) { + targetContent.classList.add("active"); + console.log(`Successfully switched to tab: ${tabName}`); + + if (tabName === "projects") { + if (typeof loadProjects === "function") { + loadProjects(); + } + } else if (tabName === "monitor") { + if (typeof refreshMonitor === "function") { + refreshMonitor(); + } + if (typeof resetMonitorStats === "function") { + resetMonitorStats(); + } + } else if (tabName === "logs") { + if (typeof loadLogs === "function") { + loadLogs(); + } + } else if (tabName === "settings") { + if (typeof loadSettings === "function") { + loadSettings(); + } + } else if (tabName === "accounts") { + if (typeof reloadAccounts === "function") { + reloadAccounts(); + } + } + } else { + console.error(`Tab content not found: tab-${tabName}`); + } +} + +async function testPush() { + if (!invoke) { + showError("Tauri API不可用,无法测试推送"); + return; + } + + if (!confirm("确定要发送测试推送吗?")) { + return; + } + + try { + const result = await invoke("push_test"); + showSuccess("测试推送已发送:" + result); + } catch (error) { + showError("设置失败: " + error); + } +} + +document.addEventListener("keydown", function (e) { + if (e.ctrlKey && e.key >= "1" && e.key <= "7") { + e.preventDefault(); + const tabIndex = parseInt(e.key) - 1; + const tabs = document.querySelectorAll(".nav-tab"); + if (tabIndex < tabs.length) { + const tabName = tabs[tabIndex].dataset.tab; + switchTab(tabName); + } + } + + if (e.key === "Escape") { + closeAddAccountModal(); + closeQrcodeModal(); + closeAddProjectModal(); + } +}); + +let grabLogs = []; +let autoScrollEnabled = true; +let logFilters = { + info: true, + debug: true, + warn: true, + error: true, + success: true, +}; + +async function loadGrabLogs() { + const container = document.getElementById("grab-logs-container"); + try { + if (!invoke) { + throw new Error("Tauri invoke function not available"); + } + const logs = await invoke("get_grab_logs"); + + if (logs && logs.length > 0) { + grabLogs = logs; + updateGrabLogsDisplay(); + } else { + container.innerHTML = '
    暂无抢票日志
    '; + updateLogStats(); + } + } catch (error) { + console.error("Failed to load grab logs:", error); + container.innerHTML = `
    加载抢票日志失败: ${error.message}
    `; + updateLogStats(); + } +} + +function updateGrabLogsDisplay() { + const container = document.getElementById("grab-logs-container"); + const filteredLogs = grabLogs.filter((log) => { + if (log.includes("INFO:")) return logFilters.info; + if (log.includes("DEBUG:")) return logFilters.debug; + if (log.includes("WARN:")) return logFilters.warn; + if (log.includes("ERROR:")) return logFilters.error; + return logFilters.success; + }); + + if (filteredLogs.length > 0) { + container.innerHTML = filteredLogs + .map((log) => formatLogEntry(log)) + .join(""); + + if (autoScrollEnabled) { + container.scrollTop = container.scrollHeight; + } + } else { + container.innerHTML = '
    暂无符合条件的日志
    '; + } + + updateLogStats(); +} + +function formatLogEntry(log) { + let levelClass = ""; + let levelText = ""; + + if (log.includes("INFO:")) { + levelClass = "info"; + levelText = "INFO"; + } else if (log.includes("DEBUG:")) { + levelClass = "debug"; + levelText = "DEBUG"; + } else if (log.includes("WARN:")) { + levelClass = "warn"; + levelText = "WARN"; + } else if (log.includes("ERROR:")) { + levelClass = "error"; + levelText = "ERROR"; + } else { + levelClass = "success"; + levelText = "SUCCESS"; + } + + const messageMatch = log.match( + /\[.*?\]\s*(?:INFO|DEBUG|WARN|ERROR|SUCCESS)?:?\s*(.*)/, + ); + const message = messageMatch ? messageMatch[1] : log; + + return ` +
    + ${levelText} + ${message} +
    + `; +} + +function updateLogStats() { + const infoCount = grabLogs.filter((log) => log.includes("INFO:")).length; + const debugCount = grabLogs.filter((log) => log.includes("DEBUG:")).length; + const warnCount = grabLogs.filter((log) => log.includes("WARN:")).length; + const errorCount = grabLogs.filter((log) => log.includes("ERROR:")).length; + const successCount = grabLogs.filter( + (log) => + !log.includes("INFO:") && + !log.includes("DEBUG:") && + !log.includes("WARN:") && + !log.includes("ERROR:"), + ).length; + + document.getElementById("grab-log-count").textContent = grabLogs.length; + document.getElementById("info-count").textContent = infoCount; + document.getElementById("debug-count").textContent = debugCount; + document.getElementById("warn-count").textContent = warnCount; + document.getElementById("error-count").textContent = errorCount; +} + +async function clearGrabLogs() { + if (!confirm("确定要清空所有抢票日志吗?此操作不可撤销!")) return; + + try { + await invoke("clear_grab_logs"); + grabLogs = []; + updateGrabLogsDisplay(); + showSuccess("抢票日志已清空"); + } catch (error) { + showError("清空抢票日志失败: " + error); + } +} + +async function exportGrabLogs() { + try { + const logs = grabLogs.join("\n"); + const blob = new Blob([logs], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `grab_logs_${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + showSuccess("抢票日志导出成功"); + } catch (error) { + showError("导出抢票日志失败: " + error); + } +} + +function updatePushSettingsVisibility() { + const methods = [ + "bark", + "pushplus", + "fangtang", + "dingtalk", + "wechat", + "smtp", + "gotify", + ]; + methods.forEach((method) => { + const checkbox = document.getElementById(`push-method-${method}`); + const settings = document.getElementById(`${method}-settings`); + if (checkbox && settings) { + settings.style.display = checkbox.checked ? "block" : "none"; + } + }); +} + +function setupPushSettingsEventListeners() { + const methods = [ + "bark", + "pushplus", + "fangtang", + "dingtalk", + "wechat", + "smtp", + "gotify", + ]; + methods.forEach((method) => { + const checkbox = document.getElementById(`push-method-${method}`); + if (checkbox) { + checkbox.addEventListener("change", updatePushSettingsVisibility); + } + }); +} + +function toggleAutoScroll() { + autoScrollEnabled = !autoScrollEnabled; + const button = document.getElementById("auto-scroll-btn"); + button.textContent = `自动滚动: ${autoScrollEnabled ? "开启" : "关闭"}`; + button.classList.toggle("btn-info", autoScrollEnabled); + button.classList.toggle("btn-secondary", !autoScrollEnabled); +} + +function toggleLogFilter(level) { + logFilters[level] = !logFilters[level]; + const button = document.getElementById(`filter-${level}-btn`); + if (button) { + button.classList.toggle("active", logFilters[level]); + } + updateGrabLogsDisplay(); +} + +function setupGrabLogsEventListeners() { + // 按钮事件监听 + document + .getElementById("refresh-grab-logs-btn") + ?.addEventListener("click", loadGrabLogs); + document + .getElementById("clear-grab-logs-btn") + ?.addEventListener("click", clearGrabLogs); + document + .getElementById("export-grab-logs-btn") + ?.addEventListener("click", exportGrabLogs); + document + .getElementById("auto-scroll-btn") + ?.addEventListener("click", toggleAutoScroll); + + // 过滤器按钮事件监听 + document + .getElementById("filter-info-btn") + ?.addEventListener("click", () => toggleLogFilter("info")); + document + .getElementById("filter-debug-btn") + ?.addEventListener("click", () => toggleLogFilter("debug")); + document + .getElementById("filter-warn-btn") + ?.addEventListener("click", () => toggleLogFilter("warn")); + document + .getElementById("filter-error-btn") + ?.addEventListener("click", () => toggleLogFilter("error")); + document + .getElementById("filter-success-btn") + ?.addEventListener("click", () => toggleLogFilter("success")); + + // 搜索功能 + const searchInput = document.getElementById("log-search"); + if (searchInput) { + searchInput.addEventListener("input", function () { + const searchTerm = this.value.toLowerCase(); + const container = document.getElementById("grab-logs-container"); + const logEntries = container.querySelectorAll(".log-entry"); + + logEntries.forEach((entry) => { + const text = entry.textContent.toLowerCase(); + entry.style.display = text.includes(searchTerm) ? "" : "none"; + }); + }); + } +} + +// 初始化抢票日志 +async function initGrabLogs() { + setupGrabLogsEventListeners(); + await loadGrabLogs(); + + // 设置自动刷新 + setInterval(async () => { + const grabLogsTab = document.getElementById("tab-grab-logs"); + if (grabLogsTab.classList.contains("active")) { + await loadGrabLogs(); + } + }, 3000); // 每3秒刷新一次 +} + +window.addEventListener("beforeunload", function () { + if (qrcodePollingInterval) { + clearInterval(qrcodePollingInterval); + } +}); diff --git a/crates/frontend/web/index.html b/crates/frontend/web/index.html new file mode 100644 index 0000000..3e75239 --- /dev/null +++ b/crates/frontend/web/index.html @@ -0,0 +1,1052 @@ + + + + + + Bilibili Ticket Rush + + + +
    + + +
    +
    +
    +

    欢迎使用 BTR

    +

    + Bilibili Ticket Rush - 哔哩哔哩会员购抢票工具 +

    +
    +
    +

    快速开始

    +

    1. 在"账号管理"中添加B站账号

    +

    + 2. 在"项目列表"中选择要抢票的项目 +

    +

    3. 在"设置"中配置抢票参数

    +

    4. 在"监控"页面开始抢票

    +
    +
    +

    系统信息

    +
    +
    +
    版本
    +
    v7.0.0
    +
    +
    +
    运行时间
    +
    0 分钟
    +
    +
    +
    账号数量
    +
    0
    +
    +
    +
    日志数量
    +
    0
    +
    +
    +
    +
    + +
    +
    +

    账号管理

    +
    + + + +
    +
    +
    +
    加载中...
    +
    + +
    +
    + +
    +
    +

    抢票监控

    + +
    + + + +
    + +
    +
    +
    运行状态
    +
    + 空闲 +
    +
    当前状态
    +
    +
    +
    尝试次数
    +
    + 0 +
    +
    总尝试次数
    +
    +
    +
    成功次数
    +
    + 0 +
    +
    成功抢到票
    +
    +
    +
    失败次数
    +
    + 0 +
    +
    抢票失败
    +
    +
    + +
    + 当前状态: + + 空闲 + +
    +
    +
    + + +
    +
    +

    项目列表

    +
    + + +
    +
    +
    +
    加载项目中...
    +
    + +
    +
    + + +
    +
    +

    抢票设置

    +
    +
    基本设置
    +
    +
    +
    抢票模式
    +
    + 选择抢票的工作模式 +
    +
    + +
    +
    +
    +
    延迟时间(秒)
    +
    + 每次请求之间的延迟时间 +
    +
    + +
    +
    +
    +
    最大尝试次数
    +
    + 每个账号的最大抢票尝试次数 +
    +
    + +
    +
    + +
    +
    推送设置
    +
    +
    +
    启用推送通知
    +
    + 抢票成功或失败时发送通知 +
    +
    + +
    + +
    +
    +
    启用推送渠道
    +
    + 选择要启用的推送渠道(至少选择一个) +
    +
    +
    + + + + + + + +
    +
    + +
    +
    +
    Bark Token
    +
    + iOS Bark推送服务的Token +
    +
    + +
    + +
    +
    +
    PushPlus Token
    +
    + PushPlus推送服务的Token +
    +
    + +
    + +
    +
    +
    方糖推送Token
    +
    + 方糖(Server酱)推送服务的Token +
    +
    + +
    + +
    +
    +
    + 钉钉机器人Token +
    +
    + 钉钉群机器人的Access Token +
    +
    + +
    + +
    +
    +
    企业微信Token
    +
    + 企业微信机器人的Webhook Key +
    +
    + +
    + +
    +
    Gotify设置
    +
    +
    +
    Gotify URL
    +
    + Gotify服务器地址 +
    +
    + +
    +
    +
    +
    + Gotify Token +
    +
    + Gotify应用的Token +
    +
    + +
    +
    + +
    +
    SMTP邮件设置
    +
    +
    +
    SMTP服务器
    +
    + 邮件服务器地址 +
    +
    + +
    +
    +
    +
    SMTP端口
    +
    + 邮件服务器端口 +
    +
    + +
    +
    +
    +
    用户名
    +
    + SMTP登录用户名 +
    +
    + +
    +
    +
    +
    密码
    +
    + SMTP登录密码 +
    +
    + +
    +
    +
    +
    发件人
    +
    + 发件人邮箱地址 +
    +
    + +
    +
    +
    +
    收件人
    +
    + 收件人邮箱地址 +
    +
    + +
    +
    +
    + +
    +
    高级设置
    +
    +
    +
    + 自定义User-Agent +
    +
    + 自定义请求的User-Agent +
    +
    + +
    +
    +
    +
    User-Agent
    +
    + 自定义的User-Agent字符串 +
    +
    + +
    +
    + +
    + + + +
    +
    +
    + +
    +
    +

    系统日志

    +
    + + + +
    +
    +
    +
    + +
    +
    +

    抢票日志

    +
    + + + + +
    +
    + + + + + + +
    +
    + 日志总数: 0 + 信息: 0 + 调试: 0 + 警告: 0 + 错误: 0 +
    +
    +
    +
    + +
    +
    +

    帮助与关于

    +

    BTR v7.0.0

    +

    + 本项目完全免费开源,仅供学习使用。 +

    + +
    +
    使用教程
    +
    +

    + 1. + 在"账号管理"中添加B站账号(支持Cookie登录和扫码登录) +

    +

    2. 在"项目列表"中选择要抢票的项目

    +

    3. 在"设置"中配置抢票参数和推送通知

    +

    4. 在"监控"页面开始抢票并查看实时状态

    +

    5. 在"日志"页面查看详细的运行日志

    +
    +
    + +
    +
    常见问题
    +
    +

    Q: 如何获取B站Cookie?

    +

    + A: + 登录B站后,按F12打开开发者工具,在控制台输入document.cookie复制结果 +

    + +

    Q: 抢票失败怎么办?

    +

    A: 检查网络连接、账号状态,或尝试更换抢票模式

    + +

    Q: 支持多账号同时抢票吗?

    +

    A: 支持,可以在账号管理页面启用多个账号

    +
    +
    + +
    +
    项目信息
    +
    +

    + GitHub: + https://github.com/biliticket/bili_ticket_rush +

    +

    许可证: GPLv3 License

    +

    反馈: 请在GitHub Issues中提交问题

    +
    +
    +
    +
    +
    + + + + + + + + + + + + diff --git a/crates/frontend/web/style.css b/crates/frontend/web/style.css new file mode 100644 index 0000000..3364ca3 --- /dev/null +++ b/crates/frontend/web/style.css @@ -0,0 +1,873 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --bg-primary: #f6f7f9; + --bg-card: #ffffff; + --text-primary: #1f2328; + --text-secondary: #6a737d; + --border-color: #d0d7de; + --accent-color: #0969da; + --success-color: #1a7f37; + --error-color: #d1242f; + --warning-color: #bf8700; +} + +body { + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, + Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + display: flex; +} + +#sidebar { + width: 250px; + background: var(--bg-card); + border-right: 1px solid var(--border-color); + padding: 20px 0; + display: flex; + flex-direction: column; +} + +.app-header { + padding: 0 20px 20px; + border-bottom: 1px solid var(--border-color); +} + +.app-title { + font-size: 20px; + font-weight: 600; + margin-bottom: 4px; +} + +.app-version { + font-size: 12px; + color: var(--text-secondary); +} + +.nav-tabs { + flex: 1; + padding: 20px 0; +} + +.nav-tab { + display: block; + padding: 12px 20px; + color: var(--text-primary); + text-decoration: none; + cursor: pointer; + transition: background 0.2s; + border-left: 3px solid transparent; + user-select: none; + -webkit-user-select: none; +} + +.nav-tab:hover { + background: rgba(0, 0, 0, 0.03); +} + +.nav-tab:active { + background: rgba(0, 0, 0, 0.05); +} + +.nav-tab.active { + background: rgba(9, 105, 218, 0.08); + border-left-color: var(--accent-color); + color: var(--accent-color); + font-weight: 500; +} + +#main-content { + flex: 1; + padding: 30px; + overflow-y: auto; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +.card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 24px; + margin-bottom: 20px; +} + +.card-title { + font-size: 18px; + font-weight: 600; + margin-bottom: 16px; +} + +.btn { + display: inline-block; + padding: 10px 20px; + border-radius: 6px; + border: none; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + margin-right: 8px; + margin-bottom: 8px; +} + +.btn-primary { + background: var(--accent-color); + color: white; +} + +.btn-primary:hover { + background: #0860ca; +} + +.btn-success { + background: var(--success-color); + color: white; +} + +.btn-danger { + background: var(--error-color); + color: white; +} + +.btn-warning { + background: var(--warning-color); + color: white; +} + +.account-list { + list-style: none; +} + +.account-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + border-bottom: 1px solid var(--border-color); +} + +.account-info { + flex: 1; +} + +.account-name { + font-weight: 500; + margin-bottom: 4px; +} + +.account-meta { + font-size: 12px; + color: var(--text-secondary); +} + +.loading { + text-align: center; + padding: 40px; + color: var(--text-secondary); +} + +.spinner { + border: 3px solid rgba(0, 0, 0, 0.1); + border-top-color: var(--accent-color); + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; + margin: 0 auto 12px; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.form-group { + margin-bottom: 16px; +} + +.form-label { + display: block; + margin-bottom: 6px; + font-weight: 500; + font-size: 14px; +} + +.form-input { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 14px; +} + +.form-input:focus { + outline: none; + border-color: var(--accent-color); +} + +.log-container { + background: #24292f; + color: #e6edf3; + padding: 16px; + border-radius: 6px; + font-family: "Consolas", "Monaco", monospace; + font-size: 12px; + max-height: 400px; + overflow-y: auto; +} + +.grab-log-container { + max-height: 500px; +} + +.log-stats { + display: flex; + align-items: center; + font-size: 14px; + color: var(--text-secondary); + padding: 8px 0; +} + +.log-stats span { + display: inline-flex; + align-items: center; +} + +.log-entry { + margin-bottom: 4px; + padding: 4px 8px; + border-radius: 4px; + transition: background-color 0.2s; +} + +.log-entry:hover { + background-color: rgba(255, 255, 255, 0.05); +} + +.log-entry.info { + color: #58a6ff; +} + +.log-entry.debug { + color: #8b949e; +} + +.log-entry.warn { + color: #d29922; +} + +.log-entry.error { + color: #f85149; +} + +.log-entry.success { + color: #3fb950; +} + +.log-timestamp { + color: #8b949e; + margin-right: 8px; +} + +.log-level { + font-weight: bold; + margin-right: 8px; + padding: 2px 6px; + border-radius: 3px; + font-size: 11px; +} + +.log-level.info { + background-color: rgba(88, 166, 255, 0.1); +} + +.log-level.debug { + background-color: rgba(139, 148, 158, 0.1); +} + +.log-level.warn { + background-color: rgba(210, 153, 34, 0.1); +} + +.log-level.error { + background-color: rgba(248, 81, 73, 0.1); +} + +.log-level.success { + background-color: rgba(63, 185, 80, 0.1); +} + +.log-message { + word-break: break-all; +} + +.log-filter-controls { + display: flex; + gap: 12px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.log-filter-btn { + padding: 6px 12px; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--bg-card); + color: var(--text-primary); + cursor: pointer; + font-size: 12px; + transition: all 0.2s; +} + +.log-filter-btn:hover { + background: rgba(0, 0, 0, 0.05); +} + +.log-filter-btn.active { + background: var(--accent-color); + color: white; + border-color: var(--accent-color); +} + +.log-search { + flex: 1; + min-width: 200px; + padding: 6px 12px; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 12px; +} + +.log-search:focus { + outline: none; + border-color: var(--accent-color); +} + +.log-entry { + margin-bottom: 4px; +} + +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + align-items: center; + justify-content: center; +} + +.modal.active { + display: flex; +} + +.modal-content { + background: white; + border-radius: 8px; + padding: 24px; + width: 90%; + max-width: 500px; + max-height: 80vh; + overflow-y: auto; +} + +.modal-title { + font-size: 18px; + font-weight: 600; + margin-bottom: 16px; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 20px; +} + +.qrcode-container { + text-align: center; + margin: 20px 0; +} + +.qrcode-img { + max-width: 200px; + max-height: 200px; +} + +.status-badge { + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.status-active { + background: rgba(26, 127, 55, 0.1); + color: var(--success-color); +} + +.status-inactive { + background: rgba(209, 36, 47, 0.1); + color: var(--error-color); +} + +.toggle-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.4s; + border-radius: 24px; +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 4px; + bottom: 4px; + background-color: white; + transition: 0.4s; + border-radius: 50%; +} + +input:checked + .toggle-slider { + background-color: var(--accent-color); +} + +input:checked + .toggle-slider:before { + transform: translateX(26px); +} + +.monitor-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 16px; + margin-top: 16px; +} + +.monitor-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; +} + +.monitor-title { + font-size: 16px; + font-weight: 600; + margin-bottom: 8px; +} + +.monitor-value { + font-size: 24px; + font-weight: 700; + color: var(--accent-color); +} + +.monitor-label { + font-size: 12px; + color: var(--text-secondary); + margin-top: 4px; +} + +.project-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 16px; + margin-top: 16px; +} + +.project-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; + cursor: pointer; + transition: all 0.2s; +} + +.project-card:hover { + border-color: var(--accent-color); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.project-name { + font-size: 16px; + font-weight: 600; + margin-bottom: 8px; +} + +.project-info { + font-size: 12px; + color: var(--text-secondary); + margin-bottom: 4px; +} + +.settings-group { + margin-bottom: 24px; +} + +.settings-title { + font-size: 16px; + font-weight: 600; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border-color); +} + +.settings-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.settings-label { + font-weight: 500; +} + +.settings-description { + font-size: 12px; + color: var(--text-secondary); + margin-top: 4px; +} + +.settings-subgroup { + margin-top: 16px; + padding: 16px; + background: rgba(0, 0, 0, 0.02); + border-radius: 6px; + border-left: 3px solid var(--accent-color); +} + +.settings-subtitle { + font-size: 14px; + font-weight: 600; + margin-bottom: 12px; + color: var(--accent-color); +} + +.select-input { + width: 200px; + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 14px; +} + +.number-input { + width: 100px; + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 14px; +} + +/* 购票人列表样式 */ +#buyer-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-card); +} + +.buyer-item { + display: flex; + align-items: center; + padding: 12px; + border-bottom: 1px solid var(--border-color); + transition: background-color 0.2s; +} + +.buyer-item:last-child { + border-bottom: none; +} + +.buyer-item:hover { + background-color: rgba(0, 0, 0, 0.02); +} + +.buyer-item input[type="checkbox"] { + width: 18px; + height: 18px; + margin-right: 12px; + cursor: pointer; + accent-color: var(--accent-color); +} + +.buyer-item label { + flex: 1; + cursor: pointer; + font-size: 14px; + display: flex; + align-items: center; +} + +.buyer-item label strong { + margin-right: 8px; +} + +#buyer-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +#buyer-error { + padding: 12px; + background-color: rgba(209, 36, 47, 0.1); + border-radius: 6px; + color: var(--error-color); + font-size: 14px; +} + +/* 通知样式 */ +#notification-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 10px; + max-width: 400px; +} + +.notification { + padding: 15px 20px; + border-radius: 8px; + box-shadow: 0 4px12px rgba(0, 0, 0, 0.15); + font-size: 14px; + font-weight: 500; + display: flex; + align-items: center; + justify-content: space-between; + animation: slideIn 0.3s ease-out; + transition: opacity 0.3s ease-out; + max-width: 400px; +} + +.notification.hiding { + opacity: 0; +} + +.notification.success { + background-color: var(--success-color); + color: white; + border-left: 4px solid #0c6b2d; +} + +.notification.error { + background-color: var(--error-color); + color: white; + border-left: 4px solid #a81c26; +} + +.notification.warning { + background-color: var(--warning-color); + color: white; + border-left: 4px solid #996500; +} + +.notification.info { + background-color: var(--accent-color); + color: white; + border-left: 4px solid #0757b8; +} + +.notification-content { + flex: 1; + margin-right: 10px; +} + +.notification-close { + background: none; + border: none; + color: white; + font-size: 18px; + cursor: pointer; + opacity: 0.7; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.notification-close:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.2); +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.text-secondary { + color: var(--text-secondary); +} + +.mb-8 { + margin-bottom: 8px; +} + +.mb-16 { + margin-bottom: 16px; +} + +.font-12 { + font-size: 12px; +} + +.font-500 { + font-weight: 500; +} + +.display-none { + display: none; +} + +.display-flex { + display: flex; +} + +.display-grid { + display: grid; +} + +.gap-12 { + gap: 12px; +} + +.gap-16 { + gap: 16px; +} + +.mb-20 { + margin-bottom: 20px; +} + +.text-center { + text-align: center; +} + +.mt-16 { + margin-top: 16px; +} + +.p-12 { + padding: 12px; +} + +.bg-light { + background: rgba(0, 0, 0, 0.02); +} + +.rounded-6 { + border-radius: 6px; +} + +.grid-cols-2 { + grid-template-columns: repeat(2, 1fr); +} + +.color-success { + color: var(--success-color); +} + +.color-error { + color: var(--error-color); +} + +/* 特定组件样式 */ +.stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +.stats-label { + font-size: 12px; + color: var(--text-secondary); +} + +.stats-value { + font-weight: 500; +} + +.monitor-controls { + display: flex; + gap: 12px; + margin-bottom: 20px; +} + +.monitor-status-box { + text-align: center; + margin-top: 16px; + padding: 12px; + background: rgba(0, 0, 0, 0.02); + border-radius: 6px; +} + +.status-label { + color: var(--text-secondary); + font-size: 14px; +} + +.quick-start-item { + color: var(--text-secondary); + margin-bottom: 8px; +} + +.quick-start-item:last-child { + margin-bottom: 0; +} diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml deleted file mode 100644 index 92708a6..0000000 --- a/frontend/Cargo.toml +++ /dev/null @@ -1,50 +0,0 @@ -[package] -name = "frontend" -version = "0.1.0" -edition = "2021" - -[dependencies] -eframe = { version = "0.23.0", features = ["default"] } -egui = "0.31.0" -winapi = { version = "0.3.9", features = ["winuser", "windef"] } - -# 字体依赖 -egui_extras = { version = "0.23.0", features = ["all_loaders"] } - -# 背景图片 -image = "0.25" - -#时间 -chrono = "0.4" - -#日志 -log = "0.4" -env_logger = "0.9" - -#序列化 -base64 = "0.13" - -#str转二维码 -qrcode = "0.14.1" - - -#网络请求 -reqwest = { version="0.11.22", features=["json", "blocking", "cookies"]} -tokio = { version = "1", features = ["full"] } - - -#rand -rand = "0.8" - -#json -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -#jsonwebtoken -jsonwebtoken = "9" - - - -common = {path = "../common"} -backend = {path = "../backend"} - diff --git a/frontend/assets/background.jpg b/frontend/assets/background.jpg deleted file mode 100644 index 091f9f9..0000000 Binary files a/frontend/assets/background.jpg and /dev/null differ diff --git a/frontend/assets/background1.jpg b/frontend/assets/background1.jpg deleted file mode 100644 index b771883..0000000 Binary files a/frontend/assets/background1.jpg and /dev/null differ diff --git a/frontend/assets/background2.jpg b/frontend/assets/background2.jpg deleted file mode 100644 index 4b027cc..0000000 Binary files a/frontend/assets/background2.jpg and /dev/null differ diff --git a/frontend/assets/default_avatar.jpg b/frontend/assets/default_avatar.jpg deleted file mode 100644 index b5f5602..0000000 Binary files a/frontend/assets/default_avatar.jpg and /dev/null differ diff --git a/frontend/src/app.rs b/frontend/src/app.rs deleted file mode 100644 index 1ee2974..0000000 --- a/frontend/src/app.rs +++ /dev/null @@ -1,1304 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; -use std::fs::File; -use std::io::{Read, Write}; -use std::time::{SystemTime, UNIX_EPOCH}; -use eframe::egui; -use reqwest::{Client, header}; -use serde_json::{json,Value}; -use serde::{Deserialize, Serialize}; -use tokio::runtime::Runtime; -use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; - - -use crate::ui; -use crate::windows; -use crate::windows::login_windows::LoginTexture; -use crate::windows::add_buyer::AddBuyerInput; -use crate::ui::error_banner::render_error_banner; - -use common::LOG_COLLECTOR; -use common::account::{Account,add_account}; -use common::utils::{*}; -use common::utility::CustomConfig; -use common::push::{*}; -use common::login::LoginInput; -use common::taskmanager::{*}; -use common::captcha::LocalCaptcha; -use common::show_orderlist::OrderResponse; -use common::cookie_manager::CookieManager; -use common::taskmanager::GetAllorderRequest; -use common::taskmanager::TaskRequest; -use common::ticket::{*}; - -use backend::taskmanager::TaskManagerImpl; - - -//UI -pub struct Myapp{ - pub app: String, - pub version: String, - pub policy: Option, - //ui - pub left_panel_width: f32, //左面板宽度 - pub selected_tab: usize, //左侧已选中标签 - //加载动画 - pub loading_angle: f32, - pub is_loading: bool, - //运行状态(显示用) - pub running_status: String, - //自定义背景图 (未启用,效果不好,预留暂时不用) - pub background_texture: Option, - //日志记录 - pub logs: Vec, - pub show_log_window: bool, - //登录窗口 - pub show_login_windows: bool, - //用户信息 - - pub default_avatar_texture: Option, // 默认头像 - - //错误提醒横幅 - pub error_banner_active: bool, - pub error_banner_text: String, - pub error_banner_start_time: Option, - pub error_banner_opacity: f32, - - //成功提醒横幅 - pub success_banner_active: bool, - pub success_banner_text: String, - pub success_banner_start_time: Option, - pub success_banner_opacity: f32, - - //抢票id - pub ticket_id: String, - - //任务管理 - pub task_manager: Box, - pub account_manager: AccountManager, - - //推送设置 - pub push_config: PushConfig, - - //config - pub config: Config, - - //自定义配置 - pub custom_config: CustomConfig, - //登录背景 - pub login_texture: LoginTexture, - - //登录方式 - pub login_method: String, - - //用于登录的client,登录后存入account - pub client: Client, - - //登录用,防止重复刷新二维码 - pub login_qrcode_url: Option, - - //登录用异步回调taskid - pub qrcode_polling_task_id: Option, - - //登录用输入 - pub login_input: LoginInput, - - //登录用发送短信任务id - pub pending_sms_task_id: Option, - - //默认ua - pub default_ua: String, - - //发送短信captcha_key - pub sms_captcha_key: String, - - //删除账号 - pub delete_account: Option, - - //cookie登录,暂存cookie - pub cookie_login: Option, - - //该账号开启抢票开关 - pub account_switch: Option, - - //添加购票人的输入 - pub add_buyer_input: AddBuyerInput, - - //添加购票人窗口 - pub show_add_buyer_window: Option, //如果是bool类型会导致无法对应申请添加的账号, - //所以使用string表示要添加购票人的账号的uid - - pub show_orderlist_window: Option, //订单列表窗口的账号uid - - pub total_order_data: Option, //订单数据缓存 - - pub orderlist_need_reload: bool, //订单列表是否需要重新加载 - - pub orderlist_last_request_time: Option, // 上次请求的时间 - pub orderlist_requesting: bool, // 是否正在请求中 - - //抢票相关 - pub status_delay: usize, //延迟时间 - - pub grab_mode: u8, // 0: 自动抢票, 1: 直接抢票, 2: 捡漏回流票 - pub selected_account_uid: Option, // 记录被选择账号的UID - - pub bilibiliticket_list: Vec, // 用于存储多个抢票实例 - - pub ticket_info: Option, //根据projectid获取的项目详情 - - pub show_screen_info: Option, //开启显示场次窗口(获取到project信息后) - - pub selected_screen_index: Option, // 当前选中的场次索引 - pub selected_screen_id: Option, // 当前选中的场次ID - pub selected_ticket_id: Option, // 当前选中的票种ID - - pub ticket_info_last_request_time: Option, // 上次请求的时间 - - pub confirm_ticket_info: Option, //确认抢票信息(购票人,预填手机号) - - pub selected_buyer_list: Option>, // 选中的购票人ID - - pub local_captcha: LocalCaptcha, // 本地打码实例 - - pub show_qr_windows: Option, //扫码支付窗口 (传二维码数据) - - pub machine_id :String, - - pub announce1: Option, //主公告 - pub announce2: Option, - pub announce3: Option,//监视公告 - pub announce4: Option, //退出公告 - - - pub public_key: String, - pub skip_words: Option>, - pub skip_words_input: String, - - } - - -//账号管理 - -pub struct AccountManager{ - pub accounts: Vec, - - pub active_tasks: HashMap, -} - -//获取全部订单结构体(便于区分) -pub struct OrderData { - pub account_id: String, - pub data : Option, -} - - - - -impl Myapp{ - pub fn new(cc: &eframe::CreationContext<'_>) -> Self{ - - //中文字体 - ui::fonts::configure_fonts(&cc.egui_ctx); - let config = match Config::load_config() { - Ok(load_config) => { - log::info!("配置文件加载成功"); - load_config - }, - Err(e) => { - log::error!("配置文件加载失败: {}", e); - log::info!("尝试迁移json配置"); - match Config::load_json_config() { - Ok(load_config) => { - log::info!("配置文件加载成功"); - match load_config.save_config() { - Ok(_) => { - log::info!("配置文件保存成功"); - match Config::delete_json_config() { - Ok(_) => { - log::info!("旧配置文件删除成功"); - }, - Err(e) => { - log::error!("旧配置文件删除失败: {}", e); - } - } - log::info!("迁移成功"); - }, - Err(e) => { - log::error!("配置文件保存失败: {}", e); - } - - } - load_config - } - Err(e) => { - log::error!("迁移失败: {}", e); - let cfg =Config::new(); - match cfg.save_config() { - Ok(_) => { - log::info!("配置文件保存成功"); - }, - Err(e) => { - log::error!("配置文件保存失败: {}", e); - } - - } - cfg - } - - } - } - }; - - - - let mut app = Self { - app: String::from("BRT"), - version: String::from("6.6.1"), - policy: None, - public_key: String::from("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApTAS0RElXIs4Kr0bO4n8\nJB+eBFF/TwXUlvtOM9FNgHjK8m13EdwXaLy9zjGTSQr8tshSRr0dQ6iaCG19Zo2Y\nXfvJrwQLqdezMN+ayMKFy58/S9EGG3Np2eGgKHUPnCOAlRicqWvBdQ/cxzTDNCxa\nORMZdJRoBvya7JijLLIC3CoqmMc6Fxe5i8eIP0zwlyZ0L0C1PQ82BcWn58y7tlPY\nTCz12cWnuKwiQ9LSOfJ4odJJQK0k7rXxwBBsYxULRno0CJ3rKfApssW4cfITYVax\nFtdbu0IUsgEeXs3EzNw8yIYnsaoZlFwLS8SMVsiAFOy2y14lR9043PYAQHm1Cjaf\noQIDAQAB\n-----END PUBLIC KEY-----"), - left_panel_width: 250.0, - selected_tab: 0, - is_loading: false, - loading_angle: 0.0, - background_texture: None, - show_log_window: false, - show_login_windows: false, - logs: Vec::new(), - client: Client::new(), - default_avatar_texture: None, - running_status: String::from("空闲ing"), - ticket_id: String::from(""), - // 初始化任务管理器 - task_manager: Box::new(TaskManagerImpl::new()), - account_manager: AccountManager { - accounts: Config::load_all_accounts(), - active_tasks: HashMap::new(), - }, - - push_config : match serde_json::from_value::(config["push_config"].clone()) { - Ok(config) => config, - Err(e) => { - log::warn!("无法解析推送配置: {}, 使用默认值", e); - PushConfig::new() - } - }, - - - - custom_config: match serde_json::from_value::(config["custom_config"].clone()) { - Ok(config) => config, - Err(e) => { - log::warn!("无法解析自定义配置: {}, 使用默认值", e); - CustomConfig::new() - } - }, - config: config.clone(), - login_texture: LoginTexture { left_conrner_texture: None , right_conrner_texture: None}, - - login_method: "扫码登录".to_string(), - - - login_qrcode_url: None, - qrcode_polling_task_id: None, - login_input: LoginInput{ - phone: String::new(), - account: String::new(), - password: String::new(), - cookie: String::new(), - sms_code: String::new(), - }, - pending_sms_task_id: None, - - default_ua: String::from("Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Mobile Safari/537.36"), - sms_captcha_key: String::new(), - delete_account: None, - cookie_login: None, - account_switch: None, - add_buyer_input: AddBuyerInput { - name: String::new(), - phone: String::new(), - id_type: 0, - id_number: String::new(), - as_default_buyer: false, - }, - show_add_buyer_window: None, - show_orderlist_window: None, - total_order_data: None, - orderlist_need_reload: false, - orderlist_last_request_time: None, - orderlist_requesting: false, - error_banner_active: false, - error_banner_text: String::new(), - error_banner_start_time: None, - error_banner_opacity: 0.0, - success_banner_active: false, - success_banner_text: String::new(), - success_banner_start_time: None, - success_banner_opacity: 0.0, - status_delay: 2, - grab_mode: 0, - selected_account_uid: None, - bilibiliticket_list: Vec::new(), - ticket_info: None, - show_screen_info: None, - selected_screen_index: None, - selected_screen_id: None, - selected_ticket_id: None, - ticket_info_last_request_time: None, - confirm_ticket_info: None, - selected_buyer_list: None, - local_captcha: LocalCaptcha::new(), - show_qr_windows: None, - announce1: None, - announce2: None, - announce3: None, - announce4: None, - machine_id: common::machine_id::get_machine_id_ob(), - skip_words: None, - skip_words_input: String::from(""), - - }; - // 初始化每个账号的 client - for account in &mut app.account_manager.accounts { - account.ensure_client(); - - log::debug!("为账号 {} 初始化了专属客户端", account.name); - log::debug!("machine_id: {}", app.machine_id); - - } - - //初始化client和ua - let random_value = generate_random_string(8); - app.default_ua = format!( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0 {}", - random_value - ); - if config["custom_config"]["enable_custom_ua"].as_bool().unwrap_or(false) && !config["custom_config"]["custom_ua"].is_null() { - app.default_ua = config["custom_config"]["custom_ua"].as_str().unwrap_or(&app.default_ua).to_string(); - - } - let new_client = create_client(app.default_ua.clone()); - app.client = new_client; - - - app - - - } - - pub fn add_log(&mut self, message: &str) { - self.logs.push(format!("{}", message)); - if self.logs.len() > 5000 { - self.logs.drain(0..2500); // 删除前一半日志 - } - // 首先检查是否为错误消息 - 给错误消息更高优先级 - if message.contains("ERROR:") || message.contains("error:") || message.contains("Error:") { - self.error_banner_active = true; - self.error_banner_text = message.to_string(); - self.error_banner_start_time = Some(std::time::Instant::now()); - self.error_banner_opacity = 1.0; - } - // 然后检查是否为成功消息,但使用更严格的条件 - else if message.contains("info:") || - message.contains("INFO:") || - message.contains("Info:") || - (message.contains("INFO:") && !message.contains("ERROR:")) || // 只有包含INFO但不包含ERROR的才算成功 - message.contains("下单成功") { - self.success_banner_active = true; - self.success_banner_text = message.to_string(); - self.success_banner_start_time = Some(std::time::Instant::now()); - self.success_banner_opacity = 1.0; - } - // 普通消息不显示横幅 - } - - async fn get_policy(&mut self) -> Value { - // 获取当前时间戳 - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - - // 构建请求数据 - let data = json!({ - "ts": timestamp, - "machine_id": self.machine_id.clone(), - - - }); - - - let url = format!("https://policy.nexaorion.cn/api/client/{}/{}/dispatch.json", - self.app, self.version); - - match self.client.post(&url) - .json(&data) - .send() - .await { - Ok(response) => { - - match response.json::().await { - Ok(resp) => { - - if let Some(code) = resp["code"].as_i64() { - if code != 0 { - log::error!("获取策略失败: {}", resp["message"]); - return json!({"allow_run": true}); - } - - - match decode_policy(&resp["data"]["data"].as_str().unwrap_or(""), &self.public_key) { - Ok(policy) => { - - if let Some(permission_token) = resp["data"]["permission"].as_str() { - match decode_permissions(permission_token, &self.public_key) { - Ok(permissions) => { - - if let Ok(mut file) = File::create("permissions") { - let _ = file.write_all(permission_token.as_bytes()); - } - self.policy = Some(permissions); - }, - Err(e) => { - log::error!("权限签名无效: {}", e); - self.policy = Some(load_local_permissions(self.public_key.clone().as_str())); - } - } - } - return policy; - }, - Err(e) => { - log::error!("策略签名无效: {}", e); - return json!({"allow_run": false}); - } - } - } - }, - Err(e) => { - log::error!("解析响应失败: {}", e); - - } - } - }, - Err(e) => { - log::error!("请求策略失败: {}", e); - } - } - - - json!({"allow_run": true}) - } - - // 处理任务结果的方法 - fn process_task_results(&mut self) { - // 获取所有可用结果 - let results = self.task_manager.get_results(); - - // 存储需要记录的日志消息 - let mut pending_logs: Vec = Vec::new(); - let mut account_updates: Vec = Vec::new(); - - for result in results { - match result { - - - //处理qrcode登录结果 - TaskResult::QrCodeLoginResult(qrcode_result) => { - // 二维码登录的处理逻辑 - match qrcode_result.status { - common::login::QrCodeLoginStatus::Success(cookie) => { - log::info!("二维码登录成功!"); - - - if let Some(cookie_str) = qrcode_result.cookie { - - self.handle_login_success(&cookie_str); - } - }, - common::login::QrCodeLoginStatus::Failed(err) => { - log::error!("二维码登录失败: {}", err); - }, - common::login::QrCodeLoginStatus::Expired => { - log::warn!("二维码已过期,请刷新"); - }, - _ => { - - } - } - } - TaskResult::LoginSmsResult(sms_result) => { - // 处理短信登录结果 - if sms_result.success { - self.sms_captcha_key = sms_result.message.clone(); - log::debug!("发送captchakey:{}",sms_result.message); - log::info!("短信发送成功 "); - } else { - log::error!("短信发送失败: {}", sms_result.message); - } - } - TaskResult::SubmitSmsLoginResult(submit_result) => { - if submit_result.success{ - if let Some(cookie_str) = submit_result.cookie { - self.handle_login_success(&cookie_str); - } - } else { - log::error!("短信登录失败: {}", submit_result.message); - } - } - TaskResult::PushResult(push_result) => { - // 处理推送结果 - if push_result.success { - log::info!("推送成功: {}", push_result.message); - } else { - log::error!("推送失败: {}", push_result.message); - } - } - TaskResult::GetAllorderRequestResult(order_result) => { - // 处理订单请求结果 - if order_result.success { - self.total_order_data = Some(OrderData { - account_id: order_result.account_id.clone(), - data: order_result.order_info.clone(), - }); - log::info!("账号 {} 订单请求成功", order_result.account_id); - } else { - log::error!("账号 {} 订单请求失败", order_result.account_id); - - } - } - TaskResult::GetTicketInfoResult(order_result) => { - if order_result.success{ - let inforesponse = match order_result.ticket_info { - Some(ref info) => info, - None => { - log::error!("获取project信息失败: {}", order_result.message); - continue; - } - }; - - let project_info = inforesponse.data.clone(); - let uid = order_result.uid.clone(); - if let Some(bilibili_ticket) = self.bilibiliticket_list - .iter_mut() - .find(|ticket| ticket.uid == uid){ - bilibili_ticket.project_info = Some(project_info.clone()); - log::debug!("获取project信息成功: {:?}", project_info); - }else{ - log::error!("未找到账号ID为 {} 的抢票对象,可能已被移除", uid); - self.show_screen_info = None; - continue; - } - - }else{ - log::error!("获取project信息失败: {}", order_result.message); - self.show_screen_info = None; - } - - } - TaskResult::GetBuyerInfoResult(get_buyerinfo_result)=>{ - if get_buyerinfo_result.success{ - let response = match get_buyerinfo_result.buyer_info { - Some(ref info) => info, - None => { - log::error!("获取购票人信息失败: {}", get_buyerinfo_result.message); - continue; - } - }; - if response.errno != 0{ - log::error!("获取购票人信息失败: {:?}", response); - continue; - } - let buyer_info = response.data.clone(); - let uid = get_buyerinfo_result.uid.clone(); - if let Some(bilibili_ticket) = self.bilibiliticket_list - .iter_mut() - .find(|ticket| ticket.uid == uid){ - bilibili_ticket.all_buyer_info = Some(buyer_info.clone()); - log::debug!("获取购票人信息成功: {:?}", buyer_info); - }else{ - log::error!("未找到账号ID为 {} 的抢票对象,可能已被移除", uid); - self.show_screen_info = None; - continue; - } - - }else{ - log::error!("获取购票人信息失败: {}", get_buyerinfo_result.message); - self.show_screen_info = None; - } - } - TaskResult::GrabTicketResult(grab_ticket_result)=>{ - if grab_ticket_result.success{ - let pay_url = match grab_ticket_result.pay_result { - Some(ref data) => { - data.code_url.clone() - }, - None => { - log::error!("获取支付链接失败: {}", grab_ticket_result.message); - continue; - } - }; - self.show_qr_windows = Some(pay_url.clone()); - let confirm_result = match grab_ticket_result.confirm_result{ - Some(data) => data, - None => { - ConfirmTicketResult { - project_name: "".to_string(), - screen_name: "".to_string(), - count: 0, - pay_money: 0, - ticket_info: ConfirmTicketInfo{ - name: "".to_string(), - count: 0, - price: 0, - } - } - } - }; - let jump_url = Some(format!("bilibili://mall/web?url=https://mall.bilibili.com/neul-next/ticket/orderDetail.html?order_id={}", grab_ticket_result.order_id.unwrap_or("".to_string()))); - let title = format!("恭喜{}抢票成功!", confirm_result.project_name); - let message = format!("抢票成功!\n项目:{}\n场次:{}\n票类型:{}\n支付链接:{}\n请尽快支付{}元,以免支付超时导致票丢失\n如果觉得本项目好用,可前往https://github.com/biliticket/bili_ticket_rush 帮我们点个小星星star收藏本项目以防走丢\n本项目完全免费开源,仅供学习使用,开发组不承担使用本软件造成的一切后果",confirm_result.project_name, confirm_result.screen_name, confirm_result.ticket_info.name, pay_url ,confirm_result.ticket_info.price * confirm_result.count as i64/ 100); - log::info!("{}",title); - log::info!("{}",message); - //这里注释是因为推送任务已经在grab_ticket任务里提交了,由于挂后台不刷新不推送 - if self.push_config.enabled{ - let push_request = TaskRequest::PushRequest(PushRequest { - title: title.clone(), - message: message.clone(), - push_type: PushType::All, - jump_url: jump_url.clone(), - push_config: self.push_config.clone(), - - }); - match self.task_manager.submit_task(push_request){ - Ok(task_id) => { - log::debug!("提交全渠道推送任务成功,任务ID: {}", task_id); - }, - Err(e) => { - log::error!("提交推送任务失败: {}", e); - } - } - - - } - //self.push_config.push_all(title.as_str(), message.as_str(), &jump_url,&mut *self.task_manager); - - } - } - } - } - - // 更新账号状态 - for account_id in account_updates { - if let Some(account) = self.account_manager.accounts.iter_mut() - .find(|a| a.uid == account_id.parse::().unwrap_or(-1)) { - account.account_status = "空闲".to_string(); - } - - } - - // 一次性添加所有日志,避免借用冲突 - for message in pending_logs { - self.add_log(&message); - } - } - - pub fn add_log_windows(&mut self) { //从env_log添加日志进窗口 - if let Some(logs) = LOG_COLLECTOR.lock().unwrap().get_logs() { - for log in logs { - self.add_log(&log); - } - } - } - - fn check_policy(&mut self) { - if let Some(policy) = &self.policy { - // 检查是否有公告信息 - if let Some(announcement) = policy.get("announcement1").and_then(|v| v.as_str()) { - log::info!("公告: {}", announcement); - // 可选:显示公告横幅 - self.success_banner_active = true; - self.success_banner_text = format!("公告: {}", announcement); - self.success_banner_start_time = Some(std::time::Instant::now()); - self.success_banner_opacity = 1.0; - self.announce1 = Some(announcement.to_string()); - } - - if let Some(announcement) = policy.get("announcement2").and_then(|v| v.as_str()) { - - self.announce2 = Some(announcement.to_string()); - } - - if let Some(announcement) = policy.get("announcement3").and_then(|v| v.as_str()) { - - self.announce3 = Some(announcement.to_string()); - } - - if let Some(announcement) = policy.get("announcement4").and_then(|v| v.as_str()) { - - self.announce4 = Some(announcement.to_string()); - } - - // 检查是否允许运行 - let allow_run = policy.get("allow_run").and_then(|v| v.as_bool()).unwrap_or(false); - if !allow_run { - if let Some(accouncement) = self.announce4.clone(){ - log::error!("公告: {}", accouncement); - } - log::error!("根据策略配置,当前版本不允许运行"); - // 显示错误横幅 - self.error_banner_active = true; - self.error_banner_text = "根据策略配置,当前版本不允许运行".to_string(); - self.error_banner_start_time = Some(std::time::Instant::now()); - self.error_banner_opacity = 1.0; - - - std::process::exit(1); - } - } - } - - pub fn handle_login_success(&mut self, cookie: &str) { - log::debug!("登录成功,cookie: {}", cookie); - match add_account(cookie, &self.client,&self.default_ua){ - Ok(account) => { - self.account_manager.accounts.push(account.clone()); - match save_config(&mut self.config, None, None, Some(account.clone())){ - Ok(_) => { - log::info!("登录成功,账号已添加"); - self.show_login_windows = false; - }, - Err(e) => { - log::error!("登录成功,但保存账号失败: {}", e); - } - } - log::info!("登录成功,账号已添加"); - }, - Err(e) => { - log::error!("登录成功,但添加账号失败: {}", e); - } - } - - } -} - - - -impl eframe::App for Myapp{ - fn update(&mut self, ctx:&egui::Context, frame: &mut eframe::Frame){ - //侧栏 - ui::sidebar::render_sidebar(self,ctx); - - //主窗口 - egui::CentralPanel::default().show(ctx, |ui|{ - ui::tabs::render_tab_content(self, ui); - } ); - - - //加载动画 - if self.is_loading{ - ui::loading::render_loading_overlay(self, ctx); - } - - //日志 - if self.show_log_window{ - windows::log_windows::show(self, ctx); - } - - //登录窗口 - if self.show_login_windows{ - - windows::login_windows::show(self, ctx); - } - - //处理异步任务结果 - self.process_task_results(); - - static mut LAST_MONITOR_TIME: Option = None; - - unsafe { - let should_monitor = match LAST_MONITOR_TIME { - Some(time) => time.elapsed() > std::time::Duration::from_secs(30), - None => true, - }; - - if should_monitor { - log::info!("资源监控 - 日志条数: {}, 任务数: {}", - self.logs.len(), - self.task_manager.get_results().len()); - LAST_MONITOR_TIME = Some(std::time::Instant::now()); - } - } - - //检查policy - if self.policy.is_none(){ - let rt = Runtime::new().unwrap(); - rt.block_on(async { - let policy = self.get_policy().await; - self.policy = Some(policy.clone()); - self.check_policy(); - self.ticket_id = policy["ticket_id"].as_str().unwrap_or("").to_string(); - }); - /* let url = format!("https://policy.rakuyoudesu.com/api/client/{}/{}/dispatch.json",self.app,self.version); - let rt= Runtime::new().unwrap(); - let timestamp = rt.block_on(get_now_time(&self.client)); - let data = serde_json::json!({ - "ts": timestamp, - "machine_id": self.machine_id, - }); - - rt.block_on(async{ - match self.client.post(&url) - .json(&data) - .send() - .await { - Ok(response) => { - match response.text().await { - Ok(text) => { - match serde_json::from_str::(&text) { - Ok(json) => { - log::debug!("获取policy成功: {}", json); - self.policy = Some(json); - }, - Err(e) => { - log::error!("解析policy响应失败: {}", e); - } - } - }, - Err(e) => { - log::error!("获取policy响应文本失败: {}", e); - } - } - }, - Err(e) => { - log::error!("请求policy失败: {}", e); - } - } - - - - }) - */ - } - - //从env_log添加日志进窗口 - self.add_log_windows(); - - // 渲染错误横幅 - if self.error_banner_active { - // 计算横幅显示时间和透明度 - if let Some(start_time) = self.error_banner_start_time { - let elapsed = start_time.elapsed().as_secs_f32(); - - // 横幅在屏幕上停留2秒,然后在0.5秒内淡出 - if elapsed < 4.5 { - // 如果超过2秒,开始淡出 - if elapsed > 4.0 { - self.error_banner_opacity = 1.0 - (elapsed - 2.0) * 2.0; // 0.5秒内从1.0淡到0 - } - - // 绘制横幅 - render_error_banner(self, ctx); - - // 持续重绘以实现动画效果 - ctx.request_repaint(); - } else { - // 超过2.5秒,停用横幅 - self.error_banner_active = false; - self.error_banner_start_time = None; - } - } - } - - // 渲染成功横幅 - if self.success_banner_active { - if let Some(start_time) = self.success_banner_start_time { - let elapsed = start_time.elapsed().as_secs_f32(); - - // 横幅在屏幕上停留3秒,然后在1秒内淡出 - if elapsed < 4.0 { - // 如果超过3秒,开始淡出 - if elapsed > 3.0 { - self.success_banner_opacity = (1.0 - (elapsed - 3.0) / 1.0).max(0.0); - } - - - render_error_banner(self, ctx); - - // 持续重绘以实现动画效果 - ctx.request_repaint(); - } else { - // 超过4秒,停用横幅 - self.success_banner_active = false; - self.success_banner_start_time = None; - } - } - } - - - //删除账号 - if let Some(account_id) = self.delete_account.take() { - self.account_manager.accounts.retain(|account| account.uid != account_id.parse::().unwrap_or(-1)); - self.config.delete_account(account_id.parse::().unwrap_or(-1)); - log::info!("账号 {} 已删除", account_id); - } - - //检测是否有cookie - if let Some(cookie) = &self.cookie_login { - log::info!("检测到cookie: {}", cookie); - if let Ok(account) = add_account(cookie, &self.client,&self.default_ua) { - self.account_manager.accounts.push(account.clone()); - match save_config(&mut self.config, None, None, Some(account.clone())){ - Ok(_) => { - log::info!("cookie登录成功,账号已添加"); - }, - Err(e) => { - log::error!("cookie登录成功,但保存账号失败: {}", e); - } - } - log::info!("cookie登录成功,账号已添加"); - self.cookie_login = None; // 清空cookie - } else { - log::error!("cookie登录失败"); - self.cookie_login = None; - } - } - - - //检测是否有更新账号开关 - if let Some(account_switch) = &self.account_switch { - log::debug!("检测到账号开关: {}", account_switch.uid); - if let Some(account) = self.account_manager.accounts.iter_mut().find(|a| a.uid == account_switch.uid.parse::().unwrap_or(-1)) { - account.is_active = account_switch.switch; - log::debug!("账号 {} 开关已更新", account_switch.uid); - } else { - log::error!("未找到账号 {}", account_switch.uid); - } - self.account_switch = None; // 清空开关 - } - - //开启添加购票人窗口? - if let Some(account_id) = &self.show_add_buyer_window { - if account_id == "0"{ - self.show_add_buyer_window = None; - - } - else{ - windows::add_buyer::show(self, ctx, account_id.clone().as_str()); - } - - } - - //开启查看订单窗口? - if let Some(uid) = &self.show_orderlist_window { - let account_id = uid.clone().parse::().unwrap_or(0); - if account_id == 0{ - self.show_orderlist_window = None; - - } - else{ - - let account = self.account_manager.accounts.iter_mut().find(|a| a.uid == account_id.clone()).unwrap(); - let cookie_manager = match account.cookie_manager.clone() { - Some(cookie_manager) => cookie_manager, - None => { - log::error!("账号 {} 的客户端未初始化", account.name); - self.show_orderlist_window = None; - return; - } - }; - if self.total_order_data.is_none() { - self.orderlist_need_reload = true; - - - }else{ - if self.total_order_data.as_ref().unwrap().account_id == uid.clone(){ - - }else{ - log::error!("账号不匹配,正在重新加载"); - self.orderlist_need_reload = true; - - - } - - } - - // 防止频繁请求的逻辑 - let should_request = self.orderlist_need_reload && !self.orderlist_requesting && - match self.orderlist_last_request_time { - Some(last_time) => last_time.elapsed() > std::time::Duration::from_secs(5), // 5秒冷却时间 - None => true, // 从未请求过,允许请求 - }; - if should_request { - log::debug!("提交订单请求 (冷却期已过)"); - self.orderlist_requesting = true; // 标记为正在请求中 - self.orderlist_last_request_time = Some(std::time::Instant::now()); - self.orderlist_need_reload = false; - submit_get_total_order(&mut self.task_manager, cookie_manager, account); - self.orderlist_need_reload = false; - } - windows::show_orderlist::show(self, ctx); - } - - } - - - //开启场次窗口 - if self.show_screen_info.is_some() { - let account_id = self.show_screen_info.clone().unwrap(); - /* log::debug!("账号id:{}", account_id); - - - log::debug!("当前列表长度: {}", self.bilibiliticket_list.len()); - for (i, ticket) in self.bilibiliticket_list.iter().enumerate() { - log::debug!("列表项 #{}: uid={}", i, ticket.uid); - } */ - - - if let Some(bilibili_ticket) = self.bilibiliticket_list - .iter_mut() - .find(|ticket| ticket.uid == account_id) - { - let should_request = bilibili_ticket.project_info.is_none() && match self.ticket_info_last_request_time{ - Some(last_time) => last_time.elapsed() > std::time::Duration::from_secs(5), - None => true, - }; - - if should_request { - log::info!("提交获取{}project请求 ", self.ticket_id); - let cookie_manager = bilibili_ticket.account.cookie_manager.clone().unwrap(); - { - let request = TaskRequest::GetTicketInfoRequest(GetTicketInfoRequest{ - task_id: "".to_string(), - uid: bilibili_ticket.uid.clone(), - project_id: self.ticket_id.clone(), - cookie_manager: cookie_manager.clone(), - }); - match self.task_manager.submit_task(request) { - Ok(task_id) => { - log::info!("提交获取project请求,任务ID: {}", task_id); - self.is_loading = true; - self.ticket_info_last_request_time = Some(std::time::Instant::now()); - windows::screen_info::show(self, ctx, account_id); - }, - Err(e) => { - log::error!("提交获取project请求失败: {}", e); - } - } - } - } else { - - windows::screen_info::show(self, ctx, account_id); - } - } else { - - log::error!("未找到账号ID为 {} 的抢票对象,可能已被移除", account_id); - self.show_screen_info = None; - } - } - - - //确认信息窗口 - if self.confirm_ticket_info.is_some() { - let confirm_uid = match self.confirm_ticket_info.clone() { - Some(uid) => { - uid.parse::().unwrap_or(0) - } - None => { - log::error!("确认信息窗口未找到账号ID,可能已被移除"); - self.show_screen_info = None; - return; - } - }; - - - if let Some(bilibili_ticket) = self.bilibiliticket_list - .iter_mut() - .find(|ticket| ticket.uid == confirm_uid) - { - let mut should_request = bilibili_ticket.all_buyer_info.is_none() && match self.ticket_info_last_request_time{ - Some(last_time) => last_time.elapsed() > std::time::Duration::from_secs(5), - None => true, - }; - let mut id_bind = match bilibili_ticket.project_info.clone(){ - Some(proj_info) => proj_info.id_bind, - None => 0, - }; - if bilibili_ticket.method == 2 { //如果是捡漏模式,直接请求购票人信息 - id_bind = 1; - } - if id_bind == 0{ - self.is_loading = false; - should_request = false; - } - if should_request{ - log::info!("提交获取购票人信息请求"); - let cookie_manager = bilibili_ticket.account.cookie_manager.clone().unwrap(); - { - let request = TaskRequest::GetBuyerInfoRequest(GetBuyerInfoRequest{ - task_id: "".to_string(), - uid: bilibili_ticket.uid.clone(), - cookie_manager: cookie_manager.clone(), - }); - match self.task_manager.submit_task(request) { - Ok(task_id) => { - log::info!("提交获取购票人信息请求,任务ID: {}", task_id); - self.is_loading = true; - self.ticket_info_last_request_time = Some(std::time::Instant::now()); - - }, - Err(e) => { - log::error!("提交获取购票人信息请求失败: {}", e); - } - } - } - } - match bilibili_ticket.method { - 0|1 => { - windows::confirm_ticket::show(self, ctx, &confirm_uid.clone()); - } - 2 => { - windows::confirm_ticket2::show(self, ctx, &confirm_uid.clone()); - } - _ => { - log::error!("未知的抢票方式: {}", bilibili_ticket.method); - self.show_screen_info = None; - return; - } - } - - } else { - log::error!("未找到账号ID为 {} 的抢票对象,可能已被移除", confirm_uid); - self.show_screen_info = None; - } - } - - //扫码支付窗口 - if self.show_qr_windows.is_some() { - windows::show_qrcode::show(self, ctx); - } - - - } - - - -} - - -pub fn submit_get_total_order(task_manager: &mut Box,cookie_manager: Arc, account: &Account){ - let request = TaskRequest::GetAllorderRequest(GetAllorderRequest{ - task_id: "".to_string(), - account_id: account.uid.to_string().clone(), - cookie_manager: cookie_manager.clone(), - cookies: account.cookie.clone(), - //ua: account.user_agent.clone(), - status: TaskStatus::Pending, - start_time: None, - }); - -match task_manager.submit_task(request) { - Ok(task_id) => { - log::info!("订单请求提交成功,任务ID: {}", task_id); - } - Err(e) => { - log::error!("查看全部订单请求提交失败:{}",e); - } -} - -} - - -pub fn create_client(user_agent: String) -> Client { - let mut headers = header::HeaderMap::new(); - - log::info!("客户端 User-Agent: {}", user_agent); - headers.insert( - header::USER_AGENT, - header::HeaderValue::from_str(&user_agent).unwrap_or_else(|_| { - header::HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") - }) - ); - - Client::builder() - .default_headers(headers) - .cookie_store(true) - .build() - .unwrap_or_default() -} - -#[derive(Debug, Serialize, Deserialize)] -struct PolicyPayload { - policy: Value, -} - -#[derive(Debug, Serialize, Deserialize)] -struct PermissionsPayload { - permissions: Value, -} - - -// 解码策略JWT令牌 -fn decode_policy(token: &str, public_key: &str) -> Result { - let decoding_key = match DecodingKey::from_rsa_pem(public_key.as_bytes()) { - Ok(key) => key, - Err(e) => return Err(format!("无效的公钥: {}", e)), - }; - - let mut validation = Validation::new(Algorithm::RS256); - validation.validate_exp = false; - validation.required_spec_claims.clear(); - - match decode::(token, &decoding_key, &validation) { - Ok(token_data) => Ok(token_data.claims.policy), - Err(e) => Err(format!("解码JWT失败: {}", e)), - } -} - -// 解码权限JWT令牌 -fn decode_permissions(token: &str, public_key: &str) -> Result { - let decoding_key = match DecodingKey::from_rsa_pem(public_key.as_bytes()) { - Ok(key) => key, - Err(e) => return Err(format!("无效的公钥: {}", e)), - }; - - let mut validation = Validation::new(Algorithm::RS256); - validation.validate_exp = false; - validation.required_spec_claims.clear(); - - match decode::(token, &decoding_key, &validation) { - Ok(token_data) => Ok(token_data.claims.permissions), - Err(e) => Err(format!("解码JWT失败: {}", e)), - } -} - -// 加载本地保存的权限 -fn load_local_permissions(public_key: &str) -> Value { - match File::open("permissions") { - Ok(mut file) => { - let mut contents = String::new(); - if file.read_to_string(&mut contents).is_ok() { - if let Ok(decoded) = decode_permissions(&contents, &public_key) { - return decoded; - } - } - }, - Err(_) => {} - } - json!({}) -} -fn generate_random_string(length: usize) -> String { - use rand::{thread_rng, Rng}; - use rand::distributions::Alphanumeric; - - thread_rng() - .sample_iter(&Alphanumeric) - .take(length) - .map(|c| c as char) - .collect() -} - -pub struct AccountSwitch { - pub uid: String, - pub switch: bool, -} \ No newline at end of file diff --git a/frontend/src/main.rs b/frontend/src/main.rs deleted file mode 100644 index 090ab55..0000000 --- a/frontend/src/main.rs +++ /dev/null @@ -1,63 +0,0 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use eframe::epaint::Vec2; -mod app; -mod ui; -mod windows; -fn main() -> Result<(), eframe::Error> { - std::env::set_var("LIBGL_ALWAYS_SOFTWARE", "1"); // 强制软件渲染 - std::env::set_var("MESA_GL_VERSION_OVERRIDE", "3.3"); // 尝试覆盖 GL 版本 - std::env::set_var("GALLIUM_DRIVER", "llvmpipe"); // 使用 llvmpipe 软件渲染器 - if let Err(e) = common::init_logger() { - eprintln!("初始化日志失败,原因: {}", e); - } - log::info!("日志初始化成功"); - - std::panic::set_hook(Box::new(|panic_info| { - if let Some(s) = panic_info.payload().downcast_ref::<&str>() { - if s.contains("swap") || s.contains("vsync") { - log::warn!("图形渲染非致命错误: {}", s); - // 继续允许程序运行 - } else { - log::error!("程序panic: {}", panic_info); - } - } else { - log::error!("程序panic: {}", panic_info); - } - })); - - // 检查程序是否已经在运行 - if !common::utils::ensure_single_instance() { - eprintln!("程序已经在运行中,请勿重复启动!"); - //增加休眠时间,防止程序过快退出 - std::thread::sleep(std::time::Duration::from_secs(5)); - std::process::exit(1); - } - - // 创建资源目录(如果不存在) - create_resources_directory(); - - let options = eframe::NativeOptions { - initial_window_size: Some(Vec2::new(1200.0, 600.0)), - min_window_size: Some(Vec2::new(800.0, 600.0)), - vsync: false, - - ..Default::default() - }; - - eframe::run_native( - "原神", - options, - Box::new(|cc| Box::new(app::Myapp::new(cc))), - ) -} - -// 确保资源目录存在 -fn create_resources_directory() { - let resources_dir = std::path::Path::new("resources/fonts"); - if !resources_dir.exists() { - if let Err(e) = std::fs::create_dir_all(resources_dir) { - log::warn!("无法创建资源目录: {}", e); - } - } -} diff --git a/frontend/src/main_old.rs b/frontend/src/main_old.rs deleted file mode 100644 index 6c9445e..0000000 --- a/frontend/src/main_old.rs +++ /dev/null @@ -1,395 +0,0 @@ -use eframe::{egui, epaint::Vec2}; -use egui::FontId; -use std::fs::read; -use chrono::Local; - -fn main() -> Result<(), eframe::Error> { - let options = eframe::NativeOptions { - initial_window_size: Some(Vec2::new(1100.0, 600.0)), - min_window_size: Some(Vec2::new(800.0, 600.0)), - ..Default::default() - }; - - eframe::run_native( - "原神", - options, - Box::new(|cc| Box::new(MyApp::new(cc))) - ) -} - -struct MyApp { - left_panel_width: f32, - selected_tab: usize, // 当前选中标签页索引 - is_loading :bool, //加载动画 - loading_angle : f32, //加载动画角度 - background_texture: Option,//背景纹理 - show_log_window: bool, - logs: Vec, - - } - -impl MyApp { - fn new(cc: &eframe::CreationContext<'_>) -> Self { - // 配置中文字体 - Self::configure_fonts(&cc.egui_ctx); - - let mut app =Self { - left_panel_width: 250.0, - selected_tab: 0, // 默认选中第一个标签 - is_loading : false, - loading_angle : 0.0, - background_texture: None, - //初始化日志 - show_log_window: false, - logs: Vec::new(), - - }; - - /* app.load_background(&cc.egui_ctx);*/ - app - } - - // 配置字体函数 - fn configure_fonts(ctx: &egui::Context) { - // 创建字体配置 - let mut fonts = egui::FontDefinitions::default(); - - // 使用std::fs::read读取字体文件 - let font_data = read("C:/Windows/Fonts/msyh.ttc").unwrap_or_else(|_| { - // 备用字体 - read("C:/Windows/Fonts/simhei.ttf").unwrap() - }); - - // 使用from_owned方法创建FontData - fonts.font_data.insert( - "microsoft_yahei".to_owned(), - egui::FontData::from_owned(font_data) - ); - - // 将中文字体添加到所有字体族中 - for family in fonts.families.values_mut() { - family.insert(0, "microsoft_yahei".to_owned()); - } - - // 应用字体 - ctx.set_fonts(fonts); - } - - // 各标签页内容渲染函数 - fn render_tab_content(&mut self, ui: &mut egui::Ui) { - match self.selected_tab { - 0 => { - ui.heading("预留抢票界面公告栏1"); - ui.separator(); - //开始抢票按钮 - - ui.vertical_centered(|ui| { - // 垂直居中 - ui.add_space(ui.available_height() * 0.2); - - // 创建按钮 - let button = egui::Button::new( - egui::RichText::new("开始抢票").size(40.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(300.0, 150.0)) - .fill(egui::Color32::from_rgb(131, 175, 155)) - .rounding(20.0); - - // 只有点击按钮时才触发 - if ui.add(button).clicked() { - self.is_loading = true; - - //待完善鉴权账号及有效信息 - } - }); - - - }, - 1 => { - self.show_log_window = true; - ui.heading("预留监视公告栏2"); - ui.separator(); - }, - 2 => { - ui.heading("抢票设置"); - ui.separator(); - ui.label("这里配置自动抢票参数"); - - ui.checkbox(&mut true, "启用自动抢票"); - ui.add_space(5.0); - - ui.horizontal(|ui| { - ui.label("刷新间隔:"); - ui.add(egui::Slider::new(&mut 1.0, 0.5..=5.0).suffix(" 秒")); - }); - - ui.horizontal(|ui| { - ui.label("最大尝试次数:"); - ui.add(egui::DragValue::new(&mut 50).clamp_range(10..=100)); - }); - }, - 3 => { - ui.heading("账号管理"); - ui.separator(); - ui.label("这里管理B站账号信息"); - - ui.horizontal(|ui| { - ui.label("用户名:"); - ui.text_edit_singleline(&mut "示例用户".to_string()); - }); - - ui.horizontal(|ui| { - ui.label("密码:"); - ui.text_edit_singleline(&mut "********".to_string()); - }); - - if ui.button("保存账号信息").clicked() { - // 保存账号信息 - } - }, - 4 => { - ui.heading("系统设置"); - ui.separator(); - ui.label("这里是系统配置项"); - - ui.checkbox(&mut true, "开机启动"); - ui.checkbox(&mut false, "启用通知提醒"); - ui.checkbox(&mut true, "自动更新"); - - ui.add_space(10.0); - ui.horizontal(|ui| { - ui.label("缓存大小:"); - ui.add(egui::Slider::new(&mut 500.0, 100.0..=1000.0).suffix(" MB")); - }); - }, - _ => unreachable!(), - } - } - //背景图 - /* fn load_background(&mut self, ctx:&egui::Context){ - let image_byte= include_bytes!("../assets/background.jpg"); - if let Ok(image) =image::load_from_memory(image_byte){ - let rgb_image = image.to_rgba8(); - let dimensions= rgb_image.dimensions(); - let image = egui::ColorImage::from_rgba_unmultiplied([dimensions.0 as usize, dimensions.1 as usize] , &rgb_image.into_raw()); - let texture = ctx.load_texture( - "background", image, Default::default()); - self.background_texture = Some(texture);}} - */ -/* fn load_background(&mut self, ctx: &egui::Context) { - println!("开始加载背景图片"); - //let image_path = "../assets/background.jpg"; - let image_byte = include_bytes!("../assets/background.jpg"); - - println!("图片数据大小: {} 字节", image_byte.len()); - - match image::load_from_memory(image_byte) { - Ok(image) => { - - let rgb_image = image.to_rgba8(); - let dimensions = rgb_image.dimensions(); - println!("图片加载成功,尺寸: {:?}", dimensions); - let image = egui::ColorImage::from_rgba_unmultiplied( - [dimensions.0 as usize, dimensions.1 as usize], - &rgb_image.into_raw() - ); - let texture = ctx.load_texture("background", image, Default::default()); - self.background_texture = Some(texture); - println!("背景纹理创建成功"); - }, - Err(e) => { - println!("图片加载失败: {}", e); - } - } -} */ - -} - -impl eframe::App for MyApp { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - /* // 加载背景 - if let Some(texture) = &self.background_texture { - let screen_rect = ctx.screen_rect(); - let painter = ctx.layer_painter(egui::LayerId::new( - egui::Order::Background, // 确保在最底层 - egui::Id::new("background_layer") - )); - - painter.image( - texture.id(), - screen_rect, - egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), - egui::Color32::from_rgba_unmultiplied(255, 255, 255, 150) - ); - } */ - // 创建左右两栏布局 - egui::SidePanel::left("left_panel") - .resizable(true) - .default_width(self.left_panel_width) - .width_range(150.0..=400.0) - .show(ctx, |ui| { - - - // 左侧五个选项 - let tab_names = ["开始抢票", "监视面板", "修改信息", "设置/微调", "帮助/关于"]; - let icons = ["😎", "🎫", "⚙️", "🔧", "🧐"]; // 使用表情符号作为简单图标 - - // 均分空间 - let available_height = ui.available_height(); - let item_count = tab_names.len(); - let item_height = available_height / item_count as f32; - - - for (idx, (name, icon)) in tab_names.iter().zip(icons.iter()).enumerate() { - let is_selected = self.selected_tab == idx; - - - ui.allocate_ui_with_layout( - egui::vec2(ui.available_width(), item_height), - egui::Layout::centered_and_justified(egui::Direction::LeftToRight), - |ui| { - // 选项样式 - 选中时突出显示 - let mut text = egui::RichText::new(format!("{} {}", icon, name)).size(16.0); - if is_selected { - text = text.strong().color(egui::Color32::from_rgb(66, 150, 250)); - } - - - - if ui.selectable_value(&mut self.selected_tab, idx, text).clicked() { - - } - } - ); - } - }); - - egui::CentralPanel::default().show(ctx, |ui| { - // 渲染右侧对应内容 - self.render_tab_content(ui); - }); - // 如果在加载中,绘制覆盖层 - if self.is_loading { - // 创建覆盖整个界面的区域 - let screen_rect = ctx.input(|i| i.screen_rect()); - let layer_id = egui::LayerId::new(egui::Order::Foreground, egui::Id::new("loading_overlay")); - let ui = ctx.layer_painter(layer_id); - - // 半透明背景 - ui.rect_filled( - screen_rect, - 0.0, - egui::Color32::from_rgba_unmultiplied(0, 0, 0, 180) - ); - - // 在屏幕中央显示加载动画 - let center = screen_rect.center(); - - // 更新动画角度 - self.loading_angle += 0.05; - if self.loading_angle > std::f32::consts::TAU { - self.loading_angle -= std::f32::consts::TAU; - } - - // 绘制动画 - // 背景圆环 - ui.circle_stroke( - center, - 30.0, - egui::Stroke::new(5.0, egui::Color32::from_gray(100)) - ); - - // 动画圆弧 - let mut points = Vec::new(); - let segments = 32; - let start_angle = self.loading_angle; - let end_angle = start_angle + std::f32::consts::PI; - - for i in 0..=segments { - let angle = start_angle + (end_angle - start_angle) * (i as f32 / segments as f32); - let point = center + 30.0 * egui::Vec2::new(angle.cos(), angle.sin()); - points.push(point); - } - - ui.add(egui::Shape::line( - points, - egui::Stroke::new(5.0, egui::Color32::from_rgb(66, 150, 250)) - )); - - // 加载文字 - ui.text( - center + egui::vec2(0.0, 50.0), - egui::Align2::CENTER_CENTER, - "加载中...", - egui::FontId::proportional(16.0), - egui::Color32::WHITE - ); - - // 强制持续重绘以保持动画 - ctx.request_repaint(); - } - - //日志窗口 - if self.show_log_window { - // Using a temporary variable to track window close action - let mut window_open = self.show_log_window; - egui::Window::new("监视面板") - .open(&mut window_open) // 使用临时变量 - .default_size([500.0, 400.0]) // 设置默认大小 - .resizable(true) // 允许调整大小 - .show(ctx, |ui| { - // 顶部工具栏 - ui.horizontal(|ui| { - if ui.button("清空日志").clicked() { - self.logs.clear(); - } - - if ui.button("添加测试日志").clicked() { - let timestamp = chrono::Local::now().format("%H:%M:%S").to_string(); - self.logs.push(format!("[{}] 测试日志消息", timestamp)); - } - - ui.with_layout(egui::Layout::right_to_left(egui::Align::RIGHT), |ui| { - if ui.button("❌").clicked() { - // 使用close_button替代直接修改window_open - self.show_log_window = false; - } - }); - }); - - ui.separator(); - - // 日志内容区域(可滚动) - egui::ScrollArea::vertical() - .auto_shrink([false, false]) - .stick_to_bottom(true) - .show(ui, |ui| { - // 显示当前状态 - ui.label(format!("当前状态: {}", - if self.is_loading {"正在抢票中..."} else {"空闲"})); - - ui.separator(); - - // 显示所有日志 - if self.logs.is_empty() { - ui.label("暂无日志记录"); - } else { - for log in &self.logs { - ui.label(log); - ui.separator(); - } - } - }); - // 底部状态栏 - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - ui.label(format!("共 {} 条日志", self.logs.len())); - }); - }); - - // 更新窗口状态 - self.show_log_window = window_open; - - } - - } -} \ No newline at end of file diff --git a/frontend/src/ui/error_banner.rs b/frontend/src/ui/error_banner.rs deleted file mode 100644 index d3d5521..0000000 --- a/frontend/src/ui/error_banner.rs +++ /dev/null @@ -1,79 +0,0 @@ -use eframe::egui; -use crate::app::Myapp; - -// 定义横幅类型枚举 -#[derive(PartialEq)] -pub enum BannerType { - Error, - Success, -} - -// 重命名函数为更通用的名称 -pub fn render_notification_banner(app: &Myapp, ctx: &egui::Context) { - let screen_rect = ctx.available_rect(); - let banner_height = 40.0; - - // 创建一个位于屏幕顶部的区域 - let banner_rect = egui::Rect::from_min_size( - egui::pos2(screen_rect.min.x, screen_rect.min.y), - egui::vec2(screen_rect.width(), banner_height) - ); - - // 使用Area绝对定位横幅 - egui::Area::new("notification_banner") - .fixed_pos(banner_rect.min) - .show(ctx, |ui| { - // 根据当前激活的横幅类型选择颜色 - let (fill_color, stroke_color) = if app.success_banner_active { - // 成功横幅 - 浅绿色 - ( - egui::Color32::from_rgba_premultiplied( - 130, 220, 130, (app.success_banner_opacity * 255.0) as u8 - ), - egui::Color32::from_rgba_premultiplied( - 100, 200, 100, (app.success_banner_opacity * 255.0) as u8 - ) - ) - } else { - // 错误横幅 - 橙红色 - ( - egui::Color32::from_rgba_premultiplied( - 245, 130, 90, (app.error_banner_opacity * 255.0) as u8 - ), - egui::Color32::from_rgba_premultiplied( - 225, 110, 70, (app.error_banner_opacity * 255.0) as u8 - ) - ) - }; - - // 设置框架样式 - let frame = egui::Frame::none() - .fill(fill_color) - .stroke(egui::Stroke::new(1.0, stroke_color)); - - frame.show(ui, |ui| { - ui.set_max_width(screen_rect.width()); - - // 居中白色文本 - ui.vertical_centered(|ui| { - ui.add_space(5.0); - let banner_text = if app.success_banner_active { - &app.success_banner_text - } else { - &app.error_banner_text - }; - let text = egui::RichText::new(banner_text) - .color(egui::Color32::WHITE) - .size(16.0) - .strong(); - ui.label(text); - ui.add_space(5.0); - }); - }); - }); -} - -// 为了向后兼容,保留原函数名,但内部调用新函数 -pub fn render_error_banner(app: &Myapp, ctx: &egui::Context) { - render_notification_banner(app, ctx); -} \ No newline at end of file diff --git a/frontend/src/ui/fonts.rs b/frontend/src/ui/fonts.rs deleted file mode 100644 index 47954c7..0000000 --- a/frontend/src/ui/fonts.rs +++ /dev/null @@ -1,93 +0,0 @@ -use eframe::egui; -use std::fs::read; -use std::path::Path; - -// 配置字体函数 -pub fn configure_fonts(ctx: &egui::Context) { - // 创建字体配置 - let mut fonts = egui::FontDefinitions::default(); - - // 根据不同操作系统选择合适的字体路径 - let font_data = load_system_font(); - - // 使用from_owned方法创建FontData - fonts.font_data.insert( - "chinese_font".to_owned(), - egui::FontData::from_owned(font_data) - ); - - // 将中文字体添加到所有字体族中 - for family in fonts.families.values_mut() { - family.insert(0, "chinese_font".to_owned()); - } - - // 应用字体 - ctx.set_fonts(fonts); -} - -// 根据操作系统加载合适的字体 -fn load_system_font() -> Vec { - #[cfg(target_os = "windows")] - { - // 尝试多个Windows字体路径 - let font_paths = [ - "C:/Windows/Fonts/msyh.ttc", - "C:/Windows/Fonts/simhei.ttf", - "C:/Windows/Fonts/simsun.ttc", - "C:/Windows/Fonts/msyh.ttf" - ]; - - for path in font_paths { - if Path::new(path).exists() { - if let Ok(data) = read(path) { - log::info!("加载字体: {}", path); - return data; - } - } - } - } - - #[cfg(target_os = "macos")] - { - // macOS系统字体路径 - let font_paths = [ - "/System/Library/Fonts/PingFang.ttc", - "/Library/Fonts/Arial Unicode.ttf", - "/System/Library/Fonts/STHeiti Light.ttc", - "/System/Library/Fonts/Hiragino Sans GB.ttc" - ]; - - for path in font_paths { - if Path::new(path).exists() { - if let Ok(data) = read(path) { - log::info!("加载字体: {}", path); - return data; - } - } - } - } - - #[cfg(target_os = "linux")] - { - // Linux系统字体路径 - let font_paths = [ - "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", - "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", - "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", - "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc" - ]; - - for path in font_paths { - if Path::new(path).exists() { - if let Ok(data) = read(path) { - log::info!("加载字体: {}", path); - return data; - } - } - } - } - - // 如果所有系统字体都无法加载,使用内置的字体 - log::warn!("无法加载系统中文字体,使用内置字体"); - include_bytes!("../../../resources/fonts/NotoSansSC-Regular.otf").to_vec() -} diff --git a/frontend/src/ui/loading.rs b/frontend/src/ui/loading.rs deleted file mode 100644 index f15129e..0000000 --- a/frontend/src/ui/loading.rs +++ /dev/null @@ -1,61 +0,0 @@ -use eframe::egui; -use crate::app::Myapp; - -pub fn render_loading_overlay(app: &mut Myapp, ctx: &egui::Context) { - // 创建覆盖整个界面的区域 - let screen_rect = ctx.screen_rect(); - let layer_id = egui::LayerId::new(egui::Order::Foreground, egui::Id::new("loading_overlay")); - let ui = ctx.layer_painter(layer_id); - - // 半透明背景 - ui.rect_filled( - screen_rect, - 0.0, - egui::Color32::from_rgba_unmultiplied(0, 0, 0, 180) - ); - - // 在屏幕中央显示加载动画 - let center = screen_rect.center(); - - // 更新动画角度 - app.loading_angle += 0.05; - if app.loading_angle > std::f32::consts::TAU { - app.loading_angle -= std::f32::consts::TAU; - } - - // 背景圆环 - ui.circle_stroke( - center, - 30.0, - egui::Stroke::new(5.0, egui::Color32::from_gray(100)) - ); - - // 动画圆弧 - let mut points = Vec::new(); - let segments = 32; - let start_angle = app.loading_angle; - let end_angle = start_angle + std::f32::consts::PI; - - for i in 0..=segments { - let angle = start_angle + (end_angle - start_angle) * (i as f32 / segments as f32); - let point = center + 30.0 * egui::Vec2::new(angle.cos(), angle.sin()); - points.push(point); - } - - ui.add(egui::Shape::line( - points, - egui::Stroke::new(5.0, egui::Color32::from_rgb(66, 150, 250)) - )); - - // 加载文字 - ui.text( - center + egui::vec2(0.0, 50.0), - egui::Align2::CENTER_CENTER, - "加载中...", - egui::FontId::proportional(16.0), - egui::Color32::WHITE - ); - - // 强制持续重绘以保持动画 - ctx.request_repaint(); -} \ No newline at end of file diff --git a/frontend/src/ui/mod.rs b/frontend/src/ui/mod.rs deleted file mode 100644 index bcf4170..0000000 --- a/frontend/src/ui/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod fonts; -pub mod sidebar; -pub mod tabs; -pub mod loading; -pub mod error_banner; \ No newline at end of file diff --git a/frontend/src/ui/sidebar.rs b/frontend/src/ui/sidebar.rs deleted file mode 100644 index 7cf394d..0000000 --- a/frontend/src/ui/sidebar.rs +++ /dev/null @@ -1,46 +0,0 @@ -use eframe::egui; -use crate::app::Myapp; - -pub fn render_sidebar(app: &mut Myapp, ctx: &egui::Context){ - // 创建左右两栏布局 - egui::SidePanel::left("left_panel") - .resizable(true) - .default_width(app.left_panel_width) - .width_range(150.0..=400.0) - .show(ctx, |ui| { - - - // 左侧五个选项 - let tab_names = ["开始抢票", "监视面板", "修改信息", "设置/微调", "帮助/关于"]; - let icons = ["😎", "🎫", "📁", "🔧", "📋"]; // 使用表情符号作为简单图标 - - // 均分空间 - let available_height = ui.available_height(); - let item_count = tab_names.len(); - let item_height = available_height / item_count as f32; - - - for (idx, (name, icon)) in tab_names.iter().zip(icons.iter()).enumerate() { - let is_selected = app.selected_tab == idx; - - - ui.allocate_ui_with_layout( - egui::vec2(ui.available_width(), item_height), - egui::Layout::centered_and_justified(egui::Direction::LeftToRight), - |ui| { - // 选项样式 - 选中时突出显示 - let mut text = egui::RichText::new(format!("{} {}", icon, name)).size(16.0); - if is_selected { - text = text.strong().color(egui::Color32::from_rgb(255, 255, 255)); - } - - - - if ui.selectable_value(&mut app.selected_tab, idx, text).clicked() { - - } - } - ); - } - }); -} \ No newline at end of file diff --git a/frontend/src/ui/tabs/account.rs b/frontend/src/ui/tabs/account.rs deleted file mode 100644 index 400e4de..0000000 --- a/frontend/src/ui/tabs/account.rs +++ /dev/null @@ -1,451 +0,0 @@ -use eframe::egui; -use crate::{app::{AccountSwitch, Myapp}}; -use common::{account::{signout_account, Account}, cookie_manager::{self, CookieManager}}; -use common::utils::load_texture_from_url; -use std::sync::Arc; - -pub fn render(app: &mut Myapp, ui: &mut egui::Ui){ - ui.heading("我的账户"); - ui.separator(); - let mut example_account = Account{ - uid: 0, - name: "请登录账号".to_string(), - vip_label: "未登录,请登录账号".to_string(), - level: "未登录".to_string(), - cookie: "".to_string(), - csrf: "".to_string(), - is_login: false, - account_status: "未登录".to_string(), - is_active: false, - avatar_url: None, - - avatar_texture:None, - - cookie_manager: None, - }; - - - // 加载默认头像 - load_default_avatar(ui.ctx(),app); - - let account_to_show = app.account_manager.accounts.first_mut().unwrap_or(&mut example_account); - let avatar_texture = load_user_avatar(ui.ctx(), account_to_show.cookie_manager.clone(), account_to_show); - - if let Some(texture) = &avatar_texture{ - show_user( - ui, - texture,account_to_show, - &mut app.delete_account, - &mut app.show_login_windows , - &mut app.config, - &mut app.account_switch, - &mut app.show_add_buyer_window, - &mut app.show_orderlist_window, - ); -} - else { - // 如果头像加载失败,显示默认头像 - if let Some(texture) = &app.default_avatar_texture { - show_user( - ui, - texture,account_to_show, - &mut app.delete_account, - &mut app.show_login_windows , - &mut app.config, - &mut app.account_switch, - &mut app.show_add_buyer_window, - &mut app.show_orderlist_window, - ); - } - } -//show_user_control(ui,&app.user_info); -ui.separator(); -if let Some(texture) = &app.default_avatar_texture { - let account_to_show = app.account_manager.accounts.get(1).unwrap_or(&example_account); - show_user( - ui, - texture,account_to_show, - &mut app.delete_account, - &mut app.show_login_windows , - &mut app.config, - &mut app.account_switch, - &mut app.show_add_buyer_window, - &mut app.show_orderlist_window, - ); - - -} -ui.separator(); - - - -} -/// 将任意图片显示为圆形 -/// - texture: 要显示的图像纹理 -/// - size: 圆形图片的直径大小 -fn draw_user_avatar(ui: &mut egui::Ui, texture: &egui::TextureHandle, size: f32) -> egui::Response { - // 分配正方形区域 - let (rect, response) = ui.allocate_exact_size( - egui::Vec2::new(size, size), - egui::Sense::click() - ); - - if ui.is_rect_visible(rect) { - // 创建一个离屏渲染的自定义形状层 - let layer_id = egui::layers::LayerId::new( - egui::layers::Order::Background, - egui::Id::new("circular_image") - ); - - let painter = ui.ctx().layer_painter(layer_id); - - // 绘制圆形背景 (这一步可选) - painter.circle_filled( - rect.center(), - size / 2.0, - egui::Color32::from_rgb(220, 220, 240) - ); - - // 使用圆形纹理蒙版技术 - // 1. 创建一个与图像大小相同的圆形遮罩 - let circle_mask = egui::Shape::circle_filled( - rect.center(), - size / 2.0 - 1.0, - egui::Color32::WHITE - ); - - // 2. 将图像绘制为自定义着色器,使用圆形遮罩 - let uv = egui::Rect::from_min_max( - egui::pos2(0.0, 0.0), - egui::pos2(1.0, 1.0) - ); - - // 使用裁剪圆绘制 - painter.add(circle_mask); - - // 以混合模式绘制图像,只在圆形区域内可见 - painter.image( - texture.id(), - rect, - uv, - egui::Color32::WHITE - ); - - // 添加边框 - painter.circle_stroke( - rect.center(), - size / 2.0, - egui::Stroke::new(1.0, egui::Color32::from_rgba_premultiplied(180, 180, 180, 180)) - ); - } - - response -} - -fn load_user_avatar(ctx: &egui::Context, cookie_manager: Option>, account: &mut Account) ->Option { - // 如果用户已登录且提供了头像路径,尝试加载 - if let Some(texture) = &account.avatar_texture { - return Some(texture.clone()); - } - if account.is_login && cookie_manager.is_some() { - if let Some(avatar_url) = &account.avatar_url { - // 尝试加载用户头像 - let texture_option = load_texture_from_url(ctx, cookie_manager.unwrap(), avatar_url, "user_avatar"); - - account.avatar_texture = texture_option.clone(); - return texture_option; - } - } - //未登录或加载失败 - None -} -// 加载默认头像 -fn load_default_avatar(ctx: &egui::Context, app: &mut Myapp) { - // 使用include_bytes!宏将图片直接嵌入到二进制文件中 - // 路径是相对于项目根目录的 - let default_avatar_bytes = include_bytes!("../../../assets/default_avatar.jpg"); - - // 从内存中加载图片 - match image::load_from_memory(default_avatar_bytes) { - Ok(image) => { - let size = [image.width() as usize, image.height() as usize]; - let image_buffer = image.to_rgba8(); - let pixels = image_buffer.as_flat_samples(); - - app.default_avatar_texture = Some(ctx.load_texture( - "default_avatar", - egui::ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()), - Default::default() - )); - }, - Err(_) => { - // 图片加载失败,生成占位符头像 - app.default_avatar_texture = generate_placeholder_avatar(ctx); - } - } -} - -// 生成一个占位符头像 -fn generate_placeholder_avatar(ctx: &egui::Context) -> Option { - let size = 128; // 头像尺寸 - let mut image_data = vec![0; size * size * 4]; - - // 生成一个简单的渐变图案 - for y in 0..size { - for x in 0..size { - let i = (y * size + x) * 4; - // 浅蓝色调渐变 - image_data[i] = 180; // R - image_data[i + 1] = 180 + (y as u8) / 2; // G - image_data[i + 2] = 230; // B - image_data[i + 3] = 255; // A - } - } - - Some(ctx.load_texture( - "default_avatar", - egui::ColorImage::from_rgba_unmultiplied([size, size], &image_data), - Default::default() - )) -} - -fn show_user( //显示用户头像等信息 - ui: &mut egui::Ui, - texture: &egui::TextureHandle, - - account: &Account, - delete_account: &mut Option, - show_login_windows: &mut bool, - config: &mut common::utils::Config, - account_switch: &mut Option, - show_add_buyer_window: &mut Option, - show_orderlist_window: &mut Option, - - -) { - let mut user = account.clone(); - // 创建圆角长方形框架 - egui::Frame::none() - .fill(egui::Color32::from_rgb(245, 245, 250)) // 背景色 - .rounding(12.0) // 圆角半径 - .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 220))) // 边框 - .inner_margin(egui::Margin { left: 10.0, right: 20.0, top: 15.0, bottom: 15.0 }) // 内边距 - .show(ui, |ui| { - // 水平布局放置图片和文字 - ui.horizontal(|ui| { - // 左侧图片区域,这里使用小尺寸的圆形图片 - let image_size = 84.0; - draw_user_avatar(ui, texture, image_size); - - ui.add_space(12.0); // 图片和文字之间的间距 - - // 右侧文字区域 - ui.vertical(|ui| { - //第一行 - //显示uid和昵称 - ui.horizontal(|ui|{ - ui.add(egui::widgets::Label::new( - egui::RichText::new(&user.name) - .size(30.0) - .strong() - .color(egui::Color32::from_rgb(60, 60, 80)) - )); - ui.add_space(15.0); - ui.add(egui::widgets::Label::new( - egui::RichText::new(format!("UID: {}", user.uid)) - .color(egui::Color32::from_rgb(100, 100, 120)) - .size(16.0) - )); - }); - //第二行 - ui.add_space(10.0); - //显示大会员 - ui.horizontal(|ui|{ - - match user.vip_label.as_str(){ - "月度大会员"=> { - egui::Frame::none() - .fill(egui::Color32::from_rgb(251, 114, 153)) // 粉色背景 #FB7299 - .rounding(10.0) // 圆角 - - .inner_margin(egui::vec2(6.0, 3.0)) // 内边距 - .show(ui, |ui| { - // 白色文字 #FFFFFF - ui.label( - egui::RichText::new("月度大会员") - .color(egui::Color32::from_rgb(255, 255, 255)) - // 白色文字 - .size(15.0) - ); - }); - } - "年度大会员" =>{ - egui::Frame::none() - .fill(egui::Color32::from_rgb(251, 114, 153)) // 粉色背景 #FB7299 - .rounding(10.0) // 圆角 - .inner_margin(egui::vec2(6.0, 3.0)) // 内边距 - .show(ui, |ui| { - // 白色文字 #FFFFFF - ui.label( - egui::RichText::new("年度大会员") - .color(egui::Color32::from_rgb(255, 255, 255)) // 白色文字 - .size(15.0) - ); - }); - } - "十年大会员" =>{ - egui::Frame::none() - .fill(egui::Color32::from_rgb(251, 114, 153)) // 粉色背景 #FB7299 - .rounding(10.0) // 圆角 - .inner_margin(egui::vec2(6.0, 3.0)) // 内边距 - .show(ui, |ui| { - // 白色文字 #FFFFFF - ui.label( - egui::RichText::new("十年大会员") - .color(egui::Color32::from_rgb(255, 255, 255)) // 白色文字 - .size(15.0) - ); - }); - } - _ => { - egui::Frame::none() - - .inner_margin(egui::vec2(6.0, 3.0)) // 内边距 - .show(ui, |ui| { - // 白色文字rgb(0, 0, 0) - ui.label( - egui::RichText::new("正式会员") - .color(egui::Color32::from_rgb(0, 0, 0)) // 白色文字 - .size(15.0) - ); - }); - } - - - - } - }) - - }); - - - }); - ui.separator(); - ui.vertical(|ui|{ - ui.add_space(15.0); - ui.horizontal(|ui|{ - ui.add_space(15.0); - if !user.is_login { - - - let button = egui::Button::new( - egui::RichText::new("登录").size(20.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(120.0,50.0)) - .fill(egui::Color32::from_rgb(102,204,255)) - .rounding(15.0);//圆角成度 - let response = ui.add(button); - if response.clicked(){ - *show_login_windows = true; - } - }else{ - let button = egui::Button::new( - egui::RichText::new("登出").size(20.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(120.0,50.0)) - .fill(egui::Color32::from_rgb(255,174,201)) - .rounding(15.0);//圆角成度 - let response = ui.add(button); - if response.clicked(){ - match signout_account(&user){ - Ok(_) => { - *delete_account = Some(user.uid.to_string().clone()); - log::info!("登出成功"); - - } - Err(e) => { - log::error!("登出失败: {}", e); - } - } - - } - } - dynamic_caculate_space(ui, 122.0, 3.0); - let button = egui::Button::new( - egui::RichText::new("查看全部订单").size(20.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(130.0,50.0)) - .fill(egui::Color32::from_rgb(102,204,255)) - .rounding(15.0);//圆角成度 - let response = ui.add(button); - if response.clicked(){ - *show_orderlist_window = Some(user.uid.to_string().clone()); - } - dynamic_caculate_space(ui, 120.0, 2.0); - let button = egui::Button::new( - egui::RichText::new("添加购票人").size(18.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(120.0,50.0)) - .fill(egui::Color32::from_rgb(102,204,255)) - .rounding(15.0); - let response = ui.add(button); - if response.clicked(){ - *show_add_buyer_window = Some(user.uid.to_string().clone()); - } - dynamic_caculate_space(ui, 120.0, 1.0); - - if user.is_active == false{ - let button = egui::Button::new( - egui::RichText::new("抢票关闭中").size(18.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(120.0,50.0)) - .fill(egui::Color32::from_rgb(255,174,201)) - .rounding(15.0); - let response = ui.add(button); - if response.clicked(){ - let switch = AccountSwitch{ - uid: user.uid.to_string(), - switch: true, - }; - *account_switch = Some(switch); - } - }else{ - let button = egui::Button::new( - egui::RichText::new("抢票开启中").size(18.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(120.0,50.0)) - .fill(egui::Color32::from_rgb(102,204,255)) - .rounding(15.0); - let response = ui.add(button); - if response.clicked(){ - let switch = AccountSwitch{ - uid: user.uid.to_string(), - switch: false, - }; - *account_switch = Some(switch); - } - } - - }); - - - - }) - }); -} - - - -fn dynamic_caculate_space( - ui :&mut egui::Ui, - obj_space: f32, //如果有三个按钮,假设每个按钮尺寸x轴长度=120.0,那么就传入120.0 - number: f32 //按钮数量 - ) { - let available_space = ui.available_width(); - let mut space = available_space/number - obj_space ; - if space < 0.0 { - space = 0.0; - } - ui.add_space(space); -} - diff --git a/frontend/src/ui/tabs/help.rs b/frontend/src/ui/tabs/help.rs deleted file mode 100644 index 4e35f7f..0000000 --- a/frontend/src/ui/tabs/help.rs +++ /dev/null @@ -1,9 +0,0 @@ -use eframe::egui; -use crate::app::Myapp; - -pub fn render(app: &mut Myapp, ui: &mut egui::Ui) { - - ui.heading("预留帮助公告栏"); - ui.separator(); - ui.label("本项目地址:https://github.com/biliticket/bili_ticket_rush"); -} \ No newline at end of file diff --git a/frontend/src/ui/tabs/home.rs b/frontend/src/ui/tabs/home.rs deleted file mode 100644 index 37ca8ea..0000000 --- a/frontend/src/ui/tabs/home.rs +++ /dev/null @@ -1,372 +0,0 @@ -use std::u32; - -use eframe::egui; -use eframe::egui::Widget; -use crate::app::Myapp; -use common::account::{Account}; -use common::taskmanager::{TaskStatus, TicketRequest, TaskManager_debug}; -use common::ticket::BilibiliTicket; - - -pub fn render(app: &mut Myapp, ui: &mut egui::Ui) { - //页面标题 - ui.vertical_centered(|ui| { - ui.add_space(20.0); - ui.heading(egui::RichText::new("仅供学习的小工具").size(32.0).strong()); - ui.add_space(10.0); - ui.label(egui::RichText::new(TaskManager_debug()) - .size(14.0) - .color(egui::Color32::from_rgb(255, 120, 50)) - .strong()); - ui.add_space(10.0); - ui.label(egui::RichText::new("请输入项目ID或粘贴票务链接,点击开始抢票").size(16.0).color(egui::Color32::GRAY)); - ui.add_space(10.0); - if let Some(accounce) = app.announce1.clone() { - ui.label(egui::RichText::new(accounce) - .size(14.0) - .color(egui::Color32::from_rgb(255, 120, 50)) - .strong()); - } - ui.add_space(25.0); - - //输入区域 - ticket_input_area(ui, app); - }); -} - -fn ticket_input_area(ui: &mut egui::Ui, app: &mut Myapp) { - //居中布局的输入框和按钮组合 - ui.vertical_centered(|ui| { - ui.spacing_mut().item_spacing = egui::vec2(0.0, 20.0); - - //输入框布局 - let response = styled_ticket_input(ui, &mut app.ticket_id); - - // 新增:账号和抢票模式选择区域 - ui.add_space(15.0); - styled_selection_area(ui, app); - ui.add_space(15.0); - - //抢票按钮 - if styled_grab_button(ui).clicked() { - if !check_input_ticket(&mut app.ticket_id) {app.show_log_window = true; return}; - if app.account_manager.accounts.is_empty() { - log::info!("没有可用账号,请登录账号"); - app.show_login_windows = true; - return - } - let select_uid = match app.selected_account_uid { - Some(uid) => uid, - None => { - log::error!("没有选择账号,请选择账号!"); - return - } - }; - let bilibili_ticket: BilibiliTicket = BilibiliTicket::new( - - &app.grab_mode, - &app.default_ua, - &app.custom_config, - &app.account_manager.accounts - .iter() - .find(|a| a.uid == select_uid) - .unwrap(), - - &app.push_config, - &app.status_delay, - &app.ticket_id, - ); - app.bilibiliticket_list.push(bilibili_ticket); - log::debug!("当前抢票对象列表:{:?}", app.bilibiliticket_list); - match app.grab_mode{ - 0|1 => { - app.show_screen_info = Some(select_uid); - } - 2 => { - app.confirm_ticket_info = Some(select_uid.to_string()); - } - _ => { - log::error!("当前模式不支持!请检查输入!"); - } - } - - - } - - //底部状态文本 - ui.add_space(30.0); - /* let status_text = match app.is_loading { - true => egui::RichText::new(&app.running_status).color(egui::Color32::from_rgb(255, 165, 0)), - false => egui::RichText::new("等待开始...").color(egui::Color32::GRAY), - }; - ui.label(status_text); */ - }); -} - -//输入框 -fn styled_ticket_input(ui: &mut egui::Ui, text: &mut String) -> egui::Response { - //创建一个适当大小的容器 - let desired_width = 250.0; - - ui.horizontal(|ui| { - ui.add_space((ui.available_width() - desired_width) / 2.0); - - egui::Frame::none() - .fill(egui::Color32::from_rgb(245, 245, 250)) - .rounding(10.0) - .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 220))) - .shadow(egui::epaint::Shadow::small_light()) - .inner_margin(egui::vec2(12.0, 10.0)) - .show(ui, |ui| { - ui.spacing_mut().item_spacing = egui::vec2(8.0, 0.0); - - // 左侧图标 - ui.label(egui::RichText::new("🎫").size(18.0)); - - // 输入框 - let font_id = egui::FontId::new(20.0, egui::FontFamily::Proportional); - ui.style_mut().override_font_id = Some(font_id.clone()); - - let input = egui::TextEdit::singleline(text) - .hint_text("输入票务ID") - .desired_width(180.0) - .text_color(egui::Color32::BLACK) //指定文本颜色防止深色模式抽风 - .margin(egui::vec2(0.0, 6.0)) - .frame(false); - - ui.add(input) - }) - .inner - }).inner -} - -//选择模式区域UI -fn styled_selection_area(ui: &mut egui::Ui, app: &mut Myapp) { - // 容器宽度与抢票按钮相同,保持一致性 - let panel_width = 400.0; - - ui.horizontal(|ui| { - ui.add_space((ui.available_width() - panel_width) / 2.0); - - egui::Frame::none() - .fill(egui::Color32::from_rgb(245, 245, 250)) - .rounding(8.0) - .stroke(egui::Stroke::new(0.5, egui::Color32::from_rgb(200, 200, 220))) - .shadow(egui::epaint::Shadow::small_light()) - .inner_margin(egui::vec2(16.0, 12.0)) - .show(ui, |ui| { - ui.set_width(panel_width - 32.0); // 减去内边距 - - ui.vertical(|ui| { - // 账号选择 - account_selection(ui, app); - - ui.add_space(12.0); - ui.separator(); - ui.add_space(12.0); - - // 抢票模式选择 - grab_mode_selection(ui, app); - }); - }); - }); -} - -// 账号选择UI -fn account_selection(ui: &mut egui::Ui, app: &mut Myapp) { - ui.horizontal(|ui| { - ui.label(egui::RichText::new("选择账号:").color(egui::Color32::BLACK).size(16.0).strong()); - - // 如果没有账号,显示提示 - if app.account_manager.accounts.is_empty() { - ui.label(egui::RichText::new("未登录账号").color(egui::Color32::RED).italics()); - ui.add_space(8.0); - if egui::Button::new(egui::RichText::new("去登录").size(14.0).color(egui::Color32::BLUE)) - .fill(egui::Color32::LIGHT_GRAY) // 设置背景颜色 - .ui(ui) - .clicked() { - app.show_login_windows = true; - } - } else { - // 初始化选中账号(如果未选择) - if app.selected_account_uid.is_none() && !app.account_manager.accounts.is_empty() { - app.selected_account_uid = Some(app.account_manager.accounts[0].uid); - } - - // 创建账号ComboBox - let selected_account = app.account_manager.accounts.iter() - .find(|a| Some(a.uid) == app.selected_account_uid); - - let selected_text = match selected_account { - Some(account) => format!("{} ({})", account.name, account.uid), - None => "选择账号".to_string(), - }; - - egui::ComboBox::from_id_source("account_selector") - .selected_text(selected_text) - .width(200.0) - .show_ui(ui, |ui| { - for account in &app.account_manager.accounts { - let text = format!("{} ({})", account.name, account.uid); - let is_selected = Some(account.uid) == app.selected_account_uid; - - if ui.selectable_label(is_selected, text).clicked() { - app.selected_account_uid = Some(account.uid); - } - } - }); - - // 显示会员等级和状态(如果有选中账号) - if let Some(account) = selected_account { - ui.add_space(10.0); - if !account.vip_label.is_empty() { - let vip_text = egui::RichText::new(&account.vip_label) - .size(13.0) - .color(egui::Color32::from_rgb(251, 114, 153)); - ui.label(vip_text); - } - - let level_text = egui::RichText::new(format!("LV{}", account.level)) - .size(13.0) - .color(egui::Color32::from_rgb(0, 161, 214)); - ui.label(level_text); - } - } - }); -} - -// 抢票模式选择UI -fn grab_mode_selection(ui: &mut egui::Ui, app: &mut Myapp) { - ui.vertical(|ui| { - ui.label(egui::RichText::new("抢票模式:").color(egui::Color32::BLACK).size(16.0).strong()); - ui.add_space(8.0); - - ui.horizontal(|ui| { - ui.style_mut().spacing.item_spacing.x = 12.0; - - // 第一种模式 - 自动抢票(推荐) - let selected = app.grab_mode == 0; - if mode_selection_button(ui, "🎫 自动抢票(推荐)", - "自动检测开票时间抢票", selected).clicked() { - app.grab_mode = 0; - } - - // 第二种模式 - 直接抢票 - let selected = app.grab_mode == 1; - if mode_selection_button(ui, "⚡ 直接抢票", - "直接开始尝试下单(适合已开票项目!,未开票项目使用会导致冻结账号!)", selected).clicked() { - app.grab_mode = 1; - } - - // 第三种模式 - 捡漏模式 - let selected = app.grab_mode == 2; - if mode_selection_button(ui, "🔄 捡漏模式", - "对于已开票项目,监测是否出现余票并尝试下单", selected).clicked() { - app.grab_mode = 2; - } - }); - }); -} - -// 抢票模式按钮 -fn mode_selection_button(ui: &mut egui::Ui, title: &str, tooltip: &str, selected: bool) -> egui::Response { - let btn = ui.add( - egui::widgets::Button::new( - egui::RichText::new(title) - .size(14.0) - .color(if selected { - egui::Color32::WHITE - } else { - egui::Color32::from_rgb(70, 70, 70) - }) - ) - .min_size(egui::vec2(110.0, 36.0)) - .fill(if selected { - egui::Color32::from_rgb(102, 204, 255) - } else { - egui::Color32::from_rgb(230, 230, 235) - }) - .rounding(6.0) - .stroke(egui::Stroke::new( - 0.5, - if selected { - egui::Color32::from_rgb(25, 118, 210) - } else { - egui::Color32::from_rgb(180, 180, 190) - } - )) - ); - - // 添加悬停提示 - btn.clone().on_hover_text(tooltip); - - btn -} -//抢票按钮 -fn styled_grab_button(ui: &mut egui::Ui) -> egui::Response { - let button_width = 200.0; - let button_height = 60.0; - - ui.horizontal(|ui| { - ui.add_space((ui.available_width() - button_width) / 2.0); - - let button = egui::Button::new( - egui::RichText::new("开始抢票") - .size(24.0) - .strong() - .color(egui::Color32::from_rgb(255,255,255)) - ) - .min_size(egui::vec2(button_width, button_height)) - .fill(egui::Color32::from_rgb(102, 204, 255)) - .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(25, 118, 210))) - .rounding(12.0); - - ui.add(button) - }).inner -} - -fn check_input_ticket(ticket_id: &mut String) -> bool{ - //检查输入的票务ID是否有效 - if ticket_id.is_empty(){ - log::info!("请输入有效的票务id"); - return false; - } - if ticket_id.contains("https") { - if let Some(position) = ticket_id.find("id="){ - let mut id = ticket_id.split_off(position+3); - if id.contains("&") { - let position = id.find("&").unwrap(); - id.truncate(position); - } - if id.len() == 5 || id.len() == 6 { - match id.parse::(){ - Ok(_) => { - log::info!("获取到的id为:{}", id); - *ticket_id = id; - return true; - } - Err(_) => { - log::error!("输入的id不合法,请检查输入,可尝试直接输入id"); - return false; - } - } - } - - - - }else{ - log::error!("未找到对应的id,请不要使用b23开头的短连接,正确链接以show.bilibili或mall.bilibili开头"); - return false; - } - } - match ticket_id.parse::() { - Ok(_) => { - log::info!("获取到的id为:{}", ticket_id); - return true; - } - Err(_) => { - log::error!("输入的id不是数字类型,请检查输入"); - } - } - return false; -} diff --git a/frontend/src/ui/tabs/mod.rs b/frontend/src/ui/tabs/mod.rs deleted file mode 100644 index 91d6c8d..0000000 --- a/frontend/src/ui/tabs/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub mod home; -pub mod monitor; -pub mod account; -pub mod settings; -pub mod help; - -use eframe::egui; -use crate::app::Myapp; - - -pub fn render_tab_content(app: &mut Myapp, ui: &mut egui::Ui) { - match app.selected_tab { - 0 => home::render(app, ui), - 1 => monitor::render(app, ui), - 2 => account::render(app, ui), - 3 => settings::render(app, ui), - 4 => help::render(app, ui), - _ => unreachable!(), - } -} \ No newline at end of file diff --git a/frontend/src/ui/tabs/monitor.rs b/frontend/src/ui/tabs/monitor.rs deleted file mode 100644 index 9d66a9a..0000000 --- a/frontend/src/ui/tabs/monitor.rs +++ /dev/null @@ -1,14 +0,0 @@ -use eframe::egui; -use crate::app::Myapp; - -pub fn render(app: &mut Myapp, ui: &mut egui::Ui){ - app.show_log_window = true; - if let Some(accounce) = app.announce3.clone() { - ui.label(accounce); - } else { - ui.label("无法连接服务器"); - } - - ui.separator(); - -} \ No newline at end of file diff --git a/frontend/src/ui/tabs/project_list.rs b/frontend/src/ui/tabs/project_list.rs deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/ui/tabs/settings.rs b/frontend/src/ui/tabs/settings.rs deleted file mode 100644 index 4d8f174..0000000 --- a/frontend/src/ui/tabs/settings.rs +++ /dev/null @@ -1,309 +0,0 @@ -use eframe::egui; -use crate::app::Myapp; -use common::utils::save_config; - -fn on_switch(ui: &mut egui::Ui, output_char: &str, on: &mut bool) -> egui::Response { - ui.label( - egui::RichText::new(output_char) - .size(15.0) - .color(egui::Color32::from_rgb(0,0,0)) - - .strong() - ); - // 开关尺寸 - let width = 55.0; - let height = 26.0; - - // 分配空间并获取响应 - let (rect, mut response) = ui.allocate_exact_size( - egui::vec2(width, height), - egui::Sense::click() - ); - - // 处理点击 - if response.clicked() { - *on = !*on; - response.mark_changed(); - } - - // 动画参数 - let animation_progress = ui.ctx().animate_bool(response.id, *on); - let radius = height / 2.0; - - // 计算滑块位置 - let circle_x = rect.left() + radius + animation_progress * (width - height); - - // 绘制轨道 - ui.painter().rect_filled( - rect.expand(-1.0), - radius, - if *on { - egui::Color32::from_rgb(102,204,255) // 启用状态颜色 - } else { - egui::Color32::from_rgb(150, 150, 150) // 禁用状态颜色 - } - ); - - // 绘制滑块 - ui.painter().circle_filled( - egui::pos2(circle_x, rect.center().y), - radius - 4.0, - egui::Color32::WHITE - ); - - response -} - -pub fn common_input( - ui: &mut egui::Ui, - title: &str, - text: &mut String, - hint: &str, - open_filter: bool, - - -) -> bool{ - ui.label( - egui::RichText::new(title) - .size(15.0) - .color(egui::Color32::from_rgb(0,0,0)) - - - ); - ui.add_space(8.0); - let input = egui::TextEdit::singleline( text) - .hint_text(hint)//提示 - .desired_rows(1)//限制1行 - .min_size(egui::vec2(120.0, 35.0)); - - - let response = ui.add(input); - if response.changed(){ - if open_filter{ - *text = text.chars()//过滤非法字符 - .filter(|c| c.is_ascii_alphanumeric() || *c == '@' || *c == '.' || *c == '-' || *c == '_') - .collect(); - } - else{ - *text = text.chars()//过滤非法字符 - .collect(); - }; - - } - response.changed() - -} -pub fn render(app: &mut Myapp, ui: &mut egui::Ui) { - - ui.horizontal(|ui|{ - ui.heading("设置"); - ui.add_space(20.0); - let button = egui::Button::new( - egui::RichText::new("保存设置").size(15.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(100.0,35.0)) - .fill(egui::Color32::from_rgb(102,204,255)) - .rounding(15.0);//圆角成度 - let response = ui.add(button); - if response.clicked(){ - match save_config(&mut app.config, Some(&app.push_config),Some(&app.custom_config), None){ - Ok(_) => { - log::info!("设置保存成功"); - }, - Err(e) => { - log::info!("设置保存失败: {}", e); - } - } - } - - }) ; - - ui.separator(); - //推送设置: - // 创建圆角长方形框架 - egui::Frame::none() - .fill(egui::Color32::from_rgb(245, 245, 250)) // 背景色 - .rounding(12.0) // 圆角半径 - .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 220))) // 边框 - .inner_margin(egui::Margin { left: 10.0, right: 20.0, top: 15.0, bottom: 15.0 }) // 内边距 - .show(ui, |ui| { - - globle_setting(app,ui); - ui.separator(); - - - }); - egui::Frame::none() - .fill(egui::Color32::from_rgb(245, 245, 250)) // 背景色 - .rounding(12.0) // 圆角半径 - .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 220))) // 边框 - .inner_margin(egui::Margin { left: 10.0, right: 20.0, top: 15.0, bottom: 15.0 }) // 内边距 - .show(ui, |ui| { - - push_setting(app,ui); // 调用推送设置 - ui.separator(); - - }); - - - -} - -pub fn globle_setting(app: &mut Myapp, ui: &mut egui::Ui){ - ui.horizontal(|ui| { - - common_input(ui, "请输入账号1预填手机号:", &mut app.custom_config.preinput_phone1, "请输入账号1绑定的手机号",true); - common_input(ui, "请输入账号2预填手机号:", &mut app.custom_config.preinput_phone1, "请输入账号2绑定的手机号,没有可不填",true); - }); - ui.separator(); - ui.horizontal(|ui|{ - ui.label("请选择验证码识别方式:"); - let options = ["本地识别", "ttocr识别", "选项3"]; - - custom_selection_control(ui, &mut app.custom_config.captcha_mode, &options) ; - match app.custom_config.captcha_mode{ - - 1 => { - dynamic_caculate_space(ui, 300.0); - common_input(ui, "请输入ttocr key:", &mut app.custom_config.ttocr_key, "请输入ttocr key",true); - - - }, - _ => { - - } - } - - }); - ui.separator(); - ui.horizontal(|ui| { - on_switch(ui, "开启自定义UA", &mut app.custom_config.open_custom_ua); - dynamic_caculate_space(ui, 180.0); - common_input(ui, "", &mut app.custom_config.custom_ua, "请输入自定义UA",false); - - }); - - - - - -} - -fn custom_selection_control(ui: &mut egui::Ui, selected: &mut usize, options: &[&str]) -> bool { - let mut changed = false; - ui.horizontal(|ui| { - for (idx, option) in options.iter().enumerate() { - let is_selected = *selected == idx; - let button = egui::Button::new( - egui::RichText::new(*option) - .size(15.0) - .color(if is_selected { egui::Color32::WHITE } else { egui::Color32::BLACK }) - ) - .min_size(egui::vec2(80.0, 30.0)) - .fill(if is_selected { - egui::Color32::from_rgb(102, 204, 255) - } else { - egui::Color32::from_rgb(245, 245, 250) - }) - .rounding(10.0); - - if ui.add(button).clicked() { - *selected = idx; - changed = true; - } - } - }); - changed -} -pub fn push_setting(app: &mut Myapp, ui: &mut egui::Ui){ - //推送开关 - - // 开关 - ui.horizontal(|ui| { - - - on_switch(ui, "开启推送",&mut app.push_config.enabled); - let available = ui.available_width(); - ui.add_space(available-100.0); - - let button = egui::Button::new( - egui::RichText::new("测试推送").size(15.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(100.0,40.0)) - .fill(egui::Color32::from_rgb(102,204,255)) - .rounding(15.0);//圆角成度 - let response = ui.add(button); - if response.clicked(){ - app.push_config.push_all("biliticket推送测试", "这是一个推送测试", &None,&mut *app.task_manager); - } - - - }); - if app.push_config.enabled{ - ui.separator(); - - - - - //推送设置 - ui.horizontal(|ui|{ - - common_input(ui, "bark推送:",&mut app.push_config.bark_token,"请输入推送地址,只填token",true); - dynamic_caculate_space(ui, 180.0); - common_input(ui, "pushplus推送:",&mut app.push_config.pushplus_token,"请输入推送地址,只填token",true); - }); - //TODO补充每个推送方式使用方法 - - ui.horizontal(|ui|{ - - common_input(ui, "方糖推送:",&mut app.push_config.fangtang_token,"请输入推送地址:SCTxxxxxxx",true); - dynamic_caculate_space(ui, 180.0); - common_input(ui, "钉钉机器人推送:",&mut app.push_config.dingtalk_token,"请输入钉钉机器人token,只填token",true); - }); - - ui.horizontal(|ui|{ - common_input(ui, "企业微信推送:",&mut app.push_config.wechat_token,"请输入企业微信机器人token",true); - dynamic_caculate_space(ui, 180.0); - - }); - ui.horizontal(|ui|{ - common_input(ui, "smtp服务器地址:",&mut app.push_config.smtp_config.smtp_server,"请输入smtp服务器地址",true); - dynamic_caculate_space(ui, 180.0); - common_input(ui, "smtp服务器端口:",&mut app.push_config.smtp_config.smtp_port,"请输入smtp服务器端口",true); - - }); - ui.horizontal(|ui|{ - - common_input(ui, "邮箱账号:",&mut app.push_config.smtp_config.smtp_from,"请输入发件人邮箱",true); - dynamic_caculate_space(ui, 180.0); - common_input(ui, "授权密码:",&mut app.push_config.smtp_config.smtp_password,"请输入授权密码",true); - dynamic_caculate_space(ui, 180.0); - - }); - ui.horizontal(|ui|{ - - - - common_input(ui, "发件人邮箱:",&mut app.push_config.smtp_config.smtp_from,"请输入发件人邮箱",true); - dynamic_caculate_space(ui, 180.0); - common_input(ui, "收件人邮箱:",&mut app.push_config.smtp_config.smtp_to,"请输入收件人邮箱",true); - - }); - ui.horizontal(|ui| { - common_input(ui, "gotify地址:",&mut app.push_config.gotify_config.gotify_url,"请输入gotify服务器地址,只填写地址",false); - dynamic_caculate_space(ui, 180.0); - common_input(ui, "gotify的token", &mut app.push_config.gotify_config.gotify_token, "请输入gotify的token", true) - }); - } - -} -pub fn dynamic_caculate_space(ui :&mut egui::Ui, next_obj_space: f32) { - let available_space = ui.available_width(); - let mut space = available_space - next_obj_space - 250.0; - if space < 0.0 { - space = 0.0; - } - ui.add_space(space); -} - - diff --git a/frontend/src/windows/add_buyer.rs b/frontend/src/windows/add_buyer.rs deleted file mode 100644 index 46c1f53..0000000 --- a/frontend/src/windows/add_buyer.rs +++ /dev/null @@ -1,280 +0,0 @@ -use crate::app::Myapp; -use eframe::egui::{self, RichText}; -use serde_json::Value; - -pub struct AddBuyerInput{ - pub name: String, - pub phone: String, - pub id_type: usize, - pub id_number: String, - pub as_default_buyer: bool, - -} -pub fn show(app: &mut Myapp, ctx: &egui::Context, uid: &str) { - let find_account = app.account_manager.accounts.iter().find(|account| account.uid.to_string() == uid); - let select_account = match find_account { - Some(account) => account, - None => return, - }; - let select_cookie_manager = select_account.cookie_manager.clone().unwrap(); - let mut window_open = app.show_add_buyer_window.is_some(); - - egui::Window::new("添加购票人") - .open(&mut window_open) - .default_size([700.0, 400.0]) - .resizable(false) - .show(ctx, |ui| { - - ui.vertical_centered(|ui|{ - ui.label(RichText::new("添加购票人") - .size(20.0) - .color(egui::Color32::from_rgb(0,0,0)) - .strong() - ); - }); - ui.add_space(20.0); - ui.horizontal(|ui|{ - ui.add_space(8.0); - common_input(ui, "姓名:", &mut app.add_buyer_input.name, "请输入你的真实姓名", false); - - - }); - ui.add_space(20.0); - ui.horizontal(|ui|{ - ui.add_space(8.0); - common_input(ui, "手机号:", &mut app.add_buyer_input.phone, "请输入你的手机号", true); - ui.add_space(20.0); - - }); - - // 添加证件类型选择器 - ui.add_space(20.0); - ui.horizontal(|ui| { - ui.add_space(8.0); - ui.label( - egui::RichText::new("证件类型:") - .size(15.0) - .color(egui::Color32::from_rgb(0,0,0)) - ); - ui.add_space(8.0); - - // 调用证件类型选择器 - id_type_selector(ui, &mut app.add_buyer_input.id_type); - }); - - // 添加证件号码输入 - ui.add_space(20.0); - ui.horizontal(|ui|{ - ui.add_space(8.0); - common_input(ui, "证件号码:", &mut app.add_buyer_input.id_number, get_id_hint(app.add_buyer_input.id_type), true); - }); - - // 添加默认购票人选项 - ui.add_space(20.0); - ui.horizontal(|ui|{ - ui.add_space(8.0); - ui.checkbox(&mut app.add_buyer_input.as_default_buyer, "设为默认购票人"); - }); - - - //确保空间大小合适 - ui.add_space(30.0); - ui.horizontal(|ui| { - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - - }); - }); - - ui.vertical_centered(|ui|{ - // 创建按钮 - let button = egui::Button::new( - egui::RichText::new("保存").size(20.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(120.0, 50.0)) - .fill(egui::Color32::from_rgb(102,204,255)) - .rounding(20.0); - let response = ui.add(button); - if response.clicked() { - let mut json_form = serde_json::Map::new(); - json_form.insert("name".to_string(), serde_json::Value::String(app.add_buyer_input.name.clone())); - json_form.insert("tel".to_string(), serde_json::Value::String(app.add_buyer_input.phone.clone())); - json_form.insert("id_type".to_string(), serde_json::Value::Number(serde_json::Number::from(app.add_buyer_input.id_type))); - json_form.insert("personal_id".to_string(), serde_json::Value::String(app.add_buyer_input.id_number.clone())); - json_form.insert("is_default".to_string(), serde_json::Value::String(check_default(app.add_buyer_input.as_default_buyer).to_string())); - json_form.insert("src".to_string(), serde_json::Value::String("ticket".to_string())); - - - - log::debug!("添加购票人数据: {:?}", json_form); - log::debug!("账号ck: {:?}", select_account.cookie.as_str()); - let rt = tokio::runtime::Runtime::new().unwrap(); - let response = rt.block_on(async{ - select_cookie_manager.post( - "https://show.bilibili.com/api/ticket/buyer/create", - - ).await - .json(&json_form) - .send() - .await - .unwrap() - }); - - if !response.status().is_success() { - log::error!("添加购票人失败: {:?}", response.status()); - return; - } - - let response_text = match rt.block_on(response.text()) { - Ok(text) => text, - Err(e) => { - log::error!("获取响应文本失败: {}", e); - return; - } - }; - - let json_value: Result = serde_json::from_str(&response_text); - let response_json_value = match json_value { - Ok(val) => val, - Err(e) => { - log::error!("解析JSON失败! 原因: {}, 响应原文: {}", e, response_text); - return; - } - }; - let errno_value = response_json_value.get("errno").and_then(|v| v.as_i64()).unwrap_or(-1); - let code_value = response_json_value.get("code").and_then(|v| v.as_i64()).unwrap_or(-1); - let code = if errno_value != -1{ - errno_value - } else { - code_value - }; - if code == 0 { - log::info!("添加购票人成功: {:?}", response_text); - app.show_add_buyer_window = None; - // 重置表单 - app.add_buyer_input = AddBuyerInput { - name: String::new(), - phone: String::new(), - id_type: 0, - id_number: String::new(), - as_default_buyer: false, - }; - } else { - log::error!("添加购票人失败: {:?}", response_text); - } - - - - - - } - }) - - }); - - //更新窗口状态 - if !window_open { - app.show_add_buyer_window = None; - } -} - -pub fn common_input( - ui: &mut egui::Ui, - title: &str, - text: &mut String, - hint: &str, - open_filter: bool, - - -) -> bool{ - ui.label( - egui::RichText::new(title) - .size(15.0) - .color(egui::Color32::from_rgb(0,0,0)) - - - ); - ui.add_space(8.0); - let input = egui::TextEdit::singleline( text) - .hint_text(hint)//提示 - .desired_rows(1)//限制1行 - .min_size(egui::vec2(120.0, 35.0)); - - - let response = ui.add(input); - if response.changed(){ - if open_filter{ - *text = text.chars()//过滤非法字符 - .filter(|c| c.is_ascii_alphanumeric() || *c == '@' || *c == '.' || *c == '-' || *c == '_') - .collect(); - } - else{ - *text = text.chars()//过滤非法字符 - .collect(); - }; - - } - response.changed() - -} - -// 证件类型的名称和值 -const ID_TYPES: [(&str, usize); 4] = [ - ("身份证", 0), - ("护照", 1), - ("港澳居民往来内地通行证", 2), - ("台湾居民往来大陆通行证", 3), -]; - -fn check_default(is_default: bool) -> &'static str { - if is_default { - "1" - } else { - "0" - } -} -fn id_type_selector(ui: &mut egui::Ui, selected_type: &mut usize) { - ui.horizontal(|ui| { - for (name, value) in ID_TYPES.iter() { - let is_selected = *selected_type == *value; - - // 创建更美观的选择按钮 - let button = egui::Button::new( - RichText::new(*name) - .size(14.0) - .color( - if is_selected { - egui::Color32::WHITE - } else { - egui::Color32::from_rgb(60, 60, 60) - } - ) - ) - .min_size(egui::vec2(0.0, 32.0)) - .fill( - if is_selected { - egui::Color32::from_rgb(102, 204, 255) - } else { - egui::Color32::from_rgb(240, 240, 240) - } - ) - .rounding(5.0); - - if ui.add(button).clicked() { - *selected_type = *value; - } - - ui.add_space(8.0); // 按钮之间的间距 - } - }); -} - -// 根据证件类型获取不同的提示文字 -fn get_id_hint(id_type: usize) -> &'static str { - match id_type { - 0 => "请输入18位身份证号码", - 1 => "请输入护照号码", - 2 => "请输入港澳通行证号码", - 3 => "请输入台湾通行证号码", - _ => "请输入证件号码", - } -} \ No newline at end of file diff --git a/frontend/src/windows/confirm_ticket.rs b/frontend/src/windows/confirm_ticket.rs deleted file mode 100644 index e5d4645..0000000 --- a/frontend/src/windows/confirm_ticket.rs +++ /dev/null @@ -1,581 +0,0 @@ -use crate::app::Myapp; -use std::sync::Arc; -use common::cookie_manager::CookieManager; -use common::ticket::{*}; -use common::taskmanager::{GrabTicketRequest, TaskStatus, TaskRequest}; -use eframe::egui; -use egui::{Color32, RichText, Vec2, Stroke}; - -pub fn show(app: &mut Myapp,ctx:&egui::Context,uid:&i64){ - - let mut open = app.confirm_ticket_info.is_some(); - if !open { - return; - } - let biliticket_index = match app.bilibiliticket_list.iter().position(|bt| bt.uid == *uid) { - Some(index) => index, - None => { - log::error!("没有找到uid为{}的抢票信息", uid); - app.confirm_ticket_info = None; - return; - } - }; - let biliticket_uid; - let biliticket_project_id; - let cookie_manager: Arc; - let id_bind; - let screen_info: Option; - let ticket_info: Option; - let buyers; - - - app.is_loading = false; - { - let biliticket = &app.bilibiliticket_list[biliticket_index]; - - biliticket_uid = biliticket.uid; - biliticket_project_id = biliticket.project_info.as_ref().map(|p| p.id.to_string()); - cookie_manager = biliticket.account.cookie_manager.clone().unwrap(); - - id_bind = match &biliticket.project_info { - Some(project) => project.id_bind, - None => 9, - }; - - // 查找当前选择的场次和票种信息 - let (screen, ticket) = match &biliticket.project_info { - Some(project) => { - let screen = project.screen_list.iter().find(|s| - s.id.to_string() == biliticket.screen_id); - - if let Some(screen) = screen { - let ticket = screen.ticket_list.iter().find(|t| - t.id == app.selected_ticket_id.unwrap_or(-1) as usize); - - (Some(screen.clone()), ticket.cloned()) - } else { - (None, None) - } - }, - None => (None, None) - }; - screen_info = screen; - ticket_info = ticket; - - - // 获取购票人列表 - let buyers_in = match &biliticket.all_buyer_info { - Some(data) => &data.list, - None => { - //log::error!("购票人列表未加载,请先获取购票人信息"); - &Vec::new() // 返回空列表 - } - }; - buyers = buyers_in.clone(); - } - let screen_info_display = screen_info.clone(); - let screen_info_button = screen_info.clone(); - let ticket_info_display = ticket_info.clone(); - - // 创建窗口 - egui::Window::new("确认购票信息") - .open(&mut open) - .collapsible(true) - .resizable(true) - .default_width(500.0) - //.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .show(ctx, |ui| { - ui.spacing_mut().item_spacing = Vec2::new(10.0, 15.0); - - // 标题区域 - ui.vertical_centered(|ui| { - ui.add_space(5.0); - ui.heading("确认购票信息"); - ui.add_space(5.0); - }); - ui.separator(); - - // 票务信息部分 - ui.add_space(5.0); - ui.heading("已选择票种"); - ui.add_space(5.0); - - let mut card =egui::Frame::none(); - - if !ctx.style().visuals.dark_mode { - card=card.fill(egui::Color32::from_rgb(245, 245, 250)); - } else { - card=card.fill(egui::Color32::from_rgb(6,6,6)); - } - - card.rounding(8.0) - .inner_margin(12.0) - .show(ui, |ui| { - // 显示项目名称 - let biliticket = &app.bilibiliticket_list[biliticket_index]; - if let Some(project) = &biliticket.project_info { - ui.label(RichText::new(&project.name).strong().size(16.0)); - } - - // 显示场次和票种信息 - if let Some(screen) = screen_info_display { - ui.label(RichText::new(format!("场次: {}", &screen.name)).size(14.0)); - - if let Some(ticket) = ticket_info_display { - ui.horizontal(|ui| { - ui.label(format!("票种: {}", &ticket.desc)); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label(RichText::new(format!("¥{:.2}", ticket.price as f64 / 100.0)) - .color(Color32::from_rgb(239, 68, 68)) - .strong()); - }); - }); - - // 添加票数选择功能 - ui.horizontal(|ui| { - ui.label("购票数量:"); - let mut count = app.bilibiliticket_list[biliticket_index].clone().count.unwrap_or(1); - - // 添加票数加减按钮 - if ui.button("-").clicked() && count > 1 { - count -= 1; - app.bilibiliticket_list[biliticket_index].count = Some(count); - } - - ui.label(format!("{} 张", count)); - - if ui.button("+").clicked() && count < 10 { - count += 1; - app.bilibiliticket_list[biliticket_index].count = Some(count); - } - - ui.label(format!("总价: ¥{:.2}", (ticket.price as f64 / 100.0) * count as f64)); - }); - } - } - }); - - match id_bind{ - 0 =>{ - ui.add_space(10.0); - ui.heading("输入联系人"); - ui.add_space(5.0); - - ui.horizontal(|ui|{ - let biliticket = &mut app.bilibiliticket_list[biliticket_index]; - - // 确保 no_bind_buyer_info 已经初始化 - if biliticket.no_bind_buyer_info.is_none() { - biliticket.no_bind_buyer_info = Some(NoBindBuyerInfo { - name: String::new(), - tel: String::new(), - uid: biliticket.uid, - }); - } - - if let Some(ref mut buyer_info) = biliticket.no_bind_buyer_info { - // 为姓名创建一个临时 Option - let mut name_option = Some(buyer_info.name.clone()); - common_input(ui, "请输入联系人姓名", &mut name_option, "请输入联系人姓名", false); - // 更新原始值 - if let Some(name) = &name_option { - buyer_info.name = name.clone(); - } - - ui.add_space(10.0); - - // 为电话创建一个临时 Option - let mut tel_option = Some(buyer_info.tel.clone()); - common_input(ui, "请输入联系人手机号", &mut tel_option, "请输入联系人手机号", true); - // 更新原始值 - if let Some(tel) = &tel_option { - buyer_info.tel = tel.clone(); - } - } - ui.add_space(10.0); - }); - - } - 1|2 =>{ - - - ui.add_space(10.0); - - if id_bind == 2 { - let selected_count = app.selected_buyer_list.as_ref().map_or(0, |list| list.len()); - - ui.horizontal(|ui| { - ui.heading("选择购票人"); - ui.add_space(5.0); - ui.label(RichText::new(format!("(已选 {} 人)", selected_count)) - .color(if selected_count > 0 { - Color32::from_rgb(74, 222, 128) - } else { - Color32::DARK_GRAY - })); - - // 显示所需购票人数提示 - let ticket_count = app.bilibiliticket_list[biliticket_index].clone().count.unwrap_or(1); - if selected_count != ticket_count as usize { - ui.add_space(10.0); - ui.label(RichText::new(format!("(请选择 {} 位购票人)", ticket_count)) - .color(Color32::from_rgb(220, 38, 38))); - } - }); - } else { - ui.heading("选择购票人"); - } - ui.add_space(5.0); - - if buyers.is_empty() { - - } else { - egui::ScrollArea::vertical() - .max_height(300.0) - .show(ui, |ui| { - // 计算可用宽度和每列宽度 - let available_width = ui.available_width(); - let card_width = 230.0; // 每个卡片的宽度 - let columns = (available_width / card_width).max(1.0).floor() as usize; - - // 创建网格布局 - egui::Grid::new("buyers_grid") - .num_columns(columns) - .spacing([10.0, 10.0]) - .show(ui, |ui| { - for (index, buyer) in buyers.iter().enumerate() { - // 判断是单选还是多选模式 - let is_multi_select = id_bind == 2; - - // 检查该购票人是否被选中 - 对单选和多选都使用 selected_buyer_list - let is_selected = app.selected_buyer_list.as_ref() - .map_or(false, |list| list.iter().any(|b| b.id == buyer.id)); - - let card_color=if !ctx.style().visuals.dark_mode { - if is_selected { - Color32::from_rgb(236, 252, 243) // 选中状态的浅绿色 - } else { - Color32::from_rgb(245, 245, 250) // 默认浅灰色 - } - } else { - //深色模式 - if is_selected { - Color32::from_rgb(6, 20, 6) // 选中状态的黑底浅绿色 - } else { - Color32::from_rgb(6, 6, 6) // 默认深黑色 - } - }; - - - - // 创建固定宽度的卡片 - ui.scope(|ui| { - ui.set_width(card_width - 10.0); // 减去间距 - - egui::Frame::none() - .fill(card_color) - .stroke(Stroke::new( - 1.0, - if is_selected { Color32::from_rgb(74, 222, 128) } else { Color32::from_gray(220) } - )) - .rounding(8.0) - .inner_margin(10.0) - .show(ui, |ui| { - let id_type_text = match buyer.id_type { - 0 => "身份证", - 1 => "护照", - 2 => "港澳通行证", - 3 => "台湾通行证", - _ => "其他证件" - }; - - ui.horizontal(|ui| { - // 添加不同样式的选择按钮 - let select_button = if is_multi_select { - // 多选模式:显示复选框样式 - if is_selected { - ui.add(egui::Button::new("☑").fill(Color32::from_rgb(74, 222, 128))) - } else { - ui.add(egui::Button::new("☐").fill(Color32::TRANSPARENT)) - } - } else { - // 单选模式:显示单选框样式 - if is_selected { - ui.add(egui::Button::new("✓").fill(Color32::from_rgb(74, 222, 128))) - } else { - ui.add(egui::Button::new("○").fill(Color32::TRANSPARENT)) - } - }; - - // 处理选择按钮点击 - if select_button.clicked() { - if is_multi_select { - // 多选模式:切换选中状态 - if app.selected_buyer_list.is_none() { - app.selected_buyer_list = Some(Vec::new()); - } - - let buyer_list = app.selected_buyer_list.as_mut().unwrap(); - - // 如果已经选中,则移除;否则添加 - if let Some(pos) = buyer_list.iter().position(|b| b.id == buyer.id) { - buyer_list.remove(pos); - log::debug!("移除购票人: {}", buyer.name); - } else { - buyer_list.push(buyer.clone()); - log::debug!("添加购票人: {}", buyer.name); - } - } else { - // 单选模式:替换当前选择的购票人 - log::debug!("选择购票人: {}", buyer.name); - //app.selected_buyer_id = Some(buyer.id); // 保持单选ID兼容 - app.selected_buyer_list = Some(vec![buyer.clone()]); // 使用List,但只有一个 - let biliticket = &mut app.bilibiliticket_list[biliticket_index]; - biliticket.buyer_info = Some(vec![buyer.clone()]); - } - } - - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.label(RichText::new(&buyer.name).strong().size(16.0)); - ui.label(RichText::new(id_type_text).weak().size(13.0)); - }); - - ui.horizontal(|ui| { - ui.label(format!("证件号: {}", mask_id(&buyer.personal_id))); - }); - - ui.horizontal(|ui| { - ui.label(format!("手机号: {}", buyer.tel)); - }); - }); - }); - }); - }); - - // 控制换行 - if (index + 1) % columns == 0 && index < buyers.len() - 1 { - ui.end_row(); - } - } - }); - - // 添加购票人按钮 - ui.add_space(10.0); - if ui.button("添加新购票人").clicked() { - app.show_add_buyer_window = Some(uid.to_string()); - app.confirm_ticket_info = None; - } - }); - } - } - _ =>{ - ui.add_space(10.0); - ui.label("该项目不支持选择购票人(未知状态码),请尝试直接购票!"); - } - } - - - // 底部按钮区域 - ui.add_space(15.0); - ui.separator(); - ui.add_space(10.0); - - ui.horizontal(|ui| { - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - // 根据模式决定按钮启用条件 - let biliticket = &app.bilibiliticket_list[biliticket_index]; - let local_captcha = app.local_captcha.clone(); - let is_hot = biliticket.project_info.clone().as_ref().map_or(false, |p| p.hot_project); - let button_enabled = match id_bind { - 0 => { - // 检查非实名购票人信息是否完整 - if let Some(info) = &biliticket.no_bind_buyer_info { - !info.name.is_empty() && !info.tel.is_empty() - } else { - false - } - - }, - 1 => app.selected_buyer_list.as_ref().map_or(false, |list| !list.is_empty()), - 2 => { - // 多选模式,需要确保选中的购票人数量与票数匹配 - let ticket_count = app.bilibiliticket_list[biliticket_index].clone().count.unwrap_or(1); - app.selected_buyer_list.as_ref().map_or(false, |list| !list.is_empty() && list.len() == ticket_count as usize) - }, - _ => false, - }; - - if ui.add_enabled( - button_enabled, - egui::Button::new("确认购票") - .fill(Color32::from_rgb(59, 130, 246)) - .min_size(Vec2::new(100.0, 36.0)) - ).clicked() { - match id_bind { - 0 => { - log::info!("确认非实名票购票"); - let biliticket = &app.bilibiliticket_list[biliticket_index]; - if let Some(ref buyer_info) = biliticket.no_bind_buyer_info { - log::info!("非实名购票人信息: {:?}", buyer_info); - if let Some(screen) = screen_info { - if let Some(ticket) = ticket_info { - // 提交抢票任务 - let grab_ticket_request = GrabTicketRequest { - task_id: "".to_string(), - uid: biliticket_uid, - project_id: biliticket_project_id.clone().unwrap_or_default(), - screen_id: screen.id.to_string(), - ticket_id: ticket.id.to_string(), - is_hot: is_hot, - count: app.bilibiliticket_list[biliticket_index].clone().count.unwrap_or(1) as i16, - buyer_info: Vec::new(), // 实名购票人信息,这里传空列表 - grab_mode: app.grab_mode, - status: TaskStatus::Pending, - start_time: None, - cookie_manager: cookie_manager.clone(), - biliticket: biliticket.clone(), - local_captcha: local_captcha.clone(), - skip_words: app.skip_words.clone(), - }; - log::debug!("提交抢票任务: {:?}", grab_ticket_request); - // 提交到任务管理器 - match app.task_manager.submit_task(TaskRequest::GrabTicketRequest(grab_ticket_request)) { - Ok(task_id) => { - log::info!("提交抢票任务成功,任务ID: {}", task_id); - app.confirm_ticket_info = None; - - - }, - Err(e) => { - log::error!("提交抢票任务失败: {}", e); - } - } - - } - } - - } - } - 1 | 2 => { - if let Some(ref buyer_list) = app.selected_buyer_list { - if !buyer_list.is_empty() { - let ids: Vec = buyer_list.iter().map(|b| b.id).collect(); - log::info!("确认购票,选择的购票人IDs: {:?}", ids); - - if let Some(screen) = screen_info_button { - if let Some(ticket) = ticket_info { - // 提交抢票任务 - let grab_ticket_request = GrabTicketRequest { - task_id: "".to_string(), - uid: biliticket_uid, - project_id: biliticket_project_id.clone().unwrap_or_default(), - screen_id: screen.id.to_string(), - ticket_id: ticket.id.to_string(), - is_hot: is_hot, - count: app.bilibiliticket_list[biliticket_index].clone().count.unwrap_or(1) as i16, - buyer_info: buyer_list.clone(), - grab_mode: app.grab_mode, - status: TaskStatus::Pending, - start_time: None, - cookie_manager: cookie_manager.clone(), - biliticket: biliticket.clone(), - local_captcha: local_captcha.clone(), - skip_words: app.skip_words.clone(), - - }; - log::debug!("提交抢票任务: {:?}", grab_ticket_request); - // 提交到任务管理器 - match app.task_manager.submit_task(TaskRequest::GrabTicketRequest(grab_ticket_request)) { - Ok(task_id) => { - log::info!("提交抢票任务成功,任务ID: {}", task_id); - app.confirm_ticket_info = None; - app.selected_buyer_list = None; - }, - Err(e) => { - log::error!("提交抢票任务失败: {}", e); - } - } - } - } - // 关闭窗口 - app.confirm_ticket_info = None; - } - } - } - - _ => { - log::error!("未知的购票人绑定状态: {}", id_bind); - } - } - } - - if ui.button("取消").clicked() { - app.confirm_ticket_info = None; - } - }); - }); - }); - - // 更新窗口打开状态 - if !open { - app.confirm_ticket_info = None; - } -} - -// 隐藏部分证件号码 -fn mask_id(id: &str) -> String { - if id.len() <= 6 { - return id.to_string(); - } - let visible_prefix = &id[..3]; - let visible_suffix = &id[id.len() - 3..]; - let mask_len = id.len() - 6; - let mask = "*".repeat(mask_len.min(6)); - - format!("{}{}{}", visible_prefix, mask, visible_suffix) -} - -pub fn common_input( - ui: &mut egui::Ui, - title: &str, - text: &mut Option, - hint: &str, - open_filter: bool, - - -) -> bool{ - if text.is_none() { - *text = Some(String::new()); - } - let text_ref = text.as_mut().unwrap(); - ui.label( - egui::RichText::new(title) - .size(15.0) - .color(egui::Color32::from_rgb(0,0,0)) - - - ); - ui.add_space(8.0); - let input = egui::TextEdit::singleline( text_ref) - .hint_text(hint)//提示 - .desired_rows(1)//限制1行 - .min_size(egui::vec2(120.0, 35.0)); - - - let response = ui.add(input); - if response.changed(){ - if open_filter{ - *text_ref = text_ref.chars()//过滤非法字符 - .filter(|c| c.is_ascii_alphanumeric() || *c == '@' || *c == '.' || *c == '-' || *c == '_') - .collect(); - } - else{ - *text_ref = text_ref.chars()//过滤非法字符 - .collect(); - }; - - } - response.changed() - -} \ No newline at end of file diff --git a/frontend/src/windows/confirm_ticket2.rs b/frontend/src/windows/confirm_ticket2.rs deleted file mode 100644 index e8e24ce..0000000 --- a/frontend/src/windows/confirm_ticket2.rs +++ /dev/null @@ -1,379 +0,0 @@ -use crate::app::Myapp; -use std::sync::Arc; -use common::cookie_manager::CookieManager; -use common::ticket::{BuyerInfo}; -use common::taskmanager::{GrabTicketRequest, TaskStatus, TaskRequest}; -use eframe::egui; -use egui::{Color32, RichText, Vec2, Stroke}; - -/// 显示捡漏模式的确认窗口 -/// 只需要选择购票人,其他信息都使用默认值 -pub fn show(app: &mut Myapp, ctx: &egui::Context, uid: &i64) { - let mut open = app.confirm_ticket_info.is_some(); - if !open { - return; - } - - let biliticket_index = match app.bilibiliticket_list.iter().position(|bt| bt.uid == *uid) { - Some(index) => index, - None => { - log::error!("没有找到uid为{}的抢票信息", uid); - app.confirm_ticket_info = None; - return; - } - }; - - let biliticket_uid; - let cookie_manager: Arc; - let buyers; - - { - let biliticket = &app.bilibiliticket_list[biliticket_index]; - - biliticket_uid = biliticket.uid; - cookie_manager = match &biliticket.account.cookie_manager { - Some(cm) => cm.clone(), - None => { - log::error!("账号未登录或cookie管理器未初始化"); - app.confirm_ticket_info = None; - return; - } - }; - - // 获取购票人列表 - let buyers_in = match &biliticket.all_buyer_info { - Some(data) => &data.list, - None => { - log::warn!("购票人列表未加载"); - &Vec::new() - } - }; - buyers = buyers_in.clone(); - } - - // 创建窗口 - egui::Window::new("捡漏模式 - 选择购票人") - .open(&mut open) - .collapsible(false) - .resizable(true) - .default_width(500.0) - .show(ctx, |ui| { - ui.spacing_mut().item_spacing = Vec2::new(10.0, 15.0); - - // 标题区域 - ui.vertical_centered(|ui| { - ui.add_space(5.0); - ui.heading("捡漏模式 - 选择购票人"); - ui.add_space(5.0); - ui.label(RichText::new("系统会自动监控可用票种并尝试抢票").color(Color32::DARK_GRAY)); - }); - ui.separator(); - - // 显示一个简单的模式说明 - ui.add_space(10.0); - egui::Frame::none() - .fill(Color32::from_rgb(253, 246, 227)) - .rounding(8.0) - .inner_margin(12.0) - .show(ui, |ui| { - ui.label(RichText::new("捡漏模式说明:").strong()); - ui.label("1. 系统将持续监控所有可能的场次和票种"); - ui.label("2. 一旦发现可购买票种,会立即尝试下单"); - /* ui.label("3. 由于速度原因,可能会遇到更多的风控验证"); */ - ui.label("3. 暂时只支持实名制票捡漏,请务必选择购票人,否则无法进行购票"); - }); - ui.add_space(10.0); - - // 计算已选择的购票人数量 - let selected_count = app.selected_buyer_list.as_ref().map_or(0, |list| list.len()); - - // 购票人选择标题 - ui.horizontal(|ui| { - ui.heading("选择购票人"); - ui.add_space(5.0); - ui.label(RichText::new(format!("(已选 {} 人)", selected_count)) - .color(if selected_count > 0 { - Color32::from_rgb(74, 222, 128) - } else { - Color32::DARK_GRAY - })); - }); - ui.add_space(5.0); - - // 购票人列表 - if buyers.is_empty() { - ui.label(RichText::new("暂无购票人信息,请先添加购票人").color(Color32::DARK_RED)); - } else { - app.is_loading = false; - egui::ScrollArea::vertical() - .max_height(300.0) - .show(ui, |ui| { - // 计算可用宽度和每列宽度 - let available_width = ui.available_width(); - let card_width = 230.0; // 每个卡片的宽度 - let columns = (available_width / card_width).max(1.0).floor() as usize; - - // 创建网格布局 - egui::Grid::new("buyers_grid") - .num_columns(columns) - .spacing([10.0, 10.0]) - .show(ui, |ui| { - for (index, buyer) in buyers.iter().enumerate() { - // 检查该购票人是否被选中 - let is_selected = app.selected_buyer_list.as_ref() - .map_or(false, |list| list.iter().any(|b| b.id == buyer.id)); - - let card_color=if !ctx.style().visuals.dark_mode { - if is_selected { - Color32::from_rgb(236, 252, 243) // 选中状态的浅绿色 - } else { - Color32::from_rgb(245, 245, 250) // 默认浅灰色 - } - } else { - //深色模式 - if is_selected { - Color32::from_rgb(6, 20, 6) // 选中状态的黑底浅绿色 - } else { - Color32::from_rgb(6, 6, 6) // 默认深黑色 - } - }; - - // 创建固定宽度的卡片 - ui.scope(|ui| { - ui.set_width(card_width - 10.0); // 减去间距 - - egui::Frame::none() - .fill(card_color) - .stroke(Stroke::new( - 1.0, - if is_selected { Color32::from_rgb(74, 222, 128) } else { Color32::from_gray(220) } - )) - .rounding(8.0) - .inner_margin(10.0) - .show(ui, |ui| { - let id_type_text = match buyer.id_type { - 0 => "身份证", - 1 => "护照", - 2 => "港澳通行证", - 3 => "台湾通行证", - _ => "其他证件" - }; - - ui.horizontal(|ui| { - // 多选复选框 - let select_button = if is_selected { - ui.add(egui::Button::new("☑").fill(Color32::from_rgb(74, 222, 128))) - } else { - ui.add(egui::Button::new("☐").fill(Color32::TRANSPARENT)) - }; - - // 处理选择按钮点击 - if select_button.clicked() { - // 多选模式:切换选中状态 - if app.selected_buyer_list.is_none() { - app.selected_buyer_list = Some(Vec::new()); - } - - let buyer_list = app.selected_buyer_list.as_mut().unwrap(); - - // 如果已经选中,则移除;否则添加 - if let Some(pos) = buyer_list.iter().position(|b| b.id == buyer.id) { - buyer_list.remove(pos); - log::debug!("移除购票人: {}", buyer.name); - } else { - buyer_list.push(buyer.clone()); - log::debug!("添加购票人: {}", buyer.name); - } - } - - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.label(RichText::new(&buyer.name).strong().size(16.0)); - ui.label(RichText::new(id_type_text).weak().size(13.0)); - }); - - ui.horizontal(|ui| { - ui.label(format!("证件号: {}", mask_id(&buyer.personal_id))); - }); - - ui.horizontal(|ui| { - ui.label(format!("手机号: {}", buyer.tel)); - }); - }); - }); - }); - }); - - // 控制换行 - if (index + 1) % columns == 0 && index < buyers.len() - 1 { - ui.end_row(); - } - } - }); - - // 添加购票人按钮 - ui.add_space(10.0); - if ui.button("添加新购票人").clicked() { - app.show_add_buyer_window = Some(uid.to_string()); - app.confirm_ticket_info = None; - } - }); - } - - // 底部按钮区域 - ui.add_space(15.0); - ui.separator(); - ui.add_space(10.0); - - ui.heading("关键词过滤"); - ui.label(RichText::new("输入需要过滤的关键词,多个关键词用空格分隔。当捡漏到包含这些关键词的标题时将自动跳过。").color(Color32::DARK_GRAY)); - ui.add_space(5.0); - - // 文本输入框 - ui.horizontal(|ui| { - ui.label("过滤关键词:"); - let text_edit = ui.text_edit_singleline(&mut app.skip_words_input); - - if text_edit.changed() { - // 当文本输入改变时,更新关键词列表 - let words: Vec = app.skip_words_input - .split_whitespace() - .map(|s| s.to_string()) - .collect(); - - if words.is_empty() { - app.skip_words = None; - } else { - app.skip_words = Some(words); - } - } - }); - - // 显示当前过滤词列表 - if let Some(words) = &app.skip_words { - if !words.is_empty() { - // 用于记录需要删除的词 - let mut word_to_delete: Option = None; - - ui.horizontal_wrapped(|ui| { - ui.label("当前过滤词:"); - for word in words.iter() { - let chip = egui::Label::new( - RichText::new(format!(" {} ", word)) - .background_color(Color32::from_rgb(59, 130, 246)) - .color(Color32::WHITE) - ) - .sense(egui::Sense::click()); - - if ui.add(chip).clicked() { - // 只记录要删除的词,不立即修改 - word_to_delete = Some(word.clone()); - } - ui.add_space(5.0); - } - }); - - // 在闭包外处理删除逻辑 - if let Some(word) = word_to_delete { - if let Some(words_mut) = &mut app.skip_words { - if let Some(pos) = words_mut.iter().position(|w| w == &word) { - words_mut.remove(pos); - - // 更新输入框内容 - app.skip_words_input = words_mut.join(" "); - - // 如果关键词列表为空,设置为None - if words_mut.is_empty() { - app.skip_words = None; - } - } - } - } - } - } - - - - ui.horizontal(|ui| { - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - // 只有选择了购票人,按钮才可用 - let has_buyers = app.selected_buyer_list.as_ref().map_or(false, |list| !list.is_empty()); - - if ui.add_enabled( - has_buyers, - egui::Button::new("开始捡漏") - .fill(Color32::from_rgb(59, 130, 246)) - .min_size(Vec2::new(100.0, 36.0)) - ).clicked() { - if let Some(ref buyer_list) = app.selected_buyer_list { - if !buyer_list.is_empty() { - let ids: Vec = buyer_list.iter().map(|b| b.id).collect(); - log::info!("开始捡漏模式,选择的购票人IDs: {:?}", ids); - - // 获取必要的数据 - let mut biliticket = &mut app.bilibiliticket_list[biliticket_index]; - let project_id = biliticket.project_id.clone(); - let local_captcha = app.local_captcha.clone(); - biliticket.id_bind = 1; - - // 创建捡漏模式的抢票请求 - let grab_ticket_request = GrabTicketRequest { - task_id: "".to_string(), - uid: biliticket_uid, - project_id, - // 在捡漏模式下,这些值会被后端动态设置,所以这里设为空字符串 - screen_id: "".to_string(), - ticket_id: "".to_string(), - count: buyer_list.len() as i16, - buyer_info: buyer_list.clone(), - is_hot: true, - grab_mode: 2, // 使用捡漏模式 - status: TaskStatus::Pending, - start_time: None, - cookie_manager, - biliticket: biliticket.clone(), - local_captcha, - skip_words: app.skip_words.clone(), - }; - - log::debug!("提交捡漏模式任务: {:?}", grab_ticket_request); - - // 提交到任务管理器 - match app.task_manager.submit_task(TaskRequest::GrabTicketRequest(grab_ticket_request)) { - Ok(task_id) => { - log::info!("提交捡漏模式任务成功,任务ID: {}", task_id); - app.confirm_ticket_info = None; - }, - Err(e) => { - log::error!("提交捡漏模式任务失败: {}", e); - } - } - } - } - } - - if ui.button("取消").clicked() { - app.confirm_ticket_info = None; - } - }); - }); - }); - - // 更新窗口打开状态 - if !open { - app.confirm_ticket_info = None; - } -} - -// 隐藏部分证件号码 - 复用已有函数 -fn mask_id(id: &str) -> String { - if id.len() <= 6 { - return id.to_string(); - } - let visible_prefix = &id[..3]; - let visible_suffix = &id[id.len() - 3..]; - let mask_len = id.len() - 6; - let mask = "*".repeat(mask_len.min(6)); - - format!("{}{}{}", visible_prefix, mask, visible_suffix) -} \ No newline at end of file diff --git a/frontend/src/windows/grab_ticket.rs b/frontend/src/windows/grab_ticket.rs deleted file mode 100644 index ab1cf47..0000000 --- a/frontend/src/windows/grab_ticket.rs +++ /dev/null @@ -1,9 +0,0 @@ -use eframe::egui; -use egui::Ui; -use common::ticket::BilibiliTicket; -use crate::app::Myapp; - - -pub fn show(app:&mut Myapp, ctx:&egui::Context, bilibiliticket: &mut BilibiliTicket){ - -} \ No newline at end of file diff --git a/frontend/src/windows/log_windows.rs b/frontend/src/windows/log_windows.rs deleted file mode 100644 index cc93428..0000000 --- a/frontend/src/windows/log_windows.rs +++ /dev/null @@ -1,67 +0,0 @@ - -use eframe::egui; -use crate::app::Myapp; - -pub fn show(app: &mut Myapp, ctx: &egui::Context) { - - let mut window_open = app.show_log_window; - let mut user_close: bool = false; - - egui::Window::new("监视面板") - .open(&mut window_open) - .default_size([550.0, 400.0]) - .resizable(true) - .show(ctx, |ui| { - // 顶部工具栏 - ui.horizontal(|ui| { - if ui.button("清空日志").clicked() { - app.logs.clear(); - } - - if ui.button("添加测试日志").clicked() { - app.add_log("测试日志消息"); - } - - ui.with_layout(egui::Layout::right_to_left(egui::Align::RIGHT), |ui| { - if ui.button("❌").clicked() { - user_close = true; - app.show_log_window = false; - } - }); - }); - - ui.separator(); - - // 日志内容区域 - egui::ScrollArea::vertical() - .auto_shrink([false, false]) - .stick_to_bottom(true) - .max_height(300.0) - .show(ui, |ui| { - // 显示当前状态 - ui.label(format!("当前状态: {}", - if app.running_status.is_empty() { "未知状态" } else { &app.running_status })); - - ui.separator(); - - // 显示所有日志 - if app.logs.is_empty() { - ui.label("暂无日志记录"); - - } else { - for log in &app.logs { - ui.label(log); - ui.separator(); - } - } - }); - - // 底部状态栏 - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - ui.label(format!("共 {} 条日志", app.logs.len())); - }); - }); - - // 更新窗口状态 - app.show_log_window = window_open; -} \ No newline at end of file diff --git a/frontend/src/windows/login_selenium.rs b/frontend/src/windows/login_selenium.rs deleted file mode 100644 index e1d5f75..0000000 --- a/frontend/src/windows/login_selenium.rs +++ /dev/null @@ -1,114 +0,0 @@ -use eframe::egui; -use crate::app::Myapp; -use common::account::Account; -use std::sync::mpsc; -use std::thread; - -// 登录状态枚举 -#[derive(Debug, Clone)] -pub enum SeleniumLoginStatus { - NotStarted, - Connecting, - WaitingForLogin, - LoggedIn(String), // 成功获取的 cookie - Failed(String), // 错误信息 -} - -pub struct SeleniumLogin { - status: SeleniumLoginStatus, - progress: f32, - cookie: Option, -} - -impl SeleniumLogin { - pub fn new() -> Self { - Self { - status: SeleniumLoginStatus::NotStarted, - progress: 0.0, - cookie: None, - } - } - - // 启动浏览器登录过程 - pub fn start_login(&mut self) -> Result<(), String> { - self.status = SeleniumLoginStatus::Connecting; - self.progress = 0.1; - - // 创建通道用于接收异步结果 - let (tx, rx) = mpsc::channel(); - - // 在后台线程执行浏览器登录 - thread::spawn(move || { - // 创建运行时 - let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(login_and_get_cookie()); - - match result { - Ok(cookie) => { - let _ = tx.send(SeleniumLoginStatus::LoggedIn(cookie)); - }, - Err(e) => { - let _ = tx.send(SeleniumLoginStatus::Failed(e.to_string())); - } - } - }); - - // 保存接收器以便稍后检查结果 - self.cookie = None; - - Ok(()) - } - - // 检查登录状态 - 在UI更新循环中调用 - pub fn check_status(&mut self, rx: &mpsc::Receiver) -> bool { - // 非阻塞检查是否有新状态 - if let Ok(status) = rx.try_recv() { - self.status = status.clone(); - - // 如果成功获取cookie - if let SeleniumLoginStatus::LoggedIn(cookie) = status { - self.cookie = Some(cookie); - self.progress = 1.0; - return true; - } - } - - // 更新进度 - match self.status { - SeleniumLoginStatus::Connecting => { - if self.progress < 0.2 { - self.progress += 0.01; - } - }, - SeleniumLoginStatus::WaitingForLogin => { - if self.progress < 0.9 { - self.progress += 0.001; - } - }, - _ => {} - } - - false - } - - // 获取当前cookie - pub fn take_cookie(&mut self) -> Option { - self.cookie.take() - } -} - -// 原有的登录逻辑保持不变 -async fn login_and_get_cookie() -> Result> { - // 实现保持不变... - // ... - Ok("cookie_value".to_string()) -} - -// 检查WebDriver是否可用 -pub fn is_webdriver_available() -> bool { - // 简单检测本地是否运行了WebDriver - match std::net::TcpStream::connect("127.0.0.1:4444") { - Ok(_) => true, - Err(_) => false, - } -} \ No newline at end of file diff --git a/frontend/src/windows/login_windows.rs b/frontend/src/windows/login_windows.rs deleted file mode 100644 index fa4af45..0000000 --- a/frontend/src/windows/login_windows.rs +++ /dev/null @@ -1,494 +0,0 @@ -use eframe::egui; -use base64; -use crate::app::Myapp; -use common::{login::*, utility::CustomConfig}; -use image::Luma; -use qrcode::QrCode; -use egui::TextureHandle; -use reqwest::Client; -pub struct LoginTexture{ - pub left_conrner_texture: Option, - pub right_conrner_texture: Option, -} - -pub fn show(app: &mut Myapp, ctx: &egui::Context) { - let mut login_method = &mut app.login_method.clone(); - let mut window_open = app.show_login_windows; - //save_texture = app.login - - // 如果图像还没加载,则加载它们 - if !app.login_texture.left_conrner_texture.is_some() { - // 左下角图片的Base64字符串 - let left_corner_base64 = get_left_base64(); - - if let Some(texture) = load_image_from_base64(ctx, "left_corner", left_corner_base64) { - app.login_texture.left_conrner_texture = Some(texture); - } - } - - if !app.login_texture.right_conrner_texture.is_some() { - // 右下角图片的Base64字符串 - let right_corner_base64 = get_right_base64(); - - if let Some(texture) = load_image_from_base64(ctx, "right_corner", right_corner_base64) { - app.login_texture.right_conrner_texture = Some(texture); - } - } - - egui::Window::new("登录窗口") - .open(&mut window_open) - .default_size([700.0, 400.0]) - .resizable(false) - .show(ctx, |ui| { - - ui.horizontal(|ui|{ - ui.set_min_width(700.0); - - ui.vertical_centered(|ui|{ - ui.add_space(15.0); - ui.horizontal(|ui|{ - ui.add_space(190.0); //居中,我实在想不到其他好办法了,高度bug卡着 - if ui.link(egui::RichText::new("扫码登录").size(18.0)).clicked() { - app.login_method = "扫码登录".to_string(); - - } - ui.add(egui::Label::new(egui::RichText::new("|").size(18.0))); - if ui.link(egui::RichText::new("密码登录").size(18.0)).clicked() { - app.login_method = String::from("密码登录"); - - } - ui.add(egui::Label::new(egui::RichText::new("|").size(18.0))); - if ui.link(egui::RichText::new("短信登录").size(18.0)).clicked() { - app.login_method = String::from("短信登录"); - - } - ui.add(egui::Label::new(egui::RichText::new("|").size(18.0))); - if ui.link(egui::RichText::new("ck登录").size(18.0)).clicked() { - app.login_method = String::from("ck登录"); - - } - - - }); - ui.vertical_centered(|ui|{ - - - match login_method .as_str(){ - "扫码登录" =>{ - ui.add_space(20.0); - ui_qrcode_login(ui, app); - } - "密码登录" =>{ - ui.add_space(40.0); - ui_password_login(ui, app); - } - "短信登录" =>{ - ui.add_space(40.0); - ui_sms_login(ui, app); - } - "ck登录" =>{ - ui.add_space(40.0); - ui_ck_login(ui, app); - } - _ => unreachable!(), - } - }); - //疑似控制高度的?勿动否则高度bug - ui.vertical(|ui|{ - ui.set_min_width(300.0); - - }); - ui.add_space(10.0); - - - //控制窗口大小 - //我也不知道是什么原因,只要竖直不addspace高度为300,窗口就会以最小显示, - //设置窗口最小尺寸也没有用 - ui.add_space(200.0); - }) - }); - // 获取窗口大小和位置信息 - let window_rect = ui.min_rect(); - let scale_factor = 0.8; //缩放比例,按80%缩小 - // 绘制左下角图片 - if let Some(texture) =&app.login_texture.left_conrner_texture { - let image_size = texture.size_vec2(); - let scaled_size = image_size * scale_factor; - let image_pos = egui::pos2( - window_rect.min.x , // 左边距离窗口左边10像素 - window_rect.max.y - scaled_size.y // 底部距离窗口底部10像素 - ); - let image_rect = egui::Rect::from_min_size(image_pos, scaled_size); - ui.painter().image( - texture.id(), - image_rect, - egui::Rect::from_min_max([0.0, 0.0].into(), [1.0, 1.0].into()), - egui::Color32::WHITE - ); - } - - // 绘制右下角图片 - if let Some(texture) = &app.login_texture.right_conrner_texture { - let image_size = texture.size_vec2(); - let scaled_size = image_size * scale_factor; - let image_pos = egui::pos2( - window_rect.max.x - scaled_size.x , // 右边距离窗口右边10像素 - window_rect.max.y - scaled_size.y // 底部距离窗口底部10像素 - ); - let image_rect = egui::Rect::from_min_size(image_pos, scaled_size); - ui.painter().image( - texture.id(), - image_rect, - egui::Rect::from_min_max([0.0, 0.0].into(), [1.0, 1.0].into()), - egui::Color32::WHITE - ); - } - }); - - app.show_login_windows = window_open; -} - -fn ui_qrcode_login(ui: &mut egui::Ui, app: &mut Myapp) { - let mut should_refresh = app.login_qrcode_url.is_none(); - - //刷新按钮 - let button = egui::Button::new( - egui::RichText::new("刷新二维码").size(15.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(150.0,40.0)) - .fill(egui::Color32::from_rgb(102,204,255)) - .rounding(15.0);//圆角成度 - let response = ui.add(button); - if response.clicked(){ - //取消轮询任务 - if let Some(task_id) = &app.qrcode_polling_task_id { - app.task_manager.cancel_task(task_id); - app.qrcode_polling_task_id = None; - } - should_refresh = true; - } - - if should_refresh{ - match common::login::qrcode_login(&app.client){ - Ok(code) =>{ - let login_string = format!("https://account.bilibili.com/h5/account-h5/auth/scan-web?navhide=1&callback=close&qrcode_key={}&from=main-fe-header",code); - if let Some(texture) = create_qrcode(ui.ctx(), &login_string) { - app.login_qrcode_url = Some(login_string.clone()); - ui.vertical_centered(|ui|{ - ui.add_space(10.0); - ui.image(&texture); - }); - - // 创建新的轮询任务 - let qrcode_req = common::taskmanager::QrCodeLoginRequest { - qrcode_key: code, - qrcode_url: app.login_qrcode_url.clone().unwrap(), - user_agent: Some(app.custom_config.custom_ua.clone()), - }; - - // 提交任务到任务管理器 - let request = common::taskmanager::TaskRequest::QrCodeLoginRequest(qrcode_req); - match app.task_manager.submit_task(request) { - Ok(task_id) => { - app.qrcode_polling_task_id = Some(task_id); - log::info!("开始轮询二维码登录状态..."); - }, - Err(e) => { - log::error!("提交二维码轮询任务失败: {}", e); - } - } - - }} - Err(e) => { - eprintln!("获取二维码失败,原因: {}", e); - return; - } - - }} - else{ - if let Some(texture) = create_qrcode(ui.ctx(), &app.login_qrcode_url.as_ref().unwrap()) { - - ui.vertical_centered(|ui|{ - ui.add_space(20.0); - ui.image(&texture); - }); -}} -} - -fn ui_password_login(ui: &mut egui::Ui, app: &mut Myapp) { - - - ui.vertical_centered(|ui|{ - common_input(ui, "账号", &mut app.login_input.account, "请输入账号", true); - ui.add_space(10.0); - common_input(ui, "密码", &mut app.login_input.password, "请输入密码", true); - ui.add_space(20.0); - let button = egui::Button::new( - egui::RichText::new("登录").size(15.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(200.0,40.0)) - .fill(egui::Color32::from_rgb(0,174,236)) - .rounding(15.0);//圆角成度 - let response = ui.add(button); - if response.clicked(){ - match password_login(&app.login_input.account, &app.login_input.password){ - Ok(log) => { - log::info!("{}", log); - } - Err(e) => { - log::error!("密码登录时出错!请尝试使用其他登陆方式{}", e); - - } - } - } - - - - }); -} - -fn ui_sms_login(ui: &mut egui::Ui, app: &mut Myapp) { - - - ui.vertical_centered(|ui|{ - //phone的要传入app,参数从里面获得 - phone_input(ui, "手机号", app, "请输入手机号", true); - app.show_log_window = true; - ui.add_space(10.0); - common_input(ui, "验证码", &mut app.login_input.sms_code, "请输入验证码", true); - ui.add_space(20.0); - let button = egui::Button::new( - egui::RichText::new("登录").size(15.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(200.0,40.0)) - .fill(egui::Color32::from_rgb(0,174,236)) - .rounding(15.0);//圆角成度 - let response = ui.add(button); - if response.clicked(){ - let request = common::taskmanager::SubmitLoginSmsRequest{ - phone: app.login_input.phone.clone(), - code: app.login_input.sms_code.clone(), - captcha_key: app.sms_captcha_key.clone(), - client: app.client.clone(), - }; - let request = common::taskmanager::TaskRequest::SubmitLoginSmsRequest(request); - match app.task_manager.submit_task(request) { - Ok(task_id) => { - app.pending_sms_task_id = Some(task_id); - log::info!("短信验证码登录中..."); - }, - Err(e) => { - log::error!("提交短信登录任务失败: {}", e); - } - } - } - - }); -} - -fn ui_ck_login(ui: &mut egui::Ui , app: &mut Myapp) { - - ui.vertical_centered(|ui|{ - common_input(ui, "请输入ck", &mut app.login_input.cookie, "请输入ck,不知道不要填写", - false); - - ui.add_space(20.0); - let button = egui::Button::new( - egui::RichText::new("登录").size(15.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(200.0,40.0)) - .fill(egui::Color32::from_rgb(0,174,236)) - .rounding(15.0);//圆角成度 - let response = ui.add(button); - if response.clicked(){ - app.cookie_login = Some(app.login_input.cookie.clone()); - } - }); - - - - -} - -pub fn common_input( - ui: &mut egui::Ui, - title: &str, - text: &mut String, - hint: &str, - open_filter: bool, - - -) -> bool{ - ui.label( - egui::RichText::new(title) - .size(15.0) - .color(egui::Color32::from_rgb(0,0,0)) - - - ); - ui.add_space(8.0); - let input = egui::TextEdit::singleline( text) - .hint_text(hint)//提示 - .desired_rows(1)//限制1行 - .min_size(egui::vec2(120.0, 35.0)); - - - let response = ui.add(input); - if response.changed(){ - if open_filter{ - *text = text.chars()//过滤非法字符 - .filter(|c| c.is_ascii_alphanumeric() || *c == '@' || *c == '.' || *c == '-' || *c == '_') - .collect(); - } - else{ - *text = text.chars()//过滤非法字符 - .collect(); - }; - - } - response.changed() - -} - -pub fn phone_input( - ui: &mut egui::Ui, - title: &str, - app: &mut Myapp, - hint: &str, - open_filter: bool, - - - -) -> bool{ - let ua = &app.default_ua; - let local_captcha = app.local_captcha.clone(); - let custom_config = app.custom_config.clone(); - let client = - ui.label( - egui::RichText::new(title) - .size(15.0) - .color(egui::Color32::from_rgb(0,0,0)) - - - ); - ui.add_space(8.0); - let input = egui::TextEdit::singleline(&mut app.login_input.phone) - .hint_text(hint)//提示 - .desired_rows(1)//限制1行 - .min_size(egui::vec2(120.0, 25.0)); - - - let response = ui.add(input); - if response.changed(){ - if open_filter{ - app.login_input.phone = app.login_input.phone.chars()//过滤非法字符 - .filter(|c| c.is_ascii_alphanumeric() || *c == '@' || *c == '.' || *c == '-' || *c == '_') - .collect(); - } - else{ - app.login_input.phone = app.login_input.phone.chars()//过滤非法字符 - .collect(); - }; - - } - if ui.link(egui::RichText::new("发送短信").size(12.0)).clicked() { - log::info!("{}", app.login_input.phone); - let sms_req = common::taskmanager::LoginSmsRequest { - phone: app.login_input.phone.clone(), - client: app.client.clone(), - custom_config: custom_config.clone(), - local_captcha: local_captcha.clone(), - }; - let request = common::taskmanager::TaskRequest::LoginSmsRequest(sms_req); - match app.task_manager.submit_task(request) { - Ok(task_id) => { - app.pending_sms_task_id = Some(task_id); - log::info!("短信验证码发送中..."); - //app.show_toast("短信发送中", "请稍候", 3.0); - }, - Err(e) => { - log::error!("提交短信任务失败: {}", e); - //app.show_toast("发送失败", &format!("错误: {}", e), 3.0); - } - } - - - } - response.changed() - -} -/// 生成二维码 -pub fn create_qrcode(ctx: &egui::Context, content: &str) -> Option { - // 创建二维码 - let qr_bcode = QrCode::new(content.as_bytes()).unwrap(); - let qr_image = qr_bcode.render::>().build(); - - // 将图像转换为ColorImage - let width = qr_image.width() as usize; - let height = qr_image.height() as usize; - let mut color_image = egui::ColorImage::new([width, height], egui::Color32::WHITE); - - // 复制像素数据 - for y in 0..height { - for x in 0..width { - let pixel = qr_image.get_pixel(x as u32, y as u32); - - let color = if pixel[0] == 0 { - egui::Color32::BLACK - } else { - egui::Color32::WHITE - }; - color_image.pixels[y * width + x] = color; - } - } - - Some(ctx.load_texture( - "qrcode", - color_image, - egui::TextureOptions::default() - )) -} - -// Base64图像加载函数 -fn load_image_from_base64(ctx: &egui::Context, name: &str, base64_data: &str) -> Option { - // 尝试解码Base64字符串 - let base64_decoded = match base64::decode(base64_data) { - Ok(data) => data, - Err(err) => { - eprintln!("Failed to decode base64 image: {}", err); - return None; - } - }; - - // 创建图像对象 - let image = match image::load_from_memory(&base64_decoded) { - Ok(img) => img, - Err(err) => { - eprintln!("Failed to load image from memory: {}", err); - return None; - } - }; - - // 将图像转换为RGBA格式 - let image_buffer = image.to_rgba8(); - let size = [image_buffer.width() as _, image_buffer.height() as _]; - let pixels = image_buffer.into_vec(); - - // 创建egui颜色图像 - let color_image = egui::ColorImage::from_rgba_unmultiplied(size, &pixels); - - // 创建并返回纹理句柄 - Some(ctx.load_texture( - name, - color_image, - egui::TextureOptions::default() - )) -} - -fn get_left_base64() -> &'static str{ - "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" -} - -fn get_right_base64() -> &'static str{ - "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" -} \ No newline at end of file diff --git a/frontend/src/windows/mod.rs b/frontend/src/windows/mod.rs deleted file mode 100644 index b4b2062..0000000 --- a/frontend/src/windows/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod log_windows; -pub mod login_windows; -pub mod login_selenium; -pub mod add_buyer; -pub mod show_orderlist; -pub mod screen_info; -pub mod confirm_ticket; -pub mod confirm_ticket2; -pub mod show_qrcode; \ No newline at end of file diff --git a/frontend/src/windows/screen_info.rs b/frontend/src/windows/screen_info.rs deleted file mode 100644 index 2fc1584..0000000 --- a/frontend/src/windows/screen_info.rs +++ /dev/null @@ -1,237 +0,0 @@ -use crate::app::Myapp; -use chrono::TimeZone; -use eframe::egui; -use serde_json::Value; - -pub fn show(app: &mut Myapp, ctx: &egui::Context, uid: i64) { - let bilibili_ticket = app - .bilibiliticket_list - .iter_mut() - .find(|ticket| ticket.uid == uid) - .unwrap(); - let mut window_open = app.show_screen_info.is_some(); - - let ticket_data = match bilibili_ticket.project_info.clone() { - Some(ticket) => { - app.is_loading = false; - ticket - } - None => { - app.is_loading = true; - return; - } - }; - //默认选择第一个场次(如果尚未选择) - if app.selected_screen_index.is_none() && !ticket_data.screen_list.is_empty() { - app.selected_screen_index = Some(0); - } - bilibili_ticket.id_bind = ticket_data.id_bind as usize; - egui::Window::new("项目详情") - .open(&mut window_open) - .default_height(600.0) - .default_width(800.0) - .resizable(true) - .show(ctx, |ui| { - egui::ScrollArea::vertical().show(ui, |ui| { - // 项目标题区 - ui.vertical_centered(|ui| { - ui.heading(&ticket_data.name); - ui.add_space(5.0); - - /* // 活动时间和地点 - if let Some(venue_info) = &ticket_data.venue_info { - ui.label(format!("{} | {}", ticket_data.project_label, venue_info.name)); - } */ - ui.label(format!("状态: {}", ticket_data.sale_flag)); - ui.add_space(10.0); - }); - - ui.separator(); - - // 场次选择区 - ui.heading("选择场次"); - ui.add_space(5.0); - - // 场次选择栏 - egui::ScrollArea::horizontal().show(ui, |ui| { - ui.horizontal_wrapped(|ui| { - for (idx, screen) in ticket_data.screen_list.iter().enumerate() { - let is_selected = app.selected_screen_index == Some(idx); - - let btn = ui.add( - egui::SelectableLabel::new( - is_selected, - format!("{} ({})", - screen.name, - &screen.sale_flag.display_name - ) - ) - - ); - - if btn.clicked() { - app.selected_screen_index = Some(idx); - } - } - }); - }); - - ui.add_space(10.0); - - // 显示选中场次的票种信息 - if let Some(idx) = app.selected_screen_index { - if idx < ticket_data.screen_list.len() { - let selected_screen = &ticket_data.screen_list[idx]; - // 场次信息卡片 - let bg_color=if !ctx.style().visuals.dark_mode { - egui::Color32::from_rgb(245, 245, 250) - } else { - egui::Color32::from_rgb(6,6,6) - }; - egui::Frame::none() - .fill(bg_color) - .rounding(8.0) - .inner_margin(10.0) - .outer_margin(10.0) - .show(ui, |ui| { - // 场次基本信息 - ui.label(format!("开始时间: {}", format_timestamp(selected_screen.start_time))); - ui.label(format!("售票开始: {}", format_timestamp(selected_screen.sale_start))); - ui.label(format!("售票结束: {}", format_timestamp(selected_screen.sale_end))); - ui.label(format!("售票状态: {}", selected_screen.sale_flag.display_name)); - - ui.add_space(8.0); - ui.separator(); - ui.add_space(8.0); - - ui.heading("票种列表"); - - // 票种表格头 - ui.horizontal(|ui| { - ui.label(egui::RichText::new("票种名称").strong()); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label(egui::RichText::new("操作").strong()); - ui.add_space(70.0); - ui.label(egui::RichText::new("状态").strong()); - ui.add_space(70.0); - ui.label(egui::RichText::new("价格").strong()); - }); - }); - - ui.separator(); - - // 票种列表 - for ticket in &selected_screen.ticket_list { - ui.add_space(5.0); - ui.horizontal(|ui| { - ui.label(&ticket.desc); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let button_text = if ticket.clickable { "选择" }else if ticket.sale_flag_number ==1 {"定时预选"} else { "不可选" }; - let button_enabled = true/* ticket.clickable */; - - if ui.add_enabled( - button_enabled, - egui::Button::new(button_text) - ).clicked() { - // 使用正确的类型赋值 - if !ticket.clickable{ - log::error!("请注意!该票种目前不可售!但是会尝试下单,如果该票持续不可售,多次下单不可售票种可能会被b站拉黑") - } - app.selected_screen_id = Some(selected_screen.id as i64); - app.selected_ticket_id = Some(ticket.id as i64); - app.show_screen_info = None; - bilibili_ticket.screen_id = selected_screen.id.to_string(); - log::debug!("{}, {} , {}",selected_screen.id,ticket.id,ticket.project_id); - - - // 将选中的票种ID保存到项目ID中,准备抢票 - app.ticket_id = ticket.project_id.to_string(); - bilibili_ticket.select_ticket_id = Some(ticket.id.to_string()); - - app.confirm_ticket_info= Some(bilibili_ticket.uid.to_string().clone()); - log::info!("已选择: {} [{}]", &ticket.desc, ticket.id); - } - - ui.add_space(20.0); - ui.label(&ticket.sale_flag.display_name); - ui.add_space(20.0); - - // 票价格式化为元 - let price = format!("¥{:.2}", ticket.price as f64 / 100.0); - ui.label(egui::RichText::new(price) - .strong() - .color(egui::Color32::from_rgb(245, 108, 108))); - }); - }); - ui.separator(); - } - }); - } - } - - // 项目详细信息区 - ui.add_space(10.0); - ui.collapsing("查看详细信息", |ui| { - ui.label("基本信息:"); - ui.indent("basic_info", |ui| { - ui.label(format!("项目ID: {}", ticket_data.id)); - - // 检查performance_desc是否存在,并显示基础信息 - if let Some(desc) = &ticket_data.performance_desc { - for item in &desc.list { - if item.module == "base_info" { - if let Some(array) = item.details.as_array() { - for info_item in array { - if let (Some(title), Some(content)) = ( - info_item.get("title").and_then(Value::as_str), - info_item.get("content").and_then(Value::as_str) - ) { - ui.horizontal(|ui| { - ui.label(egui::RichText::new(format!("{}:", title)).strong()); - ui.label(content); - }); - } - } - } - } - } - } - - - }); - }); - }); - - // 底部按钮 - ui.separator(); - /* ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { - if ui.button("关闭窗口").clicked() { - app.show_screen_info = None; - } - }); */ - }); - if !window_open { - app.show_screen_info = None; - bilibili_ticket.project_info = None; - } -} - -// 将时间戳转换为可读时间 -// 将时间戳转换为可读时间 (接受usize类型) -fn format_timestamp(timestamp: usize) -> String { - if timestamp <= 0 { - return "未设置".to_string(); - } - - // 安全地将usize转为i64 - let timestamp_i64 = match i64::try_from(timestamp) { - Ok(ts) => ts, - Err(_) => return "时间戳溢出".to_string(), // 处理极端情况 - }; - - match chrono::Local.timestamp_opt(timestamp_i64, 0) { - chrono::LocalResult::Single(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(), - _ => "无效时间".to_string(), - } -} diff --git a/frontend/src/windows/show_orderlist.rs b/frontend/src/windows/show_orderlist.rs deleted file mode 100644 index a2cc7a8..0000000 --- a/frontend/src/windows/show_orderlist.rs +++ /dev/null @@ -1,255 +0,0 @@ -use crate::app::{Myapp, OrderData}; -use eframe::egui::{self, RichText}; -use egui::{Image, TextureHandle}; -use std::sync::Arc; -use serde::{Deserialize, Serialize}; -use common::{cookie_manager::CookieManager, utils::load_texture_from_url}; - -pub fn show( - app: &mut Myapp, - ctx: &egui::Context, - -){ - let mut window_open = app.show_orderlist_window.is_some(); - - let orders_data = match &app.total_order_data { - Some(data) => {app.is_loading = false; data.clone()}, - None => {app.is_loading = true; return;}, - }; - - - // 显示窗口和订单数据 - egui::Window::new("订单列表") - .open(&mut window_open) - .default_height(600.0) - .default_width(800.0) - .resizable(true) - .show(ctx, |ui| { - ui.vertical_centered(|ui| { - ui.label(RichText::new("订单列表") - .size(20.0) - .color(egui::Color32::from_rgb(0, 0, 0)) - .strong() - ); - }); - - // 添加滚动区域 - egui::ScrollArea::vertical().show(ui, |ui| { - // 使用从内存中获取的orders_data - if let Some(order_data) = &orders_data.data { - // 显示订单数据 - for order in &order_data.data.list { - ui.add_space(12.0); - - egui::Frame::none() - .fill(ui.style().visuals.widgets.noninteractive.bg_fill) - .rounding(8.0) - .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(220, 220, 220))) - .shadow(egui::epaint::Shadow { - extrusion: 2.0, - color: egui::Color32::from_black_alpha(20), - }) - .inner_margin(egui::vec2(12.0, 12.0)) - .show(ui, |ui| { - ui.horizontal(|ui| { - // 图片处理 - let image_size = egui::vec2(80.0, 80.0); - // 处理URL格式:如果以//开头,添加https:前缀 - let image_url = if order.img.url.starts_with("//") { - format!("https:{}", order.img.url) - } else { - order.img.url.clone() - }; - - // 图片加载逻辑 - ui.add_sized(image_size, |ui: &mut egui::Ui| { - if let Some(texture) = get_image_texture(ctx, &image_url) { - ui.centered_and_justified(|ui| { - ui.add(Image::new(&texture).fit_to_exact_size(image_size)) - }).inner - } else { - let inner_response = ui.centered_and_justified(|ui| { - ui.label("图片加载中...") - }); - // log::debug!("开始加载图片: {}", image_url); - request_image_async(ctx.clone(), app,image_url); - inner_response.inner - } - }); - - ui.add_space(12.0); - - // 订单信息区域 - ui.vertical(|ui| { - ui.horizontal(|ui| { - // 活动名称 - ui.label(RichText::new(&order.item_info.name).size(16.0).strong()); - - // 订单状态 - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - // 根据订单状态设置不同颜色 - let status_color = match order.status { - 2 => egui::Color32::from_rgb(0, 150, 0), // 已完成/已付款 - 4 => egui::Color32::from_rgb(200, 80, 0), // 已取消 - _ => egui::Color32::from_rgb(100, 100, 100), - }; - ui.label(RichText::new(&order.sub_status_name) - .color(status_color) - .strong()); - }); - }); - - ui.add_space(4.0); - - // 订单详细信息 - ui.horizontal(|ui| { - ui.label(RichText::new("订单号:").color(egui::Color32::GRAY)); - ui.monospace(&order.order_id); - }); - - ui.horizontal(|ui| { - ui.label(RichText::new("场次:").color(egui::Color32::GRAY)); - ui.label(&order.item_info.screen_name); - }); - - ui.horizontal(|ui| { - ui.label(RichText::new("下单时间:").color(egui::Color32::GRAY)); - ui.label(&order.ctime); - }); - - ui.horizontal(|ui| { - ui.label(RichText::new("价格:").color(egui::Color32::GRAY)); - // 将分转换为元并格式化为价格 - let price_text = format!("¥{:.2}", order.pay_money as f64 / 100.0); - ui.label(RichText::new(price_text).strong()); - - // 显示支付方式(如果已支付) - let pay_channel = match order.pay_channel { - Some(ref channel) => channel.clone(), - None => "".to_string(), - }; - if !pay_channel.is_empty() { - ui.add_space(8.0); - ui.label(format!("(支付方式:{})", pay_channel)); - } - - // 操作按钮放在右侧 - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - // 根据订单状态决定是否显示不同按钮 - if order.status == 2 { // 已完成 - /* let button = egui::Button::new( - egui::RichText::new("查看详情").size(16.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(100.0, 36.0)) - .fill(egui::Color32::from_rgb(102, 204, 255)) - .rounding(18.0); - - let response = ui.add(button); - if response.clicked() { - log::debug!("查看订单详情: {}", order.order_id); - // 处理点击事件 - } */ - } else if order.status == 1 && order.sub_status == 1 { // 待付款 - let pay_button = egui::Button::new( - egui::RichText::new("未支付").size(16.0).color(egui::Color32::WHITE) - ) - .min_size(egui::vec2(80.0, 36.0)) - .fill(egui::Color32::from_rgb(250, 100, 0)) - .rounding(18.0); - - if ui.add(pay_button).clicked() { - log::info!("暂不支持支付订单: {}", order.order_id); - // 添加支付逻辑 - } - } - }); - }); - }); - }); - }); - } - - // 如果没有订单 - if order_data.data.list.is_empty() { - ui.vertical_centered(|ui| { - ui.add_space(50.0); - ui.label(RichText::new("暂无订单记录").size(16.0).color(egui::Color32::GRAY)); - }); - } - } else { - // 显示加载中状态 - ui.vertical_centered(|ui| { - ui.add_space(50.0); - ui.label(RichText::new("加载中...").size(16.0).color(egui::Color32::GRAY)); - }); - } - - ui.add_space(10.0); // 底部留白 - }); - - }); - - if !window_open { - app.show_orderlist_window = None; - app.orderlist_requesting = false; - app.orderlist_need_reload = true; - } -} - -// 辅助函数:从缓存获取图片纹理 -fn get_image_texture(ctx: &egui::Context, url: &str) -> Option { - - ctx.memory(|mem| { - // log::debug!("{:?}", mem.data); - mem.data.get_temp::(egui::Id::new(url)) - }) -} - -// 辅助函数:异步请求图片 -fn request_image_async(ctx: egui::Context,app:&Myapp,url: String) { - if ctx.memory(|mem| mem.data.get_temp::(egui::Id::new(&url)).is_some()){ - log::error!("图片已存在: {}", url); - return; - } - // 避免重复请求 - if ctx.memory(|mem| mem.data.get_temp::(egui::Id::new(format!("loading_{}", url))).is_some()) { - return; - } - - // 标记为正在加载 - log::debug!("<正在加载图片>: {}", url); - ctx.memory_mut(|mem| mem.data.insert_temp(egui::Id::new(format!("loading_{}", url)), true)); - - // 启动异步加载线程 - let app_client =match app.account_manager.accounts[0].cookie_manager.clone(){ - Some(client) => client, - None => { - log::error!("cookie_manager不存在"); - let rt = tokio::runtime::Runtime::new().unwrap(); - - Arc::new(rt.block_on(CookieManager::new("", None, 0))) - } - }; - let app_ua = app.default_ua.clone(); - - // 这里应该实现实际的图片加载逻辑 - // 示例: - std::thread::spawn(move || { - if let Some(texture)=load_texture_from_url(&ctx, app_client, &(url.clone()+"@74w_74h.jpeg"), &url){ - ctx.memory_mut(|mem| { - log::debug!("加载图片成功: {}", url); - mem.data.insert_temp(egui::Id::new(&url), texture); - mem.data.remove::(egui::Id::new(format!("loading_{}", url))); - }); - ctx.request_repaint(); - }else{ - ctx.memory_mut(|mem| { - log::warn!("加载图片失败_ui,retrying: {}", url); - mem.data.remove::(egui::Id::new(format!("loading_{}", url))); - }); - } - - }); - - -} diff --git a/frontend/src/windows/show_qrcode.rs b/frontend/src/windows/show_qrcode.rs deleted file mode 100644 index 0f9b6d4..0000000 --- a/frontend/src/windows/show_qrcode.rs +++ /dev/null @@ -1,34 +0,0 @@ -use eframe::egui::{self, RichText}; -use crate::app::Myapp; -use crate::windows::login_windows::create_qrcode; - -pub fn show(app: &mut Myapp, ctx: &egui::Context) { - - let mut window_open = app.show_qr_windows.is_some(); - let qr_data = app.show_qr_windows.clone().unwrap_or_default(); - - egui::Window::new("扫码支付") - .open(&mut window_open) - .resizable(false) - .default_size([700.0, 400.0]) - .show(ctx, |ui| { - - if let Some(texture) = create_qrcode(ui.ctx(), qr_data.as_str()) { - - ui.vertical_centered(|ui|{ - ui.add_space(20.0); - let rich_text= RichText::new("请使用 微信/支付宝 扫描二维码进行支付") - .size(20.0) - .color(egui::Color32::from_rgb(102, 204, 255)); - ui.label(rich_text); - ui.add_space(20.0); - ui.image(&texture); - }); - } - - }); - if !window_open { - app.show_qr_windows = None; - } - -} \ No newline at end of file diff --git a/resources/fonts/NotoSansSC-Regular.otf b/resources/fonts/NotoSansSC-Regular.otf deleted file mode 100644 index d350ffa..0000000 Binary files a/resources/fonts/NotoSansSC-Regular.otf and /dev/null differ diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..f216078 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +edition = "2024"