分享Java技术下AutojsPro7云控代码

news2024/11/17 5:53:50

引言


有图有真相,那短视频就更是真相了。下面是三大语言的短视频。

Java源码版云控示例:

Java源码版云控示例在线视频

核心技术:各个编程语言的WebSocket技术。

Java:Nettey、Net:Fleck、Python:Tornado、Autojs:自带的WS.都 写了很多代码感觉还是Java 的Nettey强大,用到的技术做个罗列。

Java:

1.java版本 JDK8(64bit)
2.开发IDE IntelliJ IDEA 2020.1.1
3.Web框架 SpringB   oot2.6.4
4.模板框架 Thymeleaf 2.2.2 (Spring推荐款个人感觉不好用)
5.UI框架BootStrap3
6.数据库框架Hibernate5.3.1
7.WebSocket 框架 Nettey 4.1.65
8.Json框架 Gson2.8.8
9.Zip压缩框架 zip4j2.9.1
10.数据库Mysql56
11.错误日志 spring自带的
12.其他 java的反射记录日志、 spring 的拦截器判断session、简化实体类插件lombok

技术篇


从技术的成熟度、稳定性、适应性到应用广度这里以Java为例子进行讲解。

核心技术(通信技术)


核心技术就是WebSocket。2011年WebSocket API被W3C定为标准。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

最主要还解决Ajax轮询带来的延迟和服务器性能损耗。

本软件服务端建立WS服务器,客户端进行连接,连接成功后进行双工通信,服务端发送任务,客户端发送【ping】。客户端是Autojs7.服务端采用了Java、net和Python多种语言的支持。网页JS连接服务端网上一大堆故此很容易,然autojs的对面的技术控看过来。这里的技术很精彩。

本软件重点解决2大问题:

一、热更

单JS脚本不可能解决所以问题,找图工作就不行因此客户端执行project势在必行,然项目的更新必然是个大问题。本项目完美解决服务端发送项目的事宜,无论是自动阅读的js还是自动阅读的project都进行完美热更。

二、断线重连

服务器宕机、WS服务重启或客户端重启都需要再次链接WS服务,然再次链接的WebSocket对象与之前的对象不一致导致客户端无法发送任务和命令。

此项目已经解决再次链接的问题且WS为同一个对象

服务端(Java)


项目结构


严格按照Java项目的命名规则进行包的命名,其中的一些方法为了迎合Net的写法故此首字母大写了。

 

从上到下依次介绍:

1.controller文件夹是控制器见名知意
2.dao是数据访问层
3.demo是一些示例的demo发布的时候可以删除
4.entity是实体类,这个仿照Net的叫法,里面有po和vo文件夹.
5.framework这个是核心框架,在框架章节会详细介绍。
6.plugin项目使用的插件和工具
7.service项目的dao与controller交互的层,理论上controller层是不准写业务代码和sql语句的
8.timer定时器目前只是检查ws的客户端是否断线
9.static 存放的是js脚本和css类和图片等信息
10.templates存放的是html页面


上面的图我使用的是【packages】模式,static和templates必须这么起名,springboot就这么查询和要求的,这个和Python的Flask比较类似。

Maven文件


项目采用的是Maven使用的是IDEA。下图是引入的jar包。


<!-- 获取计算机信息 -->
<dependency>
  <groupId>org.fusesource</groupId>
  <artifactId>sigar</artifactId>
  <version>1.6.4</version>
</dependency>
 
<!-- netty 主要是ws功能 -->
<dependency>
  <groupId>io.netty</groupId>
  <artifactId>netty-all</artifactId>
  <version>4.1.65.Final</version>
</dependency>
<!-- zip -->
<dependency>
  <groupId>net.lingala.zip4j</groupId>
  <artifactId>zip4j</artifactId>
  <version>2.9.1</version>
</dependency>

后端Java框架


整个项目的核心,项目的基础文件,基础框架功能不是很多大家多多海涵,

 

功能如下:

1.数据加密,目前采用的是Base64.UI端使用统一的Ajax方法Post数据Java端使用统一的方法接收参数(会解密参数)。示例:
2.自动构建实体类
3.自动写操作日志
通过反射方法进行日志的记录。

@Logger(description = "访问用户管理页面")

1.BaseController提供各种写Json的方法同时也提供是否加密的算法
2.BaseDataAccess提供HIB5的数据库访问session
3.提供数据库返回多参数方法统一对象ResultEntity
4.提供各种操作的工具类


前端UI框架


前端技术主要是BootStrap3和Jquery2,其中BS3封装的H+Plugins框架(公司不知道在哪里搞到的)JQ2 我自己封装了一下形成yadinghao.js文件,配合BS3的H+使用。

UI
H+4.9 下载地址:

百度网盘 请输入提取码提取码:6666

 

 JS
自定义的JS框架yadinghao.js使用JQ进行了2次封装。主要是针对Ajax的get和post进行了封装。

 

1、主要使用AjaxPost方法:请求地址、请求参数、回调函数、是否同步和是否加密。调用示例:

 

2、另一个主要封装插件:

 

这个插件基本每个页面都使用 。默认是加密的,没有做出参数。

3、另外封装的就是toast 这个最常用

 

4、还有一些其他小方法大家自行观看 吧。

 数据库(Hib)


数据使用的是Mysql,版本是5.6.DBMS使用的是Navicat15.数据库设计工具是powerdegisn15.

数据访问使用的是Hibernate5(数据量不是很大,且开发效率高于MyBatis).配置文件需要在resources 下,Spring就自动寻了。

 

WebSocket 


代码位置
ws是本软件的核心故此将其代码单独存放,路径是:com.yadinghao.service.websocket包下面的都是和ws相关的代码。真像如下:

 

核心思想
1.构建在线列表,存放服务端页面和客户端手机
2.认证通过的设备才可以加入到在线列表(需要客户端提起注册)
3.认证通过的设备会通知到服务端注册认证页面。页面可以进行发布命令和任务操作。
4.客户端接收任务或命令进行执行操作
5.客户端掉线后服务端会依据IP和端口号对设备进离线操作
6.客户端掉线后未触发服务端离线操作会丢失ping服务器的数据,服务端会依据ping的时间对客户端进行离线操作
7.重点服务端可自定义上传脚本和AJ7的项目,AJ7的格式是zip的(autojs只能解压zip)。上传的js和project版本高于客户端的版本则直接更新


