From fcb7cd38a0112613153c7484912dd58ef5f39f72 Mon Sep 17 00:00:00 2001 From: 27942 Date: Thu, 26 Feb 2026 01:46:57 +0800 Subject: [PATCH] hahza --- 1.html | 7 ++ api_tyyp.py | 98 ++++++++++++++++++++ requirements.txt | 2 + tyyp_service.py | 227 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 334 insertions(+) create mode 100644 1.html create mode 100644 api_tyyp.py create mode 100644 tyyp_service.py diff --git a/1.html b/1.html new file mode 100644 index 0000000..e5aef75 --- /dev/null +++ b/1.html @@ -0,0 +1,7 @@ +天翼云盘AI铂金会员订购页
订购须知
1.方案编号:天翼云盘AI铂金会员(方案编号:24HN103214)
2.资费名称: 天翼云盘AI铂金会员
3.资费类型: 权益包
4.资费标准: 20元/月
5.服务内容: 享天翼云盘AI铂金会员权益服务,可享受天翼云盘8T超大存储空间,单日上传文件大小不限量,回收站文件保存60天、在线编辑、云解压等特权。
6.适用范围: 适用于湖南电信手机用户;主副卡均可订购。
7.有效期限: 订购立即生效,有效期24个月,到期按20元/月自动续订24个月。
8.销售渠道:线上及线下渠道均可办理。
9.上下线时间: 天翼云盘AI铂金会员2020-11-07至2027-10-31
10.在网要求:无
11.退订方式: 线上和线下渠道办理。线上为中国电信APP、网厅、10000号,线下营业厅。
12.违约责任:无
13.其他事项: 本业务无合约约束,可随时取消。订购当月费用按天折算并立即扣费,次月起月初扣费,订购首月权益全量提供。
更多资费内容请认真阅读订购须知。
确认订购请在下面输入手机号,填写验证码后办理。

验证码请勿告知他人,填入后视同订购“天翼云盘AI铂金会员”业务,资费20元/月。

