#
Junjie
2026-01-19 2fe4c71aab0ea6a5acc6cb6b6511674161f59c24
#
8个文件已添加
1个文件已修改
1709 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/ApkBuildTaskController.java 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/ApkBuildTask.java 248 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/ApkBuildTaskMapper.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/ApkBuildTaskService.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/ApkBuildTaskServiceImpl.java 359 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/ApkBuildTaskMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sdk/platform-tools-latest-windows.zip 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/apkBuild/apkBuild.html 762 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/ApkBuildTaskController.java
New file
@@ -0,0 +1,223 @@
package com.zy.asrs.controller;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.plugins.Page;
import com.core.annotations.ManagerAuth;
import com.core.common.Cools;
import com.core.common.DateUtils;
import com.core.common.R;
import com.core.controller.AbstractBaseController;
import com.zy.asrs.entity.ApkBuildTask;
import com.zy.asrs.service.ApkBuildTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * APK打包任务Controller
 */
@RestController
public class ApkBuildTaskController extends AbstractBaseController {
    @Autowired
    private ApkBuildTaskService apkBuildTaskService;
    /**
     * 查询单个任务
     */
    @RequestMapping(value = "/apkBuildTask/{id}/auth")
    @ManagerAuth
    public R get(@PathVariable("id") Long id) {
        return R.ok(apkBuildTaskService.selectById(id));
    }
    /**
     * 分页查询任务列表
     */
    @RequestMapping(value = "/apkBuildTask/list/auth")
    @ManagerAuth
    public R list(@RequestParam(defaultValue = "1") Integer curr,
            @RequestParam(defaultValue = "10") Integer limit,
            @RequestParam(required = false) String orderByField,
            @RequestParam(required = false) String orderByType,
            @RequestParam Map<String, Object> param) {
        excludeTrash(param);
        EntityWrapper<ApkBuildTask> wrapper = new EntityWrapper<>();
        convert(param, wrapper);
        if (!Cools.isEmpty(orderByField)) {
            wrapper.orderBy(orderByField, "asc".equalsIgnoreCase(orderByType));
        } else {
            wrapper.orderBy("id", false);
        }
        return R.ok(apkBuildTaskService.selectPage(new Page<>(curr, limit), wrapper));
    }
    /**
     * 搜索参数转换
     */
    private void convert(Map<String, Object> map, EntityWrapper<ApkBuildTask> wrapper) {
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            if (value == null || "".equals(value.toString())) {
                continue;
            }
            if (key.endsWith(">")) {
                wrapper.ge(Cools.deleteChar(key), DateUtils.convert(String.valueOf(value)));
            } else if (key.endsWith("<")) {
                wrapper.le(Cools.deleteChar(key), DateUtils.convert(String.valueOf(value)));
            } else if ("status".equals(key)) {
                wrapper.eq(key, value);
            } else {
                wrapper.like(key, String.valueOf(value));
            }
        }
    }
    /**
     * 触发打包任务
     */
    @RequestMapping(value = "/apkBuildTask/build/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R triggerBuild(@RequestBody JSONObject param) {
        try {
            String buildType = param.getString("buildType");
            String repoAlias = param.getString("repoAlias");
            String branch = param.getString("branch");
            if (Cools.isEmpty(buildType)) {
                buildType = "release";
            }
            if (Cools.isEmpty(repoAlias)) {
                return R.error("仓库别名不能为空");
            }
            if (Cools.isEmpty(branch)) {
                branch = "master";
            }
            ApkBuildTask task = apkBuildTaskService.triggerBuild(buildType, repoAlias, branch);
            return R.ok(task);
        } catch (Exception e) {
            return R.error("打包任务创建失败: " + e.getMessage());
        }
    }
    /**
     * 刷新指定任务状态
     */
    @RequestMapping(value = "/apkBuildTask/refresh/{id}/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R refreshStatus(@PathVariable("id") Long id) {
        try {
            ApkBuildTask task = apkBuildTaskService.refreshStatus(id);
            return R.ok(task);
        } catch (Exception e) {
            return R.error("刷新状态失败: " + e.getMessage());
        }
    }
    /**
     * 刷新所有进行中的任务状态
     */
    @RequestMapping(value = "/apkBuildTask/refreshAll/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R refreshAllPendingTasks() {
        try {
            int count = apkBuildTaskService.refreshAllPendingTasks();
            // 返回所有任务列表
            EntityWrapper<ApkBuildTask> wrapper = new EntityWrapper<>();
            wrapper.orderBy("id", false);
            List<ApkBuildTask> tasks = apkBuildTaskService.selectList(wrapper);
            Map<String, Object> result = new HashMap<>();
            result.put("tasks", tasks);
            result.put("refreshedCount", count);
            return R.ok(result);
        } catch (Exception e) {
            return R.error("刷新状态失败: " + e.getMessage());
        }
    }
    /**
     * 下载APK到服务器
     */
    @RequestMapping(value = "/apkBuildTask/download/{id}/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R downloadApk(@PathVariable("id") Long id) {
        try {
            String localPath = apkBuildTaskService.downloadApk(id);
            Map<String, Object> result = new HashMap<>();
            result.put("apkPath", localPath);
            return R.ok(result);
        } catch (Exception e) {
            return R.error("下载APK失败: " + e.getMessage());
        }
    }
    /**
     * 通过ADB安装APK到指定设备
     */
    @RequestMapping(value = "/apkBuildTask/install/{id}/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R installApk(@PathVariable("id") Long id, @RequestBody JSONObject param) {
        try {
            String deviceIp = param.getString("deviceIp");
            if (Cools.isEmpty(deviceIp)) {
                return R.error("设备IP不能为空");
            }
            String installResult = apkBuildTaskService.installApk(id, deviceIp);
            Map<String, Object> result = new HashMap<>();
            result.put("result", installResult);
            return R.ok(result);
        } catch (Exception e) {
            return R.error("安装APK失败: " + e.getMessage());
        }
    }
    /**
     * 批量安装APK到多台设备
     */
    @RequestMapping(value = "/apkBuildTask/installBatch/{id}/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R installApkBatch(@PathVariable("id") Long id, @RequestBody JSONObject param) {
        try {
            List<String> deviceIps = param.getJSONArray("deviceIps").toJavaList(String.class);
            if (deviceIps == null || deviceIps.isEmpty()) {
                return R.error("设备IP列表不能为空");
            }
            List<String> results = apkBuildTaskService.installApkToMultipleDevices(id, deviceIps);
            return R.ok(results);
        } catch (Exception e) {
            return R.error("批量安装APK失败: " + e.getMessage());
        }
    }
    /**
     * 删除任务记录
     */
    @RequestMapping(value = "/apkBuildTask/delete/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R delete(Integer[] ids) {
        if (ids == null || ids.length == 0) {
            return R.error();
        }
        apkBuildTaskService.deleteBatchIds(Arrays.asList(ids));
        return R.ok();
    }
    /**
     * 查询进行中的任务
     */
    @RequestMapping(value = "/apkBuildTask/pending/auth")
    @ManagerAuth
    public R getPendingTasks() {
        List<ApkBuildTask> tasks = apkBuildTaskService.getPendingTasks();
        return R.ok(tasks);
    }
}
src/main/java/com/zy/asrs/entity/ApkBuildTask.java
New file
@@ -0,0 +1,248 @@
package com.zy.asrs.entity;
import com.baomidou.mybatisplus.annotations.TableField;
import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.annotations.TableName;
import com.baomidou.mybatisplus.enums.IdType;
import java.io.Serializable;
import java.util.Date;
/**
 * APK打包任务实体类
 */