核心页面
1.WS开启页面
2.云控设备页面
3.云控任务页面


核心代码
服务端启动WS代码

        启动代码

package com.yadinghao.service.websocket;
 
import com.yadinghao.framework.entity.ResultEntity;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
 
import java.net.InetSocketAddress;
 
public class WebSocketBusiness {
    EventLoopGroup bossGroup = null;
    EventLoopGroup workerGroup = null;
    Channel channel = null;
 
    public ResultEntity startWebSocket(String wsAddress, String userId) throws InterruptedException {
        ResultEntity resultEntity=new ResultEntity();
        String[] split = wsAddress.replace("ws:\\", "").replace("\\", "").replace("ws://", "").split(":");
        String ipAddress= split[0];
        String strPort= split[1].trim();
        int port=Integer.parseInt(strPort);
        bossGroup = new NioEventLoopGroup();
        workerGroup = new NioEventLoopGroup();
 
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .localAddress(new InetSocketAddress(ipAddress, port))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new HttpServerCodec()); // HTTP 协议解析,用于握手阶段
                        pipeline.addLast(new HttpObjectAggregator(65536)); // HTTP 协议解析,用于握手阶段
                        pipeline.addLast(new WebSocketServerCompressionHandler()); // WebSocket 数据压缩扩展
                        pipeline.addLast(new WebSocketServerProtocolHandler("/", null, true)); // WebSocket 握手、控制帧处理
                        pipeline.addLast(new WebSocketHandler(wsAddress,userId));
                    }
                });
        ChannelFuture f = b.bind().sync();
        channel = f.channel();
        resultEntity.setReturnValue(true);
        return resultEntity;
    }
 
    /**
     * 关闭NettyWebSocket
     * @param primary_key
     * @param userId
     * @return
     */
    public ResultEntity closeWebSocket(String primary_key, String userId){
        ResultEntity resultEntity=new ResultEntity();
        try {
 
            if (channel != null) {
                channel.close();
                workerGroup.shutdownGracefully();
                bossGroup.shutdownGracefully();
            }
            resultEntity.setReturnValue(true);
            return resultEntity;
        } catch (Exception e) {
            resultEntity.setReturnValue(false);
            resultEntity.setMessage(e.getMessage());
            return resultEntity;
        }
    }
}

        Handler代码(就是监听)

 
   

@Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
        try {
           if (frame instanceof TextWebSocketFrame) { // 此处仅处理 Text Frame
                Channel channel = ctx.channel();
                String request = ((TextWebSocketFrame) frame).text();
                System.out.println(request);
                JsonObject jsonObject = new Gson().fromJson(request, JsonObject.class);
                String category = jsonObject.get("category").getAsString();
                if ("android".equals(category)) {
                    JsonObject jsonData = jsonObject.getAsJsonObject("data");
                    String action = jsonData.get("action").getAsString();
                    String deviceToken = jsonData.get("device_token").getAsString();
                    if ("connect".equals(action)) {
                        //System.out.println(deviceToken);
                        OnlineDeviceEntity onlineDeviceEntity = WebSocketService.builderDevice(deviceToken, channel, category, this.wsAddress, this.userId);
                        WebSocketService.onlineDevices.add(onlineDeviceEntity);
                        //通知UI所有设备连接
                        WebSocketService.notifyServerPage(this.serverPageToken, deviceToken, WebSocketUtils.getIpAddress(channel));
                        //通知客户端已经链接
                        WebSocketService.notifyClient(channel); //2023-05-14 by zhangyu DL
                    } else if ("log".equals(action)) {
                        //System.out.println(deviceToken);
//                        OnlineDeviceEntity onlineDeviceEntity =  WebSocketService.getOnlineDeviceEntity(deviceToken);
//                        if(onlineDeviceEntity==null){
//                            //非法客户端 T下线
//                            ctx.channel().close();
//                        }else {
//
//                        }
                        String level = jsonData.get("level").getAsString();
                        String message = jsonData.get("message").getAsString();
                        //通知UI所有设备连接
                        WebSocketService.notifyLogMessage(this.serverPageToken, deviceToken, message,level);
                    }  else if ("close".equals(action)) {
                        //通知UI所有设备连接
                        WebSocketService.notifyServerPageDeviceOffline(this.serverPageToken, deviceToken); //S
                    }else if ("ping".equals(action)) {
                        OnlineDeviceEntity onlineDeviceEntity =  WebSocketService.getOnlineDeviceEntity(deviceToken);
                        if(onlineDeviceEntity==null){
                            ctx.channel().close();
                        }else {
                            onlineDeviceEntity.setLAST_PING_DATE(Tools.getNowDateTime());
                            //ctx.channel().writeAndFlush(new TextWebSocketFrame(WebSocketJson.jsonPingMessage()));
                        }
                    }else {
                        //请求非法终端连接
                        ctx.channel().close();
                    }
 
                } else if ("web".equals(category)) {
                    String stringData = jsonObject.get("data").getAsString();
                    //二级数据
                    JsonObject jsonData = new Gson().fromJson(stringData, JsonObject.class);
                    String action = jsonData.get("action").getAsString();
                    String deviceToken = jsonData.get("device_token").getAsString();
                    if ("authed".equals(action)) {
                        OnlineDeviceEntity onlineDeviceEntity = WebSocketService.builderDevice(deviceToken, channel, category, this.wsAddress, this.userId);
                        WebSocketService.onlineDevices.add(onlineDeviceEntity);
                        channel.writeAndFlush(new TextWebSocketFrame(WebSocketJson.jsonAuthedMessage()));
                    }
                }else if ("gui".equals(category)){
                    System.out.println(category);
                }
            }
        } catch (Exception ex) {
            System.out.println("channelRead0"+ex.getMessage());
        }
    }

 

