├── .github └── workflows │ └── build.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── backend ├── Cargo.toml └── src │ ├── api.rs │ ├── lib.rs │ ├── show_orderlist.rs │ └── taskmanager.rs ├── common ├── Cargo.toml └── src │ ├── account.rs │ ├── captcha.rs │ ├── cookie_manager.rs │ ├── gen_cp.rs │ ├── http_utils.rs │ ├── lib.rs │ ├── login.rs │ ├── machine_id.rs │ ├── push.rs │ ├── record_log.rs │ ├── show_orderlist.rs │ ├── taskmanager.rs │ ├── ticket.rs │ ├── utility.rs │ ├── utils.rs │ └── web_ck_obfuscated.rs ├── frontend ├── Cargo.toml ├── assets │ ├── background.jpg │ ├── background1.jpg │ ├── background2.jpg │ └── default_avatar.jpg └── src │ ├── app.rs │ ├── main.rs │ ├── main_old.rs │ ├── ui │ ├── error_banner.rs │ ├── fonts.rs │ ├── loading.rs │ ├── mod.rs │ ├── sidebar.rs │ └── tabs │ │ ├── account.rs │ │ ├── help.rs │ │ ├── home.rs │ │ ├── mod.rs │ │ ├── monitor.rs │ │ ├── project_list.rs │ │ └── settings.rs │ └── windows │ ├── add_buyer.rs │ ├── confirm_ticket.rs │ ├── confirm_ticket2.rs │ ├── grab_ticket.rs │ ├── log_windows.rs │ ├── login_selenium.rs │ ├── login_windows.rs │ ├── mod.rs │ ├── screen_info.rs │ ├── show_orderlist.rs │ └── show_qrcode.rs └── resources └── fonts └── NotoSansSC-Regular.otf /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | workflow_dispatch: 9 | 10 | pull_request: 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | os: [windows-latest, ubuntu-latest, macos-latest] 17 | debug: [debug, release] 18 | include: 19 | - os: windows-latest 20 | build: BiliTicketRush-${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}-Windows 21 | - os: ubuntu-latest 22 | build: BiliTicketRush-${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}-Linux 23 | - os: macos-latest 24 | build: BiliTicketRush-${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}-macOS 25 | runs-on: ${{ matrix.os }} 26 | env: 27 | debug: ${{ matrix.debug }} 28 | build_type: ${{ matrix.debug == 'release' && '正式版' || 'debug' }} 29 | CARGO_TERM_COLOR: always 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Print github.ref 33 | run: echo "${{ github.ref }}" 34 | - name: Build Release 35 | if: matrix.debug == 'release' 36 | run: cargo build --verbose --release 37 | - name: Build Debug 38 | if: matrix.debug == 'debug' 39 | run: cargo build --verbose 40 | - name: Copy artifacts on windows 41 | if: matrix.os == 'windows-latest' 42 | run: | 43 | mkdir -p target/output 44 | cp target/${{matrix.debug}}/frontend.exe target/output/${{ env.build_type }}-${{ matrix.build }}.exe 45 | cp target/${{matrix.debug}}/frontend.pdb target/output/${{ env.build_type }}-${{ matrix.build }}.pdb 46 | - name: Copy artifacts on other systems 47 | if: matrix.os != 'windows-latest' 48 | run: | 49 | mkdir -p target/output 50 | cp target/${{matrix.debug}}/frontend target/output/${{ env.build_type }}-${{ matrix.build }} 51 | - name: Upload Artifacts 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: ${{ env.build_type }}-${{ matrix.build }} 55 | path: target/output 56 | publish: 57 | needs: build 58 | if: startsWith(github.ref, 'refs/tags/') 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Print github.ref 62 | run: echo "${{ github.ref }}" 63 | - run: echo "publish running" 64 | 65 | - name: Download all workflow artifacts 66 | uses: actions/download-artifact@v4 67 | with: 68 | path: artifacts 69 | 70 | - name: List artifacts 71 | run: ls -la artifacts 72 | 73 | - name: Prepare release assets 74 | run: | 75 | mkdir release-assets 76 | find artifacts -type f -not -name "*.pdb" -exec cp {} release-assets/ \; 77 | 78 | - name: Create Release 79 | uses: softprops/action-gh-release@v2 80 | with: 81 | files: release-assets/* 82 | name: Release ${{ github.ref_name }} 83 | draft: false 84 | prerelease: false 85 | generate_release_notes: true 86 | env: 87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vscode/ 3 | /.idea/ 4 | *.sublime-* 5 | *.swp 6 | *~ 7 | /target/ 8 | **/*.rs.bk 9 | Cargo.lock 10 | *.pdb 11 | .DS_Store 12 | Thumbs.db 13 | *.tmp 14 | *.log 15 | /学习 16 | *.json 17 | *txt 18 | config.json 19 | config 20 | /Log/ 21 | /models/ 22 | /效果/ 23 | permissions 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "frontend", 4 | "backend", 5 | "common" 6 | ] 7 | 8 | [patch.crates-io] 9 | ort = { git="https://github.com/biliticket/ort" } 10 | ort-sys = { git = "https://github.com/biliticket/ort" } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BiliTicketRush 2 | **关键词**: CP31, 哔哩哔哩, 会员购, BW, 自动抢票, 脚本, 抢票 3 | 4 | > 一款简单易用的哔哩哔哩会员购自动抢票工具,基于Rust开发的跨平台GUI应用。 5 | 6 | BiliTicketRush 是专为B站会员购票务设计的抢票辅助软件,支持多种票务类型(CP、演唱会等)。通过定时抢票和捡漏功能,提高您获取心仪票务的机会。 7 | 8 | **重要声明:叔叔于近期对bw项目更新了检测手段(ctoken,ptoken),目前本软件对此没有头绪,因此!使用本软件会被!检测!b站肯定不是莫名其妙加的检测,检测后会怎么样完全!不!知!道!!使用本软件造成的一切后果自负!**‘ 9 | 10 | **重要声明:本项目仅供学习、研究与技术交流使用,严禁用于任何商业用途或违反中华人民共和国及相关地区法律法规的行为。请用户自觉遵守相关法律法规,合理使用本软件。请于下载后24小时内删除** 11 | 12 | **如本项目内容存在任何侵权、违规或不适宜公开之处,请相关权利人或监管部门通过 issue(议题) 或邮箱等与我们联系,我们将在收到通知后第一时间下架或删除相关内容。** 13 | 14 | 请注意,使用本项目即代表您同意以下条款: 15 | 16 | - 不得在国内社群明示本项目(包括biliticket其他项目)包括但不限于截图,链接等。 17 | - 我们的服务是尽最大限度提供的,若您因使用本程序造成的一切经济、法律后果由您个人承担 18 | - 不得使用本项目以任何形式牟利 19 | 20 | 若您违反上述条款,biliticket 有权终止对您及关联实体、账号继续提供服务。 21 | 22 | **本项目作者团队未以任何形式通过本项目获利!本软件完全开源免费!!如果您是从黄牛处购买的本软件,那么您上当了!** 23 | 24 | biliticket有权随时修改本条款,请及时关注本页更新 25 | 26 | 27 | ## 导航 28 | 29 | [功能特点](#功能特点) • [截图展示](#截图展示) • [安装指南](#安装指南) • [使用教程](#使用教程) • [常见问题](#常见问题) • [参与贡献](#参与贡献) • [许可证](#许可证) 30 | 31 | > **注意**:本项目仅供学习研究使用,请合理使用,尊重官方规则。 32 | 33 | ## 欢迎各位大佬为本项目建言献策以及提出PR 34 | 35 | ## 功能特点 36 | 37 | ✅ **用户友好的图形界面**:操作简单直观,易于上手 38 | ✅ **多账号管理**:支持多B站账号同时登录和管理 39 | ✅ **多种抢票模式**: 40 | - 定时抢票:在票务开售时精确抢票 41 | - 捡漏模式:持续监控回流票并自动抢购 42 | ✅ **购票人管理**:支持添加和管理多个购票人信息 43 | ✅ **实名/非实名票**:同时支持实名制和非实名制票务 44 | ✅ **风控处理**:内置验证码识别和风控处理机制 45 | ✅ **订单管理**:查看和管理抢票成功的订单 46 | ✅ **高性能**:基于Rust语言开发,性能优异 47 | ✅ **通知提醒**:抢票成功后多渠道推送通知 48 | 49 | ## 截图展示 50 | 51 | <!-- 这里可以添加应用截图 --> 52 | 53 | ## 安装指南 54 | 55 | ### 方法一:直接下载预编译版本 56 | 57 | 1. 前往 [GitHub Releases](https://github.com/biliticket/bili_ticket_rush/releases) 页面 58 | 2. 下载适合您操作系统的最新版本 59 | 3. 确保电脑能连接到互联网 60 | 4. 运行应用程序 61 | 62 | ### 方法二:从源码编译 63 | 64 | 确保您已安装 Rust 环境,然后执行: 65 | 66 | ```bash 67 | # 克隆仓库 68 | git clone https://github.com/biliticket/bili_ticket_rush.git 69 | cd bili_ticket_rush 70 | 71 | # 编译并运行 72 | cargo build --release 73 | ./target/release/frontend.exe 74 | ``` 75 | 或直接使用 76 | ```bash 77 | cargo run 78 | ``` 79 | ## 使用教程 80 | 81 | ### 1. 登录账号 82 | - 启动应用程序后,点击左侧的"账号"标签 83 | - 选择"添加账号"并使用B站账号登录 84 | - 支持扫码登录或短信验证码登录两种方式 85 | 86 | ### 2. 添加购票人信息 87 | - 在成功登录后,点击"购票人管理" 88 | - 选择"添加购票人"填写实名信息 89 | - 您可以添加多个购票人,方便后续选择 90 | 91 | ### 3. 选择演出和票种 92 | - 在"主页"标签中输入对应的展览id 93 | - 选择自动模式,点击开始抢票进入详情页 94 | - 选择场次和票种(未开售项目点击最右边的“未开售按钮”即可) 95 | - 选择购票人后,点击页面右下角的“确定”按钮,即可看到倒计时(ps:选择购票人窗口可拖动,如显示不全可按住窗口往上拖即可看到按钮) 96 | 97 | ### 4. 设置抢票 98 | 99 | #### 定时抢票模式 100 | - 选择"定时抢票"模式 101 | - 系统会自动获取官方开票时间 102 | - 选择购票人 103 | - 点击"开始抢票" 104 | 105 | #### 捡漏模式 106 | - 选择"捡漏模式" 107 | - 选择购票人 108 | - 点击"开始捡漏" 109 | - 系统会持续监控所有场次和票种,有票即抢 110 | 111 | ### 5. 查看订单和支付 112 | - 抢票成功后会收到通知提醒,并展示付款二维码 113 | - 在"订单"标签中查看抢票成功的订单 114 | - 请注意支付时限,超时订单会自动取消 115 | 116 | ## 常见问题 117 | 118 | **Q: 支持哪些类型的B站票务?** 119 | A: 支持B站所有类型的票务,包括演唱会、展览、话剧等实名和非实名票务。 120 | 121 | **Q: 同一账号可以同时抢多个场次的票吗?** 122 | A: 可以,系统支持为同一账号创建多个抢票任务,分别针对不同场次或票种。 123 | 124 | **Q: 抢票过程中遇到验证码怎么办?** 125 | A: 软件内置了验证码处理机制,大部分情况下能自动处理。如果需要手动处理,系统会弹出提示。 126 | 127 | **Q: 在部分Windows 及Windows Server系统上使用提示缺少MSVCP140.dll怎么办?** 128 | A: 这是因为缺少Visual C++ 可再发行软件包。请根据系统类型下载安装: 129 | - 32位系统:下载并安装 [VC_redist.x86.exe](https://aka.ms/vs/17/release/vc_redist.x86.exe) 130 | - 64位系统:下载并安装 [VC_redist.x64.exe](https://aka.ms/vs/17/release/vc_redist.x64.exe) 131 | - 安装完成后请重启系统 132 | 133 | **Q: 安装了VC++运行库后程序仍然闪退或电脑没有显卡怎么办?** 134 | A: 这种情况需要安装Mesa3D软件渲染器,步骤如下: 135 | 1. 参考[这篇文章](https://zhuanlan.zhihu.com/p/666093183) 136 | 2. 访问[Mesa3D发布页](https://github.com/pal1000/mesa-dist-win/releases)下载最新的`mesa3d-x.y.z-release-msvc.7z`文件并解压 137 | 3. 运行解压后的`perappdeploy.cmd`,选择一个空文件夹作为安装路径,全程按'y'确认安装,请注意安装成功后会回到主界面,不要再输入路径重复安装,如果已经再次安装了,那就等他安装完吧( 138 | 4. 运行`systemwidedeploy.cmd`,输入数字'1',选择第一项,然后按回车键 139 | 5. 完成上述步骤后,重新打开应用程序即可正常运行 140 | 141 | **Q: 为什么我看不到日志信息?** 142 | A: 请确认是否开启了日志记录功能。可在设置中调整日志级别,开发模式下默认显示更详细的日志。 143 | 144 | 145 | ## 免责声明 146 | 147 | 1. **本软件仅供学习、研究与技术交流使用**,不得用于商业用途。 148 | 2. 使用本软件造成的任何问题由用户自行承担,与开发者无关。 149 | 3. 您应当遵守并了解所在国家或地区的法律法规,若在使用本软件时出现与相关法律法规冲突的行为,请立即停止使用。 150 | 4. 任何用户若利用本软件进行攻击、破坏计算机信息系统、非法获取他人信息或任何其他违法活动,本软件开发者概不负责,亦不提供任何帮助。 151 | 5. **如有任何内容涉嫌侵权、违反法律法规或不适宜公开,请及时通过 issue 或邮箱联系我们,我们将在收到通知后第一时间处理或下架相关内容。** 152 | 153 | 154 | ## 开发计划 155 | 156 | - 开发模式和release模式 自动判断日志显示等级 157 | - 未开售项目不支持直接抢票 158 | 159 | ## 参与贡献 160 | 161 | <!-- 可以添加贡献指南 --> 162 | 163 | ## 许可证 164 | 165 | <!-- 可以添加许可证信息 --> 166 | ## Star History 167 | 168 | <a href="https://star-history.com/#biliticket/bili_ticket_rush&Date"> 169 | <picture> 170 | <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=biliticket/bili_ticket_rush&type=Date&theme=dark" /> 171 | <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=biliticket/bili_ticket_rush&type=Date" /> 172 | <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=biliticket/bili_ticket_rush&type=Date" /> 173 | </picture> 174 | </a> 175 | -------------------------------------------------------------------------------- /backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "backend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | common = { path = "../common" } 8 | tokio = { version = "1", features = ["full"] } 9 | uuid = { version = "1.3", features = ["v4"] } 10 | chrono = "0.4" 11 | 12 | # log 13 | log = "0.4" 14 | env_logger = "0.9" 15 | 16 | 17 | #json 18 | serde = { version = "1.0", features = ["derive"] } 19 | serde_json = "1.0" 20 | 21 | #reqwest 22 | reqwest = { version="0.11.22", features=["json", "blocking"]} 23 | 24 | #rand 25 | rand = "0.8" 26 | 27 | base64 = "0.22" -------------------------------------------------------------------------------- /backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod taskmanager; 2 | pub mod api; 3 | pub mod show_orderlist; -------------------------------------------------------------------------------- /backend/src/show_orderlist.rs: -------------------------------------------------------------------------------- 1 | use common::{cookie_manager::CookieManager, http_utils::request_get}; 2 | use serde_json; 3 | use std::sync::Arc; 4 | use common::show_orderlist::{*}; 5 | 6 | 7 | pub async fn get_orderlist(cookie_manager :Arc<CookieManager>) -> Result<OrderResponse, String>{ 8 | match cookie_manager.get( 9 | 10 | "https://show.bilibili.com/api/ticket/ordercenter/ticketList?page=0&page_size=10" 11 | ).await.send().await{ 12 | Ok(resp) =>{ 13 | if resp.status().is_success(){ 14 | 15 | 16 | match tokio::task::block_in_place(||{ 17 | let rt = tokio::runtime::Runtime::new().unwrap(); 18 | rt.block_on(resp.text()) 19 | }){ 20 | Ok(text) => { 21 | log::debug!("获取全部订单:{}",text); 22 | match serde_json::from_str::<OrderResponse>(&text){ 23 | Ok(order_resp) => { 24 | return Ok(order_resp); 25 | } 26 | Err(e) => {log::error!("获取全部订单json解析失败:{}",e); 27 | return Err(format!("获取全部订单json解析失败:{}",e))} 28 | 29 | } 30 | 31 | 32 | } 33 | Err(e) => { 34 | //log::error!("获取data失败: {}",e); 35 | return Err(format!("获取data失败: {}",e)) 36 | } 37 | } 38 | }else { 39 | // log::error!("获取订单不期待响应:{}", resp.status()); 40 | return Err(format!("获取订单不期待响应:{}", resp.status())) 41 | } 42 | } 43 | Err(err) => { 44 | //log::error!("请求失败: {}", err); 45 | return Err(err.to_string()); 46 | } 47 | }; 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | tokio = { version = "1", features = ["full"] } 8 | rand = "0.8" 9 | 10 | eframe = { version = "0.23.0", features = ["default"] } 11 | egui = "0.31.0" 12 | 13 | # log 14 | log = "0.4.27" 15 | env_logger = "0.9" 16 | chrono = "0.4" 17 | once_cell = "1.8" 18 | 19 | #json 20 | serde = { version = "1.0", features = ["derive"] } 21 | serde_json = "1.0" 22 | 23 | reqwest = { version="0.11.22", features=["json", "blocking", "cookies"]} 24 | 25 | base64 = "0.21" 26 | aes = "0.7.5" 27 | block-modes = "0.8.1" 28 | machine-uid = "0.5.3" 29 | lazy_static = "1.4" 30 | image = "0.25" 31 | 32 | whoami = "1.5.1" 33 | ctrlc = "3.4.0" 34 | 35 | #captcha 36 | bili_ticket_gt = { git = "https://github.com/Amorter/biliTicker_gt",branch = "rust"} 37 | 38 | md5 = "0.7.0" 39 | 40 | uuid = { version = "1.0", features = ["v4", "fast-rng"] } 41 | 42 | hmac = "0.12.1" 43 | sha2 = "0.10.7" 44 | hex = "0.4.3" 45 | cookie = "0.16" 46 | fs2 = "0.4.3" # 添加对fs2的依赖 47 | 48 | #Singleton 49 | single-instance = "0.3.3" -------------------------------------------------------------------------------- /common/src/account.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use reqwest::Client; 3 | use crate::{cookie_manager, http_utils::{request_get_sync,request_post_sync}}; 4 | use serde_json; 5 | use std::sync::Arc; 6 | use crate::cookie_manager::CookieManager; 7 | #[derive(Clone, Serialize, Deserialize)] 8 | pub struct Account{ 9 | pub uid: i64, //UID 10 | pub name: String, //昵称 11 | pub level: String, 12 | pub cookie: String, //cookie 13 | pub csrf : String, //csrf 14 | pub is_login: bool, //是否登录 15 | pub account_status: String, //账号状态 16 | pub vip_label: String, //大会员,对应/nav请求中data['vip_label']['text'] 17 | pub is_active: bool, //该账号是否启动抢票 18 | pub avatar_url: Option<String>, //头像地址 19 | #[serde(skip)] 20 | pub avatar_texture: Option<eframe::egui::TextureHandle>, //头像地址 21 | #[serde(skip)] 22 | pub cookie_manager: Option<Arc<CookieManager>>, //cookie管理器 23 | } 24 | impl std::fmt::Debug for Account{ 25 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 | f.debug_struct("Account") 27 | .field("uid", &self.uid) 28 | .field("name", &self.name) 29 | .field("level", &self.level) 30 | .field("cookie", &self.cookie) 31 | .field("csrf", &self.csrf) 32 | .field("is_login", &self.is_login) 33 | .field("account_status", &self.account_status) 34 | .field("vip_label", &self.vip_label) 35 | .field("is_active", &self.is_active) 36 | .field("avatar_url", &self.avatar_url) 37 | .field("avatar_texture", &"SKipped") 38 | .field("client", &self.cookie_manager) 39 | .finish() 40 | } 41 | } 42 | 43 | pub fn add_account(cookie: &str ,client: &Client, ua: &str) -> Result<Account, String>{ 44 | log::info!("添加账号: {}", cookie); 45 | let response = request_get_sync( 46 | client, 47 | "https://api.bilibili.com/x/web-interface/nav", 48 | Some(ua.to_string()), 49 | Some(cookie), 50 | ).map_err(|e| e.to_string())?; 51 | 52 | // 创建一个临时的运行时来执行异步代码 53 | let rt = tokio::runtime::Runtime::new().unwrap(); 54 | let json = rt.block_on(async { 55 | response.json::<serde_json::Value>().await 56 | }).map_err(|e| e.to_string())?; 57 | let cookie_manager = Arc::new(rt.block_on(async{ 58 | cookie_manager::CookieManager::new(cookie, Some(ua), 0).await 59 | })); 60 | log::debug!("获取账号信息: {:?}", json); 61 | match json.get("code") { 62 | Some(code) if code.as_i64() == Some(0) => {} // 成功 63 | _ => return Err("获取账号信息失败".to_string()), 64 | } 65 | if let Some(data) = json.get("data") { 66 | let mut account = Account { 67 | uid: data["mid"].as_i64().unwrap_or(0), 68 | name: data["uname"].as_str().unwrap_or("账号信息获取失败,请删除重新登录").to_string(), 69 | level: data["level_info"]["current_level"].as_i64().unwrap_or(0).to_string(), 70 | cookie: cookie_manager.get_all_cookies(), 71 | csrf: extract_csrf(cookie), 72 | is_login: true, 73 | account_status: "空闲".to_string(), 74 | vip_label: data["vip_label"]["text"].as_str().unwrap_or("").to_string(), 75 | is_active: true, 76 | avatar_url: Some(data["face"].as_str().unwrap_or("").to_string()), 77 | avatar_texture: None, 78 | cookie_manager: Some(cookie_manager), 79 | }; 80 | account.ensure_client(); 81 | Ok(account) 82 | } else { 83 | Err("无法获取用户信息".to_string()) 84 | } 85 | } 86 | 87 | pub fn signout_account(account: &Account) -> Result<bool, String> { 88 | let data = serde_json::json!({ 89 | "biliCSRF" : account.csrf, 90 | 91 | }); 92 | let rt = tokio::runtime::Runtime::new().unwrap(); 93 | let response = rt.block_on(async{ 94 | account.cookie_manager.clone().unwrap().post("https://passport.bilibili.com/login/exit/v2") 95 | .await 96 | .json(&data) 97 | .send() 98 | .await 99 | }); 100 | 101 | let resp = match response { 102 | Ok(res) => res, 103 | Err(e) => return Err(format!("请求失败: {}", e)), 104 | }; 105 | log::debug!("退出登录响应: {:?}",resp); 106 | Ok(resp.status().is_success()) 107 | 108 | } 109 | 110 | 111 | //提取 csrf 112 | fn extract_csrf(cookie: &str) -> String { 113 | // 打印原始cookie用于调试 114 | log::debug!("提取CSRF的原始cookie: {}", cookie); 115 | 116 | for part in cookie.split(';') { 117 | let part = part.trim(); 118 | // 检查是否以bili_jct开头(不区分大小写) 119 | if part.to_lowercase().starts_with("bili_jct=") { 120 | // 找到等号位置 121 | if let Some(pos) = part.find('=') { 122 | let value = &part[pos + 1..]; 123 | // 去除可能的引号 124 | let value = value.trim_matches('"').trim_matches('\''); 125 | log::debug!("成功提取CSRF值: {}", value); 126 | return value.to_string(); 127 | } 128 | } 129 | } 130 | 131 | // 没找到,记录并返回空字符串 132 | log::warn!("无法从cookie中提取CSRF值"); 133 | String::new() 134 | } 135 | impl Account { 136 | // 确保每个账号都有自己的 client 137 | pub fn ensure_client(&mut self) { 138 | let rt = tokio::runtime::Runtime::new().unwrap(); 139 | if self.cookie_manager.is_none() { 140 | rt.block_on(async{ 141 | self.cookie_manager = Some(Arc::new(CookieManager::new( 142 | &self.cookie, 143 | None, 144 | 0, 145 | ).await)) 146 | }); 147 | } 148 | } 149 | 150 | 151 | } 152 | 153 | // 创建client 154 | fn create_client_for_account(cookie: &str) -> reqwest::Client { 155 | use reqwest::header; 156 | 157 | 158 | let random_id = format!("{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().subsec_nanos()); 159 | 160 | 161 | let user_agent = format!( 162 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 {}", 163 | random_id 164 | ); 165 | 166 | let mut headers = header::HeaderMap::new(); 167 | headers.insert( 168 | header::USER_AGENT, 169 | header::HeaderValue::from_str(&user_agent).unwrap_or_else(|_| { 170 | // 提供一个替代值,而不是使用 unwrap_or_default() 171 | header::HeaderValue::from_static("Mozilla/5.0") 172 | }) 173 | ); 174 | 175 | // 创建 client 176 | reqwest::Client::builder() 177 | .default_headers(headers) 178 | .cookie_store(true) 179 | .build() 180 | .unwrap_or_else(|_| reqwest::Client::new()) 181 | } -------------------------------------------------------------------------------- /common/src/captcha.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use serde_json::json; 3 | use std::sync::{Arc, Mutex}; 4 | use bili_ticket_gt::click::Click; 5 | use bili_ticket_gt::slide::Slide; 6 | use crate::cookie_manager::CookieManager; 7 | use crate::{ ticket::TokenRiskParam, utility::CustomConfig}; 8 | 9 | #[derive(Clone)] 10 | pub struct LocalCaptcha{ 11 | click: Option<Arc<Mutex<Click>>>, 12 | slide: Option<Arc<Mutex<Slide>>>, 13 | } 14 | 15 | //Debug trait 16 | impl fmt::Debug for LocalCaptcha { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | f.debug_struct("LocalCaptcha") 19 | .field("click", &if self.click.is_some() { "Some(Click)" } else { "None" }) 20 | .field("slide", &if self.slide.is_some() { "Some(Slide)" } else { "None" }) 21 | .finish() 22 | } 23 | } 24 | 25 | impl LocalCaptcha { 26 | pub fn new() -> Self { 27 | LocalCaptcha { 28 | click: Some(Arc::new(Mutex::new(Click::default()))), //初始化点击对象 29 | slide: None, //暂时先不初始化滑块,疑似出现滑块概率极低 30 | } 31 | } 32 | } 33 | pub async fn captcha( 34 | custom_config: CustomConfig, 35 | gt: &str, 36 | challenge: &str, 37 | referer: &str, // referer(ttocr打码使用) 38 | captcha_type:usize, // 33对应三代点字 32对应三代滑块 39 | local_captcha: LocalCaptcha, //本地打码需要传入实例结构体 40 | ) 41 | -> Result<String, String> { 42 | // 0:本地打码 1:ttocr 43 | match custom_config.captcha_mode { 44 | 0 => { 45 | match captcha_type{ 46 | 32 => { 47 | let mut slide = match local_captcha.slide { 48 | Some(c) =>c, 49 | None => { 50 | return Err("本地打码需要传入slide对象".to_string()); 51 | } 52 | }; 53 | Err("本地打码暂不支持三代滑块".to_string()) 54 | } 55 | 33 => { //三代点字 56 | let click_mutex = match &local_captcha.click { 57 | Some(c) =>Arc::clone(c), 58 | None => { 59 | return Err("本地打码需要传入click对象".to_string()); 60 | } 61 | }; 62 | let gt_clone = gt.to_string(); 63 | let challenge_clone = challenge.to_string(); 64 | let validate = tokio::task::spawn_blocking(move || { 65 | let mut click = click_mutex.lock().unwrap(); 66 | click.simple_match_retry(>_clone, &challenge_clone) 67 | }).await 68 | .map_err(|e| format!("任务执行出错:{}",e))? 69 | .map_err(|e| format!("验证码模块出错:{}",e))?; 70 | 71 | 72 | 73 | log::info!("验证码识别结果: {:?}", validate); 74 | Ok(serde_json::to_string(&json!({ 75 | "challenge": challenge, 76 | "validate": validate, 77 | "seccode": format!("{}|jordan", validate), 78 | })).map_err(|e| format!("序列化JSON失败: {}", e))?) 79 | 80 | 81 | } 82 | _ => { 83 | return Err("无效的验证码类型".to_string()); 84 | } 85 | } 86 | }, 87 | 1 => { 88 | // ttocr 89 | let client = reqwest::Client::builder() 90 | .danger_accept_invalid_certs(true) // 禁用证书验证 91 | .build() 92 | .map_err(|e| format!("创建HTTP客户端失败: {}", e))?; 93 | let form_data = json!({ 94 | "appkey": custom_config.ttocr_key, 95 | "gt": gt, 96 | "challenge": challenge, 97 | "itemid": captcha_type,//33对应三代点字 32对应三代滑块 98 | "referer": referer, 99 | }); 100 | log::info!("验证码请求参数: {:?}", form_data); 101 | let response = client.post("http://api.ttocr.com/api/recognize") 102 | .json(&form_data) 103 | .send() 104 | .await 105 | .map_err(|e| format!("发送请求失败: {}", e))?; 106 | log::info!("验证码请求响应: {:?}", response); 107 | let text = response.text() 108 | .await 109 | .map_err(|e| format!("读取响应内容失败: {}", e))?; 110 | 111 | // 打印文本内容 112 | log::info!("响应内容: {}", text); 113 | let json_response: serde_json::Value = serde_json::from_str(&text) 114 | .map_err(|e| format!("解析JSON失败: {}", e))?; 115 | 116 | if json_response["status"].as_i64() == Some(1){ 117 | log::info!("验证码提交识别成功"); 118 | } 119 | else{ 120 | log::error!("验证码提交识别失败: {}", json_response["msg"].as_str().unwrap_or("未知错误")); 121 | return Err("验证码提交识别失败".to_string()); 122 | } 123 | let result_id = json_response["resultid"].as_str().unwrap_or(""); 124 | for _ in 0..20{ 125 | let response = client.post("http://api.ttocr.com/api/results") 126 | .json(&json!({ 127 | "appkey": custom_config.ttocr_key, 128 | "resultid": result_id, 129 | })) 130 | .send() 131 | .await 132 | .map_err(|e| format!("发送请求失败: {}", e))?; 133 | let text = response.text() 134 | .await 135 | .map_err(|e| format!("读取响应内容失败: {}", e))?; 136 | 137 | // 打印文本内容 138 | log::info!("响应内容: {}", text); 139 | let json_response: serde_json::Value = serde_json::from_str(&text) 140 | .map_err(|e| format!("解析JSON失败: {}", e))?; 141 | 142 | 143 | if json_response["status"].as_i64() == Some(1){ 144 | log::info!("验证码识别成功"); 145 | return Ok(serde_json::to_string(&json!({ 146 | "challenge": json_response["data"]["challenge"], 147 | "validate": json_response["data"]["validate"], 148 | "seccode": json_response["data"]["seccode"], 149 | })).map_err(|e| format!("序列化JSON失败: {}", e))?); 150 | 151 | } 152 | tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 153 | } 154 | 155 | Err("验证码识别超时".to_string()) 156 | }, 157 | _ => Err("无效的验证码模式".to_string()), 158 | } 159 | } 160 | 161 | 162 | 163 | 164 | pub async fn handle_risk_verification( 165 | cookie_manager: Arc<CookieManager>, 166 | risk_param: TokenRiskParam, 167 | custom_config: &CustomConfig, 168 | csrf: &str, 169 | local_captcha: LocalCaptcha, 170 | ) -> Result<(), String> { 171 | let risk_params_value = match &risk_param.risk_param { 172 | Some(value) => value, 173 | None => return Err("风控参数为空".to_string()), 174 | }; 175 | log::debug!("风控参数: {:?}", risk_params_value); 176 | let url = "https://api.bilibili.com/x/gaia-vgate/v1/register"; 177 | let response = cookie_manager.post(url).await 178 | .json(&json!(risk_params_value)) 179 | .send() 180 | .await 181 | .map_err(|e| format!("发送风控请求失败: {}", e))?; 182 | if !response.status().is_success() { 183 | return Err(format!("风控请求返回错误状态码: {}", response.status())); 184 | } 185 | 186 | let text = response.text().await 187 | .map_err(|e| format!("读取响应内容失败: {}", e))?; 188 | log::debug!("验证码请求响应: {}", text); 189 | 190 | let json_response: serde_json::Value = serde_json::from_str(&text) 191 | .map_err(|e| format!("解析JSON失败: {}", e))?; 192 | 193 | 194 | if json_response["code"].as_i64() != Some(0) { 195 | let message = json_response["message"].as_str().unwrap_or("未知错误"); 196 | return Err(format!("风控请求失败: {} (code: {})", message, json_response["code"])); 197 | } 198 | 199 | 200 | let captcha_type = json_response["data"]["type"].as_str().unwrap_or(""); 201 | 202 | match captcha_type { 203 | "geetest" => { 204 | log::info!("验证码类型: geetest"); 205 | 206 | 207 | let gt = json_response["data"]["geetest"]["gt"].as_str().unwrap_or(""); 208 | let challenge = json_response["data"]["geetest"]["challenge"].as_str().unwrap_or(""); 209 | let token = json_response["data"]["geetest"]["token"].as_str().unwrap_or(""); 210 | 211 | if gt.is_empty() || challenge.is_empty() || token.is_empty() { 212 | return Err("获取验证码参数失败".to_string()); 213 | } 214 | 215 | 216 | let captcha_result = captcha( 217 | custom_config.clone(), 218 | gt, 219 | challenge, 220 | "https://api.bilibili.com/x/gaia-vgate/v1/validate", 221 | 33 ,// 点选类型 222 | local_captcha, 223 | 224 | ).await?; 225 | 226 | 227 | let captcha_data: serde_json::Value = serde_json::from_str(&captcha_result) 228 | .map_err(|e| format!("解析验证码结果失败: {}", e))?; 229 | 230 | 231 | 232 | 233 | let params = json!({ 234 | "buvid": risk_param.buvid.unwrap_or_default(), 235 | "csrf": csrf, 236 | "geetest_challenge": captcha_data["challenge"], 237 | "geetest_seccode": captcha_data["seccode"], 238 | "geetest_validate": captcha_data["validate"], 239 | "token": token 240 | }); 241 | 242 | 243 | log::debug!("发送验证请求: {:?}", params); 244 | let validate_url = "https://api.bilibili.com/x/gaia-vgate/v1/validate"; 245 | let validate_response = cookie_manager.post(validate_url).await 246 | .json(¶ms) 247 | .send() 248 | .await 249 | .map_err(|e| format!("发送验证请求失败: {}", e))?; 250 | 251 | if !validate_response.status().is_success() { 252 | return Err(format!("验证请求返回错误状态码: {}", validate_response.status())); 253 | } 254 | 255 | let validate_json = validate_response.json::<serde_json::Value>().await 256 | .map_err(|e| format!("解析验证响应失败: {}", e))?; 257 | 258 | 259 | if validate_json["code"].as_i64() != Some(0) { 260 | let message = validate_json["message"].as_str().unwrap_or("未知错误"); 261 | return Err(format!("验证失败: {} (code: {})", message, validate_json["code"])); 262 | } 263 | 264 | let is_valid = validate_json["data"]["is_valid"].as_bool().unwrap_or(false); 265 | if !is_valid { 266 | return Err("验证未通过".to_string()); 267 | } 268 | 269 | 270 | 271 | log::info!("验证码验证成功"); 272 | Ok(()) 273 | }, 274 | _ => Err(format!("不支持的验证码类型: {}", captcha_type)) 275 | } 276 | } -------------------------------------------------------------------------------- /common/src/gen_cp.rs: -------------------------------------------------------------------------------- 1 | use base64; 2 | use base64::Engine; 3 | use base64::engine::general_purpose::STANDARD; 4 | use rand::{Rng, thread_rng}; 5 | use std::collections::HashMap; 6 | use std::time::{SystemTime, UNIX_EPOCH}; 7 | 8 | //感谢https://github.com/mikumifa/biliTickerBuy/pull/726/commits/0ff6218da458c41df89956384b8f192c7e7eae20 9 | // 提供的CToken生成代码 10 | //本代码依据上述代码 11 | 12 | pub struct CTokenGenerator { 13 | touch_event: i32, 14 | isibility_change: i32, 15 | page_unload: i32, 16 | timer: i32, 17 | time_difference: i32, 18 | scroll_x: i32, 19 | scroll_y: i32, 20 | inner_width: i32, 21 | inner_height: i32, 22 | outer_width: i32, 23 | outer_height: i32, 24 | pub screen_x: i32, 25 | pub screen_y: i32, 26 | pub screen_width: i32, 27 | pub screen_height: i32, 28 | screen_avail_width: i32, 29 | pub ticket_collection_t: i64, 30 | pub time_offset: i64, 31 | pub stay_time: i32, 32 | } 33 | 34 | impl CTokenGenerator { 35 | pub fn new(ticket_collection_t: i64, time_offset: i64, stay_time: i32) -> Self { 36 | CTokenGenerator { 37 | touch_event: 0, 38 | isibility_change: 0, 39 | page_unload: 0, 40 | timer: 0, 41 | time_difference: 0, 42 | scroll_x: 0, 43 | scroll_y: 0, 44 | inner_width: 0, 45 | inner_height: 0, 46 | outer_width: 0, 47 | outer_height: 0, 48 | screen_x: 0, 49 | screen_y: 0, 50 | screen_width: 0, 51 | screen_height: 0, 52 | screen_avail_width: 0, 53 | ticket_collection_t, 54 | time_offset, 55 | stay_time, 56 | } 57 | } 58 | 59 | fn encode(&self) -> String { 60 | let mut buffer = [0u8; 16]; 61 | let mut data_mapping = HashMap::new(); 62 | 63 | data_mapping.insert(0, (self.touch_event, 1)); 64 | data_mapping.insert(1, (self.scroll_x, 1)); 65 | data_mapping.insert(2, (self.isibility_change, 1)); 66 | data_mapping.insert(3, (self.scroll_y, 1)); 67 | data_mapping.insert(4, (self.inner_width, 1)); 68 | data_mapping.insert(5, (self.page_unload, 1)); 69 | data_mapping.insert(6, (self.inner_height, 1)); 70 | data_mapping.insert(7, (self.outer_width, 1)); 71 | data_mapping.insert(8, (self.timer, 2)); 72 | data_mapping.insert(10, (self.time_difference, 2)); 73 | data_mapping.insert(12, (self.outer_height, 1)); 74 | data_mapping.insert(13, (self.screen_x, 1)); 75 | data_mapping.insert(14, (self.screen_y, 1)); 76 | data_mapping.insert(15, (self.screen_width, 1)); 77 | 78 | let mut i = 0; 79 | while i < 16 { 80 | if let Some(&(data, length)) = data_mapping.get(&i) { 81 | if length == 1 { 82 | let value = if data > 0 { 83 | std::cmp::min(255, data) 84 | } else { 85 | data 86 | }; 87 | buffer[i] = (value & 0xFF) as u8; 88 | i += 1; 89 | } else if length == 2 { 90 | let value = if data > 0 { 91 | std::cmp::min(65535, data) 92 | } else { 93 | data 94 | }; 95 | buffer[i] = ((value >> 8) & 0xFF) as u8; 96 | buffer[i + 1] = (value & 0xFF) as u8; 97 | i += 2; 98 | } 99 | } else { 100 | let condition_value = if (4 & self.screen_height) != 0 { 101 | self.scroll_y 102 | } else { 103 | self.screen_avail_width 104 | }; 105 | buffer[i] = (condition_value & 0xFF) as u8; 106 | i += 1; 107 | } 108 | } 109 | 110 | let data_str: String = buffer.iter().map(|&b| b as char).collect(); 111 | self.to_binary(data_str) 112 | } 113 | 114 | fn to_binary(&self, data_str: String) -> String { 115 | let mut uint16_data = Vec::new(); 116 | let mut uint8_data = Vec::new(); 117 | 118 | // 第一次转换:字符串转为Uint16Array等价物 119 | for char in data_str.chars() { 120 | uint16_data.push(char as u16); 121 | } 122 | 123 | // 第二次转换:Uint16Array buffer转为Uint8Array 124 | for val in uint16_data { 125 | uint8_data.push((val & 0xFF) as u8); 126 | uint8_data.push(((val >> 8) & 0xFF) as u8); 127 | } 128 | 129 | STANDARD.encode(&uint8_data) 130 | } 131 | 132 | pub fn generate_ctoken(&mut self, is_create_v2: bool) -> String { 133 | let mut rng = thread_rng(); 134 | 135 | self.touch_event = 255; // 触摸事件数: 手机端抓包数据 136 | self.isibility_change = 2; // 可见性变化数: 手机端抓包数据 137 | self.inner_width = 255; // 窗口内部宽度: 手机端抓包数据 138 | self.inner_height = 255; // 窗口内部高度: 手机端抓包数据 139 | self.outer_width = 255; // 窗口外部宽度: 手机端抓包数据 140 | self.outer_height = 255; // 窗口外部高度: 手机端抓包数据 141 | self.screen_width = 255; // 屏幕宽度: 手机端抓包数据 142 | self.screen_height = rng.gen_range(1000..=3000); // 屏幕高度: 用于条件判断 143 | self.screen_avail_width = rng.gen_range(1..=100); // 屏幕可用宽度: 用于条件判断 144 | 145 | if is_create_v2 { 146 | // createV2阶段 147 | let current_time = SystemTime::now() 148 | .duration_since(UNIX_EPOCH) 149 | .unwrap() 150 | .as_secs() as i64; 151 | self.time_difference = 152 | (current_time + self.time_offset - self.ticket_collection_t) as i32; 153 | self.timer = self.time_difference + self.stay_time; 154 | self.page_unload = 25; // 页面卸载数: 手机端抓包数据 155 | } else { 156 | // prepare阶段 157 | self.time_difference = 0; 158 | self.timer = self.stay_time; 159 | self.touch_event = rng.gen_range(3..=10); 160 | } 161 | 162 | self.encode() 163 | } 164 | } -------------------------------------------------------------------------------- /common/src/http_utils.rs: -------------------------------------------------------------------------------- 1 | use reqwest::{Client, header, Response, Error}; 2 | use rand::seq::SliceRandom; 3 | use rand::thread_rng; 4 | use std::collections::HashMap; 5 | use serde_json; 6 | 7 | // 随机UA生成 8 | 9 | pub fn get_random_ua() -> String { 10 | let ua_list = [ 11 | "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 Chrome/98.0.4758.101", 12 | "Mozilla/5.0 (iPhone; CPU iPhone OS 15_4 like Mac OS X) AppleWebKit/605.1.15", 13 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/99.0.4844.51", 14 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/605.1.15", 15 | "Mozilla/5.0 (Linux; Android 12; SM-S908B) Chrome/101.0.4951.41", 16 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/102.0.0.0" 17 | ]; 18 | 19 | let mut rng = thread_rng(); 20 | ua_list.choose(&mut rng).unwrap_or(&ua_list[0]).to_string() 21 | } 22 | 23 | 24 | pub async fn request_get(client: &Client, url: &str, cookie: Option<&str>) -> Result<Response, Error> { 25 | 26 | 27 | let mut req = client.get(url); 28 | 29 | 30 | if let Some(cookie_str) = cookie { 31 | req = req.header(header::COOKIE, cookie_str); 32 | } 33 | 34 | req.send().await 35 | } 36 | 37 | pub async fn request_post<T: serde::Serialize + ?Sized>( 38 | client: &Client, 39 | url: &str, 40 | 41 | cookie: Option<&str>, 42 | json_data: Option<&T> 43 | ) -> Result<Response, Error> { 44 | 45 | 46 | let mut req = client.post(url); 47 | 48 | if let Some(cookie_str) = cookie { 49 | req = req.header(header::COOKIE, cookie_str); 50 | } 51 | 52 | if let Some(data) = json_data { 53 | if let Ok(json_value) = serde_json::to_value(data) { 54 | if let Some(obj) = json_value.as_object() { 55 | let mut form = std::collections::HashMap::new(); 56 | for (key, value) in obj { 57 | // 转换所有值为字符串 58 | form.insert(key, value.to_string().trim_matches('"').to_string()); 59 | } 60 | req = req.form(&form); 61 | } 62 | } 63 | } 64 | 65 | req.send().await 66 | } 67 | 68 | 69 | pub fn request_get_sync(client: &Client, url: &str, ua: Option<String>, cookie: Option<&str>) -> Result<Response, Error> { 70 | let rt = tokio::runtime::Runtime::new().unwrap(); 71 | rt.block_on(request_get(client, url, cookie)) 72 | 73 | } 74 | 75 | pub fn request_post_sync(client: &Client, url: &str, ua: Option<String>, cookie: Option<&str>, json_data: Option<&serde_json::Value>) -> Result<Response, Error> { 76 | let rt = tokio::runtime::Runtime::new().unwrap(); 77 | rt.block_on(request_post(client, url, cookie, json_data)) 78 | 79 | } 80 | 81 | pub fn request_form_sync( 82 | client: &Client, 83 | url: &str, 84 | ua: Option<String>, 85 | cookie: Option<&str>, 86 | form_data: &HashMap<String, String> 87 | ) -> Result<Response, Error> { 88 | let rt = tokio::runtime::Runtime::new().unwrap(); 89 | 90 | rt.block_on(async { 91 | let mut req = client.post(url); 92 | 93 | if let Some(cookie_str) = cookie { 94 | req = req.header(header::COOKIE, cookie_str); 95 | } 96 | 97 | if let Some(ua_str) = ua { 98 | req = req.header(header::USER_AGENT, ua_str); 99 | } 100 | 101 | req = req.form(&form_data); 102 | req.send().await 103 | }) 104 | } 105 | 106 | 107 | pub fn request_json_form_sync( 108 | client: &Client, 109 | url: &str, 110 | ua: Option<String>, 111 | referer: Option<String>, 112 | cookie: Option<&str>, 113 | json_form: &serde_json::Map<String, serde_json::Value> 114 | ) -> Result<Response, Error> { 115 | let rt = tokio::runtime::Runtime::new().unwrap(); 116 | 117 | rt.block_on(async { 118 | let mut req = client.post(url); 119 | 120 | if let Some(cookie_str) = cookie { 121 | req = req.header(header::COOKIE, cookie_str); 122 | } 123 | 124 | if let Some(ua_str) = ua { 125 | req = req.header(header::USER_AGENT, ua_str); 126 | } 127 | 128 | if let Some(referer_str) = referer { 129 | req = req.header(header::REFERER, referer_str); 130 | } 131 | // 创建一个特殊的表单,保留数字类型 132 | let mut form = std::collections::HashMap::new(); 133 | for (key, value) in json_form { 134 | match value { 135 | // 字符串类型 136 | serde_json::Value::String(s) => { 137 | form.insert(key.clone(), s.clone()); 138 | }, 139 | // 数字类型 - 直接转为字符串但不加引号 140 | serde_json::Value::Number(n) => { 141 | form.insert(key.clone(), n.to_string()); 142 | }, 143 | // 布尔类型 144 | serde_json::Value::Bool(b) => { 145 | form.insert(key.clone(), b.to_string()); 146 | }, 147 | // 其他类型 148 | _ => { 149 | form.insert(key.clone(), value.to_string().trim_matches('"').to_string()); 150 | } 151 | } 152 | } 153 | 154 | req = req.form(&form); 155 | req.send().await 156 | }) 157 | } -------------------------------------------------------------------------------- /common/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod taskmanager; 2 | pub mod record_log; 3 | pub mod account; 4 | pub mod utils; 5 | pub mod push; 6 | pub mod utility; 7 | pub mod login; 8 | pub mod http_utils; 9 | pub mod captcha; 10 | pub mod show_orderlist; 11 | pub mod ticket; 12 | 13 | pub mod cookie_manager; 14 | pub mod web_ck_obfuscated; 15 | pub mod machine_id; 16 | pub mod gen_cp; 17 | // 重导出日志收集器 18 | pub use record_log::LOG_COLLECTOR; 19 | pub use record_log::init as init_logger; 20 | 21 | 22 | -------------------------------------------------------------------------------- /common/src/login.rs: -------------------------------------------------------------------------------- 1 | use crate::account::add_account; 2 | use crate::account::Account; 3 | use crate::captcha::LocalCaptcha; 4 | use crate::http_utils::{request_get,request_post,request_get_sync}; 5 | use serde_json::json; 6 | use crate::utility::CustomConfig; 7 | use crate::captcha::captcha; 8 | use reqwest::Client; 9 | 10 | pub struct LoginInput{ 11 | pub phone: String, 12 | pub account: String, 13 | pub password: String, 14 | pub sms_code: String, 15 | pub cookie: String, 16 | } 17 | 18 | pub struct QrCodeLoginTask { 19 | pub qrcode_key: String, 20 | pub qrcode_url: String, 21 | pub start_time: std::time::Instant, 22 | pub status: QrCodeLoginStatus, 23 | } 24 | 25 | #[derive(Clone, Debug, PartialEq)] 26 | pub enum QrCodeLoginStatus { 27 | Pending, 28 | Scanning, 29 | Confirming, 30 | Success(String), //成功时返回cookie信息 31 | Failed(String), //失败时返回错误信息 32 | Expired, 33 | } 34 | 35 | #[derive(Clone, Debug, PartialEq)] 36 | pub enum SendLoginSmsStatus{ 37 | Success(String), 38 | Failed(String), 39 | } 40 | 41 | pub fn qrcode_login(client: &Client) -> Result<String, String> { 42 | // 创建一个临时的运行时来执行异步代码 43 | let rt = tokio::runtime::Runtime::new().unwrap(); 44 | rt.block_on(async { 45 | let response = request_get( 46 | client, 47 | "https://passport.bilibili.com/x/passport-login/web/qrcode/generate", 48 | 49 | None, 50 | ).await.map_err(|e| e.to_string())?; 51 | 52 | let json = response.json::<serde_json::Value>() 53 | .await.map_err(|e| e.to_string())?; 54 | 55 | 56 | if let Some(qrcode_key) = json["data"]["qrcode_key"].as_str() { 57 | Ok(qrcode_key.to_string()) 58 | } else { 59 | Err("无法获取二维码URL".to_string()) 60 | } 61 | }) 62 | } 63 | pub fn password_login(username: &str, password: &str) -> Result<String, String> { 64 | Err("暂不支持账号密码登录".to_string()) 65 | } 66 | 67 | pub async fn send_loginsms(phone: &str, client: &Client, custom_config: CustomConfig,local_captcha: LocalCaptcha) -> Result<String, String> { 68 | 69 | let response = request_get( 70 | client, 71 | "https://www.bilibili.com/", 72 | 73 | None, 74 | ).await.map_err(|e| e.to_string())?; 75 | 76 | log::debug!("{:?}", response.cookies().collect::<Vec<_>>()); 77 | 78 | 79 | 80 | // 发送请求 81 | let response = request_get( 82 | client, 83 | "https://passport.bilibili.com/x/passport-login/captcha", 84 | 85 | None, 86 | ).await.map_err(|e| e.to_string())?; 87 | log::info!("获取验证码: {:?}", response); 88 | 89 | let json = response.json::<serde_json::Value>().await.map_err(|e| e.to_string())?; 90 | let gt = json["data"]["geetest"]["gt"].as_str().unwrap_or(""); 91 | let challenge = json["data"]["geetest"]["challenge"].as_str().unwrap_or(""); 92 | let token = json["data"]["token"].as_str().unwrap_or(""); 93 | let referer = "https://passport.bilibili.com/x/passport-login/captcha"; 94 | match captcha(custom_config.clone(), gt, challenge, referer, 33,local_captcha).await { 95 | Ok(result_str) => { 96 | log::info!("验证码识别成功: {}", result_str); 97 | let result: serde_json::Value = serde_json::from_str(&result_str).map_err(|e| e.to_string())?; 98 | 99 | let json_data = json!({ 100 | "cid": 86, 101 | "tel": phone.parse::<i64>().unwrap_or(0), 102 | "token": token, 103 | "source":"main_mini", 104 | "challenge": result["challenge"], 105 | "validate": result["validate"], 106 | "seccode": result["seccode"], 107 | }); 108 | log::debug!("验证码数据: {:?}", json_data); 109 | let send_sms = request_post( 110 | client, 111 | "https://passport.bilibili.com/x/passport-login/web/sms/send", 112 | 113 | None, 114 | Some(&json_data), 115 | ).await.map_err(|e| e.to_string())?; 116 | 117 | let json_response = send_sms.json::<serde_json::Value>().await.map_err(|e| e.to_string())?; 118 | log::debug!("验证码发送响应: {:?}", json_response); 119 | if json_response["code"].as_i64() == Some(0) { 120 | let captcha_key = json_response["data"]["captcha_key"].as_str().unwrap_or(""); 121 | log::info!("验证码发送成功"); 122 | log::debug!("captcha_key: {:?}", captcha_key); 123 | Ok(captcha_key.to_string()) 124 | } else { 125 | log::error!("验证码发送失败: {}", json_response["message"].as_str().unwrap_or("未知错误")); 126 | Err("验证码发送失败".to_string()) 127 | } 128 | } 129 | Err(e) => { 130 | log::error!("验证码识别失败: {}", e); 131 | Err("验证码识别失败".to_string()) 132 | } 133 | 134 | } 135 | 136 | 137 | } 138 | 139 | pub async fn sms_login(phone: &str, sms_code: &str, captcha_key:&str, client: &Client) -> Result<String, String> { 140 | let data = serde_json::json!({ 141 | "cid": 86, 142 | "tel": phone.parse::<i64>().unwrap_or(0), 143 | "code": sms_code.parse::<i64>().unwrap_or(0), 144 | "source":"main_mini", 145 | "captcha_key":captcha_key, 146 | }); 147 | log::debug!("短信登录数据: {:?}", data); 148 | let login_response = request_post( 149 | client, 150 | "https://passport.bilibili.com/x/passport-login/web/login/sms", 151 | 152 | None, 153 | Some(&data), 154 | ).await.map_err(|e| e.to_string())?; 155 | let mut all_cookies = Vec::new(); 156 | let cookie_headers = login_response.headers().get_all(reqwest::header::SET_COOKIE); 157 | log::debug!("headers返回:{:?}",cookie_headers); 158 | for value in cookie_headers { 159 | if let Ok(cookie_str) = value.to_str() { 160 | 161 | if let Some(end_pos) = cookie_str.find(';') { 162 | all_cookies.push(cookie_str[0..end_pos].to_string()); 163 | } else { 164 | all_cookies.push(cookie_str.to_string()); 165 | } 166 | } 167 | } 168 | log::info!("获取cookie: {:?}", all_cookies); 169 | let json_response = login_response.json::<serde_json::Value>() 170 | .await 171 | .map_err(|e| format!("解析JSON失败: {}", e))?; 172 | log::debug!("登录接口响应:{:?}",json_response); 173 | if json_response["code"].as_i64() == Some(0) { 174 | log::info!("短信登录成功!"); 175 | log::info!("登录cookie:{:?}", all_cookies); 176 | return Ok(all_cookies.to_vec().join(";")); 177 | 178 | } 179 | Err("短信登录失败".to_string()) 180 | 181 | 182 | } 183 | 184 | pub fn cookie_login(cookie: &str, client: &Client, ua: &str) -> Result<Account, String> { 185 | match add_account(cookie,client,ua){ 186 | Ok(account) => { 187 | log::info!("ck登录成功"); 188 | Ok(account) 189 | }, 190 | Err(e) => { 191 | log::error!("ck登录失败: {}", e); 192 | Err(e) 193 | } 194 | } 195 | } 196 | 197 | 198 | -------------------------------------------------------------------------------- /common/src/push.rs: -------------------------------------------------------------------------------- 1 | use std::default; 2 | 3 | use serde::{Serialize, Deserialize}; 4 | use crate::taskmanager::{TaskManager, PushRequest, PushType, TaskRequest}; 5 | use reqwest::Client; 6 | 7 | //推送token 8 | #[derive(Clone, Debug, Serialize, Deserialize)] 9 | pub struct PushConfig{ 10 | pub enabled: bool, 11 | pub bark_token: String, 12 | pub pushplus_token: String, 13 | pub fangtang_token: String, 14 | pub dingtalk_token: String, 15 | pub wechat_token: String, 16 | pub gotify_config: GotifyConfig, 17 | pub smtp_config: SmtpConfig, 18 | 19 | } 20 | 21 | #[derive(Clone, Debug, Serialize, Deserialize)] 22 | pub struct GotifyConfig{ 23 | pub gotify_url: String, 24 | pub gotify_token: String, 25 | } 26 | 27 | impl GotifyConfig { 28 | pub fn new() -> Self{ 29 | Self { 30 | gotify_url: String::new(), 31 | gotify_token: String::new(), 32 | } 33 | 34 | } 35 | } 36 | //邮箱配置(属于pushconfig) 37 | #[derive(Clone, Debug, Serialize, Deserialize)] 38 | pub struct SmtpConfig{ 39 | pub smtp_server: String, 40 | pub smtp_port: String, 41 | pub smtp_username: String, 42 | pub smtp_password: String, 43 | pub smtp_from: String, 44 | pub smtp_to: String, 45 | } 46 | 47 | impl PushConfig{ 48 | pub fn new() -> Self{ 49 | Self{ 50 | enabled: false, 51 | bark_token: String::new(), 52 | pushplus_token: String::new(), 53 | fangtang_token: String::new(), 54 | dingtalk_token: String::new(), 55 | wechat_token: String::new(), 56 | gotify_config: GotifyConfig::new(), 57 | smtp_config: SmtpConfig::new(), 58 | } 59 | } 60 | 61 | pub fn push_all(&self,title:&str,message:&str,jump_url:&Option<String>, task_manager: &mut dyn TaskManager){ 62 | if !self.enabled{ 63 | return; 64 | } 65 | let push_request =TaskRequest::PushRequest( PushRequest{ 66 | title: title.to_string(), 67 | message: message.to_string(), 68 | jump_url: jump_url.clone(), 69 | push_config: self.clone(), 70 | push_type: PushType::All, 71 | }); 72 | match task_manager.submit_task(push_request){ 73 | Ok(task_id) => { 74 | log::debug!("提交全渠道推送任务成功,任务ID: {}", task_id); 75 | }, 76 | Err(e) => { 77 | log::error!("提交推送任务失败: {}", e); 78 | } 79 | } 80 | 81 | 82 | } 83 | 84 | pub async fn push_all_async(&self, title:&str, message: &str, jump_url:&Option<String>) -> (bool,String){ 85 | let mut success_count = 0; 86 | let mut failure_count = 0; 87 | let mut failures = Vec::new(); 88 | 89 | if !self.bark_token.is_empty(){ 90 | let (success, msg) = self.push_bark(title, message).await; 91 | if success{ 92 | success_count += 1; 93 | }else{ 94 | failure_count += 1; 95 | failures.push(format!("Bark推送出错: {}", msg)); 96 | } 97 | } 98 | 99 | if !self.pushplus_token.is_empty(){ 100 | let (success, msg) = self.push_pushplus(title, message).await; 101 | if success{ 102 | success_count += 1; 103 | }else{ 104 | failure_count += 1; 105 | failures.push(format!("PushPlus推送出错: {}", msg)); 106 | } 107 | } 108 | if !self.fangtang_token.is_empty(){ 109 | let (success, msg) = self.push_fangtang(title, message).await; 110 | if success{ 111 | success_count += 1; 112 | }else{ 113 | failure_count += 1; 114 | failures.push(format!("Fangtang推送出错: {}", msg)); 115 | } 116 | } 117 | if !self.dingtalk_token.is_empty(){ 118 | let (success, msg) = self.push_dingtalk(title, message).await; 119 | if success{ 120 | success_count += 1; 121 | }else{ 122 | failure_count += 1; 123 | failures.push(format!("Dingtalk推送出错: {}", msg)); 124 | } 125 | } 126 | if !self.wechat_token.is_empty(){ 127 | let (success, msg) = self.push_wechat(title, message).await; 128 | if success{ 129 | success_count += 1; 130 | }else{ 131 | failure_count += 1; 132 | failures.push(format!("WeChat推送出错: {}", msg)); 133 | } 134 | } 135 | if !self.smtp_config.smtp_server.is_empty(){ 136 | let (success, msg) = self.push_smtp(title, message).await; 137 | if success{ 138 | success_count += 1; 139 | }else{ 140 | failure_count += 1; 141 | failures.push(format!("SMTP推送出错: {}", msg)); 142 | } 143 | } 144 | if !self.gotify_config.gotify_token.is_empty(){ 145 | let (success,msg) = self.push_gotify(title, message, jump_url).await; 146 | if success{ 147 | success_count += 1; 148 | 149 | }else{ 150 | failure_count += 1; 151 | failures.push(format!("Gotify推送出错: {}", msg)); 152 | } 153 | } 154 | if success_count == 0{ 155 | return (false, format!("{} 成功 / {} 失败。失败详情: {}", 156 | success_count, failure_count, failures.join("; "))) 157 | }else{ 158 | return (true, format!("全部 {} 个渠道推送成功", success_count)) 159 | } 160 | } 161 | pub async fn push_gotify(&self, title:&str, message: &str, jump_url:&Option<String>) -> (bool, String){ 162 | let mut default_headers = reqwest::header::HeaderMap::new(); 163 | let jump_url_real = match jump_url { 164 | Some(url) => url, 165 | None => &"bilibili://mall/web?url=https://www.bilibili.com".to_string(), 166 | }; 167 | let push_target_url = if self.gotify_config.clone().gotify_url.contains("http"){ 168 | self.gotify_config.clone().gotify_url 169 | }else{ 170 | format!("http://{}", self.gotify_config.clone().gotify_url) 171 | }; 172 | default_headers.insert("Content-Type", reqwest::header::HeaderValue::from_static("application/json")); 173 | default_headers.insert("Authorization", reqwest::header::HeaderValue::from_str(&format!("Bearer {}", self.gotify_config.gotify_token)).unwrap()); 174 | let client_builder = Client::builder() 175 | .default_headers(default_headers) 176 | .timeout(std::time::Duration::from_secs(20)); 177 | let data = serde_json::json!({ 178 | "message": message, 179 | "title": title, 180 | "priority": 9, 181 | "extras": { 182 | "client::notification": { 183 | "click": {"url": jump_url_real}, 184 | }, 185 | "android::action": { 186 | "onReceive": {"intentUrl": jump_url_real} 187 | } 188 | } 189 | }); 190 | let client = match client_builder.build(){ 191 | Ok(client) => client, 192 | Err(e) => return (false, format!("创建HTTP客户端失败: {}", e)), 193 | }; 194 | let url = format!("{}/message",push_target_url); 195 | 196 | match client.post(&url) 197 | .json(&data) 198 | .send() 199 | .await { 200 | Ok(resp) => { 201 | let status = resp.status(); 202 | match resp.text().await { 203 | Ok(text) => { 204 | log::debug!("Gotify 推送响应: 状态码 {}, 内容: {}", status, text); 205 | if status.is_success() { 206 | (true, "推送成功".to_string()) 207 | } else { 208 | (false, format!("推送失败,状态码: {}", status)) 209 | } 210 | }, 211 | Err(e) => (false, format!("读取响应失败: {}", e)) 212 | } 213 | }, 214 | Err(e) => { 215 | (false, format!("推送失败: {}", e)) 216 | } 217 | 218 | } 219 | 220 | 221 | } 222 | pub async fn push_bark(&self, title:&str ,message: &str) -> (bool, String){ 223 | let client = Client::new(); 224 | let data = serde_json::json!({ 225 | "title":title, 226 | "body":message, 227 | "level":"timeSensitive", 228 | /* #推送中断级别。 229 | #active:默认值,系统会立即亮屏显示通知 230 | #timeSensitive:时效性通知,可在专注状态下显示通知。 231 | #passive:仅将通知添加到通知列表,不会亮屏提醒。 */ 232 | "badge":1, 233 | "icon":"https://sr.mihoyo.com/favicon-mi.ico", 234 | "group":"biliticket", 235 | "isArchive":1, 236 | 237 | }); 238 | let url = format!("https://api.day.app/{}/", self.bark_token); 239 | match client.post(&url) 240 | .json(&data) 241 | .send() 242 | .await{ 243 | Ok(resp) => { 244 | let status = resp.status(); 245 | match resp.text().await { 246 | Ok(text) => { 247 | log::debug!("Bark 推送响应: 状态码 {}, 内容: {}", status, text); 248 | if status.is_success() { 249 | (true, "推送成功".to_string()) 250 | } else { 251 | (false, format!("推送失败,状态码: {}", status)) 252 | } 253 | }, 254 | Err(e) => (false, format!("读取响应失败: {}", e)) 255 | } 256 | }, 257 | Err(e) => { 258 | (false, format!("推送失败: {}", e)) 259 | } 260 | } 261 | } 262 | 263 | pub async fn push_pushplus(&self, title:&str, message: &str) -> (bool, String){ 264 | let client = Client::new(); 265 | let url = "http://www.pushplus.plus/send"; 266 | let data = serde_json::json!({ 267 | "token":self.pushplus_token, 268 | "title":title, 269 | "content":message, 270 | }); 271 | match client.post(url) 272 | .json(&data) 273 | .send() 274 | .await{ 275 | Ok(resp) => { 276 | let status = resp.status(); 277 | match resp.text().await { 278 | Ok(text) => { 279 | log::debug!("PushPlus 推送响应: 状态码 {}, 内容: {}", status, text); 280 | if status.is_success() { 281 | (true, "推送成功".to_string()) 282 | } else { 283 | (false, format!("推送失败,状态码: {}", status)) 284 | } 285 | }, 286 | Err(e) => (false, format!("读取响应失败: {}", e)) 287 | } 288 | }, 289 | Err(e) => { 290 | (false, format!("推送失败: {}", e)) 291 | } 292 | } 293 | } 294 | 295 | pub async fn push_fangtang(&self, title:&str, message: &str) -> (bool, String){ 296 | let client = Client::new(); 297 | let url = format!("https://sctapi.ftqq.com/{}.send",self.fangtang_token); 298 | let data = serde_json::json!({ 299 | "title":title, 300 | "desp":message, 301 | "noip":1 302 | }); 303 | match client.post(url) 304 | .json(&data) 305 | .send() 306 | .await{ 307 | Ok(resp) => { 308 | let status = resp.status(); 309 | match resp.text().await { 310 | Ok(text) => { 311 | log::debug!("Fangtang 推送响应: 状态码 {}, 内容: {}", status, text); 312 | if status.is_success() { 313 | (true, "推送成功".to_string()) 314 | } else { 315 | (false, format!("推送失败,状态码: {}", status)) 316 | } 317 | }, 318 | Err(e) => (false, format!("读取响应失败: {}", e)) 319 | } 320 | }, 321 | Err(e) => { 322 | (false, format!("推送失败: {}", e)) 323 | } 324 | } 325 | } 326 | 327 | pub async fn push_dingtalk(&self, title:&str, message: &str) -> (bool, String){ 328 | let client = Client::new(); 329 | let url = format!("https://oapi.dingtalk.com/robot/send?access_token={}",self.dingtalk_token); 330 | let data = serde_json::json!({ 331 | "msgtype":"text", 332 | "text":{ 333 | "content":format!("{} \n {}", title, message) 334 | } 335 | }); 336 | match client.post(url) 337 | .json(&data) 338 | .header("Content-Type", "application/json") 339 | .header("Charset", "UTF-8") 340 | .send() 341 | .await{ 342 | Ok(resp) => { 343 | let status = resp.status(); 344 | match resp.text().await { 345 | Ok(text) => { 346 | log::debug!("钉钉推送响应: 状态码 {}, 内容: {}", status, text); 347 | if status.is_success() { 348 | (true, "推送成功".to_string()) 349 | } else { 350 | (false, format!("推送失败,状态码: {}", status)) 351 | } 352 | }, 353 | Err(e) => (false, format!("读取响应失败: {}", e)) 354 | } 355 | }, 356 | Err(e) => { 357 | (false, format!("推送失败: {}", e)) 358 | } 359 | } 360 | } 361 | 362 | pub async fn push_wechat(&self, title:&str, message: &str) -> (bool, String){ 363 | let client = Client::new(); 364 | let url = format!("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={}",self.wechat_token); 365 | let data = serde_json::json!({ 366 | "msgtype":"text", 367 | "text":{ 368 | "content":format!("{} \n {}", title, message) 369 | } 370 | }); 371 | match client.post(url) 372 | .json(&data) 373 | .header("Content-Type", "application/json") 374 | .header("Charset", "UTF-8") 375 | .send() 376 | .await{ 377 | Ok(resp) => { 378 | let status = resp.status(); 379 | match resp.text().await { 380 | Ok(text) => { 381 | log::debug!("微信推送响应: 状态码 {}, 内容: {}", status, text); 382 | if status.is_success() { 383 | (true, "推送成功".to_string()) 384 | } else { 385 | (false, format!("推送失败,状态码: {}", status)) 386 | } 387 | }, 388 | Err(e) => (false, format!("读取响应失败: {}", e)) 389 | } 390 | }, 391 | Err(e) => { 392 | (false, format!("推送失败: {}", e)) 393 | } 394 | } 395 | } 396 | 397 | pub async fn push_smtp(&self, title: &str, message: &str) -> (bool, String){ 398 | return (false,"SMTP推送功能未实现".to_string()) 399 | } 400 | 401 | 402 | 403 | } 404 | 405 | impl SmtpConfig{ 406 | pub fn new() -> Self{ 407 | Self{ 408 | smtp_server: String::new(), 409 | smtp_port: String::new(), 410 | smtp_username: String::new(), 411 | smtp_password: String::new(), 412 | smtp_from: String::new(), 413 | smtp_to: String::new(), 414 | } 415 | } 416 | 417 | } -------------------------------------------------------------------------------- /common/src/record_log.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | use log::{Record, Level, Metadata, LevelFilter, SetLoggerError}; 3 | use once_cell::sync::Lazy; 4 | use std::fs::{self, File, OpenOptions}; 5 | use std::io::Write; 6 | use std::path::Path; 7 | 8 | 9 | // 日志文件处理相关内容 10 | lazy_static::lazy_static! { 11 | static ref LOG_FILE: Mutex<Option<(String, File)>> = Mutex::new(None); 12 | } 13 | 14 | // 创建新的日志文件 15 | fn create_log_file() -> Option<(String, File)> { 16 | // 确保日志目录存在 17 | let log_dir = Path::new("Log"); 18 | if let Err(e) = fs::create_dir_all(log_dir) { 19 | eprintln!("无法创建日志目录: {}", e); 20 | return None; 21 | } 22 | 23 | // 创建带有时间戳的文件名 24 | let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S"); 25 | let filename = format!("Log/log_{}.log", timestamp); 26 | 27 | // 打开文件 28 | match OpenOptions::new() 29 | .write(true) 30 | .create(true) 31 | .append(true) 32 | .open(&filename) 33 | { 34 | Ok(file) => Some((filename.clone(), file)), 35 | Err(e) => { 36 | eprintln!("无法创建日志文件 {}: {}", filename, e); 37 | None 38 | } 39 | } 40 | } 41 | 42 | fn write_to_log_file(message: &str) -> bool { 43 | let mut file_guard = LOG_FILE.lock().unwrap(); 44 | 45 | // 检查是否需要创建新的日志文件 46 | let create_new_file = match &*file_guard { 47 | Some((filename, _)) => { 48 | let current_date = chrono::Local::now().format("%Y%m%d").to_string(); 49 | !filename.contains(¤t_date) 50 | }, 51 | None => true 52 | }; 53 | 54 | if create_new_file { 55 | if let Some(new_file) = create_log_file() { 56 | *file_guard = Some(new_file); 57 | } 58 | } 59 | 60 | // 写入日志 61 | if let Some((_, file)) = file_guard.as_mut() { 62 | if let Err(_) = writeln!(file, "{}", message) { 63 | return false; 64 | } 65 | if let Err(_) = file.flush() { 66 | return false; 67 | } 68 | return true; 69 | } 70 | 71 | false 72 | } 73 | 74 | //日志记录器 75 | pub struct LogCollector{ 76 | pub logs: Vec<String>, 77 | } 78 | 79 | impl LogCollector{ 80 | pub fn new() -> Self{ 81 | Self { logs: Vec::new() } 82 | } 83 | //添加日志 84 | pub fn add(&mut self, message: String){ 85 | self.logs.push(message); 86 | } 87 | 88 | //获取日志 89 | pub fn get_logs(&mut self) -> Option<Vec<String>>{ 90 | if self.logs.is_empty(){ 91 | return None; 92 | } 93 | let logs = self.logs.clone(); 94 | 95 | self.clear_logs(); 96 | Some(logs) 97 | } 98 | 99 | //清空日志 100 | pub fn clear_logs(&mut self){ 101 | self.logs.clear(); 102 | } 103 | } 104 | 105 | pub static LOG_COLLECTOR: Lazy<Arc<Mutex<LogCollector>>> = //? 106 | Lazy::new(|| Arc::new(Mutex::new(LogCollector::new()))); 107 | 108 | 109 | struct CollectorLogger; 110 | impl log::Log for CollectorLogger{ 111 | fn enabled(&self, metadata: &Metadata) -> bool{ 112 | metadata.level() <= Level::Debug 113 | } 114 | 115 | fn log(&self,record: &Record){ 116 | if self.enabled(record.metadata()){ 117 | let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S:%3f"); 118 | let log_message = format!("[{}] {}: {}", 119 | timestamp, record.level(), record.args()); 120 | 121 | { 122 | if let Ok(mut collector) = LOG_COLLECTOR.try_lock() { // 使用 try_lock 避免长时间等待 123 | collector.add(log_message.clone()); 124 | } 125 | } 126 | 127 | 128 | println!("{}", log_message); 129 | 130 | // 单独处理文件写入,避免同时持有多个锁 131 | let _ = write_to_log_file(&log_message); 132 | } 133 | } 134 | 135 | fn flush(&self) { 136 | //确保文件被刷新 137 | let mut file_guard = LOG_FILE.lock().unwrap(); 138 | if let Some((_, file)) = file_guard.as_mut() { 139 | let _ = file.flush(); 140 | } 141 | } 142 | 143 | } 144 | 145 | // 静态日志记录器 146 | static LOGGER: CollectorLogger = CollectorLogger; 147 | 148 | // 初始化日志系统 149 | pub fn init() -> Result<(), SetLoggerError> { 150 | 151 | if cfg!(debug_assertions) { 152 | println!("调试模式启动"); 153 | } else { 154 | println!("正式版"); 155 | } 156 | 157 | // 根据构建模式设置不同的日志级别 158 | log::set_logger(&LOGGER).map(|()| { 159 | if cfg!(debug_assertions) { 160 | log::set_max_level(LevelFilter::Debug) 161 | } else { 162 | log::set_max_level(LevelFilter::Info) 163 | } 164 | }) 165 | } -------------------------------------------------------------------------------- /common/src/show_orderlist.rs: -------------------------------------------------------------------------------- 1 | 2 | use serde::{Deserialize, Serialize}; 3 | #[derive(Debug, Deserialize, Serialize, Clone)] 4 | pub struct OrderResponse { 5 | pub errno: i32, 6 | pub errtag: i32, 7 | pub msg: String, 8 | pub data: OrderData, 9 | } 10 | 11 | #[derive(Debug, Deserialize, Serialize, Clone)] 12 | pub struct OrderData{ 13 | pub total: i32, 14 | pub list: Vec<Order>, 15 | } 16 | 17 | #[derive(Debug, Deserialize, Serialize, Clone)] 18 | pub struct Order{ 19 | pub order_id: String, 20 | pub order_type: i32, 21 | pub item_id: i64, 22 | pub item_info: ItemInfo, 23 | pub total_money: i64, 24 | pub count: i32, 25 | pub pay_money: i64, 26 | pub pay_channel: Option<String>, 27 | pub status: i32, 28 | pub sub_status: i32, 29 | pub ctime: String, 30 | pub img: ImageInfo, 31 | pub sub_status_name: String, 32 | } 33 | 34 | #[derive(Debug, Deserialize, Serialize, Clone)] 35 | pub struct ItemInfo{ 36 | pub name: String, 37 | pub image: Option<String>, 38 | pub screen_id: String, 39 | pub screen_name: String, 40 | pub screen_start_time: String, 41 | pub screen_end_time: String, 42 | } 43 | 44 | #[derive(Debug, Deserialize, Serialize, Clone)] 45 | pub struct ImageInfo{ 46 | pub url: String, 47 | 48 | } 49 | -------------------------------------------------------------------------------- /common/src/taskmanager.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | use reqwest::Client; 3 | use std::sync::Arc; 4 | use crate::cookie_manager::CookieManager; 5 | use crate::ticket::{*}; 6 | use crate::captcha::LocalCaptcha; 7 | use crate::push::PushConfig; 8 | use crate::utility::CustomConfig; 9 | use crate::show_orderlist::OrderResponse; 10 | 11 | 12 | 13 | 14 | // 任务状态枚举 15 | #[derive(Clone,Debug)] 16 | pub enum TaskStatus { 17 | Pending, 18 | Running, 19 | Completed(bool), 20 | Failed(String), 21 | } 22 | 23 | // 票务结果 24 | #[derive(Clone)] 25 | pub struct TicketResult { 26 | pub success: bool, 27 | pub order_id: Option<String>, 28 | pub message: Option<String>, 29 | pub ticket_info: TicketInfo, 30 | pub timestamp: Instant, 31 | } 32 | 33 | // 任务信息 34 | pub enum Task { 35 | 36 | QrCodeLoginTask(QrCodeLoginTask), 37 | LoginSmsRequestTask(LoginSmsRequestTask), 38 | PushTask(PushTask), 39 | SubmitLoginSmsRequestTask(SubmitLoginSmsRequestTask), 40 | GetAllorderRequestTask(GetAllorderRequest), 41 | GetTicketInfoTask(GetTicketInfoTask), 42 | GetBuyerInfoTask(GetBuyerInfoTask), 43 | GrabTicketTask(GrabTicketTask), 44 | } 45 | 46 | // 任务请求枚举 47 | pub enum TaskRequest { 48 | 49 | QrCodeLoginRequest(QrCodeLoginRequest), 50 | LoginSmsRequest(LoginSmsRequest), 51 | PushRequest(PushRequest), 52 | SubmitLoginSmsRequest(SubmitLoginSmsRequest), 53 | GetAllorderRequest(GetAllorderRequest), 54 | GetTicketInfoRequest(GetTicketInfoRequest), 55 | GetBuyerInfoRequest(GetBuyerInfoRequest), 56 | GrabTicketRequest(GrabTicketRequest), 57 | } 58 | 59 | // 任务结果枚举 60 | #[derive(Clone)] 61 | pub enum TaskResult { 62 | 63 | QrCodeLoginResult(TaskQrCodeLoginResult), 64 | LoginSmsResult(LoginSmsRequestResult), 65 | PushResult(PushRequestResult), 66 | SubmitSmsLoginResult(SubmitSmsLoginResult), 67 | GetAllorderRequestResult(GetAllorderRequestResult), 68 | GetTicketInfoResult(GetTicketInfoResult), 69 | GetBuyerInfoResult(GetBuyerInfoResult), 70 | GrabTicketResult(GrabTicketResult), 71 | } 72 | //抢票请求 73 | #[derive(Clone,Debug)] 74 | pub struct GrabTicketRequest { 75 | pub task_id: String, 76 | pub uid: i64, 77 | pub project_id : String, 78 | pub screen_id : String, 79 | pub ticket_id: String, 80 | pub count: i16, 81 | pub buyer_info: Vec<BuyerInfo>, 82 | pub cookie_manager: Arc<CookieManager>, 83 | pub biliticket: BilibiliTicket, 84 | pub grab_mode: u8, 85 | pub status: TaskStatus, 86 | pub start_time: Option<Instant>, 87 | pub is_hot: bool, 88 | pub local_captcha: LocalCaptcha, 89 | pub skip_words: Option<Vec<String>>, 90 | 91 | } 92 | #[derive(Clone,Debug)] 93 | pub struct GrabTicketTask { 94 | pub task_id: String, 95 | pub biliticket: BilibiliTicket, 96 | pub status: TaskStatus, 97 | pub client: Arc<Client>, 98 | pub start_time: Option<Instant>, 99 | 100 | 101 | } 102 | #[derive(Clone,Debug)] 103 | pub struct GrabTicketResult { 104 | pub task_id: String, 105 | pub uid : i64, 106 | pub success: bool, 107 | pub message:String, 108 | pub order_id: Option<String>, 109 | pub pay_token: Option<String>, 110 | pub confirm_result: Option<ConfirmTicketResult>, 111 | pub pay_result: Option<CheckFakeResultData>, 112 | } 113 | //获取购票人信息 114 | #[derive(Clone,Debug)] 115 | pub struct GetBuyerInfoRequest { 116 | pub uid : i64, 117 | pub task_id : String, 118 | pub cookie_manager: Arc<CookieManager>, 119 | } 120 | 121 | #[derive(Clone,Debug)] 122 | pub struct GetBuyerInfoResult{ 123 | pub task_id : String, 124 | pub uid : i64, 125 | pub buyer_info: Option<BuyerInfoResponse>, 126 | pub success: bool, 127 | pub message : String, 128 | } 129 | #[derive(Clone,Debug)] 130 | pub struct GetBuyerInfoTask { 131 | pub uid : i64, 132 | pub task_id : String, 133 | pub status: TaskStatus, 134 | pub start_time : Option<Instant>, 135 | pub cookie_manager: Arc<CookieManager>, 136 | } 137 | //请求project_id票详情 138 | #[derive(Clone,Debug)] 139 | pub struct GetTicketInfoRequest { 140 | pub uid : i64, 141 | pub task_id : String, 142 | pub project_id : String, 143 | pub cookie_manager: Arc<CookieManager>, 144 | 145 | } 146 | 147 | #[derive(Clone,Debug)] 148 | pub struct GetTicketInfoResult { 149 | pub task_id : String, 150 | pub uid : i64, 151 | pub ticket_info: Option<InfoResponse>, 152 | pub success: bool, 153 | pub message : String, 154 | } 155 | 156 | #[derive(Clone,Debug)] 157 | pub struct GetTicketInfoTask { 158 | pub task_id : String, 159 | pub project_id : String, 160 | pub status: TaskStatus, 161 | pub start_time : Option<Instant>, 162 | pub cookie_manager: Arc<CookieManager>, 163 | } 164 | 165 | 166 | #[derive(Clone)] 167 | pub struct PushRequest{ 168 | pub title: String, 169 | pub message: String, 170 | pub jump_url: Option<String>, 171 | pub push_config: PushConfig, 172 | pub push_type : PushType, 173 | } 174 | 175 | //推送类型 176 | #[derive(Clone,Debug)] 177 | pub enum PushType { 178 | All, 179 | Bark, 180 | PushPlus, 181 | Fangtang, 182 | Dingtalk, 183 | WeChat, 184 | Smtp, 185 | } 186 | 187 | // 推送结果结构体 188 | #[derive(Clone)] 189 | pub struct PushRequestResult { 190 | pub task_id: String, 191 | pub success: bool, 192 | pub message: String, 193 | pub push_type: PushType, 194 | } 195 | 196 | 197 | #[derive(Clone)] 198 | pub struct PushTask { 199 | pub task_id: String, 200 | pub title: String, 201 | pub message:String, 202 | pub push_type: PushType, 203 | pub status: TaskStatus, 204 | pub start_time: Option<Instant>, 205 | } 206 | 207 | pub struct TicketTask { 208 | pub task_id: String, 209 | pub account_id: String, 210 | pub ticket_id: String, 211 | pub status: TaskStatus, 212 | pub start_time: Option<Instant>, 213 | pub result: Option<TicketResult>, 214 | } 215 | 216 | pub struct QrCodeLoginTask { 217 | pub task_id: String, 218 | pub qrcode_key: String, 219 | pub qrcode_url: String, 220 | pub status: TaskStatus, 221 | pub start_time: Option<Instant>, 222 | 223 | } 224 | 225 | pub struct LoginSmsRequestTask { 226 | pub task_id: String, 227 | pub phone : String, 228 | pub status: TaskStatus, 229 | pub start_time: Option<Instant>, 230 | 231 | } 232 | 233 | pub struct SubmitLoginSmsRequestTask { 234 | pub task_id: String, 235 | pub phone : String, 236 | pub code: String, 237 | pub captcha_key: String, 238 | pub status: TaskStatus, 239 | pub start_time: Option<Instant>, 240 | } 241 | 242 | //获取全部订单信息 243 | pub struct GetAllorderRequest { 244 | pub task_id: String, 245 | pub cookie_manager: Arc<CookieManager>, 246 | pub status: TaskStatus, 247 | pub cookies: String, 248 | pub account_id: String, 249 | pub start_time: Option<Instant>, 250 | 251 | } 252 | 253 | #[derive(Clone)] 254 | pub struct GetAllorderRequestResult { 255 | pub task_id: String, 256 | pub account_id: String, 257 | pub success: bool, 258 | pub message: String, 259 | pub order_info: Option<OrderResponse>, 260 | pub timestamp: Instant, 261 | } 262 | 263 | pub struct GetAllorderTask { 264 | pub task_id: String, 265 | pub account_id: String, 266 | pub status: TaskStatus, 267 | pub start_time: Option<Instant>, 268 | } 269 | 270 | 271 | pub struct TicketRequest { 272 | pub ticket_id: String, 273 | pub account_id: String, 274 | // 其他请求参数... 275 | } 276 | 277 | pub struct QrCodeLoginRequest { 278 | pub qrcode_key: String, 279 | pub qrcode_url: String, 280 | pub user_agent: Option<String>, 281 | } 282 | 283 | pub struct LoginSmsRequest { 284 | pub phone: String, 285 | pub client: Client, 286 | pub custom_config: CustomConfig, 287 | pub local_captcha: LocalCaptcha, 288 | } 289 | 290 | pub struct SubmitLoginSmsRequest { 291 | pub phone : String, 292 | pub code: String, 293 | pub captcha_key: String, 294 | pub client: Client, 295 | 296 | } 297 | 298 | 299 | 300 | #[derive(Clone)] 301 | pub struct TaskTicketResult { 302 | pub task_id: String, 303 | pub account_id: String, 304 | pub result: Result<TicketResult, String>, 305 | } 306 | 307 | #[derive(Clone)] 308 | pub struct TaskQrCodeLoginResult { 309 | pub task_id: String, 310 | pub status: crate::login::QrCodeLoginStatus, 311 | pub cookie: Option<String>, 312 | pub error: Option<String>, 313 | } 314 | 315 | #[derive(Clone)] 316 | pub struct LoginSmsRequestResult { 317 | pub task_id: String, 318 | pub phone: String, 319 | pub success: bool, 320 | pub message: String, 321 | } 322 | 323 | #[derive(Clone)] 324 | pub struct SubmitSmsLoginResult { 325 | pub task_id: String, 326 | pub phone: String, 327 | pub success: bool, 328 | pub message: String, 329 | pub cookie: Option<String>, 330 | } 331 | // 更新 TaskManager trait 332 | pub trait TaskManager: Send + 'static { 333 | // 创建新的任务管理器 334 | fn new() -> Self where Self: Sized; 335 | 336 | // 提交任务 337 | fn submit_task(&mut self, request: TaskRequest) -> Result<String, String>; 338 | 339 | // 获取可用结果,返回 TaskResult 枚举 340 | fn get_results(&mut self) -> Vec<TaskResult>; 341 | 342 | // 取消任务 343 | fn cancel_task(&mut self, task_id: &str) -> Result<(), String>; 344 | 345 | // 获取任务状态 346 | fn get_task_status(&self, task_id: &str) -> Option<TaskStatus>; 347 | 348 | // 关闭任务管理器 349 | fn shutdown(&mut self); 350 | } 351 | 352 | pub const DISCLAIMER_TEXT_ENCODED: &str = "4p2X5pys6aG555uu5a6M5YWo5YWN6LS55byA5rqQ77yM56aB5q2i5ZWG55So5oiW5pS26LS577yM5LuF5L6b5a2m5Lmg5L2/55So77yM6K+35Zyo5LiL6L29MjTlsI/ml7blhoXliKDpmaTvvIzkvb/nlKjmnKzova/ku7bpgKDmiJDnmoTkuIDliIflkI7mnpzor7foh6rooYzmib/mi4U="; 353 | 354 | pub fn TaskManager_debug() -> String { 355 | let bytes = base64::decode(DISCLAIMER_TEXT_ENCODED).unwrap_or_default(); 356 | String::from_utf8(bytes).unwrap_or_else(|_| "本项目免费开源".to_string()) 357 | } -------------------------------------------------------------------------------- /common/src/ticket.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use reqwest::header::HeaderValue; 4 | use reqwest::{header, Client}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | 8 | use crate::cookie_manager::CookieManager; 9 | use crate::account::Account; 10 | use crate::push::PushConfig; 11 | use crate::utility::CustomConfig; 12 | 13 | //成功下单结构体 14 | #[derive(Clone,Debug,Deserialize,Serialize)] 15 | pub struct SubmitOrderResult{ 16 | #[serde(rename = "orderId")] 17 | pub order_id: i128, 18 | #[serde(rename = "orderCreateTime")] 19 | pub order_create_time: i64, 20 | #[serde(rename = "token")] 21 | pub order_token: String, 22 | } 23 | 24 | #[derive(Clone,Debug,Deserialize,Serialize)] 25 | pub struct CheckFakeResult{ 26 | #[serde(default)] 27 | pub errno: i32, 28 | #[serde(default)] 29 | pub code: i32, 30 | #[serde(default)] 31 | pub errtag: i32, 32 | #[serde(default)] 33 | pub msg: String, 34 | #[serde(default)] 35 | pub message: String, 36 | pub data: CheckFakeResultParam, 37 | } 38 | #[derive(Clone,Debug,Deserialize,Serialize)] 39 | pub struct CheckFakeResultParam{ 40 | #[serde(rename = "payParam")] 41 | pub pay_param: CheckFakeResultData, 42 | } 43 | #[derive(Clone,Debug,Deserialize,Serialize)] 44 | pub struct CheckFakeResultData{ 45 | pub sign : String, 46 | pub code_url : String, 47 | } 48 | 49 | #[derive(Clone,Debug,Deserialize,Serialize)] 50 | pub struct ConfirmTicketInfo{ 51 | pub name: String, 52 | pub count: i32, 53 | pub price: i64, 54 | } 55 | 56 | //确认订单结构体 57 | #[derive(Clone,Debug,Deserialize,Serialize)] 58 | pub struct ConfirmTicketResult{ 59 | pub count : i32, 60 | pub pay_money: i64, 61 | pub project_name: String, 62 | pub screen_name: String, 63 | pub ticket_info: ConfirmTicketInfo, 64 | 65 | } 66 | 67 | //获取token响应结构体 68 | 69 | #[derive(Clone, Debug, Deserialize, Serialize)] 70 | pub struct TokenRiskParam { 71 | #[serde(default)] 72 | pub code : i32, 73 | 74 | #[serde(default)] 75 | pub message: String, 76 | 77 | pub mid : Option<String>, 78 | pub decision_type : Option<String>, 79 | pub buvid : Option<String>, 80 | pub ip : Option<String>, 81 | pub scene: Option<String>, 82 | pub ua: Option<String>, 83 | pub v_voucher: Option<String>, 84 | pub risk_param: Option<Value>, 85 | } 86 | 87 | #[derive(Clone,Debug)] 88 | pub struct BilibiliTicket{ 89 | pub uid : i64, //UID 90 | pub method : u8, 91 | pub ua : String, 92 | pub config: CustomConfig, 93 | pub account: Account, 94 | pub push_self : PushConfig, 95 | pub status_delay : usize, 96 | pub captcha_use_type: usize, //选择的验证码方式 97 | pub cookie_manager: Option<Arc<CookieManager>>, 98 | 99 | //抢票相关 100 | pub project_id: String, 101 | pub screen_id: String, 102 | pub id_bind: usize, //是否绑定 103 | 104 | pub project_info : Option<TicketInfo>, //项目详情 105 | pub all_buyer_info: Option<BuyerInfoData>, //所有购票人信息 106 | pub buyer_info: Option<Vec<BuyerInfo>>, //购买人信息(实名票) 107 | 108 | pub no_bind_buyer_info: Option<NoBindBuyerInfo>, //不实名制购票人信息 109 | 110 | pub select_ticket_id : Option<String>, 111 | 112 | pub pay_money: Option<i64>, //支付金额 113 | pub count: Option<i32>, //购买数量 114 | pub device_id: String, //设备id 115 | 116 | } 117 | 118 | impl BilibiliTicket{ 119 | pub fn new( 120 | 121 | method: &u8, 122 | ua: &String, 123 | config: &CustomConfig, 124 | account: &Account, 125 | push_self: &PushConfig, 126 | status_delay: &usize, 127 | project_id : &str, 128 | 129 | 130 | ) -> Self{ 131 | let mut finally_ua = String::new(); 132 | if config.custom_ua != "" { 133 | log::info!("使用自定义UA:{}",config.custom_ua); 134 | finally_ua.push_str(&config.custom_ua); 135 | }else{ 136 | log::info!("使用默认UA:{}",ua); 137 | finally_ua.push_str(ua); 138 | } 139 | let mut headers = header::HeaderMap::new(); 140 | match HeaderValue::from_str(&account.cookie){ 141 | Ok(ck_value) => { 142 | headers.insert(header::COOKIE, ck_value); 143 | match HeaderValue::from_str(&finally_ua){ 144 | Ok(ua_value) => { 145 | headers.insert(header::USER_AGENT,ua_value); 146 | } 147 | Err(e) => { 148 | log::error!("client插入ua失败!原因:{}",e); 149 | } 150 | } 151 | 152 | } 153 | Err(e) => { 154 | log::error!("cookie设置失败!原因:{:?}",e); 155 | } 156 | 157 | } 158 | 159 | 160 | let client = match Client::builder() 161 | .cookie_store(true) 162 | .user_agent(ua) 163 | .default_headers(headers) 164 | 165 | .build(){ 166 | Ok(client) => client, 167 | Err(e) => { 168 | log::error!("初始化client失败!,原因:{:?}",e); 169 | Client::new() 170 | } 171 | }; 172 | let captcha_type = config.captcha_mode; 173 | 174 | 175 | 176 | let new = Self{ 177 | uid: account.uid.clone(), 178 | method: method.clone(), 179 | ua: ua.clone(), 180 | config: config.clone(), 181 | account: account.clone(), 182 | push_self: push_self.clone(), 183 | status_delay: *status_delay, 184 | captcha_use_type: captcha_type, 185 | cookie_manager: None, 186 | project_id: project_id.to_string(), 187 | screen_id: String::new(), 188 | project_info: None, 189 | buyer_info: None, 190 | all_buyer_info: None, 191 | no_bind_buyer_info: None, 192 | select_ticket_id: None, 193 | pay_money: None, 194 | count: None, 195 | device_id: "".to_string(), 196 | id_bind: 999, 197 | 198 | }; 199 | log::debug!("新建抢票对象:{:?}",new); 200 | new 201 | 202 | } 203 | 204 | } 205 | 206 | #[derive(Clone,Debug,Deserialize,Serialize)] 207 | pub struct TicketInfo { 208 | pub id: i32, 209 | pub name: String, 210 | pub is_sale: usize, 211 | pub start_time: i64, 212 | pub end_time: i64, 213 | pub pick_seat: usize, //0:不选座 1:选座 214 | pub project_type: usize, //未知作用,bw2024是type1 215 | pub express_fee: usize, //快递费 216 | pub sale_begin: i64, //开售时间 217 | pub sale_end: i64, //截止时间 218 | pub count_down: i64, //倒计时(可能有负数) 219 | pub screen_list: Vec<ScreenInfo>, //场次列表 220 | pub sale_flag_number: usize, //售票标志位 221 | #[serde(default)] 222 | pub sale_flag: String, //售票状态 223 | pub is_free: bool, 224 | pub performance_desc: Option<DescribeList>, //基础信息 225 | pub id_bind: usize, //是否绑定 226 | #[serde(rename = "hotProject")] 227 | pub hot_project: bool, //是否热门项目 228 | 229 | 230 | 231 | 232 | } 233 | 234 | #[derive(Clone,Debug,Deserialize,Serialize)] 235 | pub struct ScreenInfo { 236 | #[serde(default)] 237 | pub sale_flag: SaleFlag, 238 | pub id: usize, 239 | pub start_time: usize, 240 | pub name: String, 241 | pub ticket_type: usize, 242 | pub screen_type: usize, 243 | pub delivery_type: usize, 244 | pub pick_seat: usize, 245 | pub ticket_list: Vec<ScreenTicketInfo>, //当日票种类列表 246 | pub clickable: bool, //是否可点(可售) 247 | pub sale_end: usize, //截止时间 248 | pub sale_start: usize, //开售时间 249 | pub sale_flag_number: usize, //售票标志位 250 | pub show_date: String, //展示信息 251 | 252 | } 253 | 254 | #[derive(Clone,Debug,Deserialize,Serialize)] 255 | pub struct SaleFlag { 256 | #[serde(default)] 257 | pub number: usize, 258 | #[serde(default)] 259 | pub display_name: String, 260 | } 261 | 262 | impl Default for SaleFlag { 263 | fn default() -> Self { 264 | Self { 265 | number: 0, 266 | display_name: "未知状态".to_string(), 267 | } 268 | } 269 | } 270 | 271 | #[derive(Clone,Debug,Deserialize,Serialize)] 272 | pub struct ScreenTicketInfo{ 273 | pub saleStart : usize, //开售时间(时间戳) eg:1720260000 274 | pub saleEnd : usize, //截止时间(时间戳) 275 | pub id: usize, //票种id 276 | pub project_id: usize, //项目id 277 | pub price: usize, //票价(分) 278 | pub desc: String, //票种描述 279 | pub sale_start: String, //开售时间(字符串) eg:2024-07-06 18:00:00 280 | pub sale_end: String, //截止时间(字符串) 281 | pub r#type: usize, //类型 关键词替换,对应”type“ 282 | pub sale_type: usize, //销售状态 283 | pub is_sale: usize, //是否销售?0是1否 284 | pub num: usize, //数量 285 | pub sale_flag: SaleFlag, //售票状态 286 | pub clickable: bool, //是否可点(可售) 287 | pub sale_flag_number: usize, //售票标志位 288 | pub screen_name: String, //场次名称 289 | 290 | 291 | } 292 | 293 | #[derive(Clone,Debug,Deserialize,Serialize)] 294 | pub struct DescribeList{ 295 | pub r#type: u8, // 使用 r# 前缀处理 Rust 关键字 296 | pub list: Vec<ModuleItem>, 297 | } 298 | 299 | #[derive(Clone, Debug, Serialize, Deserialize)] 300 | pub struct ModuleItem { 301 | pub module: String, 302 | 303 | // details 可能是字符串或数组,使用 serde_json::Value 处理多态 304 | #[serde(default)] 305 | pub details: Value, 306 | 307 | // 可选字段 308 | #[serde(default)] 309 | pub module_name: Option<String>, 310 | } 311 | 312 | // 为 base_info 模块中的详情项创建结构体 313 | #[derive(Clone, Debug, Serialize, Deserialize)] 314 | pub struct BaseInfoItem { 315 | pub title: String, 316 | pub content: String, 317 | } 318 | 319 | #[derive(Clone, Debug, Serialize, Deserialize)] 320 | pub struct InfoResponse{ 321 | #[serde(default)] 322 | pub errno: i32, 323 | #[serde(default)] 324 | pub errtag: i32, 325 | #[serde(default)] 326 | pub msg: String, 327 | #[serde(default)] 328 | pub code: i32, 329 | #[serde(default)] 330 | pub message: String, 331 | pub data: TicketInfo, 332 | } 333 | 334 | #[derive(Clone, Debug, Serialize, Deserialize)] 335 | pub struct BuyerInfo{ 336 | pub id: i64, 337 | pub uid: i64, 338 | pub personal_id: String, 339 | pub name: String, 340 | pub tel: String, 341 | pub id_type: i64, 342 | pub is_default: i64, 343 | #[serde(default)] 344 | pub id_card_front: String, 345 | #[serde(default)] 346 | pub id_card_back: String, 347 | #[serde(default)] 348 | pub verify_status: i64, 349 | #[serde(default)] 350 | pub isBuyerInfoVerified: bool, 351 | #[serde(default)] 352 | pub isBuyerValid: bool, 353 | 354 | 355 | } 356 | 357 | #[derive(Clone, Debug, Serialize, Deserialize)] 358 | pub struct BuyerInfoResponse{ 359 | #[serde(default)] 360 | pub errno: i32, 361 | #[serde(default)] 362 | pub errtag: i32, 363 | #[serde(default)] 364 | pub msg: String, 365 | #[serde(default)] 366 | pub code: i32, 367 | #[serde(default)] 368 | pub message: String, 369 | pub data: BuyerInfoData, 370 | } 371 | 372 | #[derive(Clone, Debug, Serialize, Deserialize)] 373 | pub struct BuyerInfoData{ 374 | pub list: Vec<BuyerInfo>, 375 | 376 | } 377 | 378 | #[derive(Clone, Debug, Serialize, Deserialize)] 379 | pub struct NoBindBuyerInfo { 380 | pub name: String, 381 | pub tel: String, 382 | pub uid: i64, 383 | } -------------------------------------------------------------------------------- /common/src/utility.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | 3 | 4 | #[derive(Clone, Debug, Serialize, Deserialize)] 5 | pub struct CustomConfig{ 6 | pub open_custom_ua: bool, //是否开启自定义UA 7 | pub custom_ua: String, //自定义UA 8 | pub captcha_mode: usize, //验证码模式 //0:本地打码 1:ttocr 9 | pub ttocr_key: String, //ttocr key 10 | pub preinput_phone1: String, //预填账号1手机号 11 | pub preinput_phone2: String, //预填账号2手机号 12 | 13 | 14 | } 15 | 16 | impl CustomConfig{ 17 | pub fn new() -> Self{ 18 | Self{ 19 | open_custom_ua: true, 20 | 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"), 21 | captcha_mode: 0, 22 | ttocr_key: String::new(), 23 | preinput_phone1: String::new(), 24 | preinput_phone2: String::new(), 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /common/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, process}; 2 | use std::fs::File; 3 | use std::io; 4 | use std::io::Write; 5 | use std::ops::{Index, IndexMut}; 6 | use std::sync::Arc; 7 | use serde_json::{Value, json, Map}; 8 | use crate::account::Account; 9 | use crate::cookie_manager::CookieManager; 10 | use crate::push::PushConfig; 11 | use crate::utility::CustomConfig; 12 | use base64::Engine as _; 13 | use base64::engine::general_purpose::STANDARD as BASE64; 14 | use block_modes::{BlockMode, Cbc}; 15 | use block_modes::block_padding::Pkcs7; 16 | use aes::Aes128; 17 | 18 | use rand::Rng; 19 | use std::path::Path; 20 | use reqwest::Client; 21 | 22 | #[derive(Clone,Debug)] 23 | pub struct Config{ 24 | data: Value, 25 | } 26 | 27 | impl Config { 28 | pub fn delete_json_config() -> io::Result<()> { 29 | fs::remove_file("config.json") 30 | } 31 | } 32 | 33 | impl Config{ 34 | pub fn load_config() -> io::Result<Self>{ 35 | let raw_context = fs::read_to_string("./config")?; 36 | let content = raw_context.split("%").collect::<Vec<&str>>(); 37 | // base64解码后解密 38 | let iv = BASE64.decode(content[0].trim()) 39 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; 40 | let decoded = BASE64.decode(content[1].trim()) 41 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; 42 | let decrypted = decrypt_data(iv, &decoded) 43 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; 44 | let plain_text = String::from_utf8(decrypted) 45 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; 46 | let data = serde_json::from_str(&plain_text)?; 47 | Ok(Self{data}) 48 | 49 | } 50 | pub fn load_json_config() -> io::Result<Self>{ 51 | let content = fs::read_to_string("./config.json")?; 52 | let data = serde_json::from_str(&content)?; 53 | Ok(Self{data}) 54 | 55 | } 56 | 57 | pub fn new() -> Self{ 58 | let data = json!({}); 59 | Self{data} 60 | } 61 | 62 | pub fn save_config(&self) -> io::Result<()> { //后续上加密 63 | let json_str = serde_json::to_string_pretty(&self.data)?; 64 | // 加密后base64编码 65 | let (iv,encrypted) = encrypt_data(json_str.as_bytes()) 66 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 67 | let encoded_iv = BASE64.encode(&iv); 68 | let encoded_encrypted = BASE64.encode(&encrypted); 69 | fs::write("./config", encoded_iv+"%" + &*encoded_encrypted) 70 | } 71 | 72 | 73 | //添加账号 74 | pub fn add_account(&mut self, account: &Account) -> io::Result<()>{ 75 | if !self["accounts"].is_array(){ //不存在则创建 76 | self["accounts"]= json!([]); 77 | } 78 | 79 | let account_json = serde_json::to_value(account)?; 80 | 81 | if let Value::Array(ref mut accounts)= self["accounts"]{ 82 | accounts.push(account_json); 83 | } 84 | 85 | Ok(()) 86 | } 87 | 88 | //加载账号 89 | pub fn load_accounts(&self) -> Result<Vec<Account>,serde_json::Error>{ 90 | if self["accounts"].is_array(){ 91 | let accounts_json = &self["accounts"]; 92 | serde_json::from_value(accounts_json.clone()) 93 | } 94 | else{ 95 | Ok(Vec::new()) 96 | } 97 | } 98 | 99 | //账号更新(Account更新后调用这个保存,uid唯一寻找标识) 100 | pub fn update_account(&mut self, account: &Account) ->io::Result<bool>{ 101 | if !self["accounts"].is_array(){ 102 | return Ok(false); 103 | } 104 | 105 | let account_json = serde_json::to_value(account)?; 106 | if let Value::Array(ref mut accounts) = self["accounts"]{ 107 | for (index, acc) in accounts.iter_mut().enumerate() { 108 | if let Some(uid) = acc["uid"].as_i64(){ 109 | if uid == account.uid{ 110 | accounts[index] = account_json; 111 | return Ok(true); 112 | } 113 | } } 114 | } 115 | Ok(false) 116 | 117 | } 118 | 119 | //删除账号,传uid 120 | pub fn delete_account(&mut self, uid: i64) ->bool{ 121 | if !self["accounts"].is_array(){ 122 | return false; 123 | } 124 | 125 | let mut remove_flag = false; 126 | if let Value::Array(ref mut accounts )= self["accounts"]{ 127 | let old_len = accounts.len(); 128 | accounts.retain(|acc|{ 129 | if let Some(account_uid) = acc["uid"].as_i64(){ 130 | account_uid != uid 131 | } 132 | else{ 133 | true 134 | } 135 | }); 136 | remove_flag = accounts.len() != old_len; 137 | } 138 | match save_config(self, None, None, None){ 139 | Ok(_) => { 140 | log::info!("删除账号成功"); 141 | }, 142 | Err(e) => { 143 | log::error!("删除账号失败: {}", e); 144 | } 145 | } 146 | remove_flag 147 | } 148 | 149 | pub fn load_all_accounts() -> Vec<Account> { 150 | match Self::load_config() { 151 | Ok(config) => { 152 | match config.load_accounts() { 153 | Ok(accounts) => accounts, 154 | Err(e) => { 155 | log::error!("加载账号失败: {}", e); 156 | Vec::new() 157 | } 158 | } 159 | }, 160 | Err(e) => { 161 | log::error!("加载配置文件失败: {}", e); 162 | Vec::new() 163 | } 164 | } 165 | } 166 | 167 | } 168 | 169 | impl Index<&str> for Config{ 170 | type Output = Value; 171 | 172 | fn index(&self, key: &str) -> &Self::Output{ 173 | 174 | match self.data.get(key){ 175 | Some(value) => value, 176 | None => &Value::Null, 177 | } 178 | 179 | } 180 | } 181 | 182 | // 实现索引修改 183 | impl IndexMut<&str> for Config { 184 | fn index_mut(&mut self, key: &str) -> &mut Self::Output { 185 | if let Value::Object(ref mut map) = self.data { 186 | map.entry(key.to_string()).or_insert(Value::Null) 187 | } else { 188 | // 如果当前不是对象,将其转换为对象 189 | let mut map = Map::new(); 190 | map.insert(key.to_string(), Value::Null); 191 | self.data = Value::Object(map); 192 | 193 | if let Value::Object(ref mut map) = self.data { 194 | map.get_mut(key).unwrap() 195 | } else { 196 | unreachable!() // 理论上不可能到达这里 197 | } 198 | } 199 | } 200 | } 201 | 202 | pub fn save_config(config: &mut Config, push_config: Option<&PushConfig>, custon_config: Option<&CustomConfig>, account: Option<Account>) -> Result<bool, String> { 203 | if let Some(push_config) = push_config { 204 | config["push_config"] = serde_json::to_value(push_config).unwrap(); 205 | } 206 | if let Some(custon_config) = custon_config { 207 | config["custom_config"] = serde_json::to_value(custon_config).unwrap(); 208 | } 209 | if let Some(account) = account { 210 | config.add_account(&account).unwrap(); 211 | } 212 | 213 | 214 | match config.save_config(){ 215 | Ok(_) => { 216 | log::info!("配置文件保存成功"); 217 | Ok(true) 218 | }, 219 | Err(e) => { 220 | log::error!("配置文件保存失败: {}", e); 221 | Err(e.to_string()) 222 | } 223 | } 224 | 225 | } 226 | pub fn load_texture_from_path(ctx: &eframe::egui::Context, path: &str, name: &str) -> Option<eframe::egui::TextureHandle> { 227 | use std::io::Read; 228 | 229 | 230 | match File::open(path) { 231 | 232 | Ok(mut file) => { 233 | let mut bytes = Vec::new(); 234 | if file.read_to_end(&mut bytes).is_ok() { 235 | match image::load_from_memory(&bytes) { 236 | Ok(image) => { 237 | let size = [image.width() as usize, image.height() as usize]; 238 | let image_buffer = image.to_rgba8(); 239 | let pixels = image_buffer.as_flat_samples(); 240 | 241 | Some(ctx.load_texture( 242 | name, 243 | eframe::egui::ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()), 244 | Default::default() 245 | )) 246 | } 247 | Err(_) => None, 248 | } 249 | } else { 250 | None 251 | } 252 | } 253 | Err(_) => None, 254 | } 255 | } 256 | 257 | 258 | fn write_bytes_to_file(file_path: &str, bytes: &[u8]) -> io::Result<()> { 259 | let mut file = File::create(file_path)?; // 创建文件 260 | file.write_all(bytes)?; // 写入字节流 261 | file.flush()?; // 确保数据写入磁盘 262 | Ok(()) 263 | } 264 | 265 | pub fn load_texture_from_url(ctx: &eframe::egui::Context, cookie_manager: Arc<CookieManager>, url: &String, name: &str) -> Option<eframe::egui::TextureHandle> { 266 | let rt = tokio::runtime::Runtime::new().unwrap(); 267 | 268 | 269 | let bytes = rt.block_on(async { 270 | // 发送请求 271 | let resp = match cookie_manager.get(url).await.send().await { 272 | Ok(resp) => resp, 273 | Err(err) => { 274 | log::error!("HTTP请求失败: {}", err); 275 | return None; 276 | } 277 | }; 278 | 279 | // 读取响应体 280 | match resp.bytes().await { 281 | Ok(bytes) => Some(bytes), 282 | Err(err) => { 283 | log::error!("读取响应体失败: {}", err); 284 | None 285 | } 286 | } 287 | }); 288 | 289 | 290 | let bytes = match bytes { 291 | Some(b) => b, 292 | None => return None, 293 | }; 294 | 295 | // 处理图像数据 296 | match image::load_from_memory(&bytes) { 297 | Ok(image) => { 298 | let size = [image.width() as usize, image.height() as usize]; 299 | let image_buffer = image.to_rgba8(); 300 | let pixels = image_buffer.as_flat_samples(); 301 | 302 | Some(ctx.load_texture( 303 | name, 304 | eframe::egui::ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()), 305 | Default::default() 306 | )) 307 | } 308 | Err(err) => { 309 | log::warn!("加载图片至内存失败: {},url:{}", err, url); 310 | None 311 | } 312 | } 313 | } 314 | 315 | 316 | fn gen_machine_id_bytes_128b()->Vec<u8> { 317 | let id: String = machine_uid::get().unwrap(); 318 | println!("{}", id); 319 | id[..16].as_bytes().to_vec() 320 | 321 | } 322 | // 加密函数 323 | fn encrypt_data(data: &[u8]) -> Result<(Vec<u8>,Vec<u8>), block_modes::BlockModeError> { 324 | type Aes128Cbc = Cbc<Aes128, Pkcs7>; 325 | let mut iv = [0u8; 16]; 326 | rand::thread_rng() 327 | .fill(&mut iv[..]); // 填充 16 字节的随机数据 328 | let cipher = Aes128Cbc::new_from_slices(&gen_machine_id_bytes_128b(), &iv) 329 | .map_err(|_| block_modes::BlockModeError)?; // 将 InvalidKeyIvLength 转换为 BlockModeError 330 | 331 | Ok((iv.to_vec(), cipher.encrypt_vec(data))) 332 | } 333 | 334 | fn decrypt_data(iv:Vec<u8>,encrypted: &[u8]) -> Result<Vec<u8>, block_modes::BlockModeError> { 335 | type Aes128Cbc = Cbc<Aes128, Pkcs7>; 336 | let cipher = Aes128Cbc::new_from_slices(&gen_machine_id_bytes_128b(), &iv) 337 | .map_err(|_| block_modes::BlockModeError)?; // 将 InvalidKeyIvLength 转换为 BlockModeError 338 | 339 | cipher.decrypt_vec(encrypted) 340 | } 341 | 342 | // 单例锁实现,防止程序多开 343 | use single_instance::SingleInstance; 344 | 345 | // 简化后的单例检查实现 346 | pub fn ensure_single_instance() -> bool { 347 | // 使用应用程序唯一标识 348 | let app_id = "bili_ticket_rush_6BA7B79C-0E4F-4FCC-B7A2-4DA5E8D7E0F6"; // GUID 保证唯一性 349 | let instance = SingleInstance::new(app_id).unwrap(); 350 | 351 | if !instance.is_single() { 352 | log::error!("程序已经在运行中,请勿重复启动!"); 353 | eprintln!("程序已经在运行中,请勿重复启动!"); 354 | std::thread::sleep(std::time::Duration::from_secs(2)); 355 | false 356 | } else { 357 | // 保持实例在程序生命周期内 358 | Box::leak(Box::new(instance)); 359 | true 360 | } 361 | } 362 | 363 | // 为不支持的平台提供默认实现 364 | #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] 365 | fn is_process_running(_pid: u32) -> bool { 366 | false // 不支持的平台,假设进程不存在 367 | } 368 | 369 | #[cfg(target_os = "windows")] 370 | fn is_process_running(pid: u32) -> bool { 371 | use std::process::Command; 372 | 373 | // 使用 tasklist 命令检查进程 374 | let output = Command::new("tasklist") 375 | .args(&["/NH", "/FI", &format!("PID eq {}", pid)]) 376 | .output(); 377 | 378 | match output { 379 | Ok(out) => { 380 | let stdout = String::from_utf8_lossy(&out.stdout); 381 | !stdout.contains("信息: 没有运行的任务匹配指定标准") && 382 | !stdout.contains("No tasks") && 383 | stdout.contains(&format!("{}", pid)) 384 | }, 385 | Err(_) => false, // 执行命令失败,假设进程不存在 386 | } 387 | } 388 | 389 | #[cfg(target_os = "linux")] 390 | fn is_process_running(pid: u32) -> bool { 391 | Path::new(&format!("/proc/{}", pid)).exists() 392 | } 393 | 394 | #[cfg(target_os = "macos")] 395 | fn is_process_running(pid: u32) -> bool { 396 | use std::process::Command; 397 | 398 | // 使用 ps 命令检查进程 399 | let output = Command::new("ps") 400 | .args(&["-p", &format!("{}", pid)]) 401 | .output(); 402 | 403 | match output { 404 | Ok(out) => { 405 | let stdout = String::from_utf8_lossy(&out.stdout); 406 | stdout.contains(&format!("{}", pid)) 407 | }, 408 | Err(_) => false, // 执行命令失败,假设进程不存在 409 | } 410 | } 411 | 412 | pub async fn get_now_time(client: &Client) -> i64 { 413 | // 获取网络时间 (秒级) 414 | let url = "https://api.bilibili.com/x/click-interface/click/now"; 415 | 416 | let now_sec = match client.get(url).send().await { 417 | Ok(response) => { 418 | match response.text().await { 419 | Ok(text) => { 420 | log::debug!("API原始响应:{}", text); 421 | 422 | let json_data: serde_json::Value = serde_json::from_str(&text).unwrap_or( 423 | json!({ 424 | "code": 0, 425 | "data": { 426 | "now": 0 427 | } 428 | }) 429 | ); 430 | 431 | let now_sec = json_data["data"]["now"].as_i64().unwrap_or(0); 432 | log::debug!("解析出的网络时间(秒级):{}", now_sec); 433 | now_sec 434 | }, 435 | Err(e) => { 436 | log::debug!("解析网络时间响应失败:{}", e); 437 | 0 438 | } 439 | } 440 | }, 441 | Err(e) => { 442 | log::debug!("获取网络时间失败,原因:{}", e); 443 | 0 444 | } 445 | }; 446 | 447 | // 如果网络时间获取失败,使用本地时间 (转换为秒) 448 | if now_sec == 0 { 449 | log::debug!("使用本地时间"); 450 | let local_sec = chrono::Utc::now().timestamp(); 451 | log::debug!("本地时间(秒级):{}", local_sec); 452 | local_sec 453 | } else { 454 | now_sec 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /common/src/web_ck_obfuscated.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Client as _c1; 2 | use serde_json::Value as _v1; 3 | use std::time::{Duration as _d1, SystemTime as _st, UNIX_EPOCH as _ue}; 4 | use rand::{Rng as _r1, seq::SliceRandom as _sr}; 5 | use md5; 6 | use chrono::{Local as _l1, Utc as _u1}; 7 | use uuid::Uuid as _uid; 8 | use hmac::{Hmac as _h1, Mac as _m1}; 9 | use sha2::Sha256 as _s256; 10 | use std::collections::HashMap as _hm; 11 | 12 | const _A1: &[u8] = &[97, 112, 105, 46, 98, 105, 108, 105, 98, 105, 108, 105, 46, 99, 111, 109]; 13 | const _X1: &[u8] = &[120, 47, 102, 114, 111, 110, 116, 101, 110, 100, 47, 102, 105, 110, 103, 101, 114, 47, 115, 112, 105]; 14 | 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]; 15 | 16 | macro_rules! _cx { 17 | ($e:expr) => { 18 | unsafe { std::str::from_utf8_unchecked($e) } 19 | }; 20 | } 21 | 22 | fn _x1(_i1: u8, _i2: u8) -> u8 { 23 | (_i1.wrapping_add(_i2) ^ 0x33).wrapping_sub(7) 24 | } 25 | 26 | fn _x2<T: AsRef<[u8]>>(data: T) -> String { 27 | let _d = data.as_ref(); 28 | let _l = _d.len(); 29 | let _r: Vec<u8> = (0.._l).map(|i| _x1(_d[i], (i % 13) as u8)).collect(); 30 | _cx!(&_r).to_string() 31 | } 32 | 33 | fn _dx2<T: AsRef<str>>(data: T) -> String { 34 | let _s = data.as_ref().as_bytes(); 35 | let _l = _s.len(); 36 | let _r: Vec<u8> = (0.._l).map(|i| { 37 | let _x = _x1(_s[i], 0x44); 38 | (_x + (i % 13) as u8) & 0xff 39 | }).collect(); 40 | _cx!(&_r).to_string() 41 | } 42 | 43 | 44 | pub async fn gen_buvid3and4(client: _c1) -> Result<(String, String, String), String> { 45 | let _k1 = 5; 46 | let _k2 = 500; 47 | let _k3 = format!("https://{}/{}", _cx!(_A1), _cx!(_X1)); 48 | 49 | let mut _i1 = 0; 50 | 51 | loop { 52 | _i1 += 1; 53 | if _i1 > _k1 { 54 | break; 55 | } 56 | 57 | let _r1 = _z1(&client, &_k3, _i1 > 3).await; 58 | match _r1 { 59 | Ok((_a1, _a2, _a3)) => return Ok((_a1, _a2, _a3)), 60 | Err(_e1) => { 61 | if _i1 == _k1 { 62 | return Err(_x2(format!("获取 buvid 失败: {}", _e1))); 63 | } 64 | 65 | let _msg = format!("第{}次获取 buvid 失败: {},稍后重试", _i1, _e1); 66 | log::warn!("{}", _dx2(&_msg)); 67 | 68 | let _f = (_i1 * 100) as u64; 69 | std::thread::sleep(_d1::from_millis(_k2 + _f)); 70 | 71 | if _rand_bool(0.3) { 72 | _obfuscated_delay(); 73 | } 74 | } 75 | } 76 | } 77 | 78 | Err(format!("获取 buvid 重试次数已达上限")) 79 | } 80 | 81 | async fn _z1(client: &_c1, url: &str, _add_params: bool) -> Result<(String, String, String), String> { 82 | let mut _req = client.get(url); 83 | 84 | 85 | if _add_params { 86 | _req = _req.query(&[("_", _u1::now().timestamp_millis())]); 87 | } 88 | 89 | let _res = _req.send().await 90 | .map_err(|e| format!("请求失败: {}", e))?; 91 | 92 | if !_res.status().is_success() { 93 | return Err(format!("请求失败,状态码: {}", _res.status())); 94 | } 95 | 96 | let _j: _v1 = _res.json().await 97 | .map_err(|e| format!("解析 JSON 失败: {}", e))?; 98 | 99 | 100 | _extract_buv_data(_j).await 101 | } 102 | 103 | async fn _extract_buv_data(_json: _v1) -> Result<(String, String, String), String> { 104 | let _data = if let Some(d) = _json.get("data") { d } else { 105 | return Err("返回 JSON 中缺少 data 字段".to_string()); 106 | }; 107 | 108 | 109 | let _fields = ["b_3", "b_4"]; 110 | let mut _values = Vec::with_capacity(2); 111 | 112 | for &_f in &_fields { 113 | let _v = match _data.get(_f).and_then(|v| v.as_str()) { 114 | Some(s) => s.to_string(), 115 | None => return Err(format!("返回 JSON 中缺少 {} 字段", _f)) 116 | }; 117 | _values.push(_v); 118 | } 119 | 120 | 121 | let _t1 = _st::now().duration_since(_ue) 122 | .map_err(|e| format!("获取系统时间失败: {}", e))?; 123 | 124 | let _t2 = if _rand_bool(0.5) { 125 | _t1.as_secs() 126 | } else { 127 | _u1::now().timestamp() as u64 128 | }; 129 | 130 | let _b_nut = _t2.to_string(); 131 | 132 | if _rand_bool(0.7) { 133 | log::debug!("b_3: {}, b_4: {}, b_nut: {}", _values[0], _values[1], _b_nut); 134 | } 135 | 136 | Ok((_values[0].clone(), _values[1].clone(), _b_nut)) 137 | } 138 | 139 | 140 | fn random_md5() -> String { 141 | let mut _rng = rand::thread_rng(); 142 | let _complexity = _rng.gen_range(0..3); 143 | 144 | let _val = match _complexity { 145 | 0 => _rng.r#gen::<f64>(), 146 | 1 => _rng.r#gen::<u64>() as f64 / 1000.0, 147 | _ => { 148 | let _base = _rng.r#gen::<f64>(); 149 | let _factor = _rng.r#gen::<f64>() * 0.1; 150 | _base + _factor 151 | } 152 | }; 153 | 154 | let _data = _val.to_string(); 155 | let _digest = md5::compute(_data.as_bytes()); 156 | 157 | let _hex = format!("{:x}", _digest); 158 | 159 | if _rand_bool(0.2) { 160 | 161 | let _len = _hex.len(); 162 | _hex.chars().enumerate() 163 | .map(|(i, c)| if i % 7 == 3 { c } else { c }) 164 | .collect() 165 | } else { 166 | _hex 167 | } 168 | } 169 | 170 | 171 | pub fn gen_fp() -> String { 172 | _generate_fingerprint() 173 | } 174 | 175 | fn _generate_fingerprint() -> String { 176 | 177 | let _md5_val = random_md5(); 178 | let _time_str = _l1::now().format("%Y%m%d%H%M%S").to_string(); 179 | 180 | 181 | let _hex_str = _gen_random_hex(16); 182 | 183 | 184 | let _raw_fp = format!("{}{}{}", _md5_val, _time_str, _hex_str); 185 | 186 | 187 | let _check_val = _calculate_checksum(&_raw_fp); 188 | 189 | 190 | format!("{}{}", _raw_fp, _check_val) 191 | } 192 | 193 | fn _gen_random_hex(_len: usize) -> String { 194 | let _chars = "0123456789abcdef"; 195 | let _char_vec: Vec<char> = _chars.chars().collect(); 196 | let mut _rng = rand::thread_rng(); 197 | 198 | let mut _result = String::with_capacity(_len); 199 | for _ in 0.._len { 200 | let _idx = if _rand_bool(0.95) { 201 | 202 | _rng.gen_range(0.._char_vec.len()) 203 | } else { 204 | 205 | (_rng.r#gen::<u8>() as usize) % _char_vec.len() 206 | }; 207 | 208 | _result.push(_char_vec[_idx]); 209 | } 210 | 211 | _result 212 | } 213 | 214 | fn _calculate_checksum(_input: &str) -> String { 215 | 216 | let _chunks: Vec<&str> = _input 217 | .as_bytes() 218 | .chunks(2) 219 | .map(|chunk| std::str::from_utf8(chunk).unwrap_or("")) 220 | .collect(); 221 | 222 | let mut _sum = 0u32; 223 | let mut _i = 0; 224 | 225 | 226 | while _i < _chunks.len() { 227 | if _i % 2 == 0 { 228 | _sum = _sum.wrapping_add( 229 | u32::from_str_radix(_chunks[_i], 16).unwrap_or(0) 230 | ); 231 | } else { 232 | 233 | let _val = u32::from_str_radix(_chunks[_i], 16).unwrap_or(0); 234 | _sum = _sum + _val; 235 | } 236 | _i += 2; 237 | } 238 | 239 | format!("{:x}", _sum % 256) 240 | } 241 | 242 | 243 | pub fn gen_uuid_infoc() -> String { 244 | let _now = if _rand_bool(0.5) { 245 | _u1::now().timestamp_millis() 246 | } else { 247 | _st::now().duration_since(_ue).unwrap_or_default().as_millis() as i64 248 | }; 249 | 250 | let _t = (_now % 100_000) as u32; 251 | let _t_str = format!("{:0<5}", _t); 252 | 253 | 254 | let _uuid = if _rand_bool(0.6) { 255 | _uid::new_v4().to_string() 256 | } else { 257 | let mut _u = [0u8; 16]; 258 | rand::thread_rng().fill(&mut _u); 259 | _uid::from_bytes(_u).to_string() 260 | }; 261 | 262 | format!("{}{}infoc", _uuid, _t_str) 263 | } 264 | 265 | 266 | pub async fn gen_ckbili_ticket(client: _c1) -> Result<(String, String), String> { 267 | const _MAX: u32 = 5; 268 | const _DELAY: u64 = 500; 269 | 270 | let _url = format!("https://{}/{}", _cx!(_A1), _cx!(_X2)); 271 | 272 | for _i in 1..=_MAX { 273 | let _res = _get_ticket(&client, &_url).await; 274 | 275 | match _res { 276 | Ok((_t1, _t2)) => return Ok((_t1, _t2)), 277 | Err(_e) => { 278 | if _i == _MAX { 279 | return Err(format!("获取 ckbili_ticket 失败: {}", _e)); 280 | } 281 | 282 | log::warn!("第{}次获取 ckbili_ticket 失败: {},稍后重试", _i, _e); 283 | std::thread::sleep(_d1::from_millis(_DELAY + (_i as u64 * 50))); 284 | } 285 | } 286 | } 287 | 288 | Err("获取 ckbili_ticket 重试次数已达上限".to_string()) 289 | } 290 | 291 | 292 | async fn _get_ticket(client: &_c1, _url: &str) -> Result<(String, String), String> { 293 | let (_ts, _hex) = _prepare_ticket_params().await?; 294 | 295 | 296 | let mut _params = _hm::new(); 297 | _params.insert("key_id".to_string(), "ec02".to_string()); 298 | 299 | 300 | _add_ticket_params(&mut _params, _ts, _hex); 301 | 302 | // 发送请求并处理结果 303 | _send_ticket_request(client, _url, _params).await 304 | } 305 | 306 | async fn _prepare_ticket_params() -> Result<(u64, String), String> { 307 | 308 | let _ts = _st::now().duration_since(_ue) 309 | .map_err(|e| format!("获取系统时间失败: {}", e))? 310 | .as_secs(); 311 | 312 | 313 | let _key = "XgwSnGZ1p"; 314 | let _msg = format!("ts{}", _ts); 315 | let _hex = _calc_hmac(_key, &_msg)?; 316 | 317 | Ok((_ts, _hex)) 318 | } 319 | 320 | fn _add_ticket_params(_params: &mut _hm<String, String>, _ts: u64, _hex: String) { 321 | _params.insert("hexsign".to_string(), _hex); 322 | _params.insert("context[ts]".to_string(), _ts.to_string()); 323 | _params.insert("csrf".to_string(), "".to_string()); 324 | } 325 | 326 | async fn _send_ticket_request( 327 | client: &_c1, 328 | url: &str, 329 | params: _hm<String, String> 330 | ) -> Result<(String, String), String> { 331 | 332 | let _resp = client.post(url) 333 | .query(¶ms) 334 | .send() 335 | .await 336 | .map_err(|e| format!("请求失败: {}", e))?; 337 | 338 | if !_resp.status().is_success() { 339 | return Err(format!("请求失败,状态码: {}", _resp.status())); 340 | } 341 | 342 | 343 | let _json: _v1 = _resp.json() 344 | .await 345 | .map_err(|e| format!("解析 JSON 失败: {}", e))?; 346 | 347 | 348 | _extract_ticket_data(_json).await 349 | } 350 | 351 | async fn _extract_ticket_data(_json: _v1) -> Result<(String, String), String> { 352 | let _data = _json.get("data") 353 | .ok_or_else(|| "返回 JSON 中缺少 data 字段".to_string())?; 354 | 355 | 356 | let _ticket = _data.get("ticket") 357 | .and_then(|v| v.as_str()) 358 | .ok_or_else(|| "返回 JSON 中缺少 ticket 字段".to_string())? 359 | .to_string(); 360 | 361 | 362 | let _created = _data.get("created_at") 363 | .and_then(|v| v.as_i64()) 364 | .ok_or_else(|| "返回 JSON 中缺少 created_at 字段".to_string())?; 365 | 366 | let _ttl = _data.get("ttl") 367 | .and_then(|v| v.as_i64()) 368 | .ok_or_else(|| "返回 JSON 中缺少 ttl 字段".to_string())?; 369 | 370 | let _expires = (_created + _ttl).to_string(); 371 | 372 | 373 | if let (Some(_img), Some(_sub)) = ( 374 | _data.get("nav").and_then(|n| n.get("img")).and_then(|v| v.as_str()), 375 | _data.get("nav").and_then(|n| n.get("sub")).and_then(|v| v.as_str()) 376 | ) { 377 | log::debug!("获取到图片URL: {}, 子URL: {}", _img, _sub); 378 | } 379 | 380 | log::debug!("bili_ticket: {}, expires: {}", _ticket, _expires); 381 | Ok((_ticket, _expires)) 382 | } 383 | 384 | 385 | fn _calc_hmac(key: &str, message: &str) -> Result<String, String> { 386 | type _H = _h1<_s256>; 387 | 388 | let mut _mac = _H::new_from_slice(key.as_bytes()) 389 | .map_err(|e| format!("HMAC 初始化失败: {}", e))?; 390 | 391 | _mac.update(message.as_bytes()); 392 | 393 | let _result = _mac.finalize(); 394 | let _bytes = _result.into_bytes(); 395 | 396 | Ok(hex::encode(_bytes)) 397 | } 398 | 399 | pub fn gen_01x88() -> String { 400 | let _x1 = |_n: u8| -> bool { (_n & 0x2D) == 0x2D }; 401 | 402 | let _t0 = std::time::SystemTime::now(); 403 | let _r1 = rand::thread_rng().r#gen::<u16>() % 4 > 0; 404 | 405 | let _id_src = if _r1 { 406 | 407 | let _uuid_raw = uuid::Uuid::new_v4(); 408 | _uuid_raw.as_bytes().to_vec() 409 | } else { 410 | 411 | let mut _bytes = [0u8; 16]; 412 | rand::thread_rng().fill(&mut _bytes); 413 | let _u = uuid::Uuid::from_bytes(_bytes); 414 | _u.as_bytes().to_vec() 415 | }; 416 | 417 | 418 | let mut _result = String::with_capacity(32); 419 | let _hex = "0123456789abcdef".as_bytes(); 420 | 421 | for &_b in _id_src.iter() { 422 | _result.push(_hex[(_b >> 4) as usize] as char); 423 | _result.push(_hex[(_b & 0xf) as usize] as char); 424 | } 425 | 426 | 427 | let _elapsed = _t0.elapsed().unwrap_or_default(); 428 | if _elapsed.as_nanos() % 2 == 0 { 429 | _result.chars().filter(|&c| c != '-').collect() 430 | } else { 431 | _result 432 | } 433 | } 434 | 435 | fn _rand_bool(probability: f64) -> bool { 436 | rand::thread_rng().r#gen::<f64>() < probability 437 | } 438 | 439 | fn _obfuscated_delay() { 440 | let _delay = rand::thread_rng().gen_range(10..30); 441 | std::thread::sleep(_d1::from_millis(_delay)); 442 | } 443 | 444 | 445 | fn hmac_sha256(key: &str, message: &str) -> Result<String, Box<dyn std::error::Error>> { 446 | _calc_hmac(key, message).map_err(|e| e.into()) 447 | } -------------------------------------------------------------------------------- /frontend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frontend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | eframe = { version = "0.23.0", features = ["default"] } 8 | egui = "0.31.0" 9 | winapi = { version = "0.3.9", features = ["winuser", "windef"] } 10 | 11 | # 字体依赖 12 | egui_extras = { version = "0.23.0", features = ["all_loaders"] } 13 | 14 | # 背景图片 15 | image = "0.25" 16 | 17 | #时间 18 | chrono = "0.4" 19 | 20 | #日志 21 | log = "0.4" 22 | env_logger = "0.9" 23 | 24 | #序列化 25 | base64 = "0.13" 26 | 27 | #str转二维码 28 | qrcode = "0.14.1" 29 | 30 | 31 | #网络请求 32 | reqwest = { version="0.11.22", features=["json", "blocking", "cookies"]} 33 | tokio = { version = "1", features = ["full"] } 34 | 35 | 36 | #rand 37 | rand = "0.8" 38 | 39 | #json 40 | serde = { version = "1.0", features = ["derive"] } 41 | serde_json = "1.0" 42 | 43 | #jsonwebtoken 44 | jsonwebtoken = "9" 45 | 46 | 47 | 48 | common = {path = "../common"} 49 | backend = {path = "../backend"} 50 | 51 | -------------------------------------------------------------------------------- /frontend/assets/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biliticket/bili_ticket_rush/5b2926b0697a75350e387da026564b5e99bdd8b2/frontend/assets/background.jpg -------------------------------------------------------------------------------- /frontend/assets/background1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biliticket/bili_ticket_rush/5b2926b0697a75350e387da026564b5e99bdd8b2/frontend/assets/background1.jpg -------------------------------------------------------------------------------- /frontend/assets/background2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biliticket/bili_ticket_rush/5b2926b0697a75350e387da026564b5e99bdd8b2/frontend/assets/background2.jpg -------------------------------------------------------------------------------- /frontend/assets/default_avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biliticket/bili_ticket_rush/5b2926b0697a75350e387da026564b5e99bdd8b2/frontend/assets/default_avatar.jpg -------------------------------------------------------------------------------- /frontend/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | use eframe::epaint::Vec2; 4 | mod app; 5 | mod ui; 6 | mod windows; 7 | fn main() -> Result<(), eframe::Error> { 8 | std::env::set_var("LIBGL_ALWAYS_SOFTWARE", "1"); // 强制软件渲染 9 | std::env::set_var("MESA_GL_VERSION_OVERRIDE", "3.3"); // 尝试覆盖 GL 版本 10 | std::env::set_var("GALLIUM_DRIVER", "llvmpipe"); // 使用 llvmpipe 软件渲染器 11 | if let Err(e) = common::init_logger() { 12 | eprintln!("初始化日志失败,原因: {}", e); 13 | } 14 | log::info!("日志初始化成功"); 15 | 16 | std::panic::set_hook(Box::new(|panic_info| { 17 | if let Some(s) = panic_info.payload().downcast_ref::<&str>() { 18 | if s.contains("swap") || s.contains("vsync") { 19 | log::warn!("图形渲染非致命错误: {}", s); 20 | // 继续允许程序运行 21 | } else { 22 | log::error!("程序panic: {}", panic_info); 23 | } 24 | } else { 25 | log::error!("程序panic: {}", panic_info); 26 | } 27 | })); 28 | 29 | // 检查程序是否已经在运行 30 | if !common::utils::ensure_single_instance() { 31 | eprintln!("程序已经在运行中,请勿重复启动!"); 32 | //增加休眠时间,防止程序过快退出 33 | std::thread::sleep(std::time::Duration::from_secs(5)); 34 | std::process::exit(1); 35 | } 36 | 37 | // 创建资源目录(如果不存在) 38 | create_resources_directory(); 39 | 40 | let options = eframe::NativeOptions { 41 | initial_window_size: Some(Vec2::new(1200.0, 600.0)), 42 | min_window_size: Some(Vec2::new(800.0, 600.0)), 43 | vsync: false, 44 | 45 | ..Default::default() 46 | }; 47 | 48 | eframe::run_native( 49 | "原神", 50 | options, 51 | Box::new(|cc| Box::new(app::Myapp::new(cc))), 52 | ) 53 | } 54 | 55 | // 确保资源目录存在 56 | fn create_resources_directory() { 57 | let resources_dir = std::path::Path::new("resources/fonts"); 58 | if !resources_dir.exists() { 59 | if let Err(e) = std::fs::create_dir_all(resources_dir) { 60 | log::warn!("无法创建资源目录: {}", e); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend/src/main_old.rs: -------------------------------------------------------------------------------- 1 | use eframe::{egui, epaint::Vec2}; 2 | use egui::FontId; 3 | use std::fs::read; 4 | use chrono::Local; 5 | 6 | fn main() -> Result<(), eframe::Error> { 7 | let options = eframe::NativeOptions { 8 | initial_window_size: Some(Vec2::new(1100.0, 600.0)), 9 | min_window_size: Some(Vec2::new(800.0, 600.0)), 10 | ..Default::default() 11 | }; 12 | 13 | eframe::run_native( 14 | "原神", 15 | options, 16 | Box::new(|cc| Box::new(MyApp::new(cc))) 17 | ) 18 | } 19 | 20 | struct MyApp { 21 | left_panel_width: f32, 22 | selected_tab: usize, // 当前选中标签页索引 23 | is_loading :bool, //加载动画 24 | loading_angle : f32, //加载动画角度 25 | background_texture: Option<egui::TextureHandle>,//背景纹理 26 | show_log_window: bool, 27 | logs: Vec<String>, 28 | 29 | } 30 | 31 | impl MyApp { 32 | fn new(cc: &eframe::CreationContext<'_>) -> Self { 33 | // 配置中文字体 34 | Self::configure_fonts(&cc.egui_ctx); 35 | 36 | let mut app =Self { 37 | left_panel_width: 250.0, 38 | selected_tab: 0, // 默认选中第一个标签 39 | is_loading : false, 40 | loading_angle : 0.0, 41 | background_texture: None, 42 | //初始化日志 43 | show_log_window: false, 44 | logs: Vec::new(), 45 | 46 | }; 47 | 48 | /* app.load_background(&cc.egui_ctx);*/ 49 | app 50 | } 51 | 52 | // 配置字体函数 53 | fn configure_fonts(ctx: &egui::Context) { 54 | // 创建字体配置 55 | let mut fonts = egui::FontDefinitions::default(); 56 | 57 | // 使用std::fs::read读取字体文件 58 | let font_data = read("C:/Windows/Fonts/msyh.ttc").unwrap_or_else(|_| { 59 | // 备用字体 60 | read("C:/Windows/Fonts/simhei.ttf").unwrap() 61 | }); 62 | 63 | // 使用from_owned方法创建FontData 64 | fonts.font_data.insert( 65 | "microsoft_yahei".to_owned(), 66 | egui::FontData::from_owned(font_data) 67 | ); 68 | 69 | // 将中文字体添加到所有字体族中 70 | for family in fonts.families.values_mut() { 71 | family.insert(0, "microsoft_yahei".to_owned()); 72 | } 73 | 74 | // 应用字体 75 | ctx.set_fonts(fonts); 76 | } 77 | 78 | // 各标签页内容渲染函数 79 | fn render_tab_content(&mut self, ui: &mut egui::Ui) { 80 | match self.selected_tab { 81 | 0 => { 82 | ui.heading("预留抢票界面公告栏1"); 83 | ui.separator(); 84 | //开始抢票按钮 85 | 86 | ui.vertical_centered(|ui| { 87 | // 垂直居中 88 | ui.add_space(ui.available_height() * 0.2); 89 | 90 | // 创建按钮 91 | let button = egui::Button::new( 92 | egui::RichText::new("开始抢票").size(40.0).color(egui::Color32::WHITE) 93 | ) 94 | .min_size(egui::vec2(300.0, 150.0)) 95 | .fill(egui::Color32::from_rgb(131, 175, 155)) 96 | .rounding(20.0); 97 | 98 | // 只有点击按钮时才触发 99 | if ui.add(button).clicked() { 100 | self.is_loading = true; 101 | 102 | //待完善鉴权账号及有效信息 103 | } 104 | }); 105 | 106 | 107 | }, 108 | 1 => { 109 | self.show_log_window = true; 110 | ui.heading("预留监视公告栏2"); 111 | ui.separator(); 112 | }, 113 | 2 => { 114 | ui.heading("抢票设置"); 115 | ui.separator(); 116 | ui.label("这里配置自动抢票参数"); 117 | 118 | ui.checkbox(&mut true, "启用自动抢票"); 119 | ui.add_space(5.0); 120 | 121 | ui.horizontal(|ui| { 122 | ui.label("刷新间隔:"); 123 | ui.add(egui::Slider::new(&mut 1.0, 0.5..=5.0).suffix(" 秒")); 124 | }); 125 | 126 | ui.horizontal(|ui| { 127 | ui.label("最大尝试次数:"); 128 | ui.add(egui::DragValue::new(&mut 50).clamp_range(10..=100)); 129 | }); 130 | }, 131 | 3 => { 132 | ui.heading("账号管理"); 133 | ui.separator(); 134 | ui.label("这里管理B站账号信息"); 135 | 136 | ui.horizontal(|ui| { 137 | ui.label("用户名:"); 138 | ui.text_edit_singleline(&mut "示例用户".to_string()); 139 | }); 140 | 141 | ui.horizontal(|ui| { 142 | ui.label("密码:"); 143 | ui.text_edit_singleline(&mut "********".to_string()); 144 | }); 145 | 146 | if ui.button("保存账号信息").clicked() { 147 | // 保存账号信息 148 | } 149 | }, 150 | 4 => { 151 | ui.heading("系统设置"); 152 | ui.separator(); 153 | ui.label("这里是系统配置项"); 154 | 155 | ui.checkbox(&mut true, "开机启动"); 156 | ui.checkbox(&mut false, "启用通知提醒"); 157 | ui.checkbox(&mut true, "自动更新"); 158 | 159 | ui.add_space(10.0); 160 | ui.horizontal(|ui| { 161 | ui.label("缓存大小:"); 162 | ui.add(egui::Slider::new(&mut 500.0, 100.0..=1000.0).suffix(" MB")); 163 | }); 164 | }, 165 | _ => unreachable!(), 166 | } 167 | } 168 | //背景图 169 | /* fn load_background(&mut self, ctx:&egui::Context){ 170 | let image_byte= include_bytes!("../assets/background.jpg"); 171 | if let Ok(image) =image::load_from_memory(image_byte){ 172 | let rgb_image = image.to_rgba8(); 173 | let dimensions= rgb_image.dimensions(); 174 | let image = egui::ColorImage::from_rgba_unmultiplied([dimensions.0 as usize, dimensions.1 as usize] , &rgb_image.into_raw()); 175 | let texture = ctx.load_texture( 176 | "background", image, Default::default()); 177 | self.background_texture = Some(texture);}} 178 | */ 179 | /* fn load_background(&mut self, ctx: &egui::Context) { 180 | println!("开始加载背景图片"); 181 | //let image_path = "../assets/background.jpg"; 182 | let image_byte = include_bytes!("../assets/background.jpg"); 183 | 184 | println!("图片数据大小: {} 字节", image_byte.len()); 185 | 186 | match image::load_from_memory(image_byte) { 187 | Ok(image) => { 188 | 189 | let rgb_image = image.to_rgba8(); 190 | let dimensions = rgb_image.dimensions(); 191 | println!("图片加载成功,尺寸: {:?}", dimensions); 192 | let image = egui::ColorImage::from_rgba_unmultiplied( 193 | [dimensions.0 as usize, dimensions.1 as usize], 194 | &rgb_image.into_raw() 195 | ); 196 | let texture = ctx.load_texture("background", image, Default::default()); 197 | self.background_texture = Some(texture); 198 | println!("背景纹理创建成功"); 199 | }, 200 | Err(e) => { 201 | println!("图片加载失败: {}", e); 202 | } 203 | } 204 | } */ 205 | 206 | } 207 | 208 | impl eframe::App for MyApp { 209 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 210 | /* // 加载背景 211 | if let Some(texture) = &self.background_texture { 212 | let screen_rect = ctx.screen_rect(); 213 | let painter = ctx.layer_painter(egui::LayerId::new( 214 | egui::Order::Background, // 确保在最底层 215 | egui::Id::new("background_layer") 216 | )); 217 | 218 | painter.image( 219 | texture.id(), 220 | screen_rect, 221 | egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), 222 | egui::Color32::from_rgba_unmultiplied(255, 255, 255, 150) 223 | ); 224 | } */ 225 | // 创建左右两栏布局 226 | egui::SidePanel::left("left_panel") 227 | .resizable(true) 228 | .default_width(self.left_panel_width) 229 | .width_range(150.0..=400.0) 230 | .show(ctx, |ui| { 231 | 232 | 233 | // 左侧五个选项 234 | let tab_names = ["开始抢票", "监视面板", "修改信息", "设置/微调", "帮助/关于"]; 235 | let icons = ["😎", "🎫", "⚙️", "🔧", "🧐"]; // 使用表情符号作为简单图标 236 | 237 | // 均分空间 238 | let available_height = ui.available_height(); 239 | let item_count = tab_names.len(); 240 | let item_height = available_height / item_count as f32; 241 | 242 | 243 | for (idx, (name, icon)) in tab_names.iter().zip(icons.iter()).enumerate() { 244 | let is_selected = self.selected_tab == idx; 245 | 246 | 247 | ui.allocate_ui_with_layout( 248 | egui::vec2(ui.available_width(), item_height), 249 | egui::Layout::centered_and_justified(egui::Direction::LeftToRight), 250 | |ui| { 251 | // 选项样式 - 选中时突出显示 252 | let mut text = egui::RichText::new(format!("{} {}", icon, name)).size(16.0); 253 | if is_selected { 254 | text = text.strong().color(egui::Color32::from_rgb(66, 150, 250)); 255 | } 256 | 257 | 258 | 259 | if ui.selectable_value(&mut self.selected_tab, idx, text).clicked() { 260 | 261 | } 262 | } 263 | ); 264 | } 265 | }); 266 | 267 | egui::CentralPanel::default().show(ctx, |ui| { 268 | // 渲染右侧对应内容 269 | self.render_tab_content(ui); 270 | }); 271 | // 如果在加载中,绘制覆盖层 272 | if self.is_loading { 273 | // 创建覆盖整个界面的区域 274 | let screen_rect = ctx.input(|i| i.screen_rect()); 275 | let layer_id = egui::LayerId::new(egui::Order::Foreground, egui::Id::new("loading_overlay")); 276 | let ui = ctx.layer_painter(layer_id); 277 | 278 | // 半透明背景 279 | ui.rect_filled( 280 | screen_rect, 281 | 0.0, 282 | egui::Color32::from_rgba_unmultiplied(0, 0, 0, 180) 283 | ); 284 | 285 | // 在屏幕中央显示加载动画 286 | let center = screen_rect.center(); 287 | 288 | // 更新动画角度 289 | self.loading_angle += 0.05; 290 | if self.loading_angle > std::f32::consts::TAU { 291 | self.loading_angle -= std::f32::consts::TAU; 292 | } 293 | 294 | // 绘制动画 295 | // 背景圆环 296 | ui.circle_stroke( 297 | center, 298 | 30.0, 299 | egui::Stroke::new(5.0, egui::Color32::from_gray(100)) 300 | ); 301 | 302 | // 动画圆弧 303 | let mut points = Vec::new(); 304 | let segments = 32; 305 | let start_angle = self.loading_angle; 306 | let end_angle = start_angle + std::f32::consts::PI; 307 | 308 | for i in 0..=segments { 309 | let angle = start_angle + (end_angle - start_angle) * (i as f32 / segments as f32); 310 | let point = center + 30.0 * egui::Vec2::new(angle.cos(), angle.sin()); 311 | points.push(point); 312 | } 313 | 314 | ui.add(egui::Shape::line( 315 | points, 316 | egui::Stroke::new(5.0, egui::Color32::from_rgb(66, 150, 250)) 317 | )); 318 | 319 | // 加载文字 320 | ui.text( 321 | center + egui::vec2(0.0, 50.0), 322 | egui::Align2::CENTER_CENTER, 323 | "加载中...", 324 | egui::FontId::proportional(16.0), 325 | egui::Color32::WHITE 326 | ); 327 | 328 | // 强制持续重绘以保持动画 329 | ctx.request_repaint(); 330 | } 331 | 332 | //日志窗口 333 | if self.show_log_window { 334 | // Using a temporary variable to track window close action 335 | let mut window_open = self.show_log_window; 336 | egui::Window::new("监视面板") 337 | .open(&mut window_open) // 使用临时变量 338 | .default_size([500.0, 400.0]) // 设置默认大小 339 | .resizable(true) // 允许调整大小 340 | .show(ctx, |ui| { 341 | // 顶部工具栏 342 | ui.horizontal(|ui| { 343 | if ui.button("清空日志").clicked() { 344 | self.logs.clear(); 345 | } 346 | 347 | if ui.button("添加测试日志").clicked() { 348 | let timestamp = chrono::Local::now().format("%H:%M:%S").to_string(); 349 | self.logs.push(format!("[{}] 测试日志消息", timestamp)); 350 | } 351 | 352 | ui.with_layout(egui::Layout::right_to_left(egui::Align::RIGHT), |ui| { 353 | if ui.button("❌").clicked() { 354 | // 使用close_button替代直接修改window_open 355 | self.show_log_window = false; 356 | } 357 | }); 358 | }); 359 | 360 | ui.separator(); 361 | 362 | // 日志内容区域(可滚动) 363 | egui::ScrollArea::vertical() 364 | .auto_shrink([false, false]) 365 | .stick_to_bottom(true) 366 | .show(ui, |ui| { 367 | // 显示当前状态 368 | ui.label(format!("当前状态: {}", 369 | if self.is_loading {"正在抢票中..."} else {"空闲"})); 370 | 371 | ui.separator(); 372 | 373 | // 显示所有日志 374 | if self.logs.is_empty() { 375 | ui.label("暂无日志记录"); 376 | } else { 377 | for log in &self.logs { 378 | ui.label(log); 379 | ui.separator(); 380 | } 381 | } 382 | }); 383 | // 底部状态栏 384 | ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { 385 | ui.label(format!("共 {} 条日志", self.logs.len())); 386 | }); 387 | }); 388 | 389 | // 更新窗口状态 390 | self.show_log_window = window_open; 391 | 392 | } 393 | 394 | } 395 | } -------------------------------------------------------------------------------- /frontend/src/ui/error_banner.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use crate::app::Myapp; 3 | 4 | // 定义横幅类型枚举 5 | #[derive(PartialEq)] 6 | pub enum BannerType { 7 | Error, 8 | Success, 9 | } 10 | 11 | // 重命名函数为更通用的名称 12 | pub fn render_notification_banner(app: &Myapp, ctx: &egui::Context) { 13 | let screen_rect = ctx.available_rect(); 14 | let banner_height = 40.0; 15 | 16 | // 创建一个位于屏幕顶部的区域 17 | let banner_rect = egui::Rect::from_min_size( 18 | egui::pos2(screen_rect.min.x, screen_rect.min.y), 19 | egui::vec2(screen_rect.width(), banner_height) 20 | ); 21 | 22 | // 使用Area绝对定位横幅 23 | egui::Area::new("notification_banner") 24 | .fixed_pos(banner_rect.min) 25 | .show(ctx, |ui| { 26 | // 根据当前激活的横幅类型选择颜色 27 | let (fill_color, stroke_color) = if app.success_banner_active { 28 | // 成功横幅 - 浅绿色 29 | ( 30 | egui::Color32::from_rgba_premultiplied( 31 | 130, 220, 130, (app.success_banner_opacity * 255.0) as u8 32 | ), 33 | egui::Color32::from_rgba_premultiplied( 34 | 100, 200, 100, (app.success_banner_opacity * 255.0) as u8 35 | ) 36 | ) 37 | } else { 38 | // 错误横幅 - 橙红色 39 | ( 40 | egui::Color32::from_rgba_premultiplied( 41 | 245, 130, 90, (app.error_banner_opacity * 255.0) as u8 42 | ), 43 | egui::Color32::from_rgba_premultiplied( 44 | 225, 110, 70, (app.error_banner_opacity * 255.0) as u8 45 | ) 46 | ) 47 | }; 48 | 49 | // 设置框架样式 50 | let frame = egui::Frame::none() 51 | .fill(fill_color) 52 | .stroke(egui::Stroke::new(1.0, stroke_color)); 53 | 54 | frame.show(ui, |ui| { 55 | ui.set_max_width(screen_rect.width()); 56 | 57 | // 居中白色文本 58 | ui.vertical_centered(|ui| { 59 | ui.add_space(5.0); 60 | let banner_text = if app.success_banner_active { 61 | &app.success_banner_text 62 | } else { 63 | &app.error_banner_text 64 | }; 65 | let text = egui::RichText::new(banner_text) 66 | .color(egui::Color32::WHITE) 67 | .size(16.0) 68 | .strong(); 69 | ui.label(text); 70 | ui.add_space(5.0); 71 | }); 72 | }); 73 | }); 74 | } 75 | 76 | // 为了向后兼容,保留原函数名,但内部调用新函数 77 | pub fn render_error_banner(app: &Myapp, ctx: &egui::Context) { 78 | render_notification_banner(app, ctx); 79 | } -------------------------------------------------------------------------------- /frontend/src/ui/fonts.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use std::fs::read; 3 | use std::path::Path; 4 | 5 | // 配置字体函数 6 | pub fn configure_fonts(ctx: &egui::Context) { 7 | // 创建字体配置 8 | let mut fonts = egui::FontDefinitions::default(); 9 | 10 | // 根据不同操作系统选择合适的字体路径 11 | let font_data = load_system_font(); 12 | 13 | // 使用from_owned方法创建FontData 14 | fonts.font_data.insert( 15 | "chinese_font".to_owned(), 16 | egui::FontData::from_owned(font_data) 17 | ); 18 | 19 | // 将中文字体添加到所有字体族中 20 | for family in fonts.families.values_mut() { 21 | family.insert(0, "chinese_font".to_owned()); 22 | } 23 | 24 | // 应用字体 25 | ctx.set_fonts(fonts); 26 | } 27 | 28 | // 根据操作系统加载合适的字体 29 | fn load_system_font() -> Vec<u8> { 30 | #[cfg(target_os = "windows")] 31 | { 32 | // 尝试多个Windows字体路径 33 | let font_paths = [ 34 | "C:/Windows/Fonts/msyh.ttc", 35 | "C:/Windows/Fonts/simhei.ttf", 36 | "C:/Windows/Fonts/simsun.ttc", 37 | "C:/Windows/Fonts/msyh.ttf" 38 | ]; 39 | 40 | for path in font_paths { 41 | if Path::new(path).exists() { 42 | if let Ok(data) = read(path) { 43 | log::info!("加载字体: {}", path); 44 | return data; 45 | } 46 | } 47 | } 48 | } 49 | 50 | #[cfg(target_os = "macos")] 51 | { 52 | // macOS系统字体路径 53 | let font_paths = [ 54 | "/System/Library/Fonts/PingFang.ttc", 55 | "/Library/Fonts/Arial Unicode.ttf", 56 | "/System/Library/Fonts/STHeiti Light.ttc", 57 | "/System/Library/Fonts/Hiragino Sans GB.ttc" 58 | ]; 59 | 60 | for path in font_paths { 61 | if Path::new(path).exists() { 62 | if let Ok(data) = read(path) { 63 | log::info!("加载字体: {}", path); 64 | return data; 65 | } 66 | } 67 | } 68 | } 69 | 70 | #[cfg(target_os = "linux")] 71 | { 72 | // Linux系统字体路径 73 | let font_paths = [ 74 | "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", 75 | "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", 76 | "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", 77 | "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc" 78 | ]; 79 | 80 | for path in font_paths { 81 | if Path::new(path).exists() { 82 | if let Ok(data) = read(path) { 83 | log::info!("加载字体: {}", path); 84 | return data; 85 | } 86 | } 87 | } 88 | } 89 | 90 | // 如果所有系统字体都无法加载,使用内置的字体 91 | log::warn!("无法加载系统中文字体,使用内置字体"); 92 | include_bytes!("../../../resources/fonts/NotoSansSC-Regular.otf").to_vec() 93 | } 94 | -------------------------------------------------------------------------------- /frontend/src/ui/loading.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use crate::app::Myapp; 3 | 4 | pub fn render_loading_overlay(app: &mut Myapp, ctx: &egui::Context) { 5 | // 创建覆盖整个界面的区域 6 | let screen_rect = ctx.screen_rect(); 7 | let layer_id = egui::LayerId::new(egui::Order::Foreground, egui::Id::new("loading_overlay")); 8 | let ui = ctx.layer_painter(layer_id); 9 | 10 | // 半透明背景 11 | ui.rect_filled( 12 | screen_rect, 13 | 0.0, 14 | egui::Color32::from_rgba_unmultiplied(0, 0, 0, 180) 15 | ); 16 | 17 | // 在屏幕中央显示加载动画 18 | let center = screen_rect.center(); 19 | 20 | // 更新动画角度 21 | app.loading_angle += 0.05; 22 | if app.loading_angle > std::f32::consts::TAU { 23 | app.loading_angle -= std::f32::consts::TAU; 24 | } 25 | 26 | // 背景圆环 27 | ui.circle_stroke( 28 | center, 29 | 30.0, 30 | egui::Stroke::new(5.0, egui::Color32::from_gray(100)) 31 | ); 32 | 33 | // 动画圆弧 34 | let mut points = Vec::new(); 35 | let segments = 32; 36 | let start_angle = app.loading_angle; 37 | let end_angle = start_angle + std::f32::consts::PI; 38 | 39 | for i in 0..=segments { 40 | let angle = start_angle + (end_angle - start_angle) * (i as f32 / segments as f32); 41 | let point = center + 30.0 * egui::Vec2::new(angle.cos(), angle.sin()); 42 | points.push(point); 43 | } 44 | 45 | ui.add(egui::Shape::line( 46 | points, 47 | egui::Stroke::new(5.0, egui::Color32::from_rgb(66, 150, 250)) 48 | )); 49 | 50 | // 加载文字 51 | ui.text( 52 | center + egui::vec2(0.0, 50.0), 53 | egui::Align2::CENTER_CENTER, 54 | "加载中...", 55 | egui::FontId::proportional(16.0), 56 | egui::Color32::WHITE 57 | ); 58 | 59 | // 强制持续重绘以保持动画 60 | ctx.request_repaint(); 61 | } -------------------------------------------------------------------------------- /frontend/src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fonts; 2 | pub mod sidebar; 3 | pub mod tabs; 4 | pub mod loading; 5 | pub mod error_banner; -------------------------------------------------------------------------------- /frontend/src/ui/sidebar.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use crate::app::Myapp; 3 | 4 | pub fn render_sidebar(app: &mut Myapp, ctx: &egui::Context){ 5 | // 创建左右两栏布局 6 | egui::SidePanel::left("left_panel") 7 | .resizable(true) 8 | .default_width(app.left_panel_width) 9 | .width_range(150.0..=400.0) 10 | .show(ctx, |ui| { 11 | 12 | 13 | // 左侧五个选项 14 | let tab_names = ["开始抢票", "监视面板", "修改信息", "设置/微调", "帮助/关于"]; 15 | let icons = ["😎", "🎫", "📁", "🔧", "📋"]; // 使用表情符号作为简单图标 16 | 17 | // 均分空间 18 | let available_height = ui.available_height(); 19 | let item_count = tab_names.len(); 20 | let item_height = available_height / item_count as f32; 21 | 22 | 23 | for (idx, (name, icon)) in tab_names.iter().zip(icons.iter()).enumerate() { 24 | let is_selected = app.selected_tab == idx; 25 | 26 | 27 | ui.allocate_ui_with_layout( 28 | egui::vec2(ui.available_width(), item_height), 29 | egui::Layout::centered_and_justified(egui::Direction::LeftToRight), 30 | |ui| { 31 | // 选项样式 - 选中时突出显示 32 | let mut text = egui::RichText::new(format!("{} {}", icon, name)).size(16.0); 33 | if is_selected { 34 | text = text.strong().color(egui::Color32::from_rgb(255, 255, 255)); 35 | } 36 | 37 | 38 | 39 | if ui.selectable_value(&mut app.selected_tab, idx, text).clicked() { 40 | 41 | } 42 | } 43 | ); 44 | } 45 | }); 46 | } -------------------------------------------------------------------------------- /frontend/src/ui/tabs/help.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use crate::app::Myapp; 3 | 4 | pub fn render(app: &mut Myapp, ui: &mut egui::Ui) { 5 | 6 | ui.heading("预留帮助公告栏"); 7 | ui.separator(); 8 | ui.label("本项目地址:https://github.com/biliticket/bili_ticket_rush"); 9 | } -------------------------------------------------------------------------------- /frontend/src/ui/tabs/home.rs: -------------------------------------------------------------------------------- 1 | use std::u32; 2 | 3 | use eframe::egui; 4 | use eframe::egui::Widget; 5 | use crate::app::Myapp; 6 | use common::account::{Account}; 7 | use common::taskmanager::{TaskStatus, TicketRequest, TaskManager_debug}; 8 | use common::ticket::BilibiliTicket; 9 | 10 | 11 | pub fn render(app: &mut Myapp, ui: &mut egui::Ui) { 12 | //页面标题 13 | ui.vertical_centered(|ui| { 14 | ui.add_space(20.0); 15 | ui.heading(egui::RichText::new("仅供学习的小工具").size(32.0).strong()); 16 | ui.add_space(10.0); 17 | ui.label(egui::RichText::new(TaskManager_debug()) 18 | .size(14.0) 19 | .color(egui::Color32::from_rgb(255, 120, 50)) 20 | .strong()); 21 | ui.add_space(10.0); 22 | ui.label(egui::RichText::new("请输入项目ID或粘贴票务链接,点击开始抢票").size(16.0).color(egui::Color32::GRAY)); 23 | ui.add_space(10.0); 24 | if let Some(accounce) = app.announce1.clone() { 25 | ui.label(egui::RichText::new(accounce) 26 | .size(14.0) 27 | .color(egui::Color32::from_rgb(255, 120, 50)) 28 | .strong()); 29 | } 30 | ui.add_space(25.0); 31 | 32 | //输入区域 33 | ticket_input_area(ui, app); 34 | }); 35 | } 36 | 37 | fn ticket_input_area(ui: &mut egui::Ui, app: &mut Myapp) { 38 | //居中布局的输入框和按钮组合 39 | ui.vertical_centered(|ui| { 40 | ui.spacing_mut().item_spacing = egui::vec2(0.0, 20.0); 41 | 42 | //输入框布局 43 | let response = styled_ticket_input(ui, &mut app.ticket_id); 44 | 45 | // 新增:账号和抢票模式选择区域 46 | ui.add_space(15.0); 47 | styled_selection_area(ui, app); 48 | ui.add_space(15.0); 49 | 50 | //抢票按钮 51 | if styled_grab_button(ui).clicked() { 52 | if !check_input_ticket(&mut app.ticket_id) {app.show_log_window = true; return}; 53 | if app.account_manager.accounts.is_empty() { 54 | log::info!("没有可用账号,请登录账号"); 55 | app.show_login_windows = true; 56 | return 57 | } 58 | let select_uid = match app.selected_account_uid { 59 | Some(uid) => uid, 60 | None => { 61 | log::error!("没有选择账号,请选择账号!"); 62 | return 63 | } 64 | }; 65 | let bilibili_ticket: BilibiliTicket = BilibiliTicket::new( 66 | 67 | &app.grab_mode, 68 | &app.default_ua, 69 | &app.custom_config, 70 | &app.account_manager.accounts 71 | .iter() 72 | .find(|a| a.uid == select_uid) 73 | .unwrap(), 74 | 75 | &app.push_config, 76 | &app.status_delay, 77 | &app.ticket_id, 78 | ); 79 | app.bilibiliticket_list.push(bilibili_ticket); 80 | log::debug!("当前抢票对象列表:{:?}", app.bilibiliticket_list); 81 | match app.grab_mode{ 82 | 0|1 => { 83 | app.show_screen_info = Some(select_uid); 84 | } 85 | 2 => { 86 | app.confirm_ticket_info = Some(select_uid.to_string()); 87 | } 88 | _ => { 89 | log::error!("当前模式不支持!请检查输入!"); 90 | } 91 | } 92 | 93 | 94 | } 95 | 96 | //底部状态文本 97 | ui.add_space(30.0); 98 | /* let status_text = match app.is_loading { 99 | true => egui::RichText::new(&app.running_status).color(egui::Color32::from_rgb(255, 165, 0)), 100 | false => egui::RichText::new("等待开始...").color(egui::Color32::GRAY), 101 | }; 102 | ui.label(status_text); */ 103 | }); 104 | } 105 | 106 | //输入框 107 | fn styled_ticket_input(ui: &mut egui::Ui, text: &mut String) -> egui::Response { 108 | //创建一个适当大小的容器 109 | let desired_width = 250.0; 110 | 111 | ui.horizontal(|ui| { 112 | ui.add_space((ui.available_width() - desired_width) / 2.0); 113 | 114 | egui::Frame::none() 115 | .fill(egui::Color32::from_rgb(245, 245, 250)) 116 | .rounding(10.0) 117 | .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 220))) 118 | .shadow(egui::epaint::Shadow::small_light()) 119 | .inner_margin(egui::vec2(12.0, 10.0)) 120 | .show(ui, |ui| { 121 | ui.spacing_mut().item_spacing = egui::vec2(8.0, 0.0); 122 | 123 | // 左侧图标 124 | ui.label(egui::RichText::new("🎫").size(18.0)); 125 | 126 | // 输入框 127 | let font_id = egui::FontId::new(20.0, egui::FontFamily::Proportional); 128 | ui.style_mut().override_font_id = Some(font_id.clone()); 129 | 130 | let input = egui::TextEdit::singleline(text) 131 | .hint_text("输入票务ID") 132 | .desired_width(180.0) 133 | .text_color(egui::Color32::BLACK) //指定文本颜色防止深色模式抽风 134 | .margin(egui::vec2(0.0, 6.0)) 135 | .frame(false); 136 | 137 | ui.add(input) 138 | }) 139 | .inner 140 | }).inner 141 | } 142 | 143 | //选择模式区域UI 144 | fn styled_selection_area(ui: &mut egui::Ui, app: &mut Myapp) { 145 | // 容器宽度与抢票按钮相同,保持一致性 146 | let panel_width = 400.0; 147 | 148 | ui.horizontal(|ui| { 149 | ui.add_space((ui.available_width() - panel_width) / 2.0); 150 | 151 | egui::Frame::none() 152 | .fill(egui::Color32::from_rgb(245, 245, 250)) 153 | .rounding(8.0) 154 | .stroke(egui::Stroke::new(0.5, egui::Color32::from_rgb(200, 200, 220))) 155 | .shadow(egui::epaint::Shadow::small_light()) 156 | .inner_margin(egui::vec2(16.0, 12.0)) 157 | .show(ui, |ui| { 158 | ui.set_width(panel_width - 32.0); // 减去内边距 159 | 160 | ui.vertical(|ui| { 161 | // 账号选择 162 | account_selection(ui, app); 163 | 164 | ui.add_space(12.0); 165 | ui.separator(); 166 | ui.add_space(12.0); 167 | 168 | // 抢票模式选择 169 | grab_mode_selection(ui, app); 170 | }); 171 | }); 172 | }); 173 | } 174 | 175 | // 账号选择UI 176 | fn account_selection(ui: &mut egui::Ui, app: &mut Myapp) { 177 | ui.horizontal(|ui| { 178 | ui.label(egui::RichText::new("选择账号:").color(egui::Color32::BLACK).size(16.0).strong()); 179 | 180 | // 如果没有账号,显示提示 181 | if app.account_manager.accounts.is_empty() { 182 | ui.label(egui::RichText::new("未登录账号").color(egui::Color32::RED).italics()); 183 | ui.add_space(8.0); 184 | if egui::Button::new(egui::RichText::new("去登录").size(14.0).color(egui::Color32::BLUE)) 185 | .fill(egui::Color32::LIGHT_GRAY) // 设置背景颜色 186 | .ui(ui) 187 | .clicked() { 188 | app.show_login_windows = true; 189 | } 190 | } else { 191 | // 初始化选中账号(如果未选择) 192 | if app.selected_account_uid.is_none() && !app.account_manager.accounts.is_empty() { 193 | app.selected_account_uid = Some(app.account_manager.accounts[0].uid); 194 | } 195 | 196 | // 创建账号ComboBox 197 | let selected_account = app.account_manager.accounts.iter() 198 | .find(|a| Some(a.uid) == app.selected_account_uid); 199 | 200 | let selected_text = match selected_account { 201 | Some(account) => format!("{} ({})", account.name, account.uid), 202 | None => "选择账号".to_string(), 203 | }; 204 | 205 | egui::ComboBox::from_id_source("account_selector") 206 | .selected_text(selected_text) 207 | .width(200.0) 208 | .show_ui(ui, |ui| { 209 | for account in &app.account_manager.accounts { 210 | let text = format!("{} ({})", account.name, account.uid); 211 | let is_selected = Some(account.uid) == app.selected_account_uid; 212 | 213 | if ui.selectable_label(is_selected, text).clicked() { 214 | app.selected_account_uid = Some(account.uid); 215 | } 216 | } 217 | }); 218 | 219 | // 显示会员等级和状态(如果有选中账号) 220 | if let Some(account) = selected_account { 221 | ui.add_space(10.0); 222 | if !account.vip_label.is_empty() { 223 | let vip_text = egui::RichText::new(&account.vip_label) 224 | .size(13.0) 225 | .color(egui::Color32::from_rgb(251, 114, 153)); 226 | ui.label(vip_text); 227 | } 228 | 229 | let level_text = egui::RichText::new(format!("LV{}", account.level)) 230 | .size(13.0) 231 | .color(egui::Color32::from_rgb(0, 161, 214)); 232 | ui.label(level_text); 233 | } 234 | } 235 | }); 236 | } 237 | 238 | // 抢票模式选择UI 239 | fn grab_mode_selection(ui: &mut egui::Ui, app: &mut Myapp) { 240 | ui.vertical(|ui| { 241 | ui.label(egui::RichText::new("抢票模式:").color(egui::Color32::BLACK).size(16.0).strong()); 242 | ui.add_space(8.0); 243 | 244 | ui.horizontal(|ui| { 245 | ui.style_mut().spacing.item_spacing.x = 12.0; 246 | 247 | // 第一种模式 - 自动抢票(推荐) 248 | let selected = app.grab_mode == 0; 249 | if mode_selection_button(ui, "🎫 自动抢票(推荐)", 250 | "自动检测开票时间抢票", selected).clicked() { 251 | app.grab_mode = 0; 252 | } 253 | 254 | // 第二种模式 - 直接抢票 255 | let selected = app.grab_mode == 1; 256 | if mode_selection_button(ui, "⚡ 直接抢票", 257 | "直接开始尝试下单(适合已开票项目!,未开票项目使用会导致冻结账号!)", selected).clicked() { 258 | app.grab_mode = 1; 259 | } 260 | 261 | // 第三种模式 - 捡漏模式 262 | let selected = app.grab_mode == 2; 263 | if mode_selection_button(ui, "🔄 捡漏模式", 264 | "对于已开票项目,监测是否出现余票并尝试下单", selected).clicked() { 265 | app.grab_mode = 2; 266 | } 267 | }); 268 | }); 269 | } 270 | 271 | // 抢票模式按钮 272 | fn mode_selection_button(ui: &mut egui::Ui, title: &str, tooltip: &str, selected: bool) -> egui::Response { 273 | let btn = ui.add( 274 | egui::widgets::Button::new( 275 | egui::RichText::new(title) 276 | .size(14.0) 277 | .color(if selected { 278 | egui::Color32::WHITE 279 | } else { 280 | egui::Color32::from_rgb(70, 70, 70) 281 | }) 282 | ) 283 | .min_size(egui::vec2(110.0, 36.0)) 284 | .fill(if selected { 285 | egui::Color32::from_rgb(102, 204, 255) 286 | } else { 287 | egui::Color32::from_rgb(230, 230, 235) 288 | }) 289 | .rounding(6.0) 290 | .stroke(egui::Stroke::new( 291 | 0.5, 292 | if selected { 293 | egui::Color32::from_rgb(25, 118, 210) 294 | } else { 295 | egui::Color32::from_rgb(180, 180, 190) 296 | } 297 | )) 298 | ); 299 | 300 | // 添加悬停提示 301 | btn.clone().on_hover_text(tooltip); 302 | 303 | btn 304 | } 305 | //抢票按钮 306 | fn styled_grab_button(ui: &mut egui::Ui) -> egui::Response { 307 | let button_width = 200.0; 308 | let button_height = 60.0; 309 | 310 | ui.horizontal(|ui| { 311 | ui.add_space((ui.available_width() - button_width) / 2.0); 312 | 313 | let button = egui::Button::new( 314 | egui::RichText::new("开始抢票") 315 | .size(24.0) 316 | .strong() 317 | .color(egui::Color32::from_rgb(255,255,255)) 318 | ) 319 | .min_size(egui::vec2(button_width, button_height)) 320 | .fill(egui::Color32::from_rgb(102, 204, 255)) 321 | .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(25, 118, 210))) 322 | .rounding(12.0); 323 | 324 | ui.add(button) 325 | }).inner 326 | } 327 | 328 | fn check_input_ticket(ticket_id: &mut String) -> bool{ 329 | //检查输入的票务ID是否有效 330 | if ticket_id.is_empty(){ 331 | log::info!("请输入有效的票务id"); 332 | return false; 333 | } 334 | if ticket_id.contains("https") { 335 | if let Some(position) = ticket_id.find("id="){ 336 | let mut id = ticket_id.split_off(position+3); 337 | if id.contains("&") { 338 | let position = id.find("&").unwrap(); 339 | id.truncate(position); 340 | } 341 | if id.len() == 5 || id.len() == 6 { 342 | match id.parse::<u32>(){ 343 | Ok(_) => { 344 | log::info!("获取到的id为:{}", id); 345 | *ticket_id = id; 346 | return true; 347 | } 348 | Err(_) => { 349 | log::error!("输入的id不合法,请检查输入,可尝试直接输入id"); 350 | return false; 351 | } 352 | } 353 | } 354 | 355 | 356 | 357 | }else{ 358 | log::error!("未找到对应的id,请不要使用b23开头的短连接,正确连接以show.bilibili或mall.bilibili开头"); 359 | return false; 360 | } 361 | } 362 | match ticket_id.parse::<u32>() { 363 | Ok(_) => { 364 | log::info!("获取到的id为:{}", ticket_id); 365 | return true; 366 | } 367 | Err(_) => { 368 | log::error!("输入的id不是数字类型,请检查输入"); 369 | } 370 | } 371 | return false; 372 | } 373 | -------------------------------------------------------------------------------- /frontend/src/ui/tabs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod home; 2 | pub mod monitor; 3 | pub mod account; 4 | pub mod settings; 5 | pub mod help; 6 | 7 | use eframe::egui; 8 | use crate::app::Myapp; 9 | 10 | 11 | pub fn render_tab_content(app: &mut Myapp, ui: &mut egui::Ui) { 12 | match app.selected_tab { 13 | 0 => home::render(app, ui), 14 | 1 => monitor::render(app, ui), 15 | 2 => account::render(app, ui), 16 | 3 => settings::render(app, ui), 17 | 4 => help::render(app, ui), 18 | _ => unreachable!(), 19 | } 20 | } -------------------------------------------------------------------------------- /frontend/src/ui/tabs/monitor.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use crate::app::Myapp; 3 | 4 | pub fn render(app: &mut Myapp, ui: &mut egui::Ui){ 5 | app.show_log_window = true; 6 | if let Some(accounce) = app.announce3.clone() { 7 | ui.label(accounce); 8 | } else { 9 | ui.label("无法连接服务器"); 10 | } 11 | 12 | ui.separator(); 13 | 14 | } -------------------------------------------------------------------------------- /frontend/src/ui/tabs/project_list.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biliticket/bili_ticket_rush/5b2926b0697a75350e387da026564b5e99bdd8b2/frontend/src/ui/tabs/project_list.rs -------------------------------------------------------------------------------- /frontend/src/ui/tabs/settings.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use crate::app::Myapp; 3 | use common::utils::save_config; 4 | 5 | fn on_switch(ui: &mut egui::Ui, output_char: &str, on: &mut bool) -> egui::Response { 6 | ui.label( 7 | egui::RichText::new(output_char) 8 | .size(15.0) 9 | .color(egui::Color32::from_rgb(0,0,0)) 10 | 11 | .strong() 12 | ); 13 | // 开关尺寸 14 | let width = 55.0; 15 | let height = 26.0; 16 | 17 | // 分配空间并获取响应 18 | let (rect, mut response) = ui.allocate_exact_size( 19 | egui::vec2(width, height), 20 | egui::Sense::click() 21 | ); 22 | 23 | // 处理点击 24 | if response.clicked() { 25 | *on = !*on; 26 | response.mark_changed(); 27 | } 28 | 29 | // 动画参数 30 | let animation_progress = ui.ctx().animate_bool(response.id, *on); 31 | let radius = height / 2.0; 32 | 33 | // 计算滑块位置 34 | let circle_x = rect.left() + radius + animation_progress * (width - height); 35 | 36 | // 绘制轨道 37 | ui.painter().rect_filled( 38 | rect.expand(-1.0), 39 | radius, 40 | if *on { 41 | egui::Color32::from_rgb(102,204,255) // 启用状态颜色 42 | } else { 43 | egui::Color32::from_rgb(150, 150, 150) // 禁用状态颜色 44 | } 45 | ); 46 | 47 | // 绘制滑块 48 | ui.painter().circle_filled( 49 | egui::pos2(circle_x, rect.center().y), 50 | radius - 4.0, 51 | egui::Color32::WHITE 52 | ); 53 | 54 | response 55 | } 56 | 57 | pub fn common_input( 58 | ui: &mut egui::Ui, 59 | title: &str, 60 | text: &mut String, 61 | hint: &str, 62 | open_filter: bool, 63 | 64 | 65 | ) -> bool{ 66 | ui.label( 67 | egui::RichText::new(title) 68 | .size(15.0) 69 | .color(egui::Color32::from_rgb(0,0,0)) 70 | 71 | 72 | ); 73 | ui.add_space(8.0); 74 | let input = egui::TextEdit::singleline( text) 75 | .hint_text(hint)//提示 76 | .desired_rows(1)//限制1行 77 | .min_size(egui::vec2(120.0, 35.0)); 78 | 79 | 80 | let response = ui.add(input); 81 | if response.changed(){ 82 | if open_filter{ 83 | *text = text.chars()//过滤非法字符 84 | .filter(|c| c.is_ascii_alphanumeric() || *c == '@' || *c == '.' || *c == '-' || *c == '_') 85 | .collect(); 86 | } 87 | else{ 88 | *text = text.chars()//过滤非法字符 89 | .collect(); 90 | }; 91 | 92 | } 93 | response.changed() 94 | 95 | } 96 | pub fn render(app: &mut Myapp, ui: &mut egui::Ui) { 97 | 98 | ui.horizontal(|ui|{ 99 | ui.heading("设置"); 100 | ui.add_space(20.0); 101 | let button = egui::Button::new( 102 | egui::RichText::new("保存设置").size(15.0).color(egui::Color32::WHITE) 103 | ) 104 | .min_size(egui::vec2(100.0,35.0)) 105 | .fill(egui::Color32::from_rgb(102,204,255)) 106 | .rounding(15.0);//圆角成度 107 | let response = ui.add(button); 108 | if response.clicked(){ 109 | match save_config(&mut app.config, Some(&app.push_config),Some(&app.custom_config), None){ 110 | Ok(_) => { 111 | log::info!("设置保存成功"); 112 | }, 113 | Err(e) => { 114 | log::info!("设置保存失败: {}", e); 115 | } 116 | } 117 | } 118 | 119 | }) ; 120 | 121 | ui.separator(); 122 | //推送设置: 123 | // 创建圆角长方形框架 124 | egui::Frame::none() 125 | .fill(egui::Color32::from_rgb(245, 245, 250)) // 背景色 126 | .rounding(12.0) // 圆角半径 127 | .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 220))) // 边框 128 | .inner_margin(egui::Margin { left: 10.0, right: 20.0, top: 15.0, bottom: 15.0 }) // 内边距 129 | .show(ui, |ui| { 130 | 131 | globle_setting(app,ui); 132 | ui.separator(); 133 | 134 | 135 | }); 136 | egui::Frame::none() 137 | .fill(egui::Color32::from_rgb(245, 245, 250)) // 背景色 138 | .rounding(12.0) // 圆角半径 139 | .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 220))) // 边框 140 | .inner_margin(egui::Margin { left: 10.0, right: 20.0, top: 15.0, bottom: 15.0 }) // 内边距 141 | .show(ui, |ui| { 142 | 143 | push_setting(app,ui); // 调用推送设置 144 | ui.separator(); 145 | 146 | }); 147 | 148 | 149 | 150 | } 151 | 152 | pub fn globle_setting(app: &mut Myapp, ui: &mut egui::Ui){ 153 | ui.horizontal(|ui| { 154 | 155 | common_input(ui, "请输入账号1预填手机号:", &mut app.custom_config.preinput_phone1, "请输入账号1绑定的手机号",true); 156 | common_input(ui, "请输入账号2预填手机号:", &mut app.custom_config.preinput_phone1, "请输入账号2绑定的手机号,没有可不填",true); 157 | }); 158 | ui.separator(); 159 | ui.horizontal(|ui|{ 160 | ui.label("请选择验证码识别方式:"); 161 | let options = ["本地识别", "ttocr识别", "选项3"]; 162 | 163 | custom_selection_control(ui, &mut app.custom_config.captcha_mode, &options) ; 164 | match app.custom_config.captcha_mode{ 165 | 166 | 1 => { 167 | dynamic_caculate_space(ui, 300.0); 168 | common_input(ui, "请输入ttocr key:", &mut app.custom_config.ttocr_key, "请输入ttocr key",true); 169 | 170 | 171 | }, 172 | _ => { 173 | 174 | } 175 | } 176 | 177 | }); 178 | ui.separator(); 179 | ui.horizontal(|ui| { 180 | on_switch(ui, "开启自定义UA", &mut app.custom_config.open_custom_ua); 181 | dynamic_caculate_space(ui, 180.0); 182 | common_input(ui, "", &mut app.custom_config.custom_ua, "请输入自定义UA",false); 183 | 184 | }); 185 | 186 | 187 | 188 | 189 | 190 | } 191 | 192 | fn custom_selection_control(ui: &mut egui::Ui, selected: &mut usize, options: &[&str]) -> bool { 193 | let mut changed = false; 194 | ui.horizontal(|ui| { 195 | for (idx, option) in options.iter().enumerate() { 196 | let is_selected = *selected == idx; 197 | let button = egui::Button::new( 198 | egui::RichText::new(*option) 199 | .size(15.0) 200 | .color(if is_selected { egui::Color32::WHITE } else { egui::Color32::BLACK }) 201 | ) 202 | .min_size(egui::vec2(80.0, 30.0)) 203 | .fill(if is_selected { 204 | egui::Color32::from_rgb(102, 204, 255) 205 | } else { 206 | egui::Color32::from_rgb(245, 245, 250) 207 | }) 208 | .rounding(10.0); 209 | 210 | if ui.add(button).clicked() { 211 | *selected = idx; 212 | changed = true; 213 | } 214 | } 215 | }); 216 | changed 217 | } 218 | pub fn push_setting(app: &mut Myapp, ui: &mut egui::Ui){ 219 | //推送开关 220 | 221 | // 开关 222 | ui.horizontal(|ui| { 223 | 224 | 225 | on_switch(ui, "开启推送",&mut app.push_config.enabled); 226 | let available = ui.available_width(); 227 | ui.add_space(available-100.0); 228 | 229 | let button = egui::Button::new( 230 | egui::RichText::new("测试推送").size(15.0).color(egui::Color32::WHITE) 231 | ) 232 | .min_size(egui::vec2(100.0,40.0)) 233 | .fill(egui::Color32::from_rgb(102,204,255)) 234 | .rounding(15.0);//圆角成度 235 | let response = ui.add(button); 236 | if response.clicked(){ 237 | app.push_config.push_all("biliticket推送测试", "这是一个推送测试", &None,&mut *app.task_manager); 238 | } 239 | 240 | 241 | }); 242 | if app.push_config.enabled{ 243 | ui.separator(); 244 | 245 | 246 | 247 | 248 | //推送设置 249 | ui.horizontal(|ui|{ 250 | 251 | common_input(ui, "bark推送:",&mut app.push_config.bark_token,"请输入推送地址,只填token",true); 252 | dynamic_caculate_space(ui, 180.0); 253 | common_input(ui, "pushplus推送:",&mut app.push_config.pushplus_token,"请输入推送地址,只填token",true); 254 | }); 255 | //TODO补充每个推送方式使用方法 256 | 257 | ui.horizontal(|ui|{ 258 | 259 | common_input(ui, "方糖推送:",&mut app.push_config.fangtang_token,"请输入推送地址:SCTxxxxxxx",true); 260 | dynamic_caculate_space(ui, 180.0); 261 | common_input(ui, "钉钉机器人推送:",&mut app.push_config.dingtalk_token,"请输入钉钉机器人token,只填token",true); 262 | }); 263 | 264 | ui.horizontal(|ui|{ 265 | common_input(ui, "企业微信推送:",&mut app.push_config.wechat_token,"请输入企业微信机器人token",true); 266 | dynamic_caculate_space(ui, 180.0); 267 | 268 | }); 269 | ui.horizontal(|ui|{ 270 | common_input(ui, "smtp服务器地址:",&mut app.push_config.smtp_config.smtp_server,"请输入smtp服务器地址",true); 271 | dynamic_caculate_space(ui, 180.0); 272 | common_input(ui, "smtp服务器端口:",&mut app.push_config.smtp_config.smtp_port,"请输入smtp服务器端口",true); 273 | 274 | }); 275 | ui.horizontal(|ui|{ 276 | 277 | common_input(ui, "邮箱账号:",&mut app.push_config.smtp_config.smtp_from,"请输入发件人邮箱",true); 278 | dynamic_caculate_space(ui, 180.0); 279 | common_input(ui, "授权密码:",&mut app.push_config.smtp_config.smtp_password,"请输入授权密码",true); 280 | dynamic_caculate_space(ui, 180.0); 281 | 282 | }); 283 | ui.horizontal(|ui|{ 284 | 285 | 286 | 287 | common_input(ui, "发件人邮箱:",&mut app.push_config.smtp_config.smtp_from,"请输入发件人邮箱",true); 288 | dynamic_caculate_space(ui, 180.0); 289 | common_input(ui, "收件人邮箱:",&mut app.push_config.smtp_config.smtp_to,"请输入收件人邮箱",true); 290 | 291 | }); 292 | ui.horizontal(|ui| { 293 | common_input(ui, "gotify地址:",&mut app.push_config.gotify_config.gotify_url,"请输入gotify服务器地址,只填写地址",false); 294 | dynamic_caculate_space(ui, 180.0); 295 | common_input(ui, "gotify的token", &mut app.push_config.gotify_config.gotify_token, "请输入gotify的token", true) 296 | }); 297 | } 298 | 299 | } 300 | pub fn dynamic_caculate_space(ui :&mut egui::Ui, next_obj_space: f32) { 301 | let available_space = ui.available_width(); 302 | let mut space = available_space - next_obj_space - 250.0; 303 | if space < 0.0 { 304 | space = 0.0; 305 | } 306 | ui.add_space(space); 307 | } 308 | 309 | 310 | -------------------------------------------------------------------------------- /frontend/src/windows/add_buyer.rs: -------------------------------------------------------------------------------- 1 | use crate::app::Myapp; 2 | use eframe::egui::{self, RichText}; 3 | use serde_json::Value; 4 | 5 | pub struct AddBuyerInput{ 6 | pub name: String, 7 | pub phone: String, 8 | pub id_type: usize, 9 | pub id_number: String, 10 | pub as_default_buyer: bool, 11 | 12 | } 13 | pub fn show(app: &mut Myapp, ctx: &egui::Context, uid: &str) { 14 | let find_account = app.account_manager.accounts.iter().find(|account| account.uid.to_string() == uid); 15 | let select_account = match find_account { 16 | Some(account) => account, 17 | None => return, 18 | }; 19 | let select_cookie_manager = select_account.cookie_manager.clone().unwrap(); 20 | let mut window_open = app.show_add_buyer_window.is_some(); 21 | 22 | egui::Window::new("添加购票人") 23 | .open(&mut window_open) 24 | .default_size([700.0, 400.0]) 25 | .resizable(false) 26 | .show(ctx, |ui| { 27 | 28 | ui.vertical_centered(|ui|{ 29 | ui.label(RichText::new("添加购票人") 30 | .size(20.0) 31 | .color(egui::Color32::from_rgb(0,0,0)) 32 | .strong() 33 | ); 34 | }); 35 | ui.add_space(20.0); 36 | ui.horizontal(|ui|{ 37 | ui.add_space(8.0); 38 | common_input(ui, "姓名:", &mut app.add_buyer_input.name, "请输入你的真实姓名", false); 39 | 40 | 41 | }); 42 | ui.add_space(20.0); 43 | ui.horizontal(|ui|{ 44 | ui.add_space(8.0); 45 | common_input(ui, "手机号:", &mut app.add_buyer_input.phone, "请输入你的手机号", true); 46 | ui.add_space(20.0); 47 | 48 | }); 49 | 50 | // 添加证件类型选择器 51 | ui.add_space(20.0); 52 | ui.horizontal(|ui| { 53 | ui.add_space(8.0); 54 | ui.label( 55 | egui::RichText::new("证件类型:") 56 | .size(15.0) 57 | .color(egui::Color32::from_rgb(0,0,0)) 58 | ); 59 | ui.add_space(8.0); 60 | 61 | // 调用证件类型选择器 62 | id_type_selector(ui, &mut app.add_buyer_input.id_type); 63 | }); 64 | 65 | // 添加证件号码输入 66 | ui.add_space(20.0); 67 | ui.horizontal(|ui|{ 68 | ui.add_space(8.0); 69 | common_input(ui, "证件号码:", &mut app.add_buyer_input.id_number, get_id_hint(app.add_buyer_input.id_type), true); 70 | }); 71 | 72 | // 添加默认购票人选项 73 | ui.add_space(20.0); 74 | ui.horizontal(|ui|{ 75 | ui.add_space(8.0); 76 | ui.checkbox(&mut app.add_buyer_input.as_default_buyer, "设为默认购票人"); 77 | }); 78 | 79 | 80 | //确保空间大小合适 81 | ui.add_space(30.0); 82 | ui.horizontal(|ui| { 83 | ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { 84 | 85 | }); 86 | }); 87 | 88 | ui.vertical_centered(|ui|{ 89 | // 创建按钮 90 | let button = egui::Button::new( 91 | egui::RichText::new("保存").size(20.0).color(egui::Color32::WHITE) 92 | ) 93 | .min_size(egui::vec2(120.0, 50.0)) 94 | .fill(egui::Color32::from_rgb(102,204,255)) 95 | .rounding(20.0); 96 | let response = ui.add(button); 97 | if response.clicked() { 98 | let mut json_form = serde_json::Map::new(); 99 | json_form.insert("name".to_string(), serde_json::Value::String(app.add_buyer_input.name.clone())); 100 | json_form.insert("tel".to_string(), serde_json::Value::String(app.add_buyer_input.phone.clone())); 101 | json_form.insert("id_type".to_string(), serde_json::Value::Number(serde_json::Number::from(app.add_buyer_input.id_type))); 102 | json_form.insert("personal_id".to_string(), serde_json::Value::String(app.add_buyer_input.id_number.clone())); 103 | json_form.insert("is_default".to_string(), serde_json::Value::String(check_default(app.add_buyer_input.as_default_buyer).to_string())); 104 | json_form.insert("src".to_string(), serde_json::Value::String("ticket".to_string())); 105 | 106 | 107 | 108 | log::debug!("添加购票人数据: {:?}", json_form); 109 | log::debug!("账号ck: {:?}", select_account.cookie.as_str()); 110 | let rt = tokio::runtime::Runtime::new().unwrap(); 111 | let response = rt.block_on(async{ 112 | select_cookie_manager.post( 113 | "https://show.bilibili.com/api/ticket/buyer/create", 114 | 115 | ).await 116 | .json(&json_form) 117 | .send() 118 | .await 119 | .unwrap() 120 | }); 121 | 122 | if !response.status().is_success() { 123 | log::error!("添加购票人失败: {:?}", response.status()); 124 | return; 125 | } 126 | 127 | let response_text = match rt.block_on(response.text()) { 128 | Ok(text) => text, 129 | Err(e) => { 130 | log::error!("获取响应文本失败: {}", e); 131 | return; 132 | } 133 | }; 134 | 135 | let json_value: Result<Value, _> = serde_json::from_str(&response_text); 136 | let response_json_value = match json_value { 137 | Ok(val) => val, 138 | Err(e) => { 139 | log::error!("解析JSON失败! 原因: {}, 响应原文: {}", e, response_text); 140 | return; 141 | } 142 | }; 143 | let errno_value = response_json_value.get("errno").and_then(|v| v.as_i64()).unwrap_or(-1); 144 | let code_value = response_json_value.get("code").and_then(|v| v.as_i64()).unwrap_or(-1); 145 | let code = if errno_value != -1{ 146 | errno_value 147 | } else { 148 | code_value 149 | }; 150 | if code == 0 { 151 | log::info!("添加购票人成功: {:?}", response_text); 152 | app.show_add_buyer_window = None; 153 | // 重置表单 154 | app.add_buyer_input = AddBuyerInput { 155 | name: String::new(), 156 | phone: String::new(), 157 | id_type: 0, 158 | id_number: String::new(), 159 | as_default_buyer: false, 160 | }; 161 | } else { 162 | log::error!("添加购票人失败: {:?}", response_text); 163 | } 164 | 165 | 166 | 167 | 168 | 169 | } 170 | }) 171 | 172 | }); 173 | 174 | //更新窗口状态 175 | if !window_open { 176 | app.show_add_buyer_window = None; 177 | } 178 | } 179 | 180 | pub fn common_input( 181 | ui: &mut egui::Ui, 182 | title: &str, 183 | text: &mut String, 184 | hint: &str, 185 | open_filter: bool, 186 | 187 | 188 | ) -> bool{ 189 | ui.label( 190 | egui::RichText::new(title) 191 | .size(15.0) 192 | .color(egui::Color32::from_rgb(0,0,0)) 193 | 194 | 195 | ); 196 | ui.add_space(8.0); 197 | let input = egui::TextEdit::singleline( text) 198 | .hint_text(hint)//提示 199 | .desired_rows(1)//限制1行 200 | .min_size(egui::vec2(120.0, 35.0)); 201 | 202 | 203 | let response = ui.add(input); 204 | if response.changed(){ 205 | if open_filter{ 206 | *text = text.chars()//过滤非法字符 207 | .filter(|c| c.is_ascii_alphanumeric() || *c == '@' || *c == '.' || *c == '-' || *c == '_') 208 | .collect(); 209 | } 210 | else{ 211 | *text = text.chars()//过滤非法字符 212 | .collect(); 213 | }; 214 | 215 | } 216 | response.changed() 217 | 218 | } 219 | 220 | // 证件类型的名称和值 221 | const ID_TYPES: [(&str, usize); 4] = [ 222 | ("身份证", 0), 223 | ("护照", 1), 224 | ("港澳居民往来内地通行证", 2), 225 | ("台湾居民往来大陆通行证", 3), 226 | ]; 227 | 228 | fn check_default(is_default: bool) -> &'static str { 229 | if is_default { 230 | "1" 231 | } else { 232 | "0" 233 | } 234 | } 235 | fn id_type_selector(ui: &mut egui::Ui, selected_type: &mut usize) { 236 | ui.horizontal(|ui| { 237 | for (name, value) in ID_TYPES.iter() { 238 | let is_selected = *selected_type == *value; 239 | 240 | // 创建更美观的选择按钮 241 | let button = egui::Button::new( 242 | RichText::new(*name) 243 | .size(14.0) 244 | .color( 245 | if is_selected { 246 | egui::Color32::WHITE 247 | } else { 248 | egui::Color32::from_rgb(60, 60, 60) 249 | } 250 | ) 251 | ) 252 | .min_size(egui::vec2(0.0, 32.0)) 253 | .fill( 254 | if is_selected { 255 | egui::Color32::from_rgb(102, 204, 255) 256 | } else { 257 | egui::Color32::from_rgb(240, 240, 240) 258 | } 259 | ) 260 | .rounding(5.0); 261 | 262 | if ui.add(button).clicked() { 263 | *selected_type = *value; 264 | } 265 | 266 | ui.add_space(8.0); // 按钮之间的间距 267 | } 268 | }); 269 | } 270 | 271 | // 根据证件类型获取不同的提示文字 272 | fn get_id_hint(id_type: usize) -> &'static str { 273 | match id_type { 274 | 0 => "请输入18位身份证号码", 275 | 1 => "请输入护照号码", 276 | 2 => "请输入港澳通行证号码", 277 | 3 => "请输入台湾通行证号码", 278 | _ => "请输入证件号码", 279 | } 280 | } -------------------------------------------------------------------------------- /frontend/src/windows/grab_ticket.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use egui::Ui; 3 | use common::ticket::BilibiliTicket; 4 | use crate::app::Myapp; 5 | 6 | 7 | pub fn show(app:&mut Myapp, ctx:&egui::Context, bilibiliticket: &mut BilibiliTicket){ 8 | 9 | } -------------------------------------------------------------------------------- /frontend/src/windows/log_windows.rs: -------------------------------------------------------------------------------- 1 | 2 | use eframe::egui; 3 | use crate::app::Myapp; 4 | 5 | pub fn show(app: &mut Myapp, ctx: &egui::Context) { 6 | 7 | let mut window_open = app.show_log_window; 8 | let mut user_close: bool = false; 9 | 10 | egui::Window::new("监视面板") 11 | .open(&mut window_open) 12 | .default_size([550.0, 400.0]) 13 | .resizable(true) 14 | .show(ctx, |ui| { 15 | // 顶部工具栏 16 | ui.horizontal(|ui| { 17 | if ui.button("清空日志").clicked() { 18 | app.logs.clear(); 19 | } 20 | 21 | if ui.button("添加测试日志").clicked() { 22 | app.add_log("测试日志消息"); 23 | } 24 | 25 | ui.with_layout(egui::Layout::right_to_left(egui::Align::RIGHT), |ui| { 26 | if ui.button("❌").clicked() { 27 | user_close = true; 28 | app.show_log_window = false; 29 | } 30 | }); 31 | }); 32 | 33 | ui.separator(); 34 | 35 | // 日志内容区域 36 | egui::ScrollArea::vertical() 37 | .auto_shrink([false, false]) 38 | .stick_to_bottom(true) 39 | .max_height(300.0) 40 | .show(ui, |ui| { 41 | // 显示当前状态 42 | ui.label(format!("当前状态: {}", 43 | if app.running_status.is_empty() { "未知状态" } else { &app.running_status })); 44 | 45 | ui.separator(); 46 | 47 | // 显示所有日志 48 | if app.logs.is_empty() { 49 | ui.label("暂无日志记录"); 50 | 51 | } else { 52 | for log in &app.logs { 53 | ui.label(log); 54 | ui.separator(); 55 | } 56 | } 57 | }); 58 | 59 | // 底部状态栏 60 | ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { 61 | ui.label(format!("共 {} 条日志", app.logs.len())); 62 | }); 63 | }); 64 | 65 | // 更新窗口状态 66 | app.show_log_window = window_open; 67 | } -------------------------------------------------------------------------------- /frontend/src/windows/login_selenium.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use crate::app::Myapp; 3 | use common::account::Account; 4 | use std::sync::mpsc; 5 | use std::thread; 6 | 7 | // 登录状态枚举 8 | #[derive(Debug, Clone)] 9 | pub enum SeleniumLoginStatus { 10 | NotStarted, 11 | Connecting, 12 | WaitingForLogin, 13 | LoggedIn(String), // 成功获取的 cookie 14 | Failed(String), // 错误信息 15 | } 16 | 17 | pub struct SeleniumLogin { 18 | status: SeleniumLoginStatus, 19 | progress: f32, 20 | cookie: Option<String>, 21 | } 22 | 23 | impl SeleniumLogin { 24 | pub fn new() -> Self { 25 | Self { 26 | status: SeleniumLoginStatus::NotStarted, 27 | progress: 0.0, 28 | cookie: None, 29 | } 30 | } 31 | 32 | // 启动浏览器登录过程 33 | pub fn start_login(&mut self) -> Result<(), String> { 34 | self.status = SeleniumLoginStatus::Connecting; 35 | self.progress = 0.1; 36 | 37 | // 创建通道用于接收异步结果 38 | let (tx, rx) = mpsc::channel(); 39 | 40 | // 在后台线程执行浏览器登录 41 | thread::spawn(move || { 42 | // 创建运行时 43 | let rt = tokio::runtime::Runtime::new().unwrap(); 44 | let result = rt.block_on(login_and_get_cookie()); 45 | 46 | match result { 47 | Ok(cookie) => { 48 | let _ = tx.send(SeleniumLoginStatus::LoggedIn(cookie)); 49 | }, 50 | Err(e) => { 51 | let _ = tx.send(SeleniumLoginStatus::Failed(e.to_string())); 52 | } 53 | } 54 | }); 55 | 56 | // 保存接收器以便稍后检查结果 57 | self.cookie = None; 58 | 59 | Ok(()) 60 | } 61 | 62 | // 检查登录状态 - 在UI更新循环中调用 63 | pub fn check_status(&mut self, rx: &mpsc::Receiver<SeleniumLoginStatus>) -> bool { 64 | // 非阻塞检查是否有新状态 65 | if let Ok(status) = rx.try_recv() { 66 | self.status = status.clone(); 67 | 68 | // 如果成功获取cookie 69 | if let SeleniumLoginStatus::LoggedIn(cookie) = status { 70 | self.cookie = Some(cookie); 71 | self.progress = 1.0; 72 | return true; 73 | } 74 | } 75 | 76 | // 更新进度 77 | match self.status { 78 | SeleniumLoginStatus::Connecting => { 79 | if self.progress < 0.2 { 80 | self.progress += 0.01; 81 | } 82 | }, 83 | SeleniumLoginStatus::WaitingForLogin => { 84 | if self.progress < 0.9 { 85 | self.progress += 0.001; 86 | } 87 | }, 88 | _ => {} 89 | } 90 | 91 | false 92 | } 93 | 94 | // 获取当前cookie 95 | pub fn take_cookie(&mut self) -> Option<String> { 96 | self.cookie.take() 97 | } 98 | } 99 | 100 | // 原有的登录逻辑保持不变 101 | async fn login_and_get_cookie() -> Result<String, Box<dyn std::error::Error>> { 102 | // 实现保持不变... 103 | // ... 104 | Ok("cookie_value".to_string()) 105 | } 106 | 107 | // 检查WebDriver是否可用 108 | pub fn is_webdriver_available() -> bool { 109 | // 简单检测本地是否运行了WebDriver 110 | match std::net::TcpStream::connect("127.0.0.1:4444") { 111 | Ok(_) => true, 112 | Err(_) => false, 113 | } 114 | } -------------------------------------------------------------------------------- /frontend/src/windows/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod log_windows; 2 | pub mod login_windows; 3 | pub mod login_selenium; 4 | pub mod add_buyer; 5 | pub mod show_orderlist; 6 | pub mod screen_info; 7 | pub mod confirm_ticket; 8 | pub mod confirm_ticket2; 9 | pub mod show_qrcode; -------------------------------------------------------------------------------- /frontend/src/windows/screen_info.rs: -------------------------------------------------------------------------------- 1 | use crate::app::Myapp; 2 | use chrono::TimeZone; 3 | use eframe::egui; 4 | use serde_json::Value; 5 | 6 | pub fn show(app: &mut Myapp, ctx: &egui::Context, uid: i64) { 7 | let bilibili_ticket = app 8 | .bilibiliticket_list 9 | .iter_mut() 10 | .find(|ticket| ticket.uid == uid) 11 | .unwrap(); 12 | let mut window_open = app.show_screen_info.is_some(); 13 | 14 | let ticket_data = match bilibili_ticket.project_info.clone() { 15 | Some(ticket) => { 16 | app.is_loading = false; 17 | ticket 18 | } 19 | None => { 20 | app.is_loading = true; 21 | return; 22 | } 23 | }; 24 | //默认选择第一个场次(如果尚未选择) 25 | if app.selected_screen_index.is_none() && !ticket_data.screen_list.is_empty() { 26 | app.selected_screen_index = Some(0); 27 | } 28 | bilibili_ticket.id_bind = ticket_data.id_bind as usize; 29 | egui::Window::new("项目详情") 30 | .open(&mut window_open) 31 | .default_height(600.0) 32 | .default_width(800.0) 33 | .resizable(true) 34 | .show(ctx, |ui| { 35 | egui::ScrollArea::vertical().show(ui, |ui| { 36 | // 项目标题区 37 | ui.vertical_centered(|ui| { 38 | ui.heading(&ticket_data.name); 39 | ui.add_space(5.0); 40 | 41 | /* // 活动时间和地点 42 | if let Some(venue_info) = &ticket_data.venue_info { 43 | ui.label(format!("{} | {}", ticket_data.project_label, venue_info.name)); 44 | } */ 45 | ui.label(format!("状态: {}", ticket_data.sale_flag)); 46 | ui.add_space(10.0); 47 | }); 48 | 49 | ui.separator(); 50 | 51 | // 场次选择区 52 | ui.heading("选择场次"); 53 | ui.add_space(5.0); 54 | 55 | // 场次选择栏 56 | egui::ScrollArea::horizontal().show(ui, |ui| { 57 | ui.horizontal_wrapped(|ui| { 58 | for (idx, screen) in ticket_data.screen_list.iter().enumerate() { 59 | let is_selected = app.selected_screen_index == Some(idx); 60 | 61 | let btn = ui.add( 62 | egui::SelectableLabel::new( 63 | is_selected, 64 | format!("{} ({})", 65 | screen.name, 66 | &screen.sale_flag.display_name 67 | ) 68 | ) 69 | 70 | ); 71 | 72 | if btn.clicked() { 73 | app.selected_screen_index = Some(idx); 74 | } 75 | } 76 | }); 77 | }); 78 | 79 | ui.add_space(10.0); 80 | 81 | // 显示选中场次的票种信息 82 | if let Some(idx) = app.selected_screen_index { 83 | if idx < ticket_data.screen_list.len() { 84 | let selected_screen = &ticket_data.screen_list[idx]; 85 | // 场次信息卡片 86 | let bg_color=if !ctx.style().visuals.dark_mode { 87 | egui::Color32::from_rgb(245, 245, 250) 88 | } else { 89 | egui::Color32::from_rgb(6,6,6) 90 | }; 91 | egui::Frame::none() 92 | .fill(bg_color) 93 | .rounding(8.0) 94 | .inner_margin(10.0) 95 | .outer_margin(10.0) 96 | .show(ui, |ui| { 97 | // 场次基本信息 98 | ui.label(format!("开始时间: {}", format_timestamp(selected_screen.start_time))); 99 | ui.label(format!("售票开始: {}", format_timestamp(selected_screen.sale_start))); 100 | ui.label(format!("售票结束: {}", format_timestamp(selected_screen.sale_end))); 101 | ui.label(format!("售票状态: {}", selected_screen.sale_flag.display_name)); 102 | 103 | ui.add_space(8.0); 104 | ui.separator(); 105 | ui.add_space(8.0); 106 | 107 | ui.heading("票种列表"); 108 | 109 | // 票种表格头 110 | ui.horizontal(|ui| { 111 | ui.label(egui::RichText::new("票种名称").strong()); 112 | ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { 113 | ui.label(egui::RichText::new("操作").strong()); 114 | ui.add_space(70.0); 115 | ui.label(egui::RichText::new("状态").strong()); 116 | ui.add_space(70.0); 117 | ui.label(egui::RichText::new("价格").strong()); 118 | }); 119 | }); 120 | 121 | ui.separator(); 122 | 123 | // 票种列表 124 | for ticket in &selected_screen.ticket_list { 125 | ui.add_space(5.0); 126 | ui.horizontal(|ui| { 127 | ui.label(&ticket.desc); 128 | 129 | ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { 130 | let button_text = if ticket.clickable { "选择" }else if ticket.sale_flag_number ==1 {"定时预选"} else { "不可选" }; 131 | let button_enabled = true/* ticket.clickable */; 132 | 133 | if ui.add_enabled( 134 | button_enabled, 135 | egui::Button::new(button_text) 136 | ).clicked() { 137 | // 使用正确的类型赋值 138 | if !ticket.clickable{ 139 | log::error!("请注意!该票种目前不可售!但是会尝试下单,如果该票持续不可售,多次下单不可售票种可能会被b站拉黑") 140 | } 141 | app.selected_screen_id = Some(selected_screen.id as i64); 142 | app.selected_ticket_id = Some(ticket.id as i64); 143 | app.show_screen_info = None; 144 | bilibili_ticket.screen_id = selected_screen.id.to_string(); 145 | log::debug!("{}, {} , {}",selected_screen.id,ticket.id,ticket.project_id); 146 | 147 | 148 | // 将选中的票种ID保存到项目ID中,准备抢票 149 | app.ticket_id = ticket.project_id.to_string(); 150 | bilibili_ticket.select_ticket_id = Some(ticket.id.to_string()); 151 | 152 | app.confirm_ticket_info= Some(bilibili_ticket.uid.to_string().clone()); 153 | log::info!("已选择: {} [{}]", &ticket.desc, ticket.id); 154 | } 155 | 156 | ui.add_space(20.0); 157 | ui.label(&ticket.sale_flag.display_name); 158 | ui.add_space(20.0); 159 | 160 | // 票价格式化为元 161 | let price = format!("¥{:.2}", ticket.price as f64 / 100.0); 162 | ui.label(egui::RichText::new(price) 163 | .strong() 164 | .color(egui::Color32::from_rgb(245, 108, 108))); 165 | }); 166 | }); 167 | ui.separator(); 168 | } 169 | }); 170 | } 171 | } 172 | 173 | // 项目详细信息区 174 | ui.add_space(10.0); 175 | ui.collapsing("查看详细信息", |ui| { 176 | ui.label("基本信息:"); 177 | ui.indent("basic_info", |ui| { 178 | ui.label(format!("项目ID: {}", ticket_data.id)); 179 | 180 | // 检查performance_desc是否存在,并显示基础信息 181 | if let Some(desc) = &ticket_data.performance_desc { 182 | for item in &desc.list { 183 | if item.module == "base_info" { 184 | if let Some(array) = item.details.as_array() { 185 | for info_item in array { 186 | if let (Some(title), Some(content)) = ( 187 | info_item.get("title").and_then(Value::as_str), 188 | info_item.get("content").and_then(Value::as_str) 189 | ) { 190 | ui.horizontal(|ui| { 191 | ui.label(egui::RichText::new(format!("{}:", title)).strong()); 192 | ui.label(content); 193 | }); 194 | } 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | 202 | }); 203 | }); 204 | }); 205 | 206 | // 底部按钮 207 | ui.separator(); 208 | /* ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { 209 | if ui.button("关闭窗口").clicked() { 210 | app.show_screen_info = None; 211 | } 212 | }); */ 213 | }); 214 | if !window_open { 215 | app.show_screen_info = None; 216 | bilibili_ticket.project_info = None; 217 | } 218 | } 219 | 220 | // 将时间戳转换为可读时间 221 | // 将时间戳转换为可读时间 (接受usize类型) 222 | fn format_timestamp(timestamp: usize) -> String { 223 | if timestamp <= 0 { 224 | return "未设置".to_string(); 225 | } 226 | 227 | // 安全地将usize转为i64 228 | let timestamp_i64 = match i64::try_from(timestamp) { 229 | Ok(ts) => ts, 230 | Err(_) => return "时间戳溢出".to_string(), // 处理极端情况 231 | }; 232 | 233 | match chrono::Local.timestamp_opt(timestamp_i64, 0) { 234 | chrono::LocalResult::Single(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(), 235 | _ => "无效时间".to_string(), 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /frontend/src/windows/show_orderlist.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{Myapp, OrderData}; 2 | use eframe::egui::{self, RichText}; 3 | use egui::{Image, TextureHandle}; 4 | use std::sync::Arc; 5 | use serde::{Deserialize, Serialize}; 6 | use common::{cookie_manager::CookieManager, utils::load_texture_from_url}; 7 | 8 | pub fn show( 9 | app: &mut Myapp, 10 | ctx: &egui::Context, 11 | 12 | ){ 13 | let mut window_open = app.show_orderlist_window.is_some(); 14 | 15 | let orders_data = match &app.total_order_data { 16 | Some(data) => {app.is_loading = false; data.clone()}, 17 | None => {app.is_loading = true; return;}, 18 | }; 19 | 20 | 21 | // 显示窗口和订单数据 22 | egui::Window::new("订单列表") 23 | .open(&mut window_open) 24 | .default_height(600.0) 25 | .default_width(800.0) 26 | .resizable(true) 27 | .show(ctx, |ui| { 28 | ui.vertical_centered(|ui| { 29 | ui.label(RichText::new("订单列表") 30 | .size(20.0) 31 | .color(egui::Color32::from_rgb(0, 0, 0)) 32 | .strong() 33 | ); 34 | }); 35 | 36 | // 添加滚动区域 37 | egui::ScrollArea::vertical().show(ui, |ui| { 38 | // 使用从内存中获取的orders_data 39 | if let Some(order_data) = &orders_data.data { 40 | // 显示订单数据 41 | for order in &order_data.data.list { 42 | ui.add_space(12.0); 43 | 44 | egui::Frame::none() 45 | .fill(ui.style().visuals.widgets.noninteractive.bg_fill) 46 | .rounding(8.0) 47 | .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(220, 220, 220))) 48 | .shadow(egui::epaint::Shadow { 49 | extrusion: 2.0, 50 | color: egui::Color32::from_black_alpha(20), 51 | }) 52 | .inner_margin(egui::vec2(12.0, 12.0)) 53 | .show(ui, |ui| { 54 | ui.horizontal(|ui| { 55 | // 图片处理 56 | let image_size = egui::vec2(80.0, 80.0); 57 | // 处理URL格式:如果以//开头,添加https:前缀 58 | let image_url = if order.img.url.starts_with("//") { 59 | format!("https:{}", order.img.url) 60 | } else { 61 | order.img.url.clone() 62 | }; 63 | 64 | // 图片加载逻辑 65 | ui.add_sized(image_size, |ui: &mut egui::Ui| { 66 | if let Some(texture) = get_image_texture(ctx, &image_url) { 67 | ui.centered_and_justified(|ui| { 68 | ui.add(Image::new(&texture).fit_to_exact_size(image_size)) 69 | }).inner 70 | } else { 71 | let inner_response = ui.centered_and_justified(|ui| { 72 | ui.label("图片加载中...") 73 | }); 74 | // log::debug!("开始加载图片: {}", image_url); 75 | request_image_async(ctx.clone(), app,image_url); 76 | inner_response.inner 77 | } 78 | }); 79 | 80 | ui.add_space(12.0); 81 | 82 | // 订单信息区域 83 | ui.vertical(|ui| { 84 | ui.horizontal(|ui| { 85 | // 活动名称 86 | ui.label(RichText::new(&order.item_info.name).size(16.0).strong()); 87 | 88 | // 订单状态 89 | ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { 90 | // 根据订单状态设置不同颜色 91 | let status_color = match order.status { 92 | 2 => egui::Color32::from_rgb(0, 150, 0), // 已完成/已付款 93 | 4 => egui::Color32::from_rgb(200, 80, 0), // 已取消 94 | _ => egui::Color32::from_rgb(100, 100, 100), 95 | }; 96 | ui.label(RichText::new(&order.sub_status_name) 97 | .color(status_color) 98 | .strong()); 99 | }); 100 | }); 101 | 102 | ui.add_space(4.0); 103 | 104 | // 订单详细信息 105 | ui.horizontal(|ui| { 106 | ui.label(RichText::new("订单号:").color(egui::Color32::GRAY)); 107 | ui.monospace(&order.order_id); 108 | }); 109 | 110 | ui.horizontal(|ui| { 111 | ui.label(RichText::new("场次:").color(egui::Color32::GRAY)); 112 | ui.label(&order.item_info.screen_name); 113 | }); 114 | 115 | ui.horizontal(|ui| { 116 | ui.label(RichText::new("下单时间:").color(egui::Color32::GRAY)); 117 | ui.label(&order.ctime); 118 | }); 119 | 120 | ui.horizontal(|ui| { 121 | ui.label(RichText::new("价格:").color(egui::Color32::GRAY)); 122 | // 将分转换为元并格式化为价格 123 | let price_text = format!("¥{:.2}", order.pay_money as f64 / 100.0); 124 | ui.label(RichText::new(price_text).strong()); 125 | 126 | // 显示支付方式(如果已支付) 127 | let pay_channel = match order.pay_channel { 128 | Some(ref channel) => channel.clone(), 129 | None => "".to_string(), 130 | }; 131 | if !pay_channel.is_empty() { 132 | ui.add_space(8.0); 133 | ui.label(format!("(支付方式:{})", pay_channel)); 134 | } 135 | 136 | // 操作按钮放在右侧 137 | ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { 138 | // 根据订单状态决定是否显示不同按钮 139 | if order.status == 2 { // 已完成 140 | /* let button = egui::Button::new( 141 | egui::RichText::new("查看详情").size(16.0).color(egui::Color32::WHITE) 142 | ) 143 | .min_size(egui::vec2(100.0, 36.0)) 144 | .fill(egui::Color32::from_rgb(102, 204, 255)) 145 | .rounding(18.0); 146 | 147 | let response = ui.add(button); 148 | if response.clicked() { 149 | log::debug!("查看订单详情: {}", order.order_id); 150 | // 处理点击事件 151 | } */ 152 | } else if order.status == 1 && order.sub_status == 1 { // 待付款 153 | let pay_button = egui::Button::new( 154 | egui::RichText::new("未支付").size(16.0).color(egui::Color32::WHITE) 155 | ) 156 | .min_size(egui::vec2(80.0, 36.0)) 157 | .fill(egui::Color32::from_rgb(250, 100, 0)) 158 | .rounding(18.0); 159 | 160 | if ui.add(pay_button).clicked() { 161 | log::info!("暂不支持支付订单: {}", order.order_id); 162 | // 添加支付逻辑 163 | } 164 | } 165 | }); 166 | }); 167 | }); 168 | }); 169 | }); 170 | } 171 | 172 | // 如果没有订单 173 | if order_data.data.list.is_empty() { 174 | ui.vertical_centered(|ui| { 175 | ui.add_space(50.0); 176 | ui.label(RichText::new("暂无订单记录").size(16.0).color(egui::Color32::GRAY)); 177 | }); 178 | } 179 | } else { 180 | // 显示加载中状态 181 | ui.vertical_centered(|ui| { 182 | ui.add_space(50.0); 183 | ui.label(RichText::new("加载中...").size(16.0).color(egui::Color32::GRAY)); 184 | }); 185 | } 186 | 187 | ui.add_space(10.0); // 底部留白 188 | }); 189 | 190 | }); 191 | 192 | if !window_open { 193 | app.show_orderlist_window = None; 194 | app.orderlist_requesting = false; 195 | app.orderlist_need_reload = true; 196 | } 197 | } 198 | 199 | // 辅助函数:从缓存获取图片纹理 200 | fn get_image_texture(ctx: &egui::Context, url: &str) -> Option<TextureHandle> { 201 | 202 | ctx.memory(|mem| { 203 | // log::debug!("{:?}", mem.data); 204 | mem.data.get_temp::<TextureHandle>(egui::Id::new(url)) 205 | }) 206 | } 207 | 208 | // 辅助函数:异步请求图片 209 | fn request_image_async(ctx: egui::Context,app:&Myapp,url: String) { 210 | if ctx.memory(|mem| mem.data.get_temp::<TextureHandle>(egui::Id::new(&url)).is_some()){ 211 | log::error!("图片已存在: {}", url); 212 | return; 213 | } 214 | // 避免重复请求 215 | if ctx.memory(|mem| mem.data.get_temp::<bool>(egui::Id::new(format!("loading_{}", url))).is_some()) { 216 | return; 217 | } 218 | 219 | // 标记为正在加载 220 | log::debug!("<正在加载图片>: {}", url); 221 | ctx.memory_mut(|mem| mem.data.insert_temp(egui::Id::new(format!("loading_{}", url)), true)); 222 | 223 | // 启动异步加载线程 224 | let app_client =match app.account_manager.accounts[0].cookie_manager.clone(){ 225 | Some(client) => client, 226 | None => { 227 | log::error!("cookie_manager不存在"); 228 | let rt = tokio::runtime::Runtime::new().unwrap(); 229 | 230 | Arc::new(rt.block_on(CookieManager::new("", None, 0))) 231 | } 232 | }; 233 | let app_ua = app.default_ua.clone(); 234 | 235 | // 这里应该实现实际的图片加载逻辑 236 | // 示例: 237 | std::thread::spawn(move || { 238 | if let Some(texture)=load_texture_from_url(&ctx, app_client, &(url.clone()+"@74w_74h.jpeg"), &url){ 239 | ctx.memory_mut(|mem| { 240 | log::debug!("加载图片成功: {}", url); 241 | mem.data.insert_temp(egui::Id::new(&url), texture); 242 | mem.data.remove::<bool>(egui::Id::new(format!("loading_{}", url))); 243 | }); 244 | ctx.request_repaint(); 245 | }else{ 246 | ctx.memory_mut(|mem| { 247 | log::warn!("加载图片失败_ui,retrying: {}", url); 248 | mem.data.remove::<bool>(egui::Id::new(format!("loading_{}", url))); 249 | }); 250 | } 251 | 252 | }); 253 | 254 | 255 | } 256 | -------------------------------------------------------------------------------- /frontend/src/windows/show_qrcode.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{self, RichText}; 2 | use crate::app::Myapp; 3 | use crate::windows::login_windows::create_qrcode; 4 | 5 | pub fn show(app: &mut Myapp, ctx: &egui::Context) { 6 | 7 | let mut window_open = app.show_qr_windows.is_some(); 8 | let qr_data = app.show_qr_windows.clone().unwrap_or_default(); 9 | 10 | egui::Window::new("扫码支付") 11 | .open(&mut window_open) 12 | .resizable(false) 13 | .default_size([700.0, 400.0]) 14 | .show(ctx, |ui| { 15 | 16 | if let Some(texture) = create_qrcode(ui.ctx(), qr_data.as_str()) { 17 | 18 | ui.vertical_centered(|ui|{ 19 | ui.add_space(20.0); 20 | let rich_text= RichText::new("请使用 微信/支付宝 扫描二维码进行支付") 21 | .size(20.0) 22 | .color(egui::Color32::from_rgb(102, 204, 255)); 23 | ui.label(rich_text); 24 | ui.add_space(20.0); 25 | ui.image(&texture); 26 | }); 27 | } 28 | 29 | }); 30 | if !window_open { 31 | app.show_qr_windows = None; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /resources/fonts/NotoSansSC-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biliticket/bili_ticket_rush/5b2926b0697a75350e387da026564b5e99bdd8b2/resources/fonts/NotoSansSC-Regular.otf --------------------------------------------------------------------------------