nelson 3 mesiacov pred
rodič
commit
5e5cbc2b03

+ 2 - 3
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/wristwatch/AuthorizeController.java

@@ -4,7 +4,6 @@ import cn.sikey.framework.common.pojo.CommonResult;
 import cn.sikey.mcdisk.controller.app.wristwatch.vo.*;
 import cn.sikey.mcdisk.enums.RateLimit;
 import cn.sikey.mcdisk.service.AuthorizeService;
-
 import cn.sikey.selenium.api.selenium.SmsApi;
 import cn.sikey.selenium.api.selenium.dto.AuthRespVO;
 import cn.sikey.selenium.api.selenium.dto.TriggerSmsReqDTO;
@@ -62,8 +61,8 @@ public class AuthorizeController {
      * @return
      */
     @RateLimit(key = "accessToken1", type = RateLimit.LimitType.GLOBAL)
-    @PostMapping("/accessToken1")
-    public CommonResult accessToken1(@Validated @RequestBody AccessToken1ReqVO accessToken1ReqVO) {
+    @GetMapping("/accessToken1")
+    public CommonResult accessToken1(@Validated AccessToken1ReqVO accessToken1ReqVO) {
         return authorizeService.accessToken1(accessToken1ReqVO);
     }
 

+ 3 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/enums/ApiEndpointsEnum.java

@@ -37,6 +37,9 @@ public enum ApiEndpointsEnum {
     // 上传文件到家庭云
     GET_FILE_UPLOAD_URL("/richlifeApp/devapp/andAlbum/openApi/getFileUploadURL"),
 
+    // 删除家庭云文件
+    DELETE_CONTENTS("/richlifeApp/devapp/andAlbum/openApi/deleteContents"),
+
     // 中间页接入方式获取用户凭证oauth2/accessToken1
     ACCESS_TOKEN1("/open-mpplatform/oauth2/accessToken1"),
 

+ 1 - 1
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/FamilyCloudServiceImpl.java

@@ -191,7 +191,7 @@ public class FamilyCloudServiceImpl extends AbstractMcdiskService implements Fam
         }
 
         // 准备请求参数
-        String apiUrl = mcdiskConfig.getPath() + ApiEndpointsEnum.GET_FILE_DOWN_LOAD_URL.getPath();
+        String apiUrl = mcdiskConfig.getPath() + ApiEndpointsEnum.DELETE_CONTENTS.getPath();
         JSONObject jsonObject = new JSONObject();
         JSONObject accountType = new JSONObject();
         accountType.set("accountType", deleteContentsReqVO.getCommonAccountInfo().getAccountType());

+ 7 - 0
sikey-selenium-business/pom.xml

@@ -48,6 +48,7 @@
         <commons-codec.version>1.15</commons-codec.version>
 
         <selenium.version>4.11.0</selenium.version>
+        <!--<playwright.version>1.53.0</playwright.version>-->
     </properties>
 
     <dependencyManagement>
@@ -234,6 +235,12 @@
                 <version>${revision}</version>
             </dependency>
 
+            <!--<dependency>
+                <groupId>com.microsoft.playwright</groupId>
+                <artifactId>playwright</artifactId>
+                <version>${playwright.version}</version>
+            </dependency>-->
+
             <!-- RPC 远程调用相关 -->
             <!--<dependency>
                 <groupId>org.springframework.cloud</groupId>

+ 0 - 4
sikey-selenium-business/sikey-selenium-business-biz/pom.xml

@@ -46,8 +46,6 @@
         <gson.version>2.11.0</gson.version>
         <security.version>3.4.4</security.version>
         <commons-codec.version>1.15</commons-codec.version>
-
-        <selenium.version>4.11.0</selenium.version>
     </properties>
 
     <dependencies>
@@ -207,14 +205,12 @@
         <dependency>
             <groupId>org.seleniumhq.selenium</groupId>
             <artifactId>selenium-java</artifactId>
-            <version>${selenium.version}</version>
         </dependency>
 
         <!-- ChromeDriver 支持 -->
         <dependency>
             <groupId>org.seleniumhq.selenium</groupId>
             <artifactId>selenium-chrome-driver</artifactId>
-            <version>${selenium.version}</version>
         </dependency>
 
         <!-- Redis 相关 -->

+ 439 - 0
sikey-selenium-business/sikey-selenium-business-biz/src/main/java/cn/sikey/selenium/api/SmsNewServiceImpl.java

@@ -0,0 +1,439 @@
+/*
+package cn.sikey.selenium.api;
+
+import cn.sikey.framework.common.exception.ServiceException;
+import cn.sikey.framework.common.pojo.CommonResult;
+import cn.sikey.selenium.api.selenium.SmsApi;
+import cn.sikey.selenium.api.selenium.dto.AuthRespVO;
+import cn.sikey.selenium.api.selenium.dto.TriggerSmsReqDTO;
+import cn.sikey.selenium.api.selenium.dto.VerifyCodeReqDTO;
+import cn.sikey.selenium.util.AppIdGeneratorUtil;
+import cn.sikey.selenium.util.PlaywrightContextManagerUtil;
+import cn.sikey.selenium.util.PlaywrightContextManagerUtil.PlaywrightSession;
+import com.microsoft.playwright.*;
+import com.microsoft.playwright.options.WaitUntilState;
+import io.netty.handler.timeout.TimeoutException;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpStatus;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+*/
+/**
+ * @Author: nelson
+ * @Date: 2025/6/26
+ * @Description: 验证码
+ *//*
+
+@Slf4j
+@RestController // 提供 RESTful API 接口,给 Feign 调用
+@Validated
+public class SmsNewServiceImpl implements SmsApi {
+
+    @Value("${selenium.remote.url}")
+    private String url;
+
+    @Value("${selenium.remote.loginUrl}")
+    private String loginUrl;
+
+    @Value("${selenium.remote.redirectUrl}")
+    private String redirectUrl;
+
+    public static final String ACCESS_TOKEN1 = "/app-api/mcdisk/wristwatch/authorize/accessToken1";
+
+    @Value("${selenium.remote.appid}")
+    private String appid;
+
+    @Value("${selenium.remote.appkey}")
+    private String appkey;
+
+    public static final String APP_TITLE = "移动云盘";
+
+    public static final int GET_VERIFICATION_CODE_AGAIN = 10001;
+
+    public static final int UUID_EXPIRES_MINUTE = 15;
+
+    public static final String SMS_CODE_KEY = "sms:code:phone:";
+
+    public static final String SMS_SESSION_ID_KEY = "sms:session:id:";
+
+    @Resource
+    private RedisTemplate<String, Object> redisTemplate;
+
+    // 超时时间(秒)
+    private static final long SMS_TIMEOUT = 30;
+
+    // 最大重试次数
+    private static final int MAX_RETRY = 3;
+
+    */
+/**
+     * 发送验证码
+     *
+     * @param request 请求
+     * @return
+     *//*
+
+    @Override
+    public CommonResult<Void> triggerSmsVerification(TriggerSmsReqDTO request) {
+        String uuid = AppIdGeneratorUtil.generateUuid(appid);
+        PlaywrightSession session;
+
+        String phoneNumber = request.getPhoneNumber();
+        String deviceId = request.getTicket();
+
+        try {
+            // 创建 Playwright 会话(使用 Chromium 浏览器)
+            String sessionId = PlaywrightContextManagerUtil.createSession(url);
+            session = PlaywrightContextManagerUtil.getSession(sessionId);
+            Page page = session.getPage();
+
+            // 设置导航超时和操作超时
+            page.setDefaultNavigationTimeout(TimeUnit.SECONDS.toMillis(SMS_TIMEOUT));
+            page.setDefaultTimeout(TimeUnit.SECONDS.toMillis(SMS_TIMEOUT));
+
+            // 构建登录URL
+            String encodedUrl = URLEncoder.encode(redirectUrl + ACCESS_TOKEN1 + "?uuid=" + uuid + "&ticket=" + deviceId, StandardCharsets.UTF_8);
+            String toLoginUrl = String.format("%s?pageType=3&appId=%s&appKey=%s&deviceId=%s&appTitle=%s&uuid=%s&redirectUrl=%s", loginUrl, appid, appkey, deviceId, APP_TITLE, uuid, encodedUrl);
+
+            log.info("[发送验证码]获取移动云盘地址:{}", toLoginUrl);
+
+            // 导航到登录页,等待网络空闲
+            Response response = page.navigate(toLoginUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
+
+            if (!response.ok()) {
+                throw new ServiceException(HttpStatus.SC_BAD_GATEWAY, "页面加载失败,状态码: " + response.status());
+            }
+
+            // 确保页面包含关键元素
+            page.waitForSelector("//input[@type='number' and @maxlength='11']", new Page.WaitForSelectorOptions().setTimeout(SMS_TIMEOUT * 1000));
+
+            // 输入手机号 - 使用精确选择器
+            Locator phoneInput = page.locator("xpath=//input[@type='number' and @maxlength='11']");
+            phoneInput.fill(phoneNumber);
+
+            // 获取验证码按钮 - 使用文本定位
+            Locator codeBtn = page.locator("button:has-text('获取验证码')");
+
+            // 重试机制处理可能的点击失败
+            boolean clickSuccess = false;
+            for (int i = 0; i < MAX_RETRY && !clickSuccess; i++) {
+                try {
+                    // 确保按钮可见
+                    codeBtn.scrollIntoViewIfNeeded();
+
+                    // 使用 Playwright 的点击(带自动等待)
+                    codeBtn.click(new Locator.ClickOptions().setTimeout(5000) // 5秒超时
+                            .setForce(true)); // 强制点击
+
+                    clickSuccess = true;
+                    log.info("[发送验证码]第{}次点击成功", i + 1);
+                } catch (PlaywrightException e) {
+                    log.warn("[发送验证码]点击失败,重试中... 原因: {}", e.getMessage());
+
+                    // 等待可能的状态更新
+                    page.waitForTimeout(1000); // 1秒等待
+                }
+            }
+
+            if (!clickSuccess) {
+                throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "无法点击验证码按钮");
+            }
+
+            // 检查是否出现成功提示
+            Locator successTip = page.locator("text=验证码已发送");
+            if (successTip.isVisible(new Locator.IsVisibleOptions().setTimeout(3000))) {
+                log.info("[发送验证码]短信验证码已发送,手机号:{}", phoneNumber);
+            } else {
+                // 检查错误提示
+                Locator errorTip = page.locator("div.el-message--error, div.error-message");
+                if (errorTip.isVisible()) {
+                    throw new ServiceException(HttpStatus.SC_BAD_REQUEST, "发送失败: " + errorTip.textContent());
+                }
+                log.warn("[发送验证码]未检测到成功提示,但可能已发送");
+            }
+
+            // 存储到Redis
+            redisTemplate.opsForValue().set(SMS_CODE_KEY + phoneNumber, uuid, Duration.ofMinutes(UUID_EXPIRES_MINUTE));
+
+            redisTemplate.opsForValue().set(SMS_SESSION_ID_KEY + phoneNumber, sessionId, Duration.ofMinutes(UUID_EXPIRES_MINUTE));
+        } catch (Exception e) {
+            throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "触发短信验证码失败: " + e.getMessage());
+        }
+
+        return CommonResult.success();
+    }
+
+    */
+/**
+     * 验证码登录
+     *
+     * @param request 校验验证码登录
+     * @return
+     *//*
+
+    @Override
+    public CommonResult<AuthRespVO> verifyCodeAndLogin(VerifyCodeReqDTO request) {
+        String phoneNumber = request.getPhoneNumber();
+        Object uuid = redisTemplate.opsForValue().get(SMS_CODE_KEY + phoneNumber);
+        if (Objects.isNull(uuid)) {
+            throw new ServiceException(GET_VERIFICATION_CODE_AGAIN, "请重新获取验证码");
+        }
+
+        Object sessionId = redisTemplate.opsForValue().get(SMS_SESSION_ID_KEY + phoneNumber);
+        if (Objects.isNull(sessionId)) {
+            throw new ServiceException(GET_VERIFICATION_CODE_AGAIN, "会话已过期,请重新获取验证码");
+        }
+
+        String deviceId = request.getTicket();
+        String verificationCode = request.getVerificationCode();
+        String sId = String.valueOf(sessionId);
+        PlaywrightSession session;
+
+        try {
+            // 获取Playwright会话
+            session = PlaywrightContextManagerUtil.getSession(sId);
+            Page page = session.getPage();
+
+            // 设置超时
+            page.setDefaultTimeout(TimeUnit.SECONDS.toMillis(SMS_TIMEOUT));
+
+            // 构建登录URL
+            String encodedUrl = redirectUrl + ACCESS_TOKEN1;
+            String toLoginUrl = String.format(
+                    "%s?pageType=3&appId=%s&appKey=%s&deviceId=%s&appTitle=%s&uuid=%s&redirectUrl=%s",
+                    loginUrl, appid, appkey, deviceId, APP_TITLE, uuid, encodedUrl
+            );
+
+            log.info("[校验验证码登录]获取移动云盘地址:{}", toLoginUrl);
+
+            // 如果当前页面不是登录页,则导航到登录页
+            if (!page.url().contains("middlePage")) {
+                page.navigate(toLoginUrl, new Page.NavigateOptions()
+                        .setWaitUntil(WaitUntilState.NETWORKIDLE));
+
+                // 等待页面关键元素
+                page.waitForSelector("//input[@type='number' and @maxlength='11']",
+                        new Page.WaitForSelectorOptions().setTimeout(5000));
+            }
+
+            // 输入验证码
+            Locator codeInput = page.locator("xpath=//input[@placeholder='输入验证码' and @maxlength='6']");
+            if (!codeInput.isVisible()) {
+                log.warn("验证码输入框不可见,尝试滚动到视图中");
+                codeInput.scrollIntoViewIfNeeded();
+            }
+            codeInput.fill(verificationCode);
+
+            // 勾选协议
+            Locator agreementLabel = page.locator("xpath=//div[contains(@class,'login_agreementGroup')]//label[contains(@class,'el-checkbox')]");
+
+            // 确保协议复选框可见
+            if (!agreementLabel.isVisible()) {
+                agreementLabel.scrollIntoViewIfNeeded();
+            }
+
+            // 检查是否已勾选
+            Locator agreementCheckbox = page.locator("xpath=//div[contains(@class,'login_agreementGroup')]//input[@type='checkbox']");
+            if (!"true".equals(agreementCheckbox.getAttribute("aria-checked"))) {
+                log.info("尝试勾选协议复选框");
+
+                // 重试勾选协议
+                boolean agreementChecked = false;
+                for (int i = 0; i < MAX_RETRY && !agreementChecked; i++) {
+                    try {
+                        agreementLabel.click(new Locator.ClickOptions()
+                                .setForce(true)
+                                .setTimeout(3000));
+
+                        // 验证是否勾选成功
+                        if ("true".equals(agreementCheckbox.getAttribute("aria-checked"))) {
+                            agreementChecked = true;
+                            log.info("协议勾选成功");
+                        } else {
+                            log.warn("协议未勾选,尝试JavaScript点击");
+                            page.evaluate("document.querySelector('div.login_agreementGroup input[type=checkbox]').click()");
+                        }
+                    } catch (PlaywrightException e) {
+                        log.warn("勾选协议失败,重试中... ({}/3)", i + 1);
+                        page.waitForTimeout(1000); // 等待1秒
+                    }
+                }
+
+                if (!agreementChecked) {
+                    throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR,
+                            "无法勾选协议复选框");
+                }
+            }
+
+            // 3. 点击登录按钮
+            Locator loginButton = page.locator("xpath=//button[contains(@class, 'login_btn') " +
+                    "and not(contains(@class, 'one_key_login_btn')) " +
+                    "and .//span[normalize-space()='登录'] " +
+                    "and not(@disabled)]");
+
+            // 确保按钮可见
+            if (!loginButton.isVisible()) {
+                loginButton.scrollIntoViewIfNeeded();
+            }
+
+            // 重试点击登录按钮
+            boolean loginClicked = false;
+            for (int i = 0; i < MAX_RETRY && !loginClicked; i++) {
+                try {
+                    loginButton.click(new Locator.ClickOptions()
+                            .setForce(true)
+                            .setTimeout(5000));
+                    loginClicked = true;
+                    log.info("登录按钮点击成功");
+                } catch (PlaywrightException e) {
+                    log.warn("登录按钮点击失败,重试中... ({}/3)", i + 1);
+                    page.waitForTimeout(1000); // 等待1秒
+
+                    // 检查按钮是否被禁用
+                    if ("true".equals(loginButton.getAttribute("disabled"))) {
+                        log.warn("登录按钮被禁用,检查协议是否勾选");
+                        if (!"true".equals(agreementCheckbox.getAttribute("aria-checked"))) {
+                            agreementLabel.click(new Locator.ClickOptions().setForce(true));
+                        }
+                    }
+                }
+            }
+
+            if (!loginClicked) {
+                throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR,
+                        "无法点击登录按钮");
+            }
+
+            // 验证登录成功
+            verifyLoginSuccess(page);
+
+            log.info("[校验验证码登录]登录成功,手机号:{},验证码:{}", phoneNumber, verificationCode);
+        } catch (Exception e) {
+            log.error("[校验验证码登录]异常:{0}", e);
+            throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "触发校验验证码登录失败");
+        } finally {
+            // 清理会话
+            if (sessionId != null) {
+                PlaywrightContextManagerUtil.destroySession(sId);
+
+                // 清理Redis中的会话信息
+                redisTemplate.delete(SMS_CODE_KEY + phoneNumber);
+                redisTemplate.delete(SMS_SESSION_ID_KEY + phoneNumber);
+            }
+        }
+        return null;
+    }
+
+    // 验证登录成功
+    private void verifyLoginSuccess(Page page) {
+        try {
+            // 验证URL变化 - 等待重定向
+            page.waitForURL(url ->
+                            url.contains("/home") ||
+                                    url.contains("/dashboard") ||
+                                    url.contains("/console"),
+                    new Page.WaitForURLOptions().setTimeout(15000));
+
+            log.info("[校验验证码登录]登录成功!当前URL:{}", page.url());
+
+            // 验证用户信息元素
+            Locator userInfo = page.locator("xpath=//div[contains(@class, 'user-info') or contains(@class, 'user-name')]");
+            if (userInfo.isVisible(new Locator.IsVisibleOptions().setTimeout(5000))) {
+                log.info("[校验验证码登录]登录成功!用户名:{}", userInfo.textContent());
+                return;
+            }
+
+            // 检查认证Cookie
+            BrowserContext context = page.context();
+            String authToken = context.cookies().stream()
+                    .filter(cookie ->
+                            "CMCC-TOKEN".equals(cookie.name) ||
+                                    "CMCC_SESSION".equals(cookie.name))
+                    .findFirst()
+                    .map(cookie -> cookie.value)
+                    .orElse(null);
+
+            if (authToken != null) {
+                log.info("[校验验证码登录]登录成功!认证Token存在");
+                return;
+            }
+
+            // 检查本地存储
+            Object localStorageToken = page.evaluate(
+                    "() => localStorage.getItem('CMCC_AUTH_TOKEN') || " +
+                            "localStorage.getItem('access_token')");
+
+            if (localStorageToken != null) {
+                log.info("[校验验证码登录]登录成功!本地存储Token存在");
+                return;
+            }
+
+            // 如果以上验证都失败,尝试检查错误
+            checkLoginErrors(page);
+
+            // 最终验证失败
+            throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR,
+                    "无法确认登录状态,但未检测到错误");
+
+        } catch (TimeoutException e) {
+            // 处理登录超时
+            checkLoginErrors(page);
+            throw new ServiceException(HttpStatus.SC_REQUEST_TIMEOUT, "登录验证超时");
+        }
+    }
+
+    // 检查登录错误
+    private void checkLoginErrors(Page page) {
+        // 检查错误消息
+        Locator errorTip = page.locator("div.el-message--error, div.error-message");
+        if (errorTip.isVisible(new Locator.IsVisibleOptions().setTimeout(3000))) {
+            String errorText = errorTip.textContent();
+            log.error("[校验验证码登录]登录失败!原因:{}", errorText);
+
+            if (errorText.contains("验证码") || errorText.contains("验证码错误")) {
+                throw new ServiceException(HttpStatus.SC_BAD_REQUEST, "验证码错误,请重新输入");
+            } else if (errorText.contains("账号") || errorText.contains("用户不存在")) {
+                throw new ServiceException(HttpStatus.SC_BAD_REQUEST, "账号不存在或密码错误");
+            } else if (errorText.contains("协议") || errorText.contains("未同意")) {
+                throw new ServiceException(HttpStatus.SC_BAD_REQUEST, "请同意用户协议");
+            }
+            throw new ServiceException(HttpStatus.SC_BAD_REQUEST, "登录失败: " + errorText);
+        }
+
+        // 检查表单错误
+        Locator formErrors = page.locator("div.el-form-item__error");
+        if (formErrors.isVisible()) {
+            String errorText = formErrors.first().textContent();
+            log.error("[校验验证码登录]表单错误:{}", errorText);
+            throw new ServiceException(HttpStatus.SC_BAD_REQUEST, "表单错误: " + errorText);
+        }
+
+        // 检查系统错误提示
+        Locator systemErrors = page.locator("text=/状态码|错误码|系统异常/");
+        if (systemErrors.isVisible()) {
+            String errorText = systemErrors.textContent();
+            log.error("[校验验证码登录]系统错误:{}", errorText);
+            throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "系统错误: " + errorText);
+        }
+
+        // 检查页面内容中的错误关键字
+        String pageContent = page.content();
+        if (pageContent.contains("错误") || pageContent.contains("exception")) {
+            log.error("[校验验证码登录]页面包含错误信息关键字");
+            throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "系统异常,请稍后重试");
+        }
+    }
+
+}
+
+*/

+ 48 - 105
sikey-selenium-business/sikey-selenium-business-biz/src/main/java/cn/sikey/selenium/api/SmsServiceImpl.java

@@ -11,7 +11,10 @@ import cn.sikey.selenium.util.WebDriverContextManagerUtil;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.http.HttpStatus;
-import org.openqa.selenium.*;
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
 import org.openqa.selenium.interactions.Actions;
 import org.openqa.selenium.support.ui.ExpectedConditions;
 import org.openqa.selenium.support.ui.WebDriverWait;
@@ -20,10 +23,10 @@ import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.time.Duration;
-import java.util.List;
 import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * @Author: nelson
@@ -65,8 +68,6 @@ public class SmsServiceImpl implements SmsApi {
     @Resource
     private RedisTemplate<String, Object> redisTemplate;
 
-    ConcurrentHashMap<String, Object> driverMap = new ConcurrentHashMap<>();
-
     /**
      * 发送验证码
      *
@@ -85,11 +86,9 @@ public class SmsServiceImpl implements SmsApi {
             driver = WebDriverContextManagerUtil.getDriver(sessionId);
             WebDriverWait wait = WebDriverContextManagerUtil.getWait(sessionId);
 
-            String encodedUrl = redirectUrl + ACCESS_TOKEN1; //URLEncoder.encode(redirectUrl + ACCESS_TOKEN1, StandardCharsets.UTF_8); // URLEncoder.encode(redirectUrl + ACCESS_TOKEN1 + "?uuid=" + uuid + "&ticket=" + deviceId, StandardCharsets.UTF_8);
-            //driver = getDriver();
-            //WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(WAIT_FOR_SECOND));
+            String encodedUrl = URLEncoder.encode(redirectUrl + ACCESS_TOKEN1 + "?uuid=" + uuid + "&ticket=" + deviceId, StandardCharsets.UTF_8);
             String toLoginUrl = loginUrl + "?pageType=3&appId=" + appid + "&appKey=" + appkey + "&deviceId=" + deviceId + "&appTitle=" + APP_TITLE + "&uuid=" + uuid + "&redirectUrl=" + encodedUrl;
-            // String toLoginUrl = "https://miniapp.yun.139.com/middle/index.html#/middlePage?pageType=3&appId=1562129682013491200&appKey=c5499597b399ccf2a6b763ac2e330c9f&deviceId=helloworld&appTitle=name&uuid=MTU2MjEyOTY4MjAxMzQ5MTIwMF8yMDIzMTAwNzEwNTk0OTEyMzQ1Ng%3D%3D&redirectUrl=http%3A%2F%2Fopen.yun.139.com%2F%23%2Fhome";
+
             driver.get(toLoginUrl);
             log.info("[发送验证码]获取移动云盘地址:{}", toLoginUrl);
             // 等待页面加载完成
@@ -114,9 +113,7 @@ public class SmsServiceImpl implements SmsApi {
         } catch (Exception e) {
             log.info("[发送验证码]异常,页面内容:", driver.findElement(By.tagName("body")).getAttribute("outerHTML"));
             throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "触发短信验证码失败");
-        } /*finally {
-            driver.quit();
-        }*/
+        }
 
         return CommonResult.success();
     }
@@ -147,118 +144,64 @@ public class SmsServiceImpl implements SmsApi {
         String sId = "";
         try {
 
-            String encodedUrl = redirectUrl + ACCESS_TOKEN1; //+ "?uuid=" + uuid + "&ticket=" + deviceId; //URLEncoder.encode(redirectUrl + ACCESS_TOKEN1 + "?uuid=" + uuid + "&ticket=" + deviceId, StandardCharsets.UTF_8);
-            // driver = getDriver();
-            // WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(WAIT_FOR_SECOND));
+            String encodedUrl = URLEncoder.encode(redirectUrl + ACCESS_TOKEN1 + "?uuid=" + uuid + "&ticket=" + deviceId, StandardCharsets.UTF_8);
+
             sId = String.valueOf(sessionId);
             driver = WebDriverContextManagerUtil.getDriver(sId);
             WebDriverWait wait = WebDriverContextManagerUtil.getWait(sId);
 
             String toLoginUrl = loginUrl + "?pageType=3&appId=" + appid + "&appKey=" + appkey + "&deviceId=" + deviceId + "&appTitle=" + APP_TITLE + "&uuid=" + uuid + "&redirectUrl=" + encodedUrl;
-            // String toLoginUrl = "https://miniapp.yun.139.com/middle/index.html#/middlePage?pageType=3&appId=1562129682013491200&appKey=c5499597b399ccf2a6b763ac2e330c9f&deviceId=helloworld&appTitle=name&uuid=MTU2MjEyOTY4MjAxMzQ5MTIwMF8yMDIzMTAwNzEwNTk0OTEyMzQ1Ng%3D%3D&redirectUrl=http%3A%2F%2Fopen.yun.139.com%2F%23%2Fhome";
-            // driver.get(toLoginUrl);
-            log.info("[校验验证码登录]获取移动云盘地址:{}", toLoginUrl);
-            // 等待页面加载完成
-            // wait.until(ExpectedConditions.urlContains("middlePage"));
 
-            // 手机号输入框定位
-            /*WebElement phoneInput = wait.until(ExpectedConditions.presenceOfElementLocated(
-                    By.xpath("//input[@type='number' and @maxlength='11']")
-            ));
-            phoneInput.sendKeys(phoneNumber);*/
+            log.info("[校验验证码登录]获取移动云盘地址:{}", toLoginUrl);
 
             // 验证码输入框定位
             WebElement codeInput = wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//input[@placeholder='输入验证码' and @maxlength='6']")));
             codeInput.sendKeys(verificationCode);
 
             Thread.sleep(3000);
-            // 等待协议复选框的label元素(使用更精确的定位)
-            /*WebElement agreementLabel = new WebDriverWait(driver, Duration.ofSeconds(30))
-                    .until(ExpectedConditions.elementToBeClickable(
-                            // 精确匹配:使用label的class组合 + 父容器限定
-                            By.xpath("//div[@class='login_agreementGroup']//label[@class='el-checkbox login_agreement']")
-                    ));
-
-            // 首选点击label(最可靠)
-            agreementLabel.click();
-
-            // 若点击未生效,使用JavaScript点击(穿透遮挡)
-            if (!driver.findElement(By.xpath("//div[@class='login_agreementGroup']//input")).isSelected()) {
-                ((JavascriptExecutor) driver).executeScript("arguments[0].click();", agreementLabel);
-            }
-
-            // 通过父容器直接提交表单
-            WebElement loginContainer = driver.findElement(By.id("login_mobile"));
-            ((JavascriptExecutor) driver).executeScript(
-                    "var form = arguments[0].closest('form');" +
-                            "if (form) {" +
-                            "   form.submit();" +
-                            "} else {" +
-                            "   var event = new Event('submit', {bubbles: true});" +
-                            "   arguments[0].dispatchEvent(event);" +
-                            "}",
-                    loginContainer
-            );*/
 
             // 点击协议复选框(使用优化方案)
-            WebElement agreementLabel = new WebDriverWait(driver, Duration.ofSeconds(30)).until(ExpectedConditions.elementToBeClickable(By.xpath("//div[contains(@class,'login_agreementGroup')]//label[contains(@class,'el-checkbox')]")));
+            WebElement agreementLabel = new WebDriverWait(driver, Duration.ofSeconds(30)).until(ExpectedConditions.elementToBeClickable(By.xpath("//div[contains(@class,'login_agreementGroup')]//input[@type='checkbox']/ancestor::label[1]")));
 
             // 使用Actions确保点击成功
             new Actions(driver).scrollToElement(agreementLabel).moveToElement(agreementLabel).click().perform();
 
-            // 等待登录按钮变为可点击状态(非禁用状态)
-            WebElement loginButton = wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//button[contains(@class, 'login_btn') " + "and not(contains(@class, 'one_key_login_btn')) " + "and .//span[normalize-space()='登录'] " + "and not(@disabled)]")));
-
-            // 添加短暂等待确保状态更新
-            Thread.sleep(300); // 针对Vue/React的UI更新
-
-            // 使用Actions点击更可靠
-            new Actions(driver).moveToElement(loginButton).click().perform();
-
-            // 等待登录完成并验证登录状态(关键验证步骤)
-            try {
-                // 方案1:验证页面标题变化(最佳实践)
-                new WebDriverWait(driver, Duration.ofSeconds(15)).until(ExpectedConditions.not(ExpectedConditions.titleIs("中国移动云盘开放平台")));
-                log.info("[校验验证码登录]登录成功!新页面标题:{}", driver.getTitle());
-
-            } catch (TimeoutException e) {
-                // 检查中国移动云盘特有的错误提示
-                List<WebElement> errorMessages = driver.findElements(By.xpath("//div[contains(@class, 'el-message--error') or " + "contains(@class, 'error-message')]"));
-
-                if (!errorMessages.isEmpty()) {
-                    String errorText = errorMessages.get(0).getText();
-                    log.error("[校验验证码登录]登录失败!原因:{}", errorText);
-
-                    // 特殊处理:中国移动常见的错误提示
-                    if (errorText.contains("验证码")) {
-                        log.error("[校验验证码登录]可能需要重新获取验证码");
-                    } else if (errorText.contains("账号")) {
-                        log.error("[校验验证码登录]请检查账号是否正确");
-                    }
-                } else {
-                    // 检查表单错误提示
-                    List<WebElement> formErrors = driver.findElements(By.xpath("//div[contains(@class, 'el-form-item__error')]"));
-
-                    if (!formErrors.isEmpty()) {
-                        log.error("[校验验证码登录]表单错误:{}", formErrors.get(0).getText());
-                    } else {
-                        // 最后检查页面是否有异常状态码提示
-                        List<WebElement> statusElements = driver.findElements(By.xpath("//*[contains(text(), '状态码') or contains(text(), '错误码')]"));
-
-                        if (!statusElements.isEmpty()) {
-                            log.error("[校验验证码登录]系统返回错误状态:{}", statusElements.get(0).getText());
-                        } else {
-                            // 终极检查:页面源码中的错误信息
-                            if (driver.getPageSource().contains("错误") || driver.getPageSource().contains("exception")) {
-                                log.error("[校验验证码登录]页面包含错误信息关键字");
-                            } else {
-                                log.error("[校验验证码登录]登录状态验证超时,未检测到明确错误");
-                            }
-                        }
-                    }
+
+            // 定位并点击第二个登录按钮
+            By loginButtonSelector = By.xpath(
+                    "//div[@id='login_mobile']/button[contains(@class, 'login_btn') " +
+                            "and not(contains(@style, 'display: none')) " +
+                            "and position()=2]"
+            );
+
+            WebElement loginButton = wait.until(d -> {
+                WebElement btn = d.findElement(loginButtonSelector);
+
+                // 确保按钮可见且未被禁用
+                if (btn.isDisplayed() && btn.getAttribute("disabled") == null) {
+                    return btn;
                 }
-                throw new RuntimeException("登录验证失败", e);
-            }
+                return null;
+            });
+
+            // 添加可视化边框(调试用)
+            ((JavascriptExecutor) driver).executeScript(
+                    "arguments[0].style.border='3px solid green';", loginButton
+            );
+
+            // 点击前滚动到元素
+            ((JavascriptExecutor) driver).executeScript(
+                    "arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});",
+                    loginButton
+            );
+            Thread.sleep(500); // 等待滚动完成
+
+            // JavaScript点击
+            ((JavascriptExecutor) driver).executeScript("arguments[0].click();", loginButton);
+            System.out.println("已点击第二个登录按钮");
+
+            new WebDriverWait(driver, Duration.ofSeconds(20))
+                    .until(ExpectedConditions.invisibilityOf(loginButton));
 
             log.info("[校验验证码登录]登录成功,手机号:{},验证码:{}", phoneNumber, verificationCode);
         } catch (Exception e) {

+ 67 - 0
sikey-selenium-business/sikey-selenium-business-biz/src/main/java/cn/sikey/selenium/util/CustomPlaywrightFactory.java

@@ -0,0 +1,67 @@
+/*
+package cn.sikey.selenium.util;
+
+import com.microsoft.playwright.*;
+import com.microsoft.playwright.impl.PlaywrightImpl;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.microsoft.playwright.*;
+import java.util.Map;
+import java.util.Collections;
+
+*/
+/**
+ * @Author: nelson
+ * @Date: 2025/7/1
+ * @Description:
+ *//*
+
+public class CustomPlaywrightFactory {
+
+    public CustomPlaywrightFactory(){
+        // 设置系统属性
+        System.setProperty("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "1");
+        System.setProperty("PLAYWRIGHT_DISABLE_INSTALLATION_METRICS", "1");
+
+        // 设置环境变量(使用反射)
+        try {
+            Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
+            Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment");
+            theEnvironmentField.setAccessible(true);
+            Map<String, String> env = (Map<String, String>) theEnvironmentField.get(null);
+            env.put("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "1");
+            env.put("PLAYWRIGHT_DISABLE_INSTALLATION_METRICS", "1");
+        } catch (Exception e) {
+            // 备选方案:使用 System.getenv() 的可写 Map
+            try {
+                Map<String, String> env = System.getenv();
+                Field field = env.getClass().getDeclaredField("m");
+                field.setAccessible(true);
+                Map<String, String> writableEnv = (Map<String, String>) field.get(env);
+                writableEnv.put("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "1");
+                writableEnv.put("PLAYWRIGHT_DISABLE_INSTALLATION_METRICS", "1");
+            } catch (Exception ex) {
+                throw new RuntimeException("无法设置环境变量", ex);
+            }
+        }
+    }
+
+
+    // 使用官方公共 API 创建 Playwright 实例
+    public  static  Playwright createPlaywright() {
+        // 使用 Playwright 的工厂方法并传递环境变量
+        return Playwright.create(new Playwright.CreateOptions() {
+            public Map<String, String> getEnv() {
+                return Collections.unmodifiableMap(Map.of(
+                        "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "1",
+                        "PLAYWRIGHT_DISABLE_INSTALLATION_METRICS", "1"
+                ));
+            }
+        });
+    }
+}
+*/

+ 134 - 0
sikey-selenium-business/sikey-selenium-business-biz/src/main/java/cn/sikey/selenium/util/PlaywrightContextManagerUtil.java

@@ -0,0 +1,134 @@
+/*
+package cn.sikey.selenium.util;
+
+import cn.hutool.http.HttpStatus;
+import cn.sikey.framework.common.exception.ServiceException;
+import com.microsoft.playwright.*;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+*/
+/**
+ * @Author: nelson
+ * @Date: 2025/6/28
+ * @Description: 基于 Playwright 的会话管理
+ *//*
+
+public class PlaywrightContextManagerUtil {
+
+    static {
+        // 在类加载时第一时间设置(优先级最高)
+        System.setProperty("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "1");
+        System.setProperty("PLAYWRIGHT_DISABLE_INSTALLATION_METRICS", "1");
+    }
+
+    private static final Map<String, PlaywrightSession> sessionMap = new ConcurrentHashMap<>();
+    private static final long DEFAULT_TIMEOUT = 30;
+
+    // Playwright 实例(单例)
+    private static  Playwright playwright;
+
+    // 创建新的 Playwright 会话
+    public static String createSession(String wsEndpoint) {
+        return createSession(wsEndpoint, DEFAULT_TIMEOUT);
+    }
+
+    public static String createSession(String wsEndpoint, long timeoutSeconds) {
+        String sessionId = generateSessionId();
+
+        // 初始化 Playwright(单例模式)
+        if (playwright == null) {
+            Playwright.CreateOptions options = new Playwright.CreateOptions()
+                    .setEnv(Map.of("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "1","PLAYWRIGHT_DISABLE_INSTALLATION_METRICS", "1")); // 跳过浏览器下载
+            playwright = Playwright.create(options);
+        }
+
+        // 连接远程浏览器实例
+        BrowserType browserType = playwright.chromium().connect(wsEndpoint).browserType();
+
+        // 创建浏览器实例
+        Browser browser = browserType.launch(new BrowserType.LaunchOptions()
+                .setHeadless(true)
+                .setTimeout(timeoutSeconds * 1000));
+
+        // 创建浏览器上下文(会话隔离)
+        BrowserContext context = browser.newContext();
+
+        // 创建页面
+        Page page = context.newPage();
+
+        sessionMap.put(sessionId, new PlaywrightSession(browser, context, page));
+        return sessionId;
+    }
+
+    // 获取现有会话
+    public static PlaywrightSession getSession(String sessionId) {
+        PlaywrightSession session = sessionMap.get(sessionId);
+        if (session == null) {
+            throw new ServiceException(HttpStatus.HTTP_INTERNAL_ERROR, "无效的会话ID");
+        }
+        return session;
+    }
+
+    // 销毁会话
+    public static void destroySession(String sessionId) {
+        PlaywrightSession session = sessionMap.remove(sessionId);
+        if (session != null) {
+            session.close();
+        }
+    }
+
+    // 销毁所有会话和Playwright实例
+    public static void shutdown() {
+        sessionMap.values().forEach(PlaywrightSession::close);
+        sessionMap.clear();
+
+        if (playwright != null) {
+            playwright.close();
+            playwright = null;
+        }
+    }
+
+    // 生成唯一会话ID
+    private static String generateSessionId() {
+        return "SID_" + System.currentTimeMillis() + "_" + (int) (Math.random() * 1000);
+    }
+
+    // 会话数据封装类
+    public static class PlaywrightSession {
+        private final Browser browser;
+        private final BrowserContext context;
+        private final Page page;
+
+        public PlaywrightSession(Browser browser, BrowserContext context, Page page) {
+            this.browser = browser;
+            this.context = context;
+            this.page = page;
+        }
+
+        public Browser getBrowser() {
+            return browser;
+        }
+
+        public BrowserContext getContext() {
+            return context;
+        }
+
+        public Page getPage() {
+            return page;
+        }
+
+        public void close() {
+            if (page != null && !page.isClosed()) {
+                page.close();
+            }
+            if (context != null) {
+                context.close();
+            }
+            if (browser != null) {
+                browser.close();
+            }
+        }
+    }
+}*/

+ 2 - 1
sikey-selenium-business/sikey-selenium-business-biz/src/main/resources/application.yml

@@ -23,7 +23,7 @@ spring:
   cloud:
     consul:
       host: 106.75.230.4
-      #host: 127.0.0.1
+      # host: 127.0.0.1
       port: 8500
       # discovery
       discovery:
@@ -43,6 +43,7 @@ selenium:
     appkey: ffc5d87bca2bc65e13c2399219d6b220
     secretkey: 8aad1cdf23be18744dbc0a3eda0b438754fc3486e1fbb74466cc32276c336a10
     url: http://106.75.236.4:20001/wd/hub
+    # url: ws://106.75.236.4:3000/ws
     loginUrl: https://miniapp.yun.139.com/middle/index.html#/middlePage
     redirectUrl: http://mcdisk.sikey.com.cn