客户端连接服务器并进行认证

           

 if ("connect".equals(action)) {
                        //System.out.println(deviceToken);
                        OnlineDeviceEntity onlineDeviceEntity = WebSocketService.builderDevice(deviceToken, channel, category, this.wsAddress, this.userId);
                        WebSocketService.onlineDevices.add(onlineDeviceEntity);
                        //通知UI所有设备连接
                        WebSocketService.notifyServerPage(this.serverPageToken, deviceToken, WebSocketUtils.getIpAddress(channel));
                        //通知客户端已经链接
                        WebSocketService.notifyClient(channel); //2023-05-14 by zhangyu DL
                    }


客户端连接服务器日志代

else if ("log".equals(action)) {
                        System.out.println(deviceToken);
                        OnlineDeviceEntity onlineDeviceEntity =  WebSocketService.getOnlineDeviceEntity(deviceToken);
                        if(onlineDeviceEntity==null){
                            //非法客户端 T下线
                            ctx.channel().close();
                        }else {
 
                        }
                        String level = jsonData.get("level").getAsString();
                        String message = jsonData.get("message").getAsString();
                        //通知UI所有设备连接
                        WebSocketService.notifyLogMessage(this.serverPageToken, deviceToken, message,level);
                    }


服务端页面代码

UI代码

   

 <input type="hidden" id="PAGE_TOKEN" name="PAGE_TOKEN" th:value="${PAGE_TOKEN}" />
    <input type="hidden" id="WS_ADDRESS" name="WS_ADDRESS" th:value="${WS_ADDRESS}" />
    <div id="toolbar">
        <button class="btn" id="RegisterDevice_table_task"><span class="glyphicon glyphicon-comment"></span> 发布任务</button>
        <button class="btn" id="RegisterDevice_table_command"><span class="glyphicon glyphicon-comment"></span> 发布命令</button>
        <button class="btn" id="RegisterDevice_table_check"><span class="glyphicon glyphicon-check"></span> 审核通过</button>
        <button class="btn" id="RegisterDevice_table_update"><span class="glyphicon glyphicon-pencil"></span> 改别名</button>
        <button class="btn" id="RegisterDevice_table_delete"><span class="glyphicon glyphicon-remove"></span> 删除</button>
        <button class="btn" id="RegisterDevice_table_log"><span class="glyphicon glyphicon-envelope"></span> 日志</button>
        <button class="btn" id="RegisterDevice_table_address">监听地址ws://192.168.101.2:9103</button>
        <button class="btn">是否连接WS地址:<span style="color:red" id="Connection_Status">否</span></button>
    </div>
    <table id="RegisterDevice_table" data-mobile-responsive="true" data-show-columns="true">
    </table>


JS代码

let device_log = [] //设备日志
let page_token = "CloudControlDevicePage";
let is_open_modal=false
let page_device_token="" //区别传输过来的
 
CreateWebSocket()
//创建websockt  
function CreateWebSocket() {
    let ws_address = $("#WS_ADDRESS").val();
    if (ws_address === "" || ws_address === undefined || ws_address === "undefined") {
        alert("开启监听ws地址失败因为ws地址为空...")
    } else {
        $("#RegisterDevice_table_address").text("当前监听地址:" + ws_address);
        webSocket = new WebSocket(ws_address);
        webSocket.onopen = WebSokectOnOpen;
        webSocket.onmessage = WebSocketOnMessage;
        webSocket.onclose = WebSocketOnClose;
    }
}
//建立连接事件  
function WebSokectOnOpen() {
    page_token = $("#PAGE_TOKEN").val();
    let authentication_message = "{\"action\": \"authed\", \"device_token\": \"" + page_token + "\"}"
    let scriptJson = { "category": "web", "data": authentication_message }
    webSocket.send(JSON.stringify(scriptJson));
}
//监听事件
function WebSocketOnMessage(event) {
    //监听来自客户端的数据  
    let deviceJson = JSON.parse(event.data)
    let device_token = deviceJson.deviceToken
    let ws_category=deviceJson.category.toString()
    let rows;
    if (ws_category === "device") {
        let device_token = deviceJson.deviceToken
        if (device_token === undefined || device_token === "undefined") {
            return;
        }
        let ip_address = String(deviceJson.ipAddress)
        let is_online = String(deviceJson.isOnline)
        let allTableData = $("#RegisterDevice_table").bootstrapTable('getData');//获取表格的所有内容行
        for (let i = 0; i < allTableData.length; i++) {
            let ui_device_token = allTableData[i]["DEVICE_TOKEN"]
 
            if (device_token === ui_device_token) {
                rows = {
                    index: i, //更新列所在行的索引
                    field: "DEVICE_IS_ONLINE", //要更新列的field
                    value: is_online //要更新列的数据 <span style='color:green;font-size:18px;'>在线</span>
                }//更新表格数据
                $('#RegisterDevice_table').bootstrapTable("updateCell", rows);
                rows = {
                    index: i, //更新列所在行的索引
                    field: "DEVICE_CLIENT_IP", //要更新列的field
                    value: ip_address //要更新列的数据 <span style='color:green;font-size:18px;'>在线</span>
                }//更新表格数据
                $('#RegisterDevice_table').bootstrapTable("updateCell", rows);
            }
        }
    } else if (ws_category === "log") {
        if (device_token === undefined || device_token === "undefined") {
            return;
        }
        let logMessage = String(deviceJson.message)
        let logLevel = String(deviceJson.level)
        let log_array = device_token + "@" + logMessage + "@" + logLevel
        device_log.push(log_array)
        if (is_open_modal) {
            //yw9zcfbdulqwme9que9imdu2zjm1otq1zwvjnzk2ntqwotyw
            //alert(page_device_token)
            if (device_token.toLowerCase() === page_device_token.toLowerCase()) {
                if (logLevel === "log") {
                    $("#logView").append("<span class=\"f18 blue\">" + logMessage + "<br/></span>")
                } else if (logLevel === "warn") {
                    $("#logView").append("<span class=\"f18 yellow\">" + logMessage + "<br/></span>")
                } else if (logLevel === "info") {
                    $("#logView").append("<span class=\"f18 green\">" + logMessage + "<br/></span>")
                } else if (logLevel === "error") {
                    $("#logView").append("<span class=\"f18 red\">" + logMessage + "<br/></span>")
                } else {
                    $("#logView").append("<span class=\"f18\">" + logMessage + "<br/></span>")
                }
            }
        }
    } else if (ws_category === "authed") {
        let authedMessage = deviceJson.message;
        $.showSuccessToast(authedMessage);
        $("#Connection_Status").text("是")
    }
 
}
 