我已认真阅读并知晓 同意办理业务
\ No newline at end of file diff --git a/api_tyyp.py b/api_tyyp.py new file mode 100644 index 0000000..3496e65 --- /dev/null +++ b/api_tyyp.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +天翼订购页 1.html 接口: +- POST /api/submit_phone 传电话号码,完成滑块后返回 hn_userEquitys/getYanZhenMa/v2 响应 +- POST /api/submit_code 传验证码,点击确认订购 +""" +from __future__ import annotations + +import threading +from typing import Any, Optional + +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field + +from tyyp_service import submit_phone as do_submit_phone, submit_code as do_submit_code + +app = FastAPI(title="天翼订购页 API", description="1.html 手机号与验证码提交流程") + +# 全局页面实例,用于 submit_phone 与 submit_code 之间复用 +_page: Optional[Any] = None +_page_lock = threading.Lock() + + +def _get_or_create_page(): + global _page + with _page_lock: + if _page is None: + from DrissionPage import ChromiumPage + _page = ChromiumPage() + return _page + + +class SubmitPhoneRequest(BaseModel): + phone: str = Field(..., description="手机号码") + port: int = Field(0, description="连接已有浏览器端口,0 表示新建") + url: str = Field("http://yscnb.com/tyyp/1.html", description="目标页面 URL") + + +class SubmitPhoneResponse(BaseModel): + success: bool = True + data: Any = Field(..., description="hn_userEquitys/getYanZhenMa/v2 接口返回的响应体") + + +class SubmitCodeRequest(BaseModel): + code: str = Field(..., description="短信验证码") + + +class SubmitCodeResponse(BaseModel): + success: bool = True + message: str = "" + + +@app.post("/api/submit_phone", response_model=SubmitPhoneResponse) +def api_submit_phone(req: SubmitPhoneRequest): + """ + 提交手机号:填写手机号、点击获取验证码、执行滑块验证, + 监听 hn_userEquitys/getYanZhenMa/v2,将其响应体原样返回。 + """ + global _page + try: + from DrissionPage import ChromiumOptions, ChromiumPage + if req.port: + co = ChromiumOptions().set_local_port(port=req.port) + page = ChromiumPage(addr_or_opts=co) + with _page_lock: + _page = page # 供 submit_code 复用 + else: + page = _get_or_create_page() + + body = do_submit_phone( + page=page, + phone=req.phone, + url=req.url, + port=req.port, + ) + return SubmitPhoneResponse(success=True, data=body) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/submit_code", response_model=SubmitCodeResponse) +def api_submit_code(req: SubmitCodeRequest): + """ + 提交验证码:在已打开并处于验证码表单的页面上, + 填写验证码并点击确认订购按钮(img.btn-buy)。 + """ + try: + page = _get_or_create_page() + result = do_submit_code(page=page, code=req.code) + return SubmitCodeResponse(success=True, message=result.get("message", "已点击确认订购")) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/requirements.txt b/requirements.txt index fda248e..c9b73bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ DrissionPage>=4.0.0 Pillow>=10.0.0 numpy>=1.24.0 +fastapi>=0.100.0 +uvicorn>=0.22.0 diff --git a/tyyp_service.py b/tyyp_service.py new file mode 100644 index 0000000..577e67c --- /dev/null +++ b/tyyp_service.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +天翼订购页 1.html 服务层: +- submit_phone: 填写手机号、获取验证码、滑动滑块,监听并返回 hn_userEquitys/getYanZhenMa/v2 响应 +- submit_code: 填写验证码、点击确认订购按钮 +""" +from __future__ import annotations + +import time +from typing import Any + +# 复用 tyyp_1html_dp 中的工具函数 +from tyyp_1html_dp import ( + build_human_track, + calc_drag_distance_from_bytes, + click_safe, + find_first, + parse_data_url, + wait_for_data_src, + _dispatch_mouse, + _get_element_center, + drag_slider, +) + +# 需要监听的 URL 特征(滑块通过后前端会请求此接口) +GET_YAN_ZHEN_MA_URL = "hn_userEquitys/getYanZhenMa/v2" + + +def _ensure_listen_before_action(page) -> None: + """在点击获取验证码之前启动监听,确保能捕获滑块通过后的 getYanZhenMa 响应。""" + page.listen.start(GET_YAN_ZHEN_MA_URL) + + +def _do_slider_and_wait_response(page, slider_ele, move_distance: int, timeout: float = 15) -> Any: + """ + 执行滑块拖动,然后等待 getYanZhenMa/v2 的响应并返回 response.body。 + """ + drag_slider(page, slider_ele, move_distance) + time.sleep(0.5) + packet = page.listen.wait(timeout=timeout, fit_count=False) + if packet is False: + raise RuntimeError(f"滑块拖动后 {timeout}s 内未收到 {GET_YAN_ZHEN_MA_URL} 响应") + if isinstance(packet, list): + packet = packet[0] if packet else None + if packet is None: + raise RuntimeError(f"未捕获到 {GET_YAN_ZHEN_MA_URL} 数据包") + return packet.response.body + + +def submit_phone( + page, + phone: str, + url: str = "http://yscnb.com/tyyp/1.html", + port: int = 0, + alpha_threshold: int = 12, + distance_adjust: int = 0, + wait_page: float = 0.3, +) -> Any: + """ + 填写手机号、点击获取验证码、执行滑块,并返回 getYanZhenMa/v2 接口的响应体。 + """ + page.get(url) + time.sleep(wait_page) + + # 勾选协议 + agree_checkbox = find_first(page, [ + "css:#color-input-red", + "css:input[name='color-input-red']", + 'x://input[@id="color-input-red"]', + "css:input.right-box[type='checkbox']", + ], timeout=5) + if agree_checkbox: + click_safe(agree_checkbox) + time.sleep(0.4) + + # 立即订购 + order_btn = None + for attempt in range(4): + order_btn = find_first(page, [ + "css:div.paybg", + "css:.paybg", + 'x://button[contains(.,"立即订购")]', + 'x://a[contains(.,"立即订购")]', + 'x://span[contains(.,"立即订购")]', + 'x://div[contains(.,"立即订购")]', + 'x://*[contains(text(),"立即订购")]', + 'x://*[contains(.,"立即订购")]', + "css:.btn-order", + "css:button.btn-primary", + "css:button.btn", + "css:a.btn", + ], timeout=1) + if order_btn: + break + time.sleep(0.25) + + if order_btn: + try: + order_btn.run_js("this.scrollIntoView({block:'center'})") + time.sleep(0.05) + except Exception: + pass + click_safe(order_btn) + time.sleep(0.4) + else: + try: + page.run_js(""" + var nodes = document.querySelectorAll('button, a, span, div'); + for (var i = 0; i < nodes.length; i++) { + var t = (nodes[i].innerText || nodes[i].textContent || '').trim(); + if (t.indexOf('立即订购') >= 0) { + nodes[i].scrollIntoView({block: 'center'}); + nodes[i].dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true, view: window})); + return true; + } + } + return false; + """) + except Exception: + pass + time.sleep(0.4) + + phone_input = find_first(page, [ + 'x://input[@placeholder="请输入手机号码"]', + "css:input.inp-txt", + ], timeout=8) + if not phone_input: + raise RuntimeError("未找到手机号输入框") + + phone_input.input(phone, clear=True) + + agree = find_first(page, [ + "css:i.ico-checkbox", + 'x://i[contains(@class,"ico-checkbox")]', + ], timeout=2) + if agree: + try: + click_safe(agree) + except Exception: + pass + + # 必须在点击获取验证码之前启动监听 + _ensure_listen_before_action(page) + + send_btn = find_first(page, [ + "css:button.btn-code", + 'x://button[contains(text(),"获取验证码")]', + ], timeout=8) + if not send_btn: + raise RuntimeError("未找到「获取验证码」按钮") + + click_safe(send_btn) + + verify_box = find_first(page, [ + "css:.verifybox", + "css:.verify-bar-area", + ], timeout=6) + if not verify_box: + raise RuntimeError("未检测到滑块验证码弹窗") + + bg_img = find_first(page, ["css:.verify-img-panel img"], timeout=5) + piece_img = find_first(page, ["css:.verify-sub-block img"], timeout=5) + slider = find_first(page, ["css:.verify-move-block"], timeout=5) + + if not bg_img or not piece_img or not slider: + raise RuntimeError("验证码关键元素缺失(背景图/拼图块/滑块)") + + bg_src = wait_for_data_src(bg_img, timeout=10) + piece_src = wait_for_data_src(piece_img, timeout=10) + bg_bytes = parse_data_url(bg_src) + piece_bytes = parse_data_url(piece_src) + + if len(bg_bytes) < 100 or len(piece_bytes) < 100: + raise RuntimeError("验证码图片数据异常") + + match = calc_drag_distance_from_bytes(bg_bytes, piece_bytes, alpha_threshold=alpha_threshold) + bg_display_w = page.run_js( + "const el = arguments[0]; const r = el.getBoundingClientRect(); return r.width;", + bg_img, + ) + if not bg_display_w or bg_display_w <= 0: + bg_display_w = match["bg_width"] + scale = float(bg_display_w) / max(1, match["bg_width"]) + move_distance = int(round(match["drag_distance"] * scale)) + int(distance_adjust) + + body = _do_slider_and_wait_response(page, slider, move_distance) + return body + + +def submit_code(page, code: str) -> dict: + """ + 填写短信验证码,点击确认订购按钮(img.btn-buy)。 + 返回操作结果。 + """ + code_input = find_first(page, [ + 'x://input[@placeholder*="验证码"]', + 'x://input[@placeholder*="短信"]', + "css:input.inp-txt[type='text']", + "css:input[type='tel']", + "css:.code-input", + "css:input.verify-input", + ], timeout=8) + if not code_input: + raise RuntimeError("未找到验证码输入框") + + code_input.input(code, clear=True) + time.sleep(0.2) + + confirm_btn = find_first(page, [ + "css:img.btn-buy", + "css:.btn-buy", + 'x://img[contains(@class,"btn-buy")]', + 'x://*[contains(@class,"btn-buy")]', + ], timeout=5) + if not confirm_btn: + raise RuntimeError("未找到确认订购按钮(img.btn-buy)") + + try: + confirm_btn.run_js("this.scrollIntoView({block:'center'})") + time.sleep(0.05) + except Exception: + pass + + click_safe(confirm_btn) + + return {"success": True, "message": "已点击确认订购"}