@TableName("apk_build_task")
public class ApkBuildTask implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 主键ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * 远程打包任务ID
     */
    @TableField("task_id")
    private String taskId;
    /**
     * 打包类型(release/debug)
     */
    @TableField("build_type")
    private String buildType;
    /**
     * 仓库别名
     */
    @TableField("repo_alias")
    private String repoAlias;
    /**
     * 分支名称
     */
    private String branch;
    /**
     * 状态:0-等待中,1-打包中,2-成功,3-失败
     */
    private Short status;
    /**
     * 本地APK文件路径
     */
    @TableField("apk_path")
    private String apkPath;
    /**
     * 远程APK文件路径
     */
    @TableField("artifact_path")
    private String artifactPath;
    /**
     * 项目名称
     */
    @TableField("project_name")
    private String projectName;
    /**
     * 错误信息
     */
    private String error;
    /**
     * 元数据JSON
     */
    private String meta;
    /**
     * 队列大小
     */
    @TableField("queue_size")
    private Integer queueSize;
    /**
     * 创建时间
     */
    @TableField("created_at")
    private Date createdAt;
    /**
     * 开始时间
     */
    @TableField("started_at")
    private Date startedAt;
    /**
     * 完成时间
     */
    @TableField("finished_at")
    private Date finishedAt;
    public ApkBuildTask() {
    }
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getTaskId() {
        return taskId;
    }
    public void setTaskId(String taskId) {
        this.taskId = taskId;
    }
    public String getBuildType() {
        return buildType;
    }
    public void setBuildType(String buildType) {
        this.buildType = buildType;
    }
    public String getRepoAlias() {
        return repoAlias;
    }
    public void setRepoAlias(String repoAlias) {
        this.repoAlias = repoAlias;
    }
    public String getBranch() {
        return branch;
    }
    public void setBranch(String branch) {
        this.branch = branch;
    }
    public Short getStatus() {
        return status;
    }
    public void setStatus(Short status) {
        this.status = status;
    }
    /**
     * 获取状态显示文本
     */
    public String getStatus$() {
        if (null == this.status) {
            return null;
        }
        switch (this.status) {
            case 0:
                return "等待中";
            case 1:
                return "打包中";
            case 2:
                return "成功";
            case 3:
                return "失败";
            default:
                return String.valueOf(this.status);
        }
    }
    public String getApkPath() {
        return apkPath;
    }
    public void setApkPath(String apkPath) {
        this.apkPath = apkPath;
    }
    public String getArtifactPath() {
        return artifactPath;
    }
    public void setArtifactPath(String artifactPath) {
        this.artifactPath = artifactPath;
    }
    public String getProjectName() {
        return projectName;
    }
    public void setProjectName(String projectName) {
        this.projectName = projectName;
    }
    public String getError() {
        return error;
    }
    public void setError(String error) {
        this.error = error;
    }
    public String getMeta() {
        return meta;
    }
    public void setMeta(String meta) {
        this.meta = meta;
    }
    public Integer getQueueSize() {
        return queueSize;
    }
    public void setQueueSize(Integer queueSize) {
        this.queueSize = queueSize;
    }
    public Date getCreatedAt() {
        return createdAt;
    }
    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
    public Date getStartedAt() {
        return startedAt;
    }
    public void setStartedAt(Date startedAt) {
        this.startedAt = startedAt;
    }
    public Date getFinishedAt() {
        return finishedAt;
    }
    public void setFinishedAt(Date finishedAt) {
        this.finishedAt = finishedAt;
    }
}
src/main/java/com/zy/asrs/mapper/ApkBuildTaskMapper.java
New file
@@ -0,0 +1,19 @@
package com.zy.asrs.mapper;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import com.zy.asrs.entity.ApkBuildTask;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * APK打包任务Mapper接口
 */