function WebSocketOnClose() {
    //监听来自客户端的数据
    let ws_address = $("#WS_ADDRESS").val();
    $.showWaringToast("请到【云控服务管理】页面开启" + ws_address+"服务");
}

java代码 controller

显示UI代码

 
   

@RequestMapping(value = "ManageRegisterDevice")
    public String ManageRegisterDevice(HttpServletRequest request,Model model) {
        String userId=getUserId(request);
        WsAddressEntity wsAddressEntity=new WsAddressDataAccess().findWsAddressEntity(userId);
        if(wsAddressEntity.LISTEN_ADDRESS!=null){
            String PAGE_TOKEN= "CloudControlDevicePage" + userId;
            model.addAttribute("PAGE_TOKEN", PAGE_TOKEN);
            model.addAttribute("WS_ADDRESS", wsAddressEntity.WEB_SOCKET_ADDRESS);
        }
        model.addAttribute("SOFT_NAME", ConfigService.getPlatformConfig().SOFT_NAME);
        model.addAttribute("SITE_TITLE", ConfigService.getPlatformConfig().SITE_TITLE);
        model.addAttribute("KEY_WORD", ConfigService.getPlatformConfig().KEY_WORD);
        model.addAttribute("DESCRIPTON", ConfigService.getPlatformConfig().DESCRIPTON);
        return "back/cloud/ManageRegisterDevice";
    }

Handler离线代码

  @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        String ipAddress=WebSocketUtils.getIpAddress(channel);
        OnlineDeviceEntity entity = WebSocketService.getOnlineDeviceEntityByIP(ipAddress);
        if(entity== null){
            System.out.println("不在在线设备中");
        }else
        {
            String userId = entity.USER_ID;
            String serverToken = WebSocketUtils.DEVICE_PAGE_TOKEN + userId;
            String deviceToken = entity.DEVICE_TOKEN;
            WebSocketService.removeOnlineDeviceEntity(entity);
            WebSocketService.notifyServerPageDeviceOffline(serverToken, deviceToken);
        }
 
    }

WebAPI


对外提供给移动端的方法。方法返回的都是统一的数据格式json字符串。Json格式依据不同的业务而不同。对外AIP都在Controller下, 移动端的就存放在mobile文件夹下。

 

对外方法函数签名如下:

1.String AppFindRecommendSoft()


查询系统推荐的软件

1.String AppFindRandomAd(HttpServletRequest request)


查询系统随机发放的广告,目前仅仅读取管理员发布的。

1.String AppFindCloudList(HttpServletRequest request)
查询云控自动阅读app集合

1.String AppRegisterDevice(HttpServletRequest request)
2.String AppRegisterIsCheckIn(HttpServletRequest request)

客户端(Autojs7)


客户端是Aj7的项目

核心架构


通信技术
1、核心通信技术就是ws,autojs7以上才支持ws。Ws客户端连接服务端一点也不难代码如下:

 

function testWSAddress(ws_address){
    let ws = web.newWebSocket(ws_address);
    let result_ws=""
    ws.on("open", (res, ws) => {
        log("WebSocket已连接");
        result_ws= true;
    }).on("failure", (err, res, ws) => {
        log("WebSocket连接失败");
        console.error(err);
        result_ws= false;
    })
    sleep(3000)
    return result_ws
}
console.show()
toastLog(testWSAddress(ws_address="ws://192.168.3.170:8686"))

上述代码主要是判断客户端是否连接到服务器。这个是很有必要的,Python语言的Flask创建的服务器就是无法连接。

测试网站:WebSocket在线测试工具

2、登录、UI显示和下载等网络技术使用的是http的get和post。安卓要求网络访问是线程模型。HTTP请求代码示例:

 

function initializeRegisterStatus(){
    var result_threads = threads.disposable();
    threads.start(function () {
        try {
            //let rootUrl = adenStorage.get("rootUrl");//顶级域名
            let device_token = adenTools.getDeviceToken()
            let url_address = rootUrl + "/cloud/AppRegisterIsCheckIn"
            var response = http.post(url_address, {
                "device_token": device_token
            });
            var json = response.body.json();
            if (response.statusCode == 200) {
 
                if (json.success || json.success == "true") {
                    adenStorage.put("AppRegisterIsCheckIn", "true");
                    dict_result = [true, json.message]
                } else {
                    console.log(json.message)
                    dict_result = [false, json.message]
                }
 
            } else if (response.statusCode == 404) {
                console.log("注册服务访问服务器出现404错误")
                dict_result = [false, "注册服务访问服务器出现404错误"]
            }
            else {
                console.log("发生未知错误请联系开发人员,或者稍候再试...")
                dict_result = [false, "发生未知错误请联系开发人员,或者稍候再试..."]
            }
            
        } catch (error) {
            console.log("检测注册服务出现错误可能是服务器地址不正确参考错误" + error);
            dict_result = [false, "检测注册服务出现错误可能是服务器地址不正确参考错误"]
            adenBase.adenStorage().put("AppRegisterIsCheckIn", "false"); 
        }
        result_threads.setAndNotify(dict_result);
    });
    result_threads = result_threads.blockedGet()
    if (result_threads[0] == false) {
        adenBase.adenStorage().put("AppRegisterIsCheckIn", "false"); 
    }else{
        adenBase.adenStorage().put("AppRegisterIsCheckIn", "true"); 
    }
}

框架技术
就是Autojs的UI技术,在配合一些脚本通信、脚本引擎和多线程等技术。

项目结构


项目采用autojsPro7版本创建的项目名称叫YadinghaoHunter,项目启动js是HunterFrame.js,项目名称和启动js均可以修改。不想服务器生邀请码就修改主类的登录信息,打包已经设置好了放到手机端的autojs App下即可。具体项目结果如下图:

 

