feat(device): 新增应用管理功能
新增应用启动、清除数据、卸载等远程操作功能,并优化应用列表接口支持图标展示。 同时修正了移动端上报接口的日志操作类型,补充了应用相关的操作枚举。
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
package com.youlai.boot.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC 配置
|
||||
*
|
||||
* @author TTSTD
|
||||
* @since 2026-08-05
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 应用图标静态资源映射:
|
||||
* /api/v1/device/app_icon/{sn}/{fileName} -> 磁盘 apkIcon 目录
|
||||
*/
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
String location = "file:" + FilePath.getApkIconPath().replace("\\", "/");
|
||||
registry.addResourceHandler("/api/v1/device/app_icon/**")
|
||||
.addResourceLocations(location);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,20 @@ public enum ActionTypeEnum implements IBaseEnum<Integer> {
|
||||
LOCATE(22, "定位"),
|
||||
RESTORE(23, "重置"),
|
||||
DEVELOPER(24, "开发者选项"),
|
||||
UPDATE_SYSTEM_INFO(25, "更新系统信息"),
|
||||
UPDATE_OTHER_INFO(26, "更新其他信息"),
|
||||
UPDATE_HARDWARE_INFO(27, "更新硬件信息"),
|
||||
UPDATE_NETWORK_INFO(28, "更新网络信息"),
|
||||
UPDATE_SECURITY_INFO(29, "更新安全信息"),
|
||||
VIEW_DEVELOPER_OPTIONS(30, "查看开发者选项"),
|
||||
UPLOAD_APK_LIST(31, "上传应用列表"),
|
||||
VIEW_APK_LIST(32, "查看应用列表"),
|
||||
LAUNCH_APP(33, "打开应用"),
|
||||
CLEAR_APP_DATA(34, "清除应用数据"),
|
||||
UNINSTALL_APP(35, "卸载应用"),
|
||||
UPLOAD_APP_ICON(36, "上传应用图标"),
|
||||
LOCK_APP_ICON(37, "锁定应用图标"),
|
||||
UNLOCK_APP_ICON(38, "解锁应用图标"),
|
||||
OTHER(99, "其他");
|
||||
|
||||
@EnumValue
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.youlai.boot.device.controller;
|
||||
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.device.model.vo.AppIconVO;
|
||||
import com.youlai.boot.device.service.AppIconService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 应用图标库控制层
|
||||
*
|
||||
* @author TTSTD
|
||||
* @since 2026-08-05
|
||||
*/
|
||||
@Tag(name = "应用图标库")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/app-icons")
|
||||
@RequiredArgsConstructor
|
||||
public class AppIconController {
|
||||
|
||||
private final AppIconService appIconService;
|
||||
|
||||
@Operation(summary = "获取应用当前生效图标信息")
|
||||
@GetMapping("/{packageName}")
|
||||
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.VIEW)
|
||||
public Result<AppIconVO> getCurrentIcon(@PathVariable String packageName) {
|
||||
return Result.success(appIconService.getCurrentIcon(packageName));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取应用图标历史版本列表(含当前,按版本倒序)")
|
||||
@GetMapping("/{packageName}/history")
|
||||
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.LIST)
|
||||
public Result<List<AppIconVO>> getHistory(@PathVariable String packageName) {
|
||||
return Result.success(appIconService.getHistory(packageName));
|
||||
}
|
||||
|
||||
@Operation(summary = "选取历史版本并锁定")
|
||||
@PostMapping("/{packageName}/lock")
|
||||
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.LOCK_APP_ICON)
|
||||
public Result<?> lock(
|
||||
@Parameter(description = "应用包名") @PathVariable String packageName,
|
||||
@Parameter(description = "选中图标记录ID") @RequestParam Long iconId
|
||||
) {
|
||||
return Result.judge(appIconService.lockIcon(packageName, iconId));
|
||||
}
|
||||
|
||||
@Operation(summary = "解锁当前图标")
|
||||
@PostMapping("/{packageName}/unlock")
|
||||
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.UNLOCK_APP_ICON)
|
||||
public Result<?> unlock(@Parameter(description = "应用包名") @PathVariable String packageName) {
|
||||
return Result.judge(appIconService.unlockIcon(packageName));
|
||||
}
|
||||
|
||||
@Operation(summary = "上传应用图标(全局库)")
|
||||
@PostMapping("/{packageName}/upload")
|
||||
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.UPLOAD_APP_ICON)
|
||||
public Result<String> upload(
|
||||
@Parameter(description = "应用包名") @PathVariable String packageName,
|
||||
@Parameter(description = "图标文件(jpg/jpeg/png/webp)") @RequestPart(value = "file") MultipartFile file
|
||||
) {
|
||||
return Result.success(appIconService.uploadIcon(packageName, file, "admin"));
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
package com.youlai.boot.device.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.result.PageResult;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.device.model.query.DeviceQuery;
|
||||
import com.youlai.boot.device.model.vo.DeviceApkInfoVO;
|
||||
import com.youlai.boot.device.model.vo.DeviceHardwareInfoVO;
|
||||
import com.youlai.boot.device.model.vo.DeviceNetworkInfoVO;
|
||||
import com.youlai.boot.device.model.vo.DeviceOtherInfoVO;
|
||||
import com.youlai.boot.device.model.vo.DevicePageVO;
|
||||
import com.youlai.boot.device.model.vo.DeviceSecurityInfoVO;
|
||||
import com.youlai.boot.device.model.vo.DeviceSystemInfoVO;
|
||||
import com.youlai.boot.device.service.ApkInstallService;
|
||||
import com.youlai.boot.device.service.DeviceService;
|
||||
import com.youlai.boot.device.service.HardwareInfoService;
|
||||
import com.youlai.boot.device.service.NetworkInfoService;
|
||||
@@ -26,6 +31,8 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设备管理控制器
|
||||
*
|
||||
@@ -45,6 +52,7 @@ public class DeviceController {
|
||||
private final NetworkInfoService networkInfoService;
|
||||
private final SecurityInfoService securityInfoService;
|
||||
private final OtherInfoService otherInfoService;
|
||||
private final ApkInstallService apkInstallService;
|
||||
|
||||
@Operation(summary = "设备分页列表")
|
||||
@GetMapping("/page")
|
||||
@@ -102,4 +110,13 @@ public class DeviceController {
|
||||
DeviceOtherInfoVO vo = deviceService.getDeviceOtherInfo(sn);
|
||||
return Result.success(vo);
|
||||
}
|
||||
|
||||
@Operation(summary = "设备已安装应用列表")
|
||||
@GetMapping("/{sn}/apk_list")
|
||||
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.VIEW_APK_LIST)
|
||||
public Result<List<DeviceApkInfoVO>> getApkList(
|
||||
@Parameter(description = "设备序列号") @PathVariable("sn") String sn) {
|
||||
return Result.success(apkInstallService.getDeviceApkInfo(sn));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.device.model.entity.SnDeviceInfo;
|
||||
import com.youlai.boot.device.model.form.AppActionForm;
|
||||
import com.youlai.boot.device.model.form.DeveloperForm;
|
||||
import com.youlai.boot.device.model.vo.DevicePageVO;
|
||||
import com.youlai.boot.device.service.DeviceService;
|
||||
@@ -129,4 +130,28 @@ public class DeviceOpsController {
|
||||
boolean result = deviceService.deleteDeveloperConfig(sn);
|
||||
return Result.judge(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "打开应用")
|
||||
@PostMapping("/{sn}/app/launch")
|
||||
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.LAUNCH_APP)
|
||||
public Result<?> launchApp(@PathVariable String sn, @Valid @RequestBody AppActionForm form) {
|
||||
boolean result = deviceService.launchApp(sn, form.getPackageName());
|
||||
return Result.judge(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "清除应用数据")
|
||||
@PostMapping("/{sn}/app/clear_data")
|
||||
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.CLEAR_APP_DATA)
|
||||
public Result<?> clearAppData(@PathVariable String sn, @Valid @RequestBody AppActionForm form) {
|
||||
boolean result = deviceService.clearAppData(sn, form.getPackageName());
|
||||
return Result.judge(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "卸载应用")
|
||||
@PostMapping("/{sn}/app/uninstall")
|
||||
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.UNINSTALL_APP)
|
||||
public Result<?> uninstallApp(@PathVariable String sn, @Valid @RequestBody AppActionForm form) {
|
||||
boolean result = deviceService.uninstallApp(sn, form.getPackageName());
|
||||
return Result.judge(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ public class MobileController {
|
||||
private final LocationService locationService;
|
||||
private final DeveloperService developerService;
|
||||
private final ApkInstallService apkInstallService;
|
||||
private final AppIconService appIconService;
|
||||
private final SystemInfoService systemInfoService;
|
||||
private final HardwareInfoService hardwareInfoService;
|
||||
private final NetworkInfoService networkInfoService;
|
||||
@@ -89,7 +90,7 @@ public class MobileController {
|
||||
|
||||
@Operation(summary = "上传设备硬件信息")
|
||||
@PostMapping("/update_system_info")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.UPDATE_SYSTEM_INFO)
|
||||
public Result<Void> updateSystemInfo(
|
||||
@RequestHeader(value = "X-Device-SN") String sn,
|
||||
@RequestBody SnSystemInfoReq snSystemInfoReq
|
||||
@@ -136,7 +137,7 @@ public class MobileController {
|
||||
|
||||
@Operation(summary = "上传设备其他信息")
|
||||
@PostMapping("/update_other_info")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.UPDATE_OTHER_INFO)
|
||||
public Result<Void> updateOtherInfo(
|
||||
@RequestHeader(value = "X-Device-SN") String sn,
|
||||
@RequestBody SnOtherInfoReq snOtherInfoReq
|
||||
@@ -183,7 +184,7 @@ public class MobileController {
|
||||
|
||||
@Operation(summary = "上传设备硬件信息")
|
||||
@PostMapping("/update_hardware_info")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.UPDATE_HARDWARE_INFO)
|
||||
public Result<Void> updateHardwareInfo(
|
||||
@RequestHeader(value = "X-Device-SN") String sn,
|
||||
@RequestBody SnHardwareInfoReq snHardwareInfoReq
|
||||
@@ -230,7 +231,7 @@ public class MobileController {
|
||||
|
||||
@Operation(summary = "上传设备网络信息")
|
||||
@PostMapping("/update_network_info")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.UPDATE_NETWORK_INFO)
|
||||
public Result<Void> updateNetworkInfo(
|
||||
@RequestHeader(value = "X-Device-SN") String sn,
|
||||
@RequestBody SnNetworkInfoReq snNetworkInfoReq
|
||||
@@ -277,7 +278,7 @@ public class MobileController {
|
||||
|
||||
@Operation(summary = "上传设备安全信息")
|
||||
@PostMapping("/update_security_info")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.UPDATE_SECURITY_INFO)
|
||||
public Result<Void> updateSecurityInfo(
|
||||
@RequestHeader(value = "X-Device-SN") String sn,
|
||||
@RequestBody SnSecurityInfoReq snSecurityInfoReq
|
||||
@@ -432,7 +433,7 @@ public class MobileController {
|
||||
|
||||
@Operation(summary = "获取开发者选项开关")
|
||||
@GetMapping("/get_developer_options")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.VIEW_DEVELOPER_OPTIONS)
|
||||
public Result<DeveloperOptionsVO> getDeveloperOptions(@RequestHeader(value = "X-Device-SN") String sn) {
|
||||
try {
|
||||
if (sn == null || sn.trim().isEmpty()) {
|
||||
@@ -453,7 +454,7 @@ public class MobileController {
|
||||
|
||||
@Operation(summary = "上传设备已安装应用列表")
|
||||
@PostMapping("/upload_install_apks")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.UPLOAD_APK_LIST)
|
||||
public Result<Void> uploadInstallApks(
|
||||
@RequestHeader(value = "X-Device-SN") String sn,
|
||||
@RequestBody List<ApkInstallInfoReq> apkInfos
|
||||
@@ -482,23 +483,26 @@ public class MobileController {
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取设备已安装应用列表")
|
||||
@GetMapping("/get_install_apks")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
|
||||
public Result<List<ApkInstallInfoReq>> getInstallApks(@RequestHeader(value = "X-Device-SN") String sn) {
|
||||
@Operation(summary = "上传应用图标")
|
||||
@PostMapping("/upload_app_icon")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.UPLOAD_APP_ICON)
|
||||
public Result<String> uploadAppIcon(
|
||||
@RequestHeader(value = "X-Device-SN") String sn,
|
||||
@RequestParam String packageName,
|
||||
@RequestPart(value = "file") MultipartFile file
|
||||
) {
|
||||
try {
|
||||
if (sn == null || sn.trim().isEmpty()) {
|
||||
return Result.failed("设备序列号不能为空");
|
||||
}
|
||||
|
||||
List<ApkInstallInfoReq> apkInfos = apkInstallService.getDeviceApkInfo(sn);
|
||||
|
||||
logger.info("获取应用列表成功, sn: {}, count: {}", sn, apkInfos.size());
|
||||
return Result.success(apkInfos);
|
||||
|
||||
if (packageName == null || packageName.trim().isEmpty()) {
|
||||
return Result.failed("应用包名不能为空");
|
||||
}
|
||||
String iconUrl = appIconService.uploadIcon(packageName, file, "device");
|
||||
return Result.success(iconUrl);
|
||||
} catch (Exception e) {
|
||||
logger.error("getInstallApks error, sn: {}", sn, e);
|
||||
return Result.failed("获取应用列表失败: " + e.getMessage());
|
||||
logger.error("uploadAppIcon error, sn: {}", sn, e);
|
||||
return Result.failed("上传应用图标失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.youlai.boot.device.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.device.model.entity.AppIcon;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 应用图标库Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface AppIconMapper extends BaseMapper<AppIcon> {
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 应用图标库实体(含历史版本)
|
||||
* <p>
|
||||
* 同一包名可存在多个版本:status=1 为当前生效(唯一一条),
|
||||
* status=0 为历史版本;locked=1 的当前图标不可被上传覆盖。
|
||||
*
|
||||
* @author TTSTD
|
||||
* @since 2026-08-05
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("app_icon")
|
||||
public class AppIcon extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 应用包名
|
||||
*/
|
||||
private String packageName;
|
||||
|
||||
/**
|
||||
* 图标访问地址(相对路径,如 /api/v1/device/app_icon/{md5}.png)
|
||||
*/
|
||||
private String iconUrl;
|
||||
|
||||
/**
|
||||
* 图标文件MD5(内容指纹,用于去重与换图标检测)
|
||||
*/
|
||||
private String iconMd5;
|
||||
|
||||
/**
|
||||
* 版本号(同包名内自增,从1开始)
|
||||
*/
|
||||
private Integer version;
|
||||
|
||||
/**
|
||||
* 状态(1当前生效 0历史版本)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 是否锁定(0未锁定 1已锁定,锁定后不可上传覆盖)
|
||||
*/
|
||||
private Integer locked;
|
||||
|
||||
/**
|
||||
* 图标来源(device设备端上报 admin管理端上传)
|
||||
*/
|
||||
private String source;
|
||||
|
||||
/**
|
||||
* 创建人ID
|
||||
*/
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 更新人ID
|
||||
*/
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 逻辑删除(0未删 1已删)
|
||||
*/
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.youlai.boot.device.model.form;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 设备应用操作表单(打开/清除数据/卸载)
|
||||
*
|
||||
* @author TTSTD
|
||||
* @since 2026-08-05
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "设备应用操作Form")
|
||||
public class AppActionForm implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotBlank(message = "应用包名不能为空")
|
||||
@Schema(description = "应用包名")
|
||||
private String packageName;
|
||||
}
|
||||
52
src/main/java/com/youlai/boot/device/model/vo/AppIconVO.java
Normal file
52
src/main/java/com/youlai/boot/device/model/vo/AppIconVO.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package com.youlai.boot.device.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 应用图标VO
|
||||
*
|
||||
* @author TTSTD
|
||||
* @since 2026-08-05
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "应用图标VO")
|
||||
public class AppIconVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "应用包名")
|
||||
private String packageName;
|
||||
|
||||
@Schema(description = "图标访问地址")
|
||||
private String iconUrl;
|
||||
|
||||
@Schema(description = "图标文件MD5")
|
||||
private String iconMd5;
|
||||
|
||||
@Schema(description = "版本号(同包名内自增)")
|
||||
private Integer version;
|
||||
|
||||
@Schema(description = "状态(1当前生效 0历史版本)")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "是否锁定(0未锁定 1已锁定)")
|
||||
private Integer locked;
|
||||
|
||||
@Schema(description = "图标来源(device/admin)")
|
||||
private String source;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.youlai.boot.device.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 设备已安装应用信息 VO(前端展示用)
|
||||
*
|
||||
* @author TTSTD
|
||||
* @since 2026-08-05
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "设备已安装应用信息VO")
|
||||
public class DeviceApkInfoVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "包名")
|
||||
private String packageName;
|
||||
|
||||
@Schema(description = "应用名称")
|
||||
private String appName;
|
||||
|
||||
@Schema(description = "版本名称")
|
||||
private String versionName;
|
||||
|
||||
@Schema(description = "版本号")
|
||||
private Long versionCode;
|
||||
|
||||
@Schema(description = "安装时间")
|
||||
private Date installTime;
|
||||
|
||||
@Schema(description = "最后更新时间")
|
||||
private Date lastUpdateTime;
|
||||
|
||||
@Schema(description = "Apk文件大小")
|
||||
private Long apkSize;
|
||||
|
||||
@Schema(description = "App数据占用空间")
|
||||
private Long dataSize;
|
||||
|
||||
@Schema(description = "App缓存占用空间")
|
||||
private Long cacheSize;
|
||||
|
||||
@Schema(description = "apk MD5")
|
||||
private String md5;
|
||||
|
||||
@Schema(description = "是否为系统应用")
|
||||
private boolean systemApp;
|
||||
|
||||
@Schema(description = "应用图标访问地址")
|
||||
private String iconUrl;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.youlai.boot.device.service;
|
||||
|
||||
import com.youlai.boot.device.model.req.ApkInstallInfoReq;
|
||||
import com.youlai.boot.device.model.vo.DeviceApkInfoVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -8,5 +9,5 @@ public interface ApkInstallService {
|
||||
|
||||
boolean saveOrUpdateDeviceApkInfo(String sn, List<ApkInstallInfoReq> apkInfos);
|
||||
|
||||
List<ApkInstallInfoReq> getDeviceApkInfo(String sn);
|
||||
List<DeviceApkInfoVO> getDeviceApkInfo(String sn);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.youlai.boot.device.service;
|
||||
|
||||
import com.youlai.boot.device.model.vo.AppIconVO;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 应用图标库服务接口
|
||||
*
|
||||
* @author TTSTD
|
||||
* @since 2026-08-05
|
||||
*/
|
||||
public interface AppIconService {
|
||||
|
||||
/**
|
||||
* 获取应用当前生效图标
|
||||
*
|
||||
* @param packageName 应用包名
|
||||
* @return 当前生效图标(无则返回 null)
|
||||
*/
|
||||
AppIconVO getCurrentIcon(String packageName);
|
||||
|
||||
/**
|
||||
* 获取应用全部图标版本(含当前,按版本倒序)
|
||||
*
|
||||
* @param packageName 应用包名
|
||||
* @return 图标版本列表
|
||||
*/
|
||||
List<AppIconVO> getHistory(String packageName);
|
||||
|
||||
/**
|
||||
* 选取历史版本并锁定(选中版本成为当前生效且锁定,原当前版本转为历史)
|
||||
*
|
||||
* @param packageName 应用包名
|
||||
* @param iconId 选中图标记录ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean lockIcon(String packageName, Long iconId);
|
||||
|
||||
/**
|
||||
* 解锁当前生效图标(解锁后可上传新版本覆盖)
|
||||
*
|
||||
* @param packageName 应用包名
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean unlockIcon(String packageName);
|
||||
|
||||
/**
|
||||
* 上传应用图标(全局库:同包名未锁定时新版本替换当前,锁定则拒绝)
|
||||
*
|
||||
* @param packageName 应用包名
|
||||
* @param file 图标文件(jpg/jpeg/png/webp)
|
||||
* @param source 来源(device/admin)
|
||||
* @return 图标访问地址
|
||||
*/
|
||||
String uploadIcon(String packageName, MultipartFile file, String source);
|
||||
|
||||
/**
|
||||
* 批量查询多个应用当前生效图标地址
|
||||
*
|
||||
* @param packageNames 应用包名集合
|
||||
* @return packageName -> iconUrl
|
||||
*/
|
||||
Map<String, String> getActiveIconUrls(Collection<String> packageNames);
|
||||
}
|
||||
@@ -103,6 +103,33 @@ public interface DeviceService extends IService<SnDeviceInfo> {
|
||||
|
||||
boolean setDeviceDeveloper(String sn);
|
||||
|
||||
/**
|
||||
* 打开应用
|
||||
*
|
||||
* @param sn 设备序列号
|
||||
* @param packageName 应用包名
|
||||
* @return 是否推送成功
|
||||
*/
|
||||
boolean launchApp(String sn, String packageName);
|
||||
|
||||
/**
|
||||
* 清除应用数据
|
||||
*
|
||||
* @param sn 设备序列号
|
||||
* @param packageName 应用包名
|
||||
* @return 是否推送成功
|
||||
*/
|
||||
boolean clearAppData(String sn, String packageName);
|
||||
|
||||
/**
|
||||
* 卸载应用
|
||||
*
|
||||
* @param sn 设备序列号
|
||||
* @param packageName 应用包名
|
||||
* @return 是否推送成功
|
||||
*/
|
||||
boolean uninstallApp(String sn, String packageName);
|
||||
|
||||
/**
|
||||
* 新增开发者选项配置
|
||||
*
|
||||
|
||||
@@ -2,8 +2,10 @@ package com.youlai.boot.device.service.impl;
|
||||
|
||||
import com.youlai.boot.device.model.document.ApkInstallDocument;
|
||||
import com.youlai.boot.device.model.req.ApkInstallInfoReq;
|
||||
import com.youlai.boot.device.model.vo.DeviceApkInfoVO;
|
||||
import com.youlai.boot.device.repository.ApkInstallRepository;
|
||||
import com.youlai.boot.device.service.ApkInstallService;
|
||||
import com.youlai.boot.device.service.AppIconService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
@@ -13,6 +15,7 @@ import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -22,6 +25,7 @@ import java.util.stream.Collectors;
|
||||
public class ApkInstallServiceImpl implements ApkInstallService {
|
||||
|
||||
private final ApkInstallRepository apkInstallRepository;
|
||||
private final AppIconService appIconService;
|
||||
|
||||
@Override
|
||||
public boolean saveOrUpdateDeviceApkInfo(String sn, List<ApkInstallInfoReq> apkInfos) {
|
||||
@@ -65,7 +69,7 @@ public class ApkInstallServiceImpl implements ApkInstallService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ApkInstallInfoReq> getDeviceApkInfo(String sn) {
|
||||
public List<DeviceApkInfoVO> getDeviceApkInfo(String sn) {
|
||||
Optional<ApkInstallDocument> documentOpt = apkInstallRepository.findBySn(sn);
|
||||
|
||||
if (!documentOpt.isPresent() || documentOpt.get().getApkList() == null) {
|
||||
@@ -75,19 +79,27 @@ public class ApkInstallServiceImpl implements ApkInstallService {
|
||||
ApkInstallDocument document = documentOpt.get();
|
||||
List<ApkInstallDocument.ApkInfo> apkInfoList = document.getApkList();
|
||||
|
||||
// 批量查询全局图标库中这些应用的当前生效图标
|
||||
List<String> packageNames = apkInfoList.stream()
|
||||
.map(ApkInstallDocument.ApkInfo::getPackageName)
|
||||
.filter(p -> p != null && !p.isBlank())
|
||||
.toList();
|
||||
Map<String, String> activeIconUrls = appIconService.getActiveIconUrls(packageNames);
|
||||
|
||||
return apkInfoList.stream().map(apkInfo -> {
|
||||
ApkInstallInfoReq req = new ApkInstallInfoReq();
|
||||
BeanUtils.copyProperties(apkInfo, req);
|
||||
DeviceApkInfoVO vo = new DeviceApkInfoVO();
|
||||
BeanUtils.copyProperties(apkInfo, vo);
|
||||
vo.setIconUrl(activeIconUrls.get(apkInfo.getPackageName()));
|
||||
|
||||
if (apkInfo.getInstallTime() != null) {
|
||||
req.setInstallTime(Date.from(apkInfo.getInstallTime().atZone(ZoneId.systemDefault()).toInstant()));
|
||||
vo.setInstallTime(Date.from(apkInfo.getInstallTime().atZone(ZoneId.systemDefault()).toInstant()));
|
||||
}
|
||||
|
||||
if (apkInfo.getLastUpdateTime() != null) {
|
||||
req.setLastUpdateTime(Date.from(apkInfo.getLastUpdateTime().atZone(ZoneId.systemDefault()).toInstant()));
|
||||
vo.setLastUpdateTime(Date.from(apkInfo.getLastUpdateTime().atZone(ZoneId.systemDefault()).toInstant()));
|
||||
}
|
||||
|
||||
return req;
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
package com.youlai.boot.device.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.common.config.FilePath;
|
||||
import com.youlai.boot.common.util.HashUtils;
|
||||
import com.youlai.boot.device.mapper.AppIconMapper;
|
||||
import com.youlai.boot.device.model.entity.AppIcon;
|
||||
import com.youlai.boot.device.model.vo.AppIconVO;
|
||||
import com.youlai.boot.device.service.AppIconService;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 应用图标库服务实现类
|
||||
*
|
||||
* @author TTSTD
|
||||
* @since 2026-08-05
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AppIconServiceImpl extends ServiceImpl<AppIconMapper, AppIcon> implements AppIconService {
|
||||
|
||||
/** 状态:当前生效 */
|
||||
private static final int STATUS_ACTIVE = 1;
|
||||
/** 状态:历史版本 */
|
||||
private static final int STATUS_HISTORY = 0;
|
||||
/** 锁定 */
|
||||
private static final int LOCKED = 1;
|
||||
/** 未锁定 */
|
||||
private static final int UNLOCKED = 0;
|
||||
|
||||
/** 允许上传的图标格式 */
|
||||
private static final Set<String> ICON_EXTENSIONS = Set.of("jpg", "jpeg", "png", "webp");
|
||||
|
||||
@Override
|
||||
public AppIconVO getCurrentIcon(String packageName) {
|
||||
AppIcon entity = this.lambdaQuery()
|
||||
.eq(AppIcon::getPackageName, packageName)
|
||||
.eq(AppIcon::getStatus, STATUS_ACTIVE)
|
||||
.one();
|
||||
return entity == null ? null : toVo(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AppIconVO> getHistory(String packageName) {
|
||||
List<AppIcon> entities = this.lambdaQuery()
|
||||
.eq(AppIcon::getPackageName, packageName)
|
||||
.orderByDesc(AppIcon::getVersion)
|
||||
.list();
|
||||
return entities.stream().map(this::toVo).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean lockIcon(String packageName, Long iconId) {
|
||||
Assert.isTrue(packageName != null && packageName.matches("[A-Za-z0-9._]+"), "应用包名不合法");
|
||||
Assert.notNull(iconId, "图标记录ID不能为空");
|
||||
|
||||
AppIcon target = this.getById(iconId);
|
||||
Assert.notNull(target, "图标记录不存在");
|
||||
Assert.isTrue(packageName.equals(target.getPackageName()), "图标记录不属于该应用");
|
||||
|
||||
AppIcon current = getActiveEntity(packageName);
|
||||
if (current != null && current.getId().equals(iconId)) {
|
||||
// 已是当前版本,仅置为锁定
|
||||
if (target.getLocked() == LOCKED) {
|
||||
return true;
|
||||
}
|
||||
target.setLocked(LOCKED);
|
||||
target.setUpdateBy(SecurityUtils.getUserId());
|
||||
return this.updateById(target);
|
||||
}
|
||||
|
||||
// 从历史版本中选取:目标成为当前生效并锁定,原当前版本转为历史
|
||||
if (current != null) {
|
||||
current.setStatus(STATUS_HISTORY);
|
||||
this.updateById(current);
|
||||
}
|
||||
target.setStatus(STATUS_ACTIVE);
|
||||
target.setLocked(LOCKED);
|
||||
target.setUpdateBy(SecurityUtils.getUserId());
|
||||
return this.updateById(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unlockIcon(String packageName) {
|
||||
Assert.isTrue(packageName != null && packageName.matches("[A-Za-z0-9._]+"), "应用包名不合法");
|
||||
|
||||
AppIcon current = getActiveEntity(packageName);
|
||||
Assert.notNull(current, "该应用暂无图标,无法解锁");
|
||||
|
||||
if (current.getLocked() == UNLOCKED) {
|
||||
return true;
|
||||
}
|
||||
current.setLocked(UNLOCKED);
|
||||
current.setUpdateBy(SecurityUtils.getUserId());
|
||||
return this.updateById(current);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadIcon(String packageName, MultipartFile file, String source) {
|
||||
Assert.isTrue(file != null && !file.isEmpty(), "上传文件不能为空");
|
||||
Assert.isTrue(packageName != null && packageName.matches("[A-Za-z0-9._]+"), "应用包名不合法");
|
||||
|
||||
String extension = FilenameUtils.getExtension(file.getOriginalFilename());
|
||||
Assert.isTrue(extension != null && ICON_EXTENSIONS.contains(extension.toLowerCase()),
|
||||
"仅支持 jpg/jpeg/png/webp 格式图标");
|
||||
|
||||
// 计算内容指纹(先算 md5,再做后续判断)
|
||||
String md5;
|
||||
try {
|
||||
md5 = HashUtils.calculateMultipartFileMd5(file);
|
||||
} catch (NoSuchAlgorithmException | IOException e) {
|
||||
log.error("计算图标文件MD5失败, packageName: {}", packageName, e);
|
||||
throw new RuntimeException("计算图标文件MD5失败", e);
|
||||
}
|
||||
|
||||
AppIcon current = getActiveEntity(packageName);
|
||||
|
||||
// 内容未变化:直接返回现有图标地址
|
||||
if (current != null && md5.equals(current.getIconMd5())) {
|
||||
return current.getIconUrl();
|
||||
}
|
||||
|
||||
// 已锁定:拒绝覆盖(规避三方 App 换图标带广告)
|
||||
if (current != null && current.getLocked() == LOCKED) {
|
||||
throw new RuntimeException("图标已锁定,不可修改,请先解锁");
|
||||
}
|
||||
|
||||
// 保存文件(内容寻址 {md5}.{ext},文件已存在则复用)
|
||||
String iconUrl = saveIconFile(md5, extension.toLowerCase(), file);
|
||||
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
if (current != null) {
|
||||
// 原当前版本转为历史
|
||||
current.setStatus(STATUS_HISTORY);
|
||||
current.setUpdateBy(userId);
|
||||
this.updateById(current);
|
||||
|
||||
AppIcon next = new AppIcon();
|
||||
next.setPackageName(packageName);
|
||||
next.setIconUrl(iconUrl);
|
||||
next.setIconMd5(md5);
|
||||
next.setVersion(current.getVersion() + 1);
|
||||
next.setStatus(STATUS_ACTIVE);
|
||||
next.setLocked(UNLOCKED);
|
||||
next.setSource(source);
|
||||
next.setCreateBy(userId);
|
||||
next.setUpdateBy(userId);
|
||||
this.save(next);
|
||||
} else {
|
||||
AppIcon icon = new AppIcon();
|
||||
icon.setPackageName(packageName);
|
||||
icon.setIconUrl(iconUrl);
|
||||
icon.setIconMd5(md5);
|
||||
icon.setVersion(1);
|
||||
icon.setStatus(STATUS_ACTIVE);
|
||||
icon.setLocked(UNLOCKED);
|
||||
icon.setSource(source);
|
||||
icon.setCreateBy(userId);
|
||||
icon.setUpdateBy(userId);
|
||||
this.save(icon);
|
||||
}
|
||||
|
||||
log.info("应用图标上传成功, packageName: {}, md5: {}, iconUrl: {}", packageName, md5, iconUrl);
|
||||
return iconUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getActiveIconUrls(Collection<String> packageNames) {
|
||||
if (packageNames == null || packageNames.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<AppIcon> activeIcons = this.lambdaQuery()
|
||||
.in(AppIcon::getPackageName, packageNames)
|
||||
.eq(AppIcon::getStatus, STATUS_ACTIVE)
|
||||
.list();
|
||||
Map<String, String> urlMap = new HashMap<>();
|
||||
for (AppIcon icon : activeIcons) {
|
||||
urlMap.put(icon.getPackageName(), icon.getIconUrl());
|
||||
}
|
||||
return urlMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前生效的图标记录
|
||||
*/
|
||||
private AppIcon getActiveEntity(String packageName) {
|
||||
return this.lambdaQuery()
|
||||
.eq(AppIcon::getPackageName, packageName)
|
||||
.eq(AppIcon::getStatus, STATUS_ACTIVE)
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图标文件(内容寻址 {md5}.{ext},文件已存在则复用)
|
||||
*/
|
||||
private String saveIconFile(String md5, String extension, MultipartFile file) {
|
||||
String iconDirPath = FilePath.getApkIconPath();
|
||||
File iconDir = new File(iconDirPath);
|
||||
if (!iconDir.exists() && !iconDir.mkdirs()) {
|
||||
throw new RuntimeException("创建图标目录失败: " + iconDirPath);
|
||||
}
|
||||
|
||||
String fileName = md5 + "." + extension;
|
||||
File destFile = new File(iconDir, fileName);
|
||||
if (destFile.exists()) {
|
||||
return "/api/v1/device/app_icon/" + fileName;
|
||||
}
|
||||
|
||||
try {
|
||||
file.transferTo(destFile);
|
||||
} catch (IOException e) {
|
||||
log.error("保存图标文件失败, fileName: {}", fileName, e);
|
||||
throw new RuntimeException("保存应用图标失败");
|
||||
}
|
||||
return "/api/v1/device/app_icon/" + fileName;
|
||||
}
|
||||
|
||||
private AppIconVO toVo(AppIcon entity) {
|
||||
AppIconVO vo = new AppIconVO();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@@ -291,6 +291,65 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, SnDeviceInfo> i
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建携带应用包名的推送参数
|
||||
*/
|
||||
private PushSendParam getAppPushSendParam(String sn, String type, String title, String content, String packageName) {
|
||||
PushSendParam pushSendParam = getSinglePushSendParam(sn, type, title, content);
|
||||
Map<String, Object> extras = new HashMap<>();
|
||||
extras.put("package_name", packageName);
|
||||
pushSendParam.getCustom().setExtras(extras);
|
||||
return pushSendParam;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean launchApp(String sn, String packageName) {
|
||||
PushSendParam pushSendParam = getAppPushSendParam(sn, "8", "appLaunch", "launch", packageName);
|
||||
try {
|
||||
PushSendResult result = pushApi.send(pushSendParam);
|
||||
log.info("send success:{}", result);
|
||||
return true;
|
||||
} catch (ApiErrorException e) {
|
||||
int httpStatus = e.getStats();
|
||||
int errorCode = e.getApiError().getError().getCode();
|
||||
String errorMessage = e.getApiError().getError().getMessage();
|
||||
log.error("launchApp send error, httpStatus:{} code:{}, message:{}", httpStatus, errorCode, errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clearAppData(String sn, String packageName) {
|
||||
PushSendParam pushSendParam = getAppPushSendParam(sn, "9", "appClearData", "clearData", packageName);
|
||||
try {
|
||||
PushSendResult result = pushApi.send(pushSendParam);
|
||||
log.info("send success:{}", result);
|
||||
return true;
|
||||
} catch (ApiErrorException e) {
|
||||
int httpStatus = e.getStats();
|
||||
int errorCode = e.getApiError().getError().getCode();
|
||||
String errorMessage = e.getApiError().getError().getMessage();
|
||||
log.error("clearAppData send error, httpStatus:{} code:{}, message:{}", httpStatus, errorCode, errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean uninstallApp(String sn, String packageName) {
|
||||
PushSendParam pushSendParam = getAppPushSendParam(sn, "10", "appUninstall", "uninstall", packageName);
|
||||
try {
|
||||
PushSendResult result = pushApi.send(pushSendParam);
|
||||
log.info("send success:{}", result);
|
||||
return true;
|
||||
} catch (ApiErrorException e) {
|
||||
int httpStatus = e.getStats();
|
||||
int errorCode = e.getApiError().getError().getCode();
|
||||
String errorMessage = e.getApiError().getError().getMessage();
|
||||
log.error("uninstallApp send error, httpStatus:{} code:{}, message:{}", httpStatus, errorCode, errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addDeveloperConfig(String sn, Integer developerOptions) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user