@Mapper
public interface ApkBuildTaskMapper extends BaseMapper<ApkBuildTask> {
    /**
     * 查询所有进行中的任务(等待中或打包中)
     */
    List<ApkBuildTask> selectPendingTasks();
}
src/main/java/com/zy/asrs/service/ApkBuildTaskService.java
New file
@@ -0,0 +1,70 @@
package com.zy.asrs.service;
import com.baomidou.mybatisplus.service.IService;
import com.zy.asrs.entity.ApkBuildTask;
import java.util.List;
/**
 * APK打包任务Service接口
 */
public interface ApkBuildTaskService extends IService<ApkBuildTask> {
    /**
     * 触发打包任务
     *
     * @param buildType 打包类型(release/debug)
     * @param repoAlias 仓库别名
     * @param branch    分支名称
     * @return 创建的任务实体
     */
    ApkBuildTask triggerBuild(String buildType, String repoAlias, String branch) throws Exception;
    /**
     * 刷新指定任务的状态
     *
     * @param id 任务ID
     * @return 更新后的任务实体
     */
    ApkBuildTask refreshStatus(Long id) throws Exception;
    /**
     * 刷新所有进行中的任务状态
     *
     * @return 更新的任务数量
     */
    int refreshAllPendingTasks();
    /**
     * 下载APK文件到本地服务器
     *
     * @param id 任务ID
     * @return 本地APK文件路径
     */
    String downloadApk(Long id) throws Exception;
    /**
     * 通过ADB安装APK到指定设备
     *
     * @param id       任务ID
     * @param deviceIp 设备IP地址
     * @return 安装结果信息
     */
    String installApk(Long id, String deviceIp) throws Exception;
    /**
     * 批量安装APK到多台设备
     *
     * @param id        任务ID
     * @param deviceIps 设备IP列表
     * @return 安装结果列表
     */
    List<String> installApkToMultipleDevices(Long id, List<String> deviceIps);
    /**
     * 查询所有进行中的任务
     *
     * @return 进行中的任务列表
     */
    List<ApkBuildTask> getPendingTasks();
}
src/main/java/com/zy/asrs/service/impl/ApkBuildTaskServiceImpl.java
New file
@@ -0,0 +1,359 @@
package com.zy.asrs.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.zy.asrs.entity.ApkBuildTask;
import com.zy.asrs.mapper.ApkBuildTaskMapper;
import com.zy.asrs.service.ApkBuildTaskService;
import com.zy.common.utils.HttpHandler;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
 * APK打包任务Service实现类
 */