从上到下依次介绍:

1.build 打包(生成APK)时候自动生成的.
2.config是核心配置主要是软件版本和根地址
3.image是项目所用到的图片包含找图使用的
4.log是项目记录日志的插件
5.page是项目所有单读页面的文件夹


1.plugin是项目工具文件夹

 
1.repository 是项自动阅读App的插件仓储
2.res打包(生成APK)时候自动生成的.
3.Test作者测试文件夹可以删除
4.打包.docx 在AutojsPro7下如何打包
5.免登修改.docx 源码模式下无限制安装打包成已经登录模式。
HunerFrame.js APP的主类也是启动类,负责调用各个page和plugins。使用require调用插件类,如下图:


1.project.json autojsPro7创建项目自动生成的配置文件
2.YadinghaoHunter.code-workspace VSCode的工程文件 

网络访问


重点中的重点,安卓要求网络访问必须是子线程(多线程)不能影响主线程及阻塞主线程,所以这里就涉及一个回调的问题。AutojsPro回掉采用:var result_threads = threads.disposable();定义后result_threads就可在线程内调用, result_threads可以接收线程返回的对象我的这个示例是字典类型

 

dict_result = [true, json.message]

dict_result = [false, "注册服务访问服务器出现404错误"]

将线程内的字典对象给result_threads

result_threads.setAndNotify(dict_result);

在线程外部将变转换下

result_threads = result_threads.blockedGet()

这样在线程外部就可以调用result_threads回调结果了

if (result_threads[0] == false) 这样就可判断字典的返回值了。具体代码如下(示例是读取云控服务器是否对设备审核通过):

脚本通信


由于是项目所以存在很多单独的文件文件之间进行通信就显得非常必要。

1.变量通信
将要显示或判断的值记录到手机的storages - 本地存储中这样就可以在其他地方进行调用。本地XML、Json和SQLite3原理是一样的只是api写法不一样。

1.脚本通知
这个功能还是比较好用的,示例代码,子界面广播事件

events.broadcast.emit("ui_refresh", "UI刷新");

主界面函数:

/**

     * UI刷新事件

     */

    events.broadcast.on("ui_refresh", function (message) {
        initializeUIRight()

    });

脚本引擎


官方API肯定比我说的好,这里主要是讲解下应用场景。

一、主框架Frame启动子页面,这类的启动和require不一样引用的类还得引用。

engines.execScriptFile("./Page/Login.js");


下图是Login.js页面

 

二、在云控启动js和project中应用,尤其是启动project。必须指定资源路径否则找图将失败。

 

adenTools.engineProjectFile=function(projectId,appName,dataFileFullName,projectExecPath) {
    let zipFileName = appName+projectId + ".zip"
    let zipFileFullName = projectExecPath + zipFileName// 压缩文件路径
    files.copy(dataFileFullName, zipFileFullName)
    let outputDir = projectExecPath+"/"+appName+projectId; // 解压路径 执行路径加项目名称和ID
    $zip.unzip(zipFileFullName, outputDir);
    let projectJsonFile=outputDir+"/project.json"
    let adenProjectJson=JSON.parse(files.read(projectJsonFile))
    let mainJsName=adenProjectJson.main
    let mainjSFile=outputDir+"/"+mainJsName
    scriptEngine = engines.execScriptFile(mainjSFile, {
        path : outputDir
    });
    return scriptEngine
}

 

WebSocket


核心思想
根据配置的WS地址,自动连接WS服务器,若连接不上则尝试连接。当连接上时候。客户端发送连接信息,若连接成则反馈已经连接的信息。客户端依据连接信息等待服务端发送的指令。

测试连接
根据配置的WS地址,软件 提供测试WS地址的功能,同时此功能也是断线重连的基础功能,此功能需要注意的地方是保证测试或断线重连的WS地址是一个(内存对象是一个),具体代码如下:

/**
 * 测试地址
 * @param {URL websocket地址} ws_address 
 * @returns 
 */
CloudControl.testWSAddress = function (ws_address) {
    try {
        var result_threads = threads.disposable();
        threads.start(function () {
            let dict_result = []
            try {
                let ws = web.newWebSocket(ws_address);
                let result_ws = false
                let message = ""
                ws.on("open", (res, ws) => {
                    result_ws = true;
                }).on("failure", (err, res, ws) => {
                    result_ws = false;
                    message = err;
                })
                sleep(1000)
                if (result_ws == false || result_ws == "false") {
                    dict_result = [false, message]
                } else {
                    dict_result = [true, ws]
                }
                result_threads.setAndNotify(dict_result);
            }
            catch (error) {
                dict_result = [false, error]
                result_threads.setAndNotify(dict_result);
            }
        });
        result_threads = result_threads.blockedGet()
        return result_threads
    } catch (e) {
        toastLog("testWSAddress方法发生异常:" + e)
        return [false, "testWSAddress方法发生异常:" + e]
    }
}

连接认证(认证信息)
根据配置的WS地址,软件会连接服务器,连接服务器后会发送认证信息服务器接收认证后反馈信息。具体代码如下:

   

 let authentication_message = { "action": "connect", "device_token": "" + adenTools.getDeviceToken() + "" }
    let scriptJson = { "category": "android", "data": authentication_message } //入服务器认证
    ws.send(JSON.stringify(scriptJson));


客户端向服务器发送一段JSON认证信息,认证信息需要包含设备的Token信息,Token信息是在服务器有记录的必须携带防止乱发乱认证。

 

 接收命令(预定义)
命令目前是预定义三个息屏、打开微信和显示桌面。获取源码自定义追加即可。

服务器发送命令的方式和发送任务的方式是一致的 。

 

接收任务(自动阅读App的Js或Project)
此功能是此软件的核心功能。服务端发送任务客户端执行,服务端发送的任务是js或者是project(找图)。

Js任务:接收到服务端发送的Json数据,进行类别判断后对Js文件进行判断,判断本地是否存在判断版本之后进行下载。

adenTools.downLoadScript(downUrl, tempFolder, fileName)下载文件,注意downUrl 要严格遵守MVC资源请求的命名大小写和反斜杠都要注意

Zip任务:zip文件有如下的要求

文件名必须是汉语拼音或者是英文的不能是中文的,因为AutoJs解压缩不支持汉语。
Zip文件必须是AutojsPro创建的项目且压缩zip时候Project.json文件必须在压缩包的第一层
Zip文件必须是正版文件不能是修改扩展名的文件


客户端接收到zip文件后进行解压判断,判断文件是不是AutoJsPro创建的Project。

 

处理完毕zip文件后就是执行Project,使用engines执行。需要注意的是engines执行的文件和主文件是相互不影响的,也就是说执行的Js或者Project里面的信息外面无法直接获取,外部的ws等信息执行的脚本也获取不到。脚本运行时间可以用脚本通信技术解决。

断线重连
服务端重启、客户端掉线或客户端进程被Kill等操作。客户端会重新连接服务端,若服务器性能不高且不希望手机耗电多则可以设置尝试次数。

Ping消息
当客户端与服务端连接后确保双方都在线则需要客户端与服务端互ping,一般的WS框架都提供此功能。我们这里是自己实现的因为有其他操作故此自我实现ping的消息:

let message = { "action": "ping", "device_token": ""+device_token+"" }

let scriptJson = { "category": "android", "data": message }

相对严谨ping也带上token,下图是云控调用ping功能。

 

客户端定期ping服务器,服务器根据ping保证客户端在线如超过设定未ping服务器则认为掉线。

 

定时更新服务端LAST_PING_DATE属性保证设备在线。

发送日志
服务端显示日志是必要的,可以查看设备的运行状态设备发送日志是根据ws地址空将日志发送至服务器,需要对日志类进行封装。

 

       

 if (isSendLog || isSendLog=="true"){
            threads.start(function () {
                let authentication_message = { "action": "log", "level": ""+level+"", "message": ""+loginfo+"", "device_token": ""+getDeviceToken()+"" }
                let scriptJson = { "category": "android", "data": authentication_message }
                let ws = web.newWebSocket(ws_address);
                ws.send(JSON.stringify(scriptJson));
                ws.close(1000, "log");
            });
        }


业务篇


这里是云控版介绍,故此不赘述非云控的其他功能。

客户端


运行模式


客户端App提供三种运行模式,兼容模式、找图模式和云控模式。云控配置只能在云控模式打开。即云控模式接收的是云端下发的脚本。

兼容模式:即传统的找元素模型兼容各个设备,是原始的薅羊毛专业版。

找图模式:除了使用元素定位,还使用坐标定位和找图功能,这2个技术都是依赖手机分辨率的。所以此模式只兼容特定机型(OPPR9sk)该款手机是开发者使用机型

云控模式:服务端推送脚本,完全自定义脚本。脚本具有何种功能手机就执行何种功能

 

云控配置(注册服务)


只有在云控模式下才可以使用的页面。配置页面如下图所示:

 

是否开启云控:一般都要开启开启后将自动连接云控服务器

是否发送日志:如使用熟练服务器是免费版的化则不开启,因为会影响服务器性能

是否无线尝试连接:断线宕机都会自动连接,如果是无限次数会耗费一些电量

尝试次数:不开启无限尝试连接才起到作用

尝试时间间隔(单位秒):无论何种模式都需要配置

设备注册地址:服务端生成的http地址。

云控服务器WS地址:服务端生成的WS地址

注册是否通过:检查注册状态

测试连接WS服务按钮:测试WS地址是否正确

注册设备按钮:将设备信息发送至服务端等待服务端审核,服务端审核通过才可以WS连接

保存配置按钮:先保存后测试和注册

下载脚本 


客户端App会手动下载云端脚本,下载过的脚本被执行云控任务的时候就不需要再次下载,提升执行效率。

自动连接


【云控配置】页面,是否开启云控选项开启的时候客户端App将会自动连接【云控配置】云控服务器WS地址中的WS地址,连接原则也是依据配置。

 

断线重连


当服务器WS断线、宕机或客户端重启等操作,客户端会依据【云控配置】进行重连服务器,下图是服务端未开启的示例:

 

下图是断线重连的日志过程示意图:

 

执行任务


最核心的也是最关键的,客户端执行服务端自定义的任务,执行单JS文件或者ZIP的解压后项目文件。

 

客户端接收任务->客户端解析任务->下载脚本->执行脚本->监听脚本。

服务端


生成注册地址


设备必须注册为合法设备才能够发送任务、发送命令、客户端发ping和客户端日志。设备注册地址是由【设备注册地址】页面生成,生成后将地址复制,粘贴至客户端的云控配置页面,进行注册。

生成页面,点击生成再点击提交即可。

 

生成WS地址


重点中的重点,每个用户只能创建一个WS地址。此WS地址是所以客户端连接的地址,创建规则就是以ws开始(~ ̄(OO) ̄)ブ本机IP和端口号为中间体和http地址类似。创建完成后可以使用互联网的ws测试地址测试一下是否开通。

 

重点是地址名称无所谓。新增后WS地址直接开启。

 

可以根据需求就行实际操作。

任务管理


新增任务稍微复杂一些。任务区分单JS和AutoJsPro创建的项目。AutoJsPro创建项目Project,使用ZIP格式进行压缩,压缩成ZIP有如下要求:

文件名必须是汉语拼音或者是英文的不能是中文的,因为AutoJs解压缩不支持汉语。
Zip文件必须是AutojsPro创建的项目且压缩zip时候Project.json文件必须在压缩包的第一层,下图是示例:


Zip文件必须是正版文件不能是修改扩展名的文件

 

 


脚本名称:重名无所谓,但是必须与被阅读的App名称一致

脚本类型:上传文件的类型是JS还是ZIP(zip解压后是project)

脚本使用机型:默认是全机型,选择下拉可自定义。

脚本编码:就是脚本顺序或者自定义也好

脚本版本:热更的关键格式是1.1.1 三位的格式。

脚本运行时间:客户端脚本被执行的时间单位分钟

脚本文件:这个是附件格式是JS和ZIP

更新日志:非强制填写

主界面功能介绍

 