@Service
public class ApkBuildTaskServiceImpl extends ServiceImpl<ApkBuildTaskMapper, ApkBuildTask>
        implements ApkBuildTaskService {
    private static final Logger log = LoggerFactory.getLogger(ApkBuildTaskServiceImpl.class);
    @Value("${apk-build.build-server-url}")
    private String buildServerUrl;
    @Value("${apk-build.download-server-url}")
    private String downloadServerUrl;
    @Value("${apk-build.x-token}")
    private String xToken;
    @Value("${apk-build.apk-download-path}")
    private String apkDownloadPath;
    @Value("${adb.path}")
    private String adbPath;
    @Override
    public ApkBuildTask triggerBuild(String buildType, String repoAlias, String branch) throws Exception {
        // 构建请求JSON
        JSONObject requestBody = new JSONObject();
        requestBody.put("build_type", buildType);
        requestBody.put("repo_alias", repoAlias);
        requestBody.put("branch", branch);
        // 发送打包请求
        Map<String, Object> headers = new HashMap<>();
        headers.put("X-Token", xToken);
        String response = new HttpHandler.Builder()
                .setUri(buildServerUrl)
                .setPath("/build")
                .setHeaders(headers)
                .setJson(requestBody.toJSONString())
                .build()
                .doPost();
        log.info("触发打包响应: {}", response);
        if (response == null) {
            throw new RuntimeException("打包服务无响应");
        }
        JSONObject result = JSON.parseObject(response);
        String taskId = result.getString("task_id");
        String status = result.getString("status");
        Integer queueSize = result.getInteger("queue_size");
        if (taskId == null || taskId.isEmpty()) {
            throw new RuntimeException("打包服务返回的task_id为空");
        }
        // 创建本地任务记录
        ApkBuildTask task = new ApkBuildTask();
        task.setTaskId(taskId);
        task.setBuildType(buildType);
        task.setRepoAlias(repoAlias);
        task.setBranch(branch);
        task.setStatus(convertStatus(status));
        task.setQueueSize(queueSize);
        task.setCreatedAt(new Date());
        this.insert(task);
        return task;
    }
    @Override
    public ApkBuildTask refreshStatus(Long id) throws Exception {
        ApkBuildTask task = this.selectById(id);
        if (task == null) {
            throw new RuntimeException("任务不存在");
        }
        // 如果任务已完成或失败,无需刷新
        if (task.getStatus() == 2 || task.getStatus() == 3) {
            return task;
        }
        // 查询远程状态
        Map<String, Object> headers = new HashMap<>();
        headers.put("X-Token", xToken);
        String response = new HttpHandler.Builder()
                .setUri(buildServerUrl)
                .setPath("/build/" + task.getTaskId())
                .setHeaders(headers)
                .build()
                .doGet();
        log.info("查询状态响应: {}", response);
        if (response == null) {
            throw new RuntimeException("查询状态服务无响应");
        }
        JSONObject result = JSON.parseObject(response);
        String status = result.getString("status");
        String artifactPath = result.getString("artifact_path");
        String projectName = result.getString("project_name");
        String error = result.getString("error");
        String meta = result.getString("meta");
        Long createdAtTimestamp = result.getLong("created_at");
        Long startedAtTimestamp = result.getLong("started_at");
        Long finishedAtTimestamp = result.getLong("finished_at");
        // 更新任务信息
        task.setStatus(convertStatus(status));
        task.setArtifactPath(artifactPath);
        task.setProjectName(projectName);
        task.setError(error);
        task.setMeta(meta);
        if (createdAtTimestamp != null) {
            task.setCreatedAt(new Date((long) (createdAtTimestamp * 1000)));
        }
        if (startedAtTimestamp != null) {
            task.setStartedAt(new Date((long) (startedAtTimestamp * 1000)));
        }
        if (finishedAtTimestamp != null) {
            task.setFinishedAt(new Date((long) (finishedAtTimestamp * 1000)));
        }
        // 如果打包成功,自动下载APK
        if ("SUCCESS".equals(status) && artifactPath != null && task.getApkPath() == null) {
            try {
                String localPath = downloadApkInternal(task);
                task.setApkPath(localPath);
            } catch (Exception e) {
                log.error("自动下载APK失败: {}", e.getMessage());
            }
        }
        this.updateById(task);
        return task;
    }
    @Override
    public int refreshAllPendingTasks() {
        List<ApkBuildTask> pendingTasks = this.baseMapper.selectPendingTasks();
        int count = 0;
        for (ApkBuildTask task : pendingTasks) {
            try {
                refreshStatus(task.getId());
                count++;
            } catch (Exception e) {
                log.error("刷新任务 {} 状态失败: {}", task.getId(), e.getMessage());
            }
        }
        return count;
    }
    @Override
    public String downloadApk(Long id) throws Exception {
        ApkBuildTask task = this.selectById(id);
        if (task == null) {
            throw new RuntimeException("任务不存在");
        }
        if (task.getStatus() != 2) {
            throw new RuntimeException("任务尚未完成,无法下载");
        }
        if (task.getApkPath() != null && new File(task.getApkPath()).exists()) {
            return task.getApkPath();
        }
        String localPath = downloadApkInternal(task);
        task.setApkPath(localPath);
        this.updateById(task);
        return localPath;
    }
    /**
     * 内部下载APK方法
     */
    private String downloadApkInternal(ApkBuildTask task) throws Exception {
        if (task.getTaskId() == null) {
            throw new RuntimeException("任务ID为空");
        }
        String downloadUrl = downloadServerUrl + "/build/" + task.getTaskId() + "/download";
        log.info("开始下载APK: {}", downloadUrl);
        // 创建下载目录
        Path downloadDir = Paths.get(apkDownloadPath);
        if (!Files.exists(downloadDir)) {
            Files.createDirectories(downloadDir);
        }
        // 生成本地文件名
        String fileName = task.getProjectName() != null ? task.getProjectName() + ".apk"
                : task.getTaskId() + ".apk";
        Path localFile = downloadDir.resolve(fileName);
        // 下载文件
        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(60, TimeUnit.SECONDS)
                .readTimeout(300, TimeUnit.SECONDS)
                .build();
        Request request = new Request.Builder()
                .url(downloadUrl)
                .addHeader("X-Token", xToken)
                .build();
        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new RuntimeException("下载失败,HTTP状态码: " + response.code());
            }
            try (InputStream is = response.body().byteStream();
                    OutputStream os = new FileOutputStream(localFile.toFile())) {
                byte[] buffer = new byte[8192];
                int bytesRead;
                while ((bytesRead = is.read(buffer)) != -1) {
                    os.write(buffer, 0, bytesRead);
                }
            }
        }
        log.info("APK下载完成: {}", localFile.toString());
        return localFile.toString();
    }
    @Override
    public String installApk(Long id, String deviceIp) throws Exception {
        ApkBuildTask task = this.selectById(id);
        if (task == null) {
            throw new RuntimeException("任务不存在");
        }
        if (task.getApkPath() == null || !new File(task.getApkPath()).exists()) {
            throw new RuntimeException("APK文件不存在,请先下载");
        }
        StringBuilder result = new StringBuilder();
        // 连接设备
        String connectResult = executeAdbCommand("connect", deviceIp + ":5555");
        result.append("连接设备: ").append(connectResult).append("\n");
        // 等待设备连接稳定
        Thread.sleep(1000);
        // 安装APK
        String installResult = executeAdbCommand("-s", deviceIp + ":5555", "install", "-r", task.getApkPath());
        result.append("安装结果: ").append(installResult);
        log.info("ADB安装结果: {}", result.toString());
        return result.toString();
    }
    @Override
    public List<String> installApkToMultipleDevices(Long id, List<String> deviceIps) {
        List<String> results = new ArrayList<>();
        for (String deviceIp : deviceIps) {
            try {
                String result = installApk(id, deviceIp);
                results.add(deviceIp + ": " + result);
            } catch (Exception e) {
                results.add(deviceIp + ": 安装失败 - " + e.getMessage());
            }
        }
        return results;
    }
    @Override
    public List<ApkBuildTask> getPendingTasks() {
        return this.baseMapper.selectPendingTasks();
    }
    /**
     * 执行ADB命令
     */
    private String executeAdbCommand(String... args) throws Exception {
        List<String> command = new ArrayList<>();
        command.add(adbPath);
        command.addAll(Arrays.asList(args));
        log.info("执行ADB命令: {}", String.join(" ", command));
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.redirectErrorStream(true);
        Process process = pb.start();
        StringBuilder output = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }
        }
        int exitCode = process.waitFor();
        String result = output.toString().trim();
        if (exitCode != 0 && !result.contains("Success")) {
            log.warn("ADB命令执行返回码: {}, 输出: {}", exitCode, result);
        }
        return result;
    }
    /**
     * 转换状态字符串为数字
     */
    private Short convertStatus(String status) {
        if (status == null) {
            return 0;
        }
        switch (status.toUpperCase()) {
            case "PENDING":
                return 0;
            case "BUILDING":
            case "RUNNING":
                return 1;
            case "SUCCESS":
                return 2;
            case "FAILED":
            case "ERROR":
                return 3;
            default:
                return 0;
        }
    }
}
src/main/resources/application.yml
@@ -57,4 +57,20 @@
  pwd: xltys1995
swagger:
  enable: false
  enable: false