删除:就是将上传的脚本删掉不影响已经下载和执行的客户端

更版:和新增一样就是版本号要高于原始版本附件必须上传

历史版本:查看当前脚本的历史记录即何时更版过

云控设备管理


重点中的重点、核心中的核心、关键中关键。云控设备管理是本软件的关键节点,云控设备管理是本软件的关键节点,云控设备管理是本软件的关键节点,重要的事情说三遍。云控设备重点核心关键在那里?有如下几点:

任务和命令下发的页面
设备是否在线页面
设备运行日志页面
设备审核
有图有真相看图说话:

 

发布任务:选择在线设备发送预定义的任务,发送后可以查看执行日志。下图是发布任务的界面

 

发布命令:选择在线设备发送预定义的命令。

 

审核通过:审核待审的设备

 

改别名:非常重要的功能,就十几个设备无所谓都能记得账,然而工作室有几百个设备,这时候设备别名就很重要了。

进阶篇(二次开发)


​​​​​​​Autojs客户端开发(脚本开发)


核心框架


最主要的就是修改免登和发布(项目中含免登和发布的文件)。如果想要修改项目信息则按照下面步骤进行:

1、YadinghaoHunter文件夹名称修改成你想要的

2、修改工程名称和启动类名称

3、修改project.json内的启动类和公司信息

4、修改BaseConfig.js内的信息。soft_Version、root_Url和soft_Name。其他信息是否修正自己决定。soft_Version决定是否升级root_Url决定flash加载页面、云端脚本下载页面、推荐页面、升级页、登录功能和云控注册功能。soft_Name就是显示客户端的名称。

 

5、Plugin下的Tools.js是整体项目的工具类,比如万能找图、滑动找元素、点击元素、监听ws等超级功能。其中构建找图方法会经常用到adenTools.buildImageArray("精选", "./Image/快手", 3);构建小图,之后在大图里找小图(little_image_array)。

adenTools.clickAreaForFindImage(little_image_array)下图是示例代码:

 

脚本开发(JS)


一般情况下但Js脚本都是兼容所以机型的。

直接将repository/ single/KSA.js文件复制一份修改就可以。

 

1.appName字段


2、ClickVideo方法修改一下,修改成自己App进入的方法

 

 

1.改一下WS地址改成你自己的。


单文件是可以运行的,

 

 

配置读取移动端本地没有则按照默认配置。

项目开发(AutoJsPro的项目)


复制YadinghaoKS.Zip将其解压然后将YadinghaoKS改成你想要的名字,例如YadinghaoKSJS。再将其工程名称修改,例如:YadinghaoKSJS.code-workspace,在将启动类修改成KSJSFrame.js。

 

双击工程启动项目修改project.json第19行和20行,

 

19行改成启动项目名称,20行改成你自己想要的合理名。修改内部的方法是非常简单的有如下几个地方需要修该:

1、appName字段修改成你要运行的App名称如:快手极速版

 

2、修改ClickVideo方法一下,修改成自己App进入的方法

3、修改保持自动阅读方法,改成你自己的保持方法。

4、关键一步在执行任务的时间函数里修改自己的任务,时间函数是adenTools.mod(parseInt(minute), 8) == 0。其中最主要的任务就是签到和体现。下图是时间函数和App的任务。

 

5、里面涉及到找图方法已经在上面的章节描述过

Java服务端开发


构建页面
使用的是SpringBootMVC技术UI是thymeleaf技术,Spring推荐thymeleaf其实也是Spring推荐的。项目结构如下:

 

将创建的页面放到指定位置back是后台页面文件夹,front是前台页面。将创建好的html页面放到指定的文件夹里,样式表、JS和UI复制其中的一个页面的就可以下图是示例:

 

JS文件、CCS文件和Image文件都在static下。

构建JavaScript
JS文件夹如下所示,复制一个JS即可进行修改。

 

Js也区分前台和后台,其中后台为了方便调用同样也进行了封装,写了个yadinghao.js文件作为JS的基础类。

JS文件里面的方法有很多具体参考文件

 

 

构建DataAccess
构建数据访问之前还需要构建数据实体类。下图所示是实体类文件夹结构:

 

将对应的实体类放入到指定的文件夹,然后在创建数据访问层。实体的创建采用的是HIB5和Lombok,对象相对简单。

 

HIB下的数据访问也很简单,常规的2项分页查询和增删改:

 

构建Controller
Controller的创建可以复制其他的页面。将位置存放正确,就算放不对也能访问但是不好找。下图是controller的使用图参考一下还不自信就看看代码

 

资源篇


源码下载


Java版源码链接:

百度网盘 请输入提取码

提取码:27yy

 

​​​​​​​环境软件下载


Java安装环境所需软件链接:

百度网盘 请输入提取码

提取码:usab


 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/853027.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android Framework底层原理之WMS的启动流程

一 概述 今天&#xff0c;我们介绍 WindowManagerService&#xff08;后续简称 WMS&#xff09;的启动流程&#xff0c;WMS 是 Android 系统中&#xff0c;负责窗口显示的的服务。在 Android 中它也起着承上启下的作用。 如下图&#xff0c;就是《深入理解 Android》书籍中的…

模拟实现消息队列项目(完结) -- 基于MQ的生产者消费者模型

目录 前言 1. 生产者 2. 消费者 3. 启动消息队列服务器 4. 运行效果 结语 前言 在上一章节,我们完成了消息队列的客户端部分,至此我们整个消息队列项目就构建完成了,那我们做的这个消息队列到底有什么效果,以及如何去使用我们自己的消息队列呢?那么本文,就将我们的MQ进行实战操…

GSEA富集分析结果详解

1. GSEA富集分析原理图 2. GSEA富集分析过程 1. 计算富集分数&#xff08;ES&#xff09; 富集分数&#xff1a;S 反应基因集&#xff08;比如某个通路内的基因集&#xff09;成员 s 在排序基因集 L&#xff08;比如根据 logFC 排序的差异基因集&#xff0c;默认降序&#xf…

“为爱起航,一村一书院”在阳朔落地

2023年8月1-5 日&#xff0c;“关爱祖国下一代&#xff0c;助力乡村振兴” 之为爱起航项目在阳朔举行。 本次活动由千里思乡村振兴促进会联合中国文化交流大使组委会携同大湾区19位师生加入到首批“为爱起航&#xff0c;一村一书院”项目中&#xff0c;同时&#xff0c;本项目得…

分页查询从接口到实现,统一对日期类型进行格式化处理

编写Service实现类编写Mapper的sql&#xff0c;但复杂的sql语句需要写到mapper对应的xml文件中日期类型格式化处理 /*** 扩展springmvc框架的消息转换器* param converters*/Overrideprotected void extendMessageConverters(List<HttpMessageConverter<?>> conve…

初识Container

1. 什么是Container&#xff08;容器&#xff09; 要有Container首先要有Image&#xff0c;也就是说Container是通过image创建的。 Container是在原先的Image之上新加的一层&#xff0c;称作Container layer&#xff0c;这一层是可读可写的&#xff08;Image是只读的&#xff0…

天津农商银行智能加密锁管理工具常见问题

天津农商银行智能加密锁管理工具&#xff0c;在使用过程中&#xff0c;可能出现一些莫名的错误&#xff0c;针对亲身遇到的坑&#xff0c;分享给大家&#xff0c;以备不时之需。 一、转账业务导入文件中文汉字出现乱码&#xff0c;如下图。 原因是文件编码不正确&#xff0c;…

MySQL:表的约束和基本查询

表的约束 表的约束——为了让插入的数据符合预期。 表的约束很多&#xff0c;这里主要介绍如下几个&#xff1a; null/not null,default, comment, zerofill&#xff0c;primary key&#xff0c;auto_increment&#xff0c;unique key 。 空属性 两个值&#xff1a;null&am…

【设计模式——学习笔记】23种设计模式——备忘录模式Memento(原理讲解+应用场景介绍+案例介绍+Java代码实现)

案例引入 游戏角色有攻击力和防御力&#xff0c;在大战Boss前保存自身的状态(攻击力和防御力)&#xff0c;当大战Boss后攻击力和防御力下降&#xff0c;可以从备忘录对象恢复到大战前的状态 传统设计方案 针对每一种角色&#xff0c;设计一个类来存储该角色的状态 【分析】…

cpu util margin,cpu freq margin

【cpufreq governor】cpu util 和 cpu margin怎么计算的_悟空明镜的博客-CSDN博客 cpu util margin&#xff0c;cpu freq margin 根据policy_util schedtune_margin 作为算力选对应的cpu cluster或调频

EXCEL表格操作

1.带格式合并&#xff1a;D6&"欢迎光临"&E6 2.带格式复制粘贴&#xff1a;ctrlc 复制&#xff0c;选择对于单元格点击选择性粘贴&#xff1a;粘贴值和数字格式

docker-compose 安装kafka集群

点击关注《golang技术实验室》公众号****&#xff0c;将****获取更多干货 介绍 Kafka是一种高性能的分布式流处理平台&#xff0c;它的集群工作原理如下&#xff1a; 假设你是一个快递员&#xff0c;Kafka集群就是一个快递中转站。在这个中转站中&#xff0c;有很多个小窗口…

基于TF-IDF+TensorFlow+词云+LDA 新闻自动文摘推荐系统—深度学习算法应用(含ipynb源码)+训练数据集

目录 前言总体设计系统整体结构图系统流程图 运行环境Python 环境TensorFlow环境方法一方法二 模块实现1. 数据预处理1&#xff09;导入数据2&#xff09;数据清洗3&#xff09;统计词频 2. 词云构建3. 关键词提取4. 语音播报5. LDA主题模型6. 模型构建 系统测试工程源代码下载…

分布式 - 消息队列Kafka:Kafka生产者发送消息流程和3种方式

文章目录 1. Kafka 生产者2. kafaka 命令行操作3. Kafka 生产者发送消息流程4. Kafka 生产者发送消息的3种方式1. 发送即忘记2. 同步发送3. 异步发送 5. Kafka 消息对象 ProducerRecord 1. Kafka 生产者 Kafka 生产者是指使用 Apache Kafka 消息系统的应用程序&#xff0c;它们…

wsl(在windows中使用呢linux系统)适用于windows的linux子系统

步骤可参考微软官方文档https://learn.microsoft.com/zh-cn/windows/wsl/install-manual#step-4—download-the-linux-kernel-update-package 在这里主要列举一些需要注意的点 wsl2的要求 一定要检查下windows版本&#xff0c;版本不对的先升级版本不然无法使用wsl2 wsl支持…

P4381 [IOI2008] Island (求基环树直径)

也许更好的阅读体验 D e s c r i p t i o n \mathcal{Description} Description 给一个基环树森林&#xff0c;求每棵树的直径的和&#xff0c;基环树的直径定义为&#xff0c;从一个点出发只能走到没走过的点&#xff08;即一个环不能把所有边都选&#xff09;&#xff0c;所经…

史上最细,自动化测试-logging日志采集详细实战(二)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、测试场景 给登…

固态硬盘数据恢复方法有哪些?三种恢复方法助您解忧

近年来固态硬盘比较流行&#xff0c;因为工作的需要我也在使用固态硬盘&#xff0c;它真的给我带来了很多的方便。但是最近&#xff0c;我固态硬盘里的文件有些不知道怎么就丢失了&#xff0c;这给我带来了很大的困扰。有什么方法可以找回来吗&#xff1f; 固态硬盘&#xff08…

Netty客户端同步获取结果

上次服务间通信是异步的&#xff0c;现在想实现客户端同步拿到服务端响应结果。实现如下&#xff1a; 在NettyClientHandler类中增加一个结果缓存器 Map<Long,Protocol<ResponseMsg>> resultMap new ConcurrentHashMap<>();修改方法 Override protected vo…

【文献阅读笔记】深度异常检测模型

文章目录 导读相关关键词及其英文描述记录深度异常检测模型Supervised deep anomaly detection 有监督深度异常检测Semi-Supervised deep anomaly detection 半监督深度异常检测Hybrid deep anomaly detection 混合深度异常检测One-class neural network for anomaly detection…