# APK打包服务配置
apk-build:
  # 打包和查询状态的服务器地址
  build-server-url: http://127.0.0.1:8088
  # 下载APK的服务器地址
  download-server-url: http://127.0.0.1:8088
  # API认证Token
  x-token: uzCHJUQSb7tl2ZTjB0wI3XViqMgxRhvYL9EP
  # APK下载保存路径
  apk-download-path: /stock/out/apk/
# ADB配置
adb:
  # ADB可执行文件路径,如果在系统PATH中可直接填写adb
  path: D:\platform-tools\adb.exe
src/main/resources/mapper/ApkBuildTaskMapper.xml
New file
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zy.asrs.mapper.ApkBuildTaskMapper">
    <!-- 查询所有进行中的任务(等待中或打包中) -->
    <select id="selectPendingTasks" resultType="com.zy.asrs.entity.ApkBuildTask">
        SELECT * FROM apk_build_task WHERE status IN (0, 1) ORDER BY created_at DESC
    </select>
</mapper>
src/main/resources/sdk/platform-tools-latest-windows.zip
Binary files differ
src/main/webapp/views/apkBuild/apkBuild.html
New file
@@ -0,0 +1,762 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>APK打包管理</title>
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <link rel="stylesheet" href="../../static/vue/element/element.css">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
            background: #f5f7fa;
            padding: 15px;
        }
        .app-container {
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
            padding: 20px;
        }
        .header-section {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            flex-wrap: wrap;
            gap: 10px;
        }
        .search-section {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            align-items: center;
        }
        .action-buttons {
            display: flex;
            gap: 10px;
        }
        .auto-refresh-indicator {
            display: flex;
            align-items: center;
            font-size: 12px;
            color: #909399;
            margin-left: 10px;
        }
        .auto-refresh-indicator.active {
            color: #67c23a;
        }
        .auto-refresh-indicator .dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background: #909399;
            margin-right: 6px;
        }
        .auto-refresh-indicator.active .dot {
            background: #67c23a;
            animation: pulse 1.5s infinite;
        }
        @keyframes pulse {
            0%,
            100% {
                opacity: 1;
                transform: scale(1);
            }
            50% {
                opacity: 0.5;
                transform: scale(0.8);
            }
        }
        .status-tag {
            font-weight: 500;
        }
        .building-animation {
            animation: blink 1s infinite;
        }
        @keyframes blink {
            0%,
            100% {
                opacity: 1;
            }
            50% {
                opacity: 0.4;
            }
        }
        .table-section {
            margin-top: 15px;
        }
        .pagination-section {
            margin-top: 20px;
            display: flex;
            justify-content: flex-end;
        }
        .dialog-footer {
            text-align: right;
        }
        .form-tip {
            font-size: 12px;
            color: #909399;
            margin-top: 5px;
        }
        .install-textarea {
            width: 100%;
            min-height: 120px;
        }
        .detail-row {
            margin-bottom: 15px;
        }
        .detail-label {
            font-weight: 500;
            color: #606266;
            margin-bottom: 5px;
        }
        .detail-value {
            color: #303133;
            word-break: break-all;
        }
        .error-text {
            color: #f56c6c;
        }
    </style>
</head>
<body>
    <div id="app">
        <div class="app-container" v-loading="tableLoading">
            <!-- 头部区域 -->
            <div class="header-section">
                <!-- 搜索区域 -->
                <div class="search-section">
                    <el-select v-model="searchForm.status" placeholder="全部状态" clearable size="small"
                        style="width: 120px;">
                        <el-option label="等待中" value="0"></el-option>
                        <el-option label="打包中" value="1"></el-option>
                        <el-option label="成功" value="2"></el-option>
                        <el-option label="失败" value="3"></el-option>
                    </el-select>
                    <el-input v-model="searchForm.repo_alias" placeholder="仓库别名" size="small" style="width: 150px;"
                        clearable></el-input>
                    <el-input v-model="searchForm.branch" placeholder="分支名称" size="small" style="width: 120px;"
                        clearable></el-input>
                    <el-button type="primary" size="small" icon="el-icon-search" @click="handleSearch">搜索</el-button>
                    <el-button size="small" icon="el-icon-refresh-left" @click="handleReset">重置</el-button>
                </div>
                <!-- 操作按钮 -->
                <div class="action-buttons">
                    <el-button type="primary" size="small" icon="el-icon-plus" @click="showBuildDialog">新建打包</el-button>
                    <el-button type="success" size="small" icon="el-icon-refresh" @click="refreshAllTasks"
                        :loading="refreshing">刷新状态</el-button>
                    <el-button type="danger" size="small" icon="el-icon-delete" @click="handleBatchDelete"
                        :disabled="selectedRows.length === 0">删除</el-button>
                    <div class="auto-refresh-indicator" :class="{ active: autoRefreshEnabled }">
                        <span class="dot"></span>
                        <span>{{ autoRefreshEnabled ? '自动刷新中' : '自动刷新已关闭' }}</span>
                    </div>
                </div>
            </div>
            <!-- 表格区域 -->
            <div class="table-section">
                <el-table :data="tableData" border stripe @selection-change="handleSelectionChange" style="width: 100%;"
                    :header-cell-style="{ background: '#f5f7fa', color: '#606266' }">
                    <el-table-column type="selection" width="50" align="center"></el-table-column>
                    <el-table-column prop="id" label="ID" width="60" align="center"></el-table-column>
                    <el-table-column prop="taskId" label="任务ID" min-width="200" show-overflow-tooltip></el-table-column>
                    <el-table-column prop="repoAlias" label="仓库别名" width="120" align="center"></el-table-column>
                    <el-table-column prop="branch" label="分支" width="100" align="center"></el-table-column>
                    <el-table-column prop="buildType" label="类型" width="80" align="center">
                        <template slot-scope="scope">
                            <el-tag size="mini" :type="scope.row.buildType === 'release' ? 'success' : 'warning'">
                                {{ scope.row.buildType }}
                            </el-tag>
                        </template>
                    </el-table-column>
                    <el-table-column prop="status" label="状态" width="100" align="center">
                        <template slot-scope="scope">
                            <el-tag size="small" :type="getStatusType(scope.row.status)"
                                :class="{ 'building-animation': scope.row.status === 1 }">
                                {{ getStatusText(scope.row.status) }}
                            </el-tag>
                        </template>
                    </el-table-column>
                    <el-table-column prop="projectName" label="项目名称" min-width="150"
                        show-overflow-tooltip></el-table-column>
                    <el-table-column prop="createdAt" label="创建时间" width="160" align="center">
                        <template slot-scope="scope">
                            {{ formatDate(scope.row.createdAt) }}
                        </template>
                    </el-table-column>
                    <el-table-column label="操作" width="180" align="center" fixed="right">
                        <template slot-scope="scope">
                            <el-button type="text" size="small" @click="showDetail(scope.row)">详情</el-button>
                            <el-button type="text" size="small" v-if="scope.row.status === 2 && !scope.row.apkPath"
                                @click="downloadApk(scope.row)" :loading="scope.row.downloading">下载</el-button>
                            <el-button type="text" size="small" style="color: #67c23a;"
                                v-if="scope.row.status === 2 && scope.row.apkPath"
                                @click="showInstallDialog(scope.row)">安装</el-button>
                        </template>
                    </el-table-column>
                </el-table>
            </div>
            <!-- 分页 -->
            <div class="pagination-section">
                <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total"
                    :page-size="pageSize" :current-page="currentPage" :page-sizes="[10, 20, 50, 100]"
                    @size-change="handleSizeChange" @current-change="handlePageChange">
                </el-pagination>
            </div>
        </div>
        <!-- 新建打包对话框 -->
        <el-dialog title="新建打包任务" :visible.sync="buildDialogVisible" width="450px" :close-on-click-modal="false">
            <el-form :model="buildForm" :rules="buildRules" ref="buildFormRef" label-width="100px">
                <el-form-item label="打包类型" prop="buildType">
                    <el-radio-group v-model="buildForm.buildType">
                        <el-radio label="release">Release</el-radio>
                        <el-radio label="debug">Debug</el-radio>
                    </el-radio-group>
                </el-form-item>
                <el-form-item label="仓库别名" prop="repoAlias">
                    <el-input v-model="buildForm.repoAlias" placeholder="请输入仓库别名"></el-input>
                </el-form-item>
                <el-form-item label="分支名称" prop="branch">
                    <el-input v-model="buildForm.branch" placeholder="请输入分支名称"></el-input>
                    <div class="form-tip">默认为 master 分支</div>
                </el-form-item>
            </el-form>
            <div slot="footer" class="dialog-footer">
                <el-button @click="buildDialogVisible = false">取消</el-button>
                <el-button type="primary" @click="submitBuild" :loading="buildSubmitting">提交打包</el-button>
            </div>
        </el-dialog>
        <!-- 任务详情对话框 -->
        <el-dialog title="任务详情" :visible.sync="detailDialogVisible" width="550px">
            <div v-if="currentDetail">
                <el-row :gutter="20">
                    <el-col :span="12">
                        <div class="detail-row">
                            <div class="detail-label">任务ID</div>
                            <div class="detail-value">{{ currentDetail.taskId }}</div>
                        </div>
                    </el-col>
                    <el-col :span="12">
                        <div class="detail-row">
                            <div class="detail-label">状态</div>
                            <div class="detail-value">
                                <el-tag size="small" :type="getStatusType(currentDetail.status)">
                                    {{ getStatusText(currentDetail.status) }}
                                </el-tag>
                            </div>
                        </div>
                    </el-col>
                </el-row>
                <el-row :gutter="20">
                    <el-col :span="12">
                        <div class="detail-row">
                            <div class="detail-label">仓库别名</div>
                            <div class="detail-value">{{ currentDetail.repoAlias }}</div>
                        </div>
                    </el-col>
                    <el-col :span="12">
                        <div class="detail-row">
                            <div class="detail-label">分支</div>
                            <div class="detail-value">{{ currentDetail.branch }}</div>
                        </div>
                    </el-col>
                </el-row>
                <el-row :gutter="20">
                    <el-col :span="12">
                        <div class="detail-row">
                            <div class="detail-label">打包类型</div>
                            <div class="detail-value">{{ currentDetail.buildType }}</div>
                        </div>
                    </el-col>
                    <el-col :span="12">
                        <div class="detail-row">
                            <div class="detail-label">项目名称</div>
                            <div class="detail-value">{{ currentDetail.projectName || '-' }}</div>
                        </div>
                    </el-col>
                </el-row>
                <div class="detail-row">
                    <div class="detail-label">APK路径</div>
                    <div class="detail-value">{{ currentDetail.apkPath || '未下载' }}</div>
                </div>
                <el-row :gutter="20">
                    <el-col :span="8">
                        <div class="detail-row">
                            <div class="detail-label">创建时间</div>
                            <div class="detail-value">{{ formatDate(currentDetail.createdAt) }}</div>
                        </div>
                    </el-col>
                    <el-col :span="8">
                        <div class="detail-row">
                            <div class="detail-label">开始时间</div>
                            <div class="detail-value">{{ formatDate(currentDetail.startedAt) || '-' }}</div>
                        </div>
                    </el-col>
                    <el-col :span="8">
                        <div class="detail-row">
                            <div class="detail-label">完成时间</div>
                            <div class="detail-value">{{ formatDate(currentDetail.finishedAt) || '-' }}</div>
                        </div>
                    </el-col>
                </el-row>
                <div class="detail-row" v-if="currentDetail.error">
                    <div class="detail-label">错误信息</div>
                    <div class="detail-value error-text">{{ currentDetail.error }}</div>
                </div>
            </div>
            <div slot="footer" class="dialog-footer">
                <el-button @click="detailDialogVisible = false">关闭</el-button>
            </div>
        </el-dialog>
        <!-- 安装对话框 -->
        <el-dialog title="安装APK到设备" :visible.sync="installDialogVisible" width="450px" :close-on-click-modal="false">
            <el-form label-width="80px">
                <el-form-item label="设备IP">
                    <el-input type="textarea" v-model="installIps" class="install-textarea"
                        placeholder="请输入设备IP地址,多个设备请换行分隔&#10;例如:&#10;192.168.1.100&#10;192.168.1.101">
                    </el-input>
                    <div class="form-tip">支持批量安装,每行一个IP地址</div>
                </el-form-item>
            </el-form>
            <div slot="footer" class="dialog-footer">
                <el-button @click="installDialogVisible = false">取消</el-button>
                <el-button type="primary" @click="submitInstall" :loading="installing">确认安装</el-button>
            </div>
        </el-dialog>
    </div>
    <script src="../../static/vue/js/vue.min.js"></script>
    <script src="../../static/vue/element/element.js"></script>
    <script src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
    <script src="../../static/js/common.js"></script>
    <script>
        new Vue({
            el: '#app',
            data: {
                // 表格数据
                tableData: [],
                tableLoading: false,
                total: 0,
                currentPage: 1,
                pageSize: 10,
                selectedRows: [],
                // 搜索表单
                searchForm: {
                    status: '',
                    repo_alias: '',
                    branch: ''
                },
                // 自动刷新
                autoRefreshEnabled: false,
                autoRefreshTimer: null,
                refreshing: false,
                // 新建打包对话框
                buildDialogVisible: false,
                buildSubmitting: false,
                buildForm: {
                    buildType: 'release',
                    repoAlias: 'zy-monitor',
                    branch: 'master'
                },
                buildRules: {
                    repoAlias: [{ required: true, message: '请输入仓库别名', trigger: 'blur' }]
                },
                // 详情对话框
                detailDialogVisible: false,
                currentDetail: null,
                // 安装对话框
                installDialogVisible: false,
                installIps: '',
                installing: false,
                currentInstallTask: null
            },
            created() {
                this.loadData();
            },
            beforeDestroy() {
                this.stopAutoRefresh();
            },
            methods: {
                // 获取请求头
                getHeaders() {
                    return { 'token': localStorage.getItem('token') };
                },
                // 加载数据
                loadData() {
                    this.tableLoading = true;
                    const params = {
                        curr: this.currentPage,
                        limit: this.pageSize,
                        ...this.searchForm
                    };
                    $.ajax({
                        url: baseUrl + '/apkBuildTask/list/auth',
                        headers: this.getHeaders(),
                        data: params,
                        success: (res) => {
                            this.tableLoading = false;
                            if (res.code === 200) {
                                this.tableData = res.data.records || [];
                                this.total = res.data.total || 0;
                                this.checkAutoRefresh();
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + '/';
                            } else {
                                this.$message.error(res.msg || '加载失败');
                            }
                        },
                        error: () => {
                            this.tableLoading = false;
                            this.$message.error('请求失败');
                        }
                    });
                },
                // 检查是否需要自动刷新
                checkAutoRefresh() {
                    const hasPending = this.tableData.some(item => item.status === 0 || item.status === 1);
                    if (hasPending && !this.autoRefreshEnabled) {
                        this.startAutoRefresh();
                    } else if (!hasPending && this.autoRefreshEnabled) {
                        this.stopAutoRefresh();
                    }
                },
                // 开启自动刷新
                startAutoRefresh() {
                    if (this.autoRefreshTimer) return;
                    this.autoRefreshEnabled = true;
                    this.autoRefreshTimer = setInterval(() => {
                        this.silentRefresh();
                    }, 5000);
                },
                // 停止自动刷新
                stopAutoRefresh() {
                    if (this.autoRefreshTimer) {
                        clearInterval(this.autoRefreshTimer);
                        this.autoRefreshTimer = null;
                    }
                    this.autoRefreshEnabled = false;
                },
                // 静默刷新
                silentRefresh() {
                    $.ajax({
                        url: baseUrl + '/apkBuildTask/refreshAll/auth',
                        headers: this.getHeaders(),
                        method: 'POST',
                        success: (res) => {
                            if (res.code === 200) {
                                this.loadData();
                            }
                        }
                    });
                },
                // 手动刷新所有任务
                refreshAllTasks() {
                    this.refreshing = true;
                    $.ajax({
                        url: baseUrl + '/apkBuildTask/refreshAll/auth',
                        headers: this.getHeaders(),
                        method: 'POST',
                        success: (res) => {
                            this.refreshing = false;
                            if (res.code === 200) {
                                this.$message.success(`刷新完成,更新了 ${res.data.refreshedCount} 个任务`);
                                this.loadData();
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + '/';
                            } else {
                                this.$message.error(res.msg || '刷新失败');
                            }
                        },
                        error: () => {
                            this.refreshing = false;
                            this.$message.error('请求失败');
                        }
                    });
                },
                // 搜索
                handleSearch() {
                    this.currentPage = 1;
                    this.loadData();
                },
                // 重置
                handleReset() {
                    this.searchForm = { status: '', repo_alias: '', branch: '' };
                    this.currentPage = 1;
                    this.loadData();
                },
                // 分页
                handlePageChange(page) {
                    this.currentPage = page;
                    this.loadData();
                },
                handleSizeChange(size) {
                    this.pageSize = size;
                    this.currentPage = 1;
                    this.loadData();
                },
                // 选择行
                handleSelectionChange(selection) {
                    this.selectedRows = selection;
                },
                // 批量删除
                handleBatchDelete() {
                    if (this.selectedRows.length === 0) return;
                    this.$confirm(`确定删除选中的 ${this.selectedRows.length} 条记录吗?`, '提示', {
                        type: 'warning'
                    }).then(() => {
                        const ids = this.selectedRows.map(row => row.id);
                        $.ajax({
                            url: baseUrl + '/apkBuildTask/delete/auth',
                            headers: this.getHeaders(),
                            method: 'POST',
                            data: { ids: ids },
                            traditional: true,
                            success: (res) => {
                                if (res.code === 200) {
                                    this.$message.success('删除成功');
                                    this.loadData();
                                } else {
                                    this.$message.error(res.msg || '删除失败');
                                }
                            }
                        });
                    }).catch(() => { });
                },
                // 显示新建打包对话框
                showBuildDialog() {
                    this.buildForm = {
                        buildType: 'release',
                        repoAlias: 'zy-monitor',
                        branch: 'master'
                    };
                    this.buildDialogVisible = true;
                },
                // 提交打包
                submitBuild() {
                    this.$refs.buildFormRef.validate((valid) => {
                        if (!valid) return;
                        this.buildSubmitting = true;
                        $.ajax({
                            url: baseUrl + '/apkBuildTask/build/auth',
                            headers: this.getHeaders(),
                            method: 'POST',
                            contentType: 'application/json;charset=UTF-8',
                            data: JSON.stringify(this.buildForm),
                            success: (res) => {
                                this.buildSubmitting = false;
                                if (res.code === 200) {
                                    this.$message.success('打包任务已创建');
                                    this.buildDialogVisible = false;
                                    this.loadData();
                                } else if (res.code === 403) {
                                    top.location.href = baseUrl + '/';
                                } else {
                                    this.$message.error(res.msg || '创建失败');
                                }
                            },
                            error: () => {
                                this.buildSubmitting = false;
                                this.$message.error('请求失败');
                            }
                        });
                    });
                },
                // 显示详情
                showDetail(row) {
                    this.currentDetail = row;
                    this.detailDialogVisible = true;
                },
                // 下载APK
                downloadApk(row) {
                    this.$set(row, 'downloading', true);
                    $.ajax({
                        url: baseUrl + '/apkBuildTask/download/' + row.id + '/auth',
                        headers: this.getHeaders(),
                        method: 'POST',
                        success: (res) => {
                            this.$set(row, 'downloading', false);
                            if (res.code === 200) {
                                this.$message.success('APK下载完成: ' + res.data.apkPath);
                                this.loadData();
                            } else {
                                this.$message.error(res.msg || '下载失败');
                            }
                        },
                        error: () => {
                            this.$set(row, 'downloading', false);
                            this.$message.error('请求失败');
                        }
                    });
                },
                // 显示安装对话框
                showInstallDialog(row) {
                    this.currentInstallTask = row;
                    this.installIps = '';
                    this.installDialogVisible = true;
                },
                // 提交安装
                submitInstall() {
                    if (!this.installIps.trim()) {
                        this.$message.warning('请输入设备IP地址');
                        return;
                    }
                    const ips = this.installIps.split('\n')
                        .map(ip => ip.trim())
                        .filter(ip => ip !== '');
                    if (ips.length === 0) {
                        this.$message.warning('请输入有效的设备IP地址');
                        return;
                    }
                    this.installing = true;
                    const taskId = this.currentInstallTask.id;
                    if (ips.length === 1) {
                        // 单设备安装
                        $.ajax({
                            url: baseUrl + '/apkBuildTask/install/' + taskId + '/auth',
                            headers: this.getHeaders(),
                            method: 'POST',
                            contentType: 'application/json;charset=UTF-8',
                            data: JSON.stringify({ deviceIp: ips[0] }),
                            success: (res) => {
                                this.installing = false;
                                this.installDialogVisible = false;
                                if (res.code === 200) {
                                    this.$alert(res.data.result, '安装结果', { type: 'success' });
                                } else {
                                    this.$message.error(res.msg || '安装失败');
                                }
                            },
                            error: () => {
                                this.installing = false;
                                this.$message.error('请求失败');
                            }
                        });
                    } else {
                        // 批量安装
                        $.ajax({
                            url: baseUrl + '/apkBuildTask/installBatch/' + taskId + '/auth',
                            headers: this.getHeaders(),
                            method: 'POST',
                            contentType: 'application/json;charset=UTF-8',
                            data: JSON.stringify({ deviceIps: ips }),
                            success: (res) => {
                                this.installing = false;
                                this.installDialogVisible = false;
                                if (res.code === 200) {
                                    this.$alert(res.data.join('\n'), '批量安装结果', { type: 'success' });
                                } else {
                                    this.$message.error(res.msg || '安装失败');
                                }
                            },
                            error: () => {
                                this.installing = false;
                                this.$message.error('请求失败');
                            }
                        });
                    }
                },
                // 获取状态类型
                getStatusType(status) {
                    const types = { 0: 'info', 1: '', 2: 'success', 3: 'danger' };
                    return types[status] || 'info';
                },
                // 获取状态文本
                getStatusText(status) {
                    const texts = { 0: '等待中', 1: '打包中', 2: '成功', 3: '失败' };
                    return texts[status] || '未知';
                },
                // 格式化日期
                formatDate(timestamp) {
                    if (!timestamp) return '';
                    const date = new Date(timestamp);
                    const year = date.getFullYear();
                    const month = String(date.getMonth() + 1).padStart(2, '0');
                    const day = String(date.getDate()).padStart(2, '0');
                    const hours = String(date.getHours()).padStart(2, '0');
                    const minutes = String(date.getMinutes()).padStart(2, '0');
                    const seconds = String(date.getSeconds()).padStart(2, '0');
                    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
                }
            }
        });
    </script>
</body>
</html>