This commit is contained in:
2025-04-07 21:57:17 +08:00
parent d5445fdc3e
commit f4a5b789db
29 changed files with 1752 additions and 3 deletions

View File

@@ -1,9 +1,16 @@
package com.mir4updater.backend;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@MapperScan({"com.mir4updater.backend.mapper", "com.mir4updater.backend.api"})
@EnableCaching
public class Application {
public static void main(String[] args) {

View File

@@ -0,0 +1,45 @@
package com.mir4updater.backend.controller;
import com.mir4updater.backend.result.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
//引入 redis
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/")
public Result getMethodName() {
return Result.ok();
}
/**
* 存储
*
* @return
*/
@PostMapping("/set")
public String setRedis(@RequestParam(value = "username") String username) {
//存储 key-value 键值对: "username"-"jaychou"
stringRedisTemplate.opsForValue().set("username", username);
return "redis 存储成功!";
}
/**
* 读取
*
* @return
*/
@GetMapping("/get")
public String getRedis() {
//通过 key 值读取 value
String result = stringRedisTemplate.opsForValue().get("username");
return result;
}
}

View File

@@ -0,0 +1,54 @@
package com.mir4updater.backend.controller.apk;
import com.mir4updater.backend.entity.ApkInfo;
import com.mir4updater.backend.result.Result;
import com.mir4updater.backend.service.ApkInfoService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
@RestController
public class CheckUpdateController {
public static Logger logger = LogManager.getLogger(UploadApkController.class);
@Autowired
private ApkInfoService apkInfoService;
@GetMapping("/android/check_update")
public Result checkUpdate(@RequestParam("pkg") String pkg, @RequestParam(value = "version_code", required = false, defaultValue = "0") Long versionCode) {
List<ApkInfo> apkInfoList = apkInfoService.getApkInfo(pkg);
Optional<ApkInfo> apkInfoOptional = apkInfoList.stream().max(new Comparator<ApkInfo>() {
@Override
public int compare(ApkInfo o1, ApkInfo o2) {
return Long.compare(o1.getVersionCode(), o2.getVersionCode());
}
});
if (apkInfoOptional.isPresent()) {
if (versionCode == 0) {
return Result.ok().data("apkInfo", apkInfoOptional.get());
} else {
if (apkInfoOptional.get().getVersionCode() > versionCode) {
return Result.ok().data("apkInfo", apkInfoOptional.get());
} else {
return Result.ok().message("没有更新");
}
}
} else {
return Result.notFound();
}
}
@GetMapping("/android/get_all")
public Result getAll() {
List<ApkInfo> apkInfoList = apkInfoService.getAll();
return Result.ok().data("apkInfo", apkInfoList);
}
}

View File

@@ -0,0 +1,265 @@
package com.mir4updater.backend.controller.apk;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mir4updater.backend.entity.ApkInfo;
import com.mir4updater.backend.entity.XapkManifest;
import com.mir4updater.backend.result.Result;
import com.mir4updater.backend.service.ApkInfoService;
import com.mir4updater.backend.utils.FileUtils;
import com.mir4updater.backend.utils.HashUtils;
import net.dongliu.apk.parser.ApkFile;
import net.dongliu.apk.parser.bean.ApkMeta;
import net.dongliu.apk.parser.bean.IconFace;
import net.dongliu.apk.parser.bean.UseFeature;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tomcat.util.http.fileupload.FileUploadException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
@RestController
public class UploadApkController {
public static Logger logger = LogManager.getLogger(UploadApkController.class);
@Autowired
private ApkInfoService apkInfoService;
private static final String UPLOAD_PATH = "uploadApk";
private static final String ApkFilePath = "file";
private static final String ICON_PATH = "icon";
@PostMapping("/android/upload_apk")
public Result uploadApk(@RequestParam("file") MultipartFile multipartFile) throws Exception {
if (multipartFile != null && !multipartFile.isEmpty()) {
String projectPath = System.getProperty("user.dir");
logger.info("当前项目路径为:" + projectPath);
String dirPath = projectPath + File.separator + UPLOAD_PATH + File.separator + ApkFilePath;
String iconPath = projectPath + File.separator + UPLOAD_PATH + File.separator + ICON_PATH + File.separator;
File dirFile = new File(dirPath);
if (!dirFile.exists()) {
dirFile.mkdirs();
}
File iconFilePath = new File(iconPath);
if (!iconFilePath.exists()) {
iconFilePath.mkdirs();
}
//1.接收上传的应用,写入到硬盘
String originalFilename = multipartFile.getOriginalFilename();
File file = new File(dirPath + File.separator + originalFilename);
try {
multipartFile.transferTo(file);
} catch (FileUploadException e) {
logger.error(e.getMessage());
}
if (!isAPK(file) && !isXAPK(file)) {
return Result.error().message("请上传apk或xapk文件");
}
String md5 = HashUtils.getFileMD5(file);
logger.info("file md5 = " + md5);
String sha1 = HashUtils.calculateSHA1(file);
logger.info("file sha1 = " + sha1);
String sha256 = HashUtils.calculateSHA256(file);
logger.info("file sha256 = " + sha256);
File iconFile = new File(iconPath + md5 + ".png");
if (isAPK(file)) {
logger.info("file type is apk");
//调用工具类解析apk
//analysisAPK(dirFile.getAbsolutePath(), file.getAbsolutePath());
ApkFile apkFile = new ApkFile(file);
ApkMeta apkMeta = apkFile.getApkMeta();
logger.info(apkMeta.getLabel());
logger.info(apkMeta.getPackageName());
logger.info(apkMeta.getVersionCode());
logger.info(apkMeta.getVersionName());
for (UseFeature feature : apkMeta.getUsesFeatures()) {
logger.info(feature.getName());
}
ApkInfo apkInfo = new ApkInfo();
apkInfo.setAppName(apkMeta.getLabel());
apkInfo.setPackageName(apkMeta.getPackageName());
apkInfo.setVersionCode(apkMeta.getVersionCode());
apkInfo.setVersionName(apkMeta.getVersionName());
apkInfo.setSha1(sha1);
apkInfo.setFilePath(file.getPath());
apkInfo.setIconPath(iconFile.getPath());
apkInfo.setSize(file.length());
apkInfo.setXapk(false);
try {
apkInfoService.addApk(apkInfo);
List<IconFace> allIcons = apkFile.getAllIcons();
Optional<IconFace> maxNumber = allIcons.stream().filter(iconFace -> iconFace.getPath().endsWith(".png"))
.max(Comparator.comparingInt(o -> o.getData().length));
if (maxNumber.isPresent()) {
logger.info(maxNumber.get().getPath());
FileUtils.byteToFile(maxNumber.get().getData(), iconFile);
}
} catch (DataIntegrityViolationException e) {
e.printStackTrace();
logger.info(e.getMessage());
return Result.error().message("apk文件无效");
}
return Result.ok().message("上传成功");
} else if (isXAPK(file)) {
logger.info("file type is xapk");
try (ZipFile zipFile = new ZipFile(file)) {
// 遍历ZIP文件中的所有条目
zipFile.stream().forEach(zipEntry -> {
logger.info(zipEntry.getName());
});
Optional<? extends ZipEntry> manifestOptional = zipFile.stream().filter(zipEntry -> "manifest.json".equals(zipEntry.getName())).findFirst();
if (manifestOptional.isPresent()) {
ZipEntry manifestZipEntry = manifestOptional.get();
try {
InputStream manifestInputStream = zipFile.getInputStream(manifestZipEntry);
// 读取文件内容
String content = new String(manifestInputStream.readAllBytes(), StandardCharsets.UTF_8);
// 创建ObjectMapper实例
ObjectMapper objectMapper = new ObjectMapper();
// 将Java对象转换为JSON字符串
String jsonString = objectMapper.writeValueAsString(content);
XapkManifest xapkManifest = objectMapper.readValue(content, XapkManifest.class);
// logger.info(xapkManifest);
logger.info(xapkManifest.getName());
logger.info(xapkManifest.getPackage_name());
logger.info(xapkManifest.getVersion_code());
logger.info(xapkManifest.getVersion_name());
ApkInfo apkInfo = new ApkInfo();
apkInfo.setAppName(xapkManifest.getName());
apkInfo.setPackageName(xapkManifest.getPackage_name());
apkInfo.setVersionCode(xapkManifest.getVersion_code());
apkInfo.setVersionName(xapkManifest.getVersion_name());
apkInfo.setSha1(sha1);
apkInfo.setFilePath(file.getPath());
apkInfo.setIconPath(iconFile.getPath());
apkInfo.setSize(file.length());
apkInfo.setXapk(true);
apkInfo.setXapk_version(xapkManifest.getXapk_version());
try {
apkInfoService.addApk(apkInfo);
String iconName = xapkManifest.getIcon();
Optional<? extends ZipEntry> iconNameOptional = zipFile.stream().filter(zipEntry -> iconName.equals(zipEntry.getName())).findFirst();
if (iconNameOptional.isPresent()) {
ZipEntry iconZipEntry = iconNameOptional.get();
InputStream iconInputStream = zipFile.getInputStream(iconZipEntry);
FileUtils.byteToFile(iconInputStream.readAllBytes(), iconFile);
}
} catch (DataIntegrityViolationException e) {
logger.info(e.getMessage());
// 手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return Result.error().message("xapk文件重复");
}
return Result.ok().message("上传成功");
} catch (IOException e) {
e.printStackTrace();
System.err.println("读取文件失败: " + manifestZipEntry.getName());
return Result.error().message("读取文件失败");
}
} else {
return Result.error().message("读取xapk配置失败");
}
} catch (IOException e) {
e.printStackTrace();
System.err.println("打开ZIP文件失败: " + e.getMessage());
return Result.error().message("打开ZIP文件失败");
}
}
} else {
return Result.error().message("文件为空");
}
return Result.error().message("未知错误");
}
// 判断是否为APK
public static boolean isAPK(File file) {
// 检查扩展名是否为.apk
if (file.getName().toLowerCase().endsWith(".apk")) {
// 进一步验证是否为有效的APK文件
try (ZipFile zipFile = new ZipFile(file)) {
return zipFile.getEntry("AndroidManifest.xml") != null;
} catch (IOException e) {
return false;
}
}
return false;
}
// 判断是否为XAPK
public static boolean isXAPK(File file) {
// 检查扩展名是否为.xapk
if (file.getName().toLowerCase().endsWith(".xapk")) {
return true; // 扩展名为.xapk通常直接视为XAPK
}
// 若扩展名不明确,检查内容结构
try (ZipFile zipFile = new ZipFile(file)) {
boolean hasJson = zipFile.getEntry("manifest.json") != null;
boolean hasAPK = false;
// boolean hasOBB = false;
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
// 检查是否包含APK文件
if (name.endsWith(".apk")) {
hasAPK = true;
}
// 检查是否包含OBB目录
// if (name.startsWith("Android/obb/")) {
// hasOBB = true;
// }
}
// XAPK通常包含至少一个APK或OBB数据
return hasAPK || hasJson;
} catch (IOException e) {
return false;
}
}
private void analysisAPK(String dirPath, String filePath) throws Exception {
logger.info(filePath);
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("aapt d --values badging " + filePath, null, new File(dirPath));
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
inputStream.close();
String strOutput = sb.toString();
logger.info(strOutput);
}
}

View File

@@ -0,0 +1,73 @@
package com.mir4updater.backend.controller.apk;
import com.mir4updater.backend.dto.PakFileInfo;
import com.mir4updater.backend.result.Result;
import com.mir4updater.backend.utils.HashUtils;
import jakarta.validation.Valid;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.InputStream;
import java.util.Collections;
@RestController
public class UploadPakFileController {
public static Logger logger = LogManager.getLogger(UploadApkController.class);
private static final String UPLOAD_PATH = "uploadPak";
static {
String projectPath = System.getProperty("user.dir");
String dirPath = projectPath + File.separator + UPLOAD_PATH + File.separator;
File dirFile = new File(dirPath);
if (!dirFile.exists()) {
logger.info("mkdir " + dirFile.getPath() + "\t" + dirFile.mkdirs());
}
}
@Autowired
private StringRedisTemplate stringRedisTemplate;
@PostMapping(path = "/android/upload_pak_file")
public Result uploadPakFile(@Valid @ModelAttribute PakFileInfo pakFileInfo) throws Exception {
String jsonUuid = pakFileInfo.json_uuid();
if (!Boolean.TRUE.equals(stringRedisTemplate.hasKey("data:" + Collections.singletonMap("uuid", jsonUuid)))) {
return Result.error().message("json_uuid不存在");
}
InputStream inputStream = pakFileInfo.file().getInputStream();
String sha1 = HashUtils.calculateSHA1(inputStream);
logger.info("origin_sha1 = " + pakFileInfo.sha1());
logger.info("sha1 = " + sha1);
if (!sha1.equalsIgnoreCase(pakFileInfo.sha1())) {
return Result.error().message("sha1不匹配");
}
return Result.ok();
}
@PostMapping("/android/upload_pak_file_old")
public Result uploadPakFile(@RequestParam("file") MultipartFile multipartFile, @RequestParam("name") String name,
@RequestParam("path") String path, @RequestParam("sha1") String origin_sha1,
@RequestParam("json_id") String jsonId) throws Exception {
// if (TextUtils.isEmpty(jsonId)){
// return Result.error().message("json_id不能为空");
// }
InputStream inputStream = multipartFile.getInputStream();
String sha1 = HashUtils.calculateSHA1(inputStream);
logger.info("origin_sha1 = " + origin_sha1);
logger.info("sha1 = " + sha1);
if (!sha1.equalsIgnoreCase(origin_sha1)) {
return Result.error().message("sha1不匹配");
}
return Result.ok();
}
}

View File

@@ -0,0 +1,28 @@
package com.mir4updater.backend.controller.apk;
import com.mir4updater.backend.dto.PakInfoJsonRequest;
import com.mir4updater.backend.result.Result;
import com.mir4updater.backend.service.PakInfoService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
@RestController
@RequestMapping("/android/upload_pak_info")
public class UploadPakInfoController {
@Autowired
private PakInfoService pakInfoService;
@PostMapping
public Result createData(@Valid @RequestBody PakInfoJsonRequest request) {
String id = pakInfoService.processData(request);
return Result.ok().data(Collections.singletonMap("uuid", id));
}
}

View File

@@ -0,0 +1,19 @@
package com.mir4updater.backend.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.web.multipart.MultipartFile;
public record PakFileInfo(
@NotNull(message = "文件不能为空")
MultipartFile file,
@NotBlank(message = "name不能为空")
String name,
@NotBlank(message = "path不能为空")
String path,
@NotBlank(message = "sha1不能为空")
String sha1,
@NotBlank(message = "uuid不能为空")
String json_uuid
) {
}

View File

@@ -0,0 +1,8 @@
package com.mir4updater.backend.dto;
import com.fasterxml.jackson.databind.node.ObjectNode;
public record PakInfoJsonRequest(
ObjectNode json
) {
}

View File

@@ -0,0 +1,136 @@
package com.mir4updater.backend.entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@Data
@Entity
@Table(name = "apk_info")
@EqualsAndHashCode
public class ApkInfo implements Serializable {
@Embedded
private ApkInfoPK id; // 复合主键
@Id
@Column(name = "app_id", nullable = false, length = 20)
@GeneratedValue(strategy = GenerationType.IDENTITY) // 主键自增
int appId;
@Column(name = "app_name", nullable = false, length = 128)
String appName;
@Column(name = "package_name", nullable = false)
String packageName;
@Column(name = "version_code", nullable = false, length = 11)
long versionCode;
@Column(name = "version_name", nullable = false)
String versionName;
@Column(name = "sha1", nullable = false, length = 160)
String sha1;
@Column(name = "file_path", nullable = false)
String filePath;
@Column(name = "icon_path", nullable = false)
String iconPath;
@Column(name = "size", nullable = false, length = 20)
long size;
@Column(name = "is_xapk", columnDefinition = "bit(1) default 0")
boolean xapk = false;
@Column(name = "xapk_version", length = 11)
int xapk_version;
public ApkInfo() {
}
public String getAppName() {
return appName;
}
public void setAppName(String appName) {
this.appName = appName;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public long getVersionCode() {
return versionCode;
}
public void setVersionCode(long versionCode) {
this.versionCode = versionCode;
}
public String getVersionName() {
return versionName;
}
public void setVersionName(String versionName) {
this.versionName = versionName;
}
public String getSha1() {
return sha1;
}
public void setSha1(String sha1) {
this.sha1 = sha1;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getIconPath() {
return iconPath;
}
public void setIconPath(String iconPath) {
this.iconPath = iconPath;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
public boolean isXapk() {
return xapk;
}
public void setXapk(boolean xapk) {
this.xapk = xapk;
}
public int getXapk_version() {
return xapk_version;
}
public void setXapk_version(int xapk_version) {
this.xapk_version = xapk_version;
}
}

View File

@@ -0,0 +1,56 @@
package com.mir4updater.backend.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import java.io.Serializable;
// 使用 @EmbeddedId
@Embeddable
public class ApkInfoPK implements Serializable {
@Column(insertable = false, updatable = false)
String sha1;
@Column(name = "package_name", insertable = false, updatable = false)
String packageName;
@Column(name = "version_code", insertable = false, updatable = false)
String versionCode;
// getters, setters, equals, hashCode
public ApkInfoPK() {
}
public ApkInfoPK(String sha1, String packageName, String version_code) {
this.sha1 = sha1;
this.packageName = packageName;
this.versionCode = version_code;
}
public String getSha1() {
return sha1;
}
public void setSha1(String sha1) {
this.sha1 = sha1;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public String getVersionCode() {
return versionCode;
}
public void setVersionCode(String version_code) {
this.versionCode = version_code;
}
}

View File

@@ -0,0 +1,43 @@
package com.mir4updater.backend.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.annotations.UuidGenerator;
import org.hibernate.type.SqlTypes;
import java.io.Serializable;
import java.util.Map;
import java.util.UUID;
@Entity
public class PakInfo implements Serializable {
@Id
@UuidGenerator(style = UuidGenerator.Style.TIME)
private UUID uuid;
@JdbcTypeCode(SqlTypes.JSON)
@Column(nullable = false, columnDefinition = "JSON")
private Map<String, Object> json;
public PakInfo() {
}
public UUID getId() {
return uuid;
}
public void setId(UUID id) {
this.uuid = id;
}
public Map<String, Object> getJson() {
return json;
}
public void setJson(Map<String, Object> json) {
this.json = json;
}
}

View File

@@ -0,0 +1,57 @@
package com.mir4updater.backend.entity;
import java.io.Serial;
import java.io.Serializable;
public class UserInfo implements Serializable {
@Serial
private static final long serialVersionUID = 4389921857866822592L;
long user_id;
String user_name;
String user_phone;
String salt;
String regist_time;
public long getUser_id() {
return user_id;
}
public void setUser_id(long user_id) {
this.user_id = user_id;
}
public String getUser_name() {
return user_name;
}
public void setUser_name(String user_name) {
this.user_name = user_name;
}
public String getUser_phone() {
return user_phone;
}
public void setUser_phone(String user_phone) {
this.user_phone = user_phone;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public String getRegist_time() {
return regist_time;
}
public void setRegist_time(String regist_time) {
this.regist_time = regist_time;
}
}

View File

@@ -0,0 +1,149 @@
package com.mir4updater.backend.entity;
import com.google.gson.Gson;
import com.google.gson.JsonParser;
import java.io.Serializable;
import java.util.List;
public class XapkManifest implements Serializable {
public int xapk_version;
public String package_name;
public String name;
public long version_code;
public String version_name;
public String min_sdk_version;
public String target_sdk_version;
public List<String> permissions;
public List<String> split_configs;
public long total_size;
public String icon;
public List<SplitApk> split_apks;
public static class SplitApk {
public String file;
public String id;
}
public XapkManifest() {
}
public XapkManifest(int xapk_version, String package_name, String name, long version_code, String version_name,
String min_sdk_version, String target_sdk_version, List<String> permissions, List<String> split_configs,
long total_size, String icon, List<SplitApk> split_apks) {
this.xapk_version = xapk_version;
this.package_name = package_name;
this.name = name;
this.version_code = version_code;
this.version_name = version_name;
this.min_sdk_version = min_sdk_version;
this.target_sdk_version = target_sdk_version;
this.permissions = permissions;
this.split_configs = split_configs;
this.total_size = total_size;
this.icon = icon;
this.split_apks = split_apks;
}
public int getXapk_version() {
return xapk_version;
}
public void setXapk_version(int xapk_version) {
this.xapk_version = xapk_version;
}
public String getPackage_name() {
return package_name;
}
public void setPackage_name(String package_name) {
this.package_name = package_name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getVersion_code() {
return version_code;
}
public void setVersion_code(long version_code) {
this.version_code = version_code;
}
public String getVersion_name() {
return version_name;
}
public void setVersion_name(String version_name) {
this.version_name = version_name;
}
public String getMin_sdk_version() {
return min_sdk_version;
}
public void setMin_sdk_version(String min_sdk_version) {
this.min_sdk_version = min_sdk_version;
}
public String getTarget_sdk_version() {
return target_sdk_version;
}
public void setTarget_sdk_version(String target_sdk_version) {
this.target_sdk_version = target_sdk_version;
}
public List<String> getPermissions() {
return permissions;
}
public void setPermissions(List<String> permissions) {
this.permissions = permissions;
}
public List<String> getSplit_configs() {
return split_configs;
}
public void setSplit_configs(List<String> split_configs) {
this.split_configs = split_configs;
}
public long getTotal_size() {
return total_size;
}
public void setTotal_size(long total_size) {
this.total_size = total_size;
}
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
public List<SplitApk> getSplit_apks() {
return split_apks;
}
public void setSplit_apks(List<SplitApk> split_apks) {
this.split_apks = split_apks;
}
@Override
public String toString() {
return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
}
}

View File

@@ -0,0 +1,28 @@
package com.mir4updater.backend.redis;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key序列化为字符串
template.setKeySerializer(new StringRedisSerializer());
// Value序列化为JSON
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 对于Hash结构根据需要配置
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}

View File

@@ -0,0 +1,13 @@
package com.mir4updater.backend.repository;
import com.mir4updater.backend.entity.ApkInfo;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ApkInfoRepository extends JpaRepository<ApkInfo, Long> {
// 自定义查询方法(可选)
List<ApkInfo> findApkInfosByPackageName(String pkg);
List<ApkInfo> findAll();
}

View File

@@ -0,0 +1,7 @@
package com.mir4updater.backend.repository;
import com.mir4updater.backend.entity.PakInfo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PakInfoRepository extends JpaRepository<PakInfo, Long> {
}

View File

@@ -0,0 +1,60 @@
package com.mir4updater.backend.result;
/**
* 接口返回工具类
*/
public class JsonData {
private int code;
private Object data;
private String msg;
public JsonData() {
}
public JsonData(int code, Object data) {
this.code = code;
this.data = data;
}
public JsonData(int code, Object data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}
public static JsonData buildSuccess(Object data) {
return new JsonData(0, data);
}
public static JsonData buildError(String msg) {
return new JsonData(-1, "", msg);
}
public static JsonData buildError(int code, String msg) {
return new JsonData(code, "", msg);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

View File

@@ -0,0 +1,129 @@
package com.mir4updater.backend.result;
import java.util.HashMap;
import java.util.Map;
/**
* 统一返回格式类
*
* @author 爷爷的茶七里香
* @date 2022/05/30
*/
public class Result {
/**
* 是否成功
*/
private Boolean success;
/**
* 状态码
*/
private Integer code;
/**
* 返回的消息
*/
private String message;
/**
* 放置响应的数据
*/
private Map<String, Object> data = new HashMap<>();
public Result() {
}
/** 以下是定义一些常用到的格式,可以看到调用了我们创建的枚举类 */
public static Result ok() {
Result r = new Result();
r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
return r;
}
public static Result error() {
Result r = new Result();
r.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getSuccess());
r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode());
r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage());
return r;
}
public static Result notFound() {
Result r = new Result();
r.setSuccess(ResultCodeEnum.NOT_FOUND.getSuccess());
r.setCode(ResultCodeEnum.NOT_FOUND.getCode());
r.setMessage(ResultCodeEnum.NOT_FOUND.getMessage());
return r;
}
public static Result setResult(ResultCodeEnum resultCodeEnum) {
Result r = new Result();
r.setSuccess(resultCodeEnum.getSuccess());
r.setCode(resultCodeEnum.getCode());
r.setMessage(resultCodeEnum.getMessage());
return r;
}
public Result success(Boolean success) {
this.setSuccess(success);
return this;
}
public Result message(String message) {
this.setMessage(message);
return this;
}
public Result code(Integer code) {
this.setCode(code);
return this;
}
public Result data(String key, Object value) {
this.data.put(key, value);
return this;
}
public Result data(Map<String, Object> map) {
this.setData(map);
return this;
}
/** 以下是get/set方法如果项目有集成lombok可以使用@Data注解代替 */
public Boolean getSuccess() {
return success;
}
public void setSuccess(Boolean success) {
this.success = success;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Map<String, Object> getData() {
return data;
}
public void setData(Map<String, Object> data) {
this.data = data;
}
}

View File

@@ -0,0 +1,45 @@
package com.mir4updater.backend.result;
/**
* 状态码
*
* @author 爷爷的茶七里香
* @date 2022/05/30
*/
public enum ResultCodeEnum {
SUCCESS(true, 20000, "成功"),
UNKNOWN_REASON(false, 20001, "未知错误"),
NOT_FOUND(true, 20004, "没有数据");
private final Boolean success;
private final Integer code;
private final String message;
ResultCodeEnum(Boolean success, Integer code, String message) {
this.success = success;
this.code = code;
this.message = message;
}
public Boolean getSuccess() {
return success;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
@Override
public String toString() {
return "ResultCodeEnum{" + "success=" + success + ", code=" + code + ", message='" + message + '\'' + '}';
}
}

View File

@@ -0,0 +1,27 @@
package com.mir4updater.backend.service;
import com.mir4updater.backend.entity.ApkInfo;
import com.mir4updater.backend.repository.ApkInfoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ApkInfoService {
@Autowired
private ApkInfoRepository apkInfoRepository;
public void addApk(ApkInfo user) {
apkInfoRepository.save(user);
}
public List<ApkInfo> getApkInfo(String pkg) {
return apkInfoRepository.findApkInfosByPackageName(pkg);
}
public List<ApkInfo> getAll() {
return apkInfoRepository.findAll();
}
}

View File

@@ -0,0 +1,49 @@
package com.mir4updater.backend.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.mir4updater.backend.dto.PakInfoJsonRequest;
import com.mir4updater.backend.entity.PakInfo;
import com.mir4updater.backend.repository.PakInfoRepository;
import jakarta.transaction.Transactional;
import jakarta.validation.ValidationException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.Map;
@Service
public class PakInfoService {
private final PakInfoRepository pakInfoRepository;
private final RedisTemplate<String, Object> redisTemplate;
public PakInfoService(PakInfoRepository pakInfoRepository, RedisTemplate<String, Object> redisTemplate) {
this.pakInfoRepository = pakInfoRepository;
this.redisTemplate = redisTemplate;
}
@Transactional
public String processData(PakInfoJsonRequest request) {
// 创建实体
PakInfo entity = new PakInfo();
entity.setJson(convertToMap(request.json()));
// 保存到数据库
pakInfoRepository.save(entity);
// 保存到Redis有效期1小时
redisTemplate.opsForValue().set(
"data:" + Collections.singletonMap("uuid", entity.getId()),
request.json()
);
return entity.getId().toString();
}
private Map<String, Object> convertToMap(ObjectNode node) {
try {
return new ObjectMapper().convertValue(node, new TypeReference<>() {});
} catch (IllegalArgumentException e) {
throw new ValidationException("Invalid JSON structure");
}
}
}

View File

@@ -0,0 +1,66 @@
package com.mir4updater.backend.utils;
import jakarta.servlet.http.HttpServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
//生成验证码并将验证码存入session中以ip为key
public class CheckCodeUtils {
public static BufferedImage getCheckCode(HttpServletRequest request, String ip) {
int width = 60, height = 30;
//创建一个图像宽60 高30
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandomColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
g.setColor(getRandomColor(160, 200));
//干扰线生成
for (int i = 0; i < 10; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
String strCode = "";
for (int i = 0; i < 4; i++) {
String strNumber = String.valueOf(random.nextInt(10));
strCode = strCode + strNumber;
//设置字体颜色
g.drawString(strNumber, 13 * i + 6, 20);
}
System.out.println("当前ip" + ip);
System.out.println("当前验证码" + strCode);
request.getSession().setAttribute(ip, strCode);
g.dispose();
return image;
}
/**
* 随机获取颜色的方法
*
* @return
*/
public static Color getRandomColor(int fc, int bc) {
Random random = new Random();
Color reandomColor = null;
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
reandomColor = new Color(r, g, b);
return reandomColor;
}
}

View File

@@ -0,0 +1,190 @@
package com.mir4updater.backend.utils;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.OutputStream;
import java.util.Random;
/**
* 生成 数字+字母 验证码图片
*/
public class CreateImageCode {
//图片的宽度
private int width = 160;
//图片的高度
private int height = 40;
//验证码字符个数
private int codeCount = 4;
//验证码干扰线数
private int lineCount = 20;
//验证码
private String code = null;
//验证码图片buffer
private BufferedImage buffImg = null;
//创建一个生成随机数对象
Random random = new Random();
//构造函数
public CreateImageCode() {
creatImage();
}
public CreateImageCode(int width, int height) {
this.width = width;
this.height = height;
creatImage();
}
public CreateImageCode(int width, int height, int codeCount) {
this.width = width;
this.height = height;
this.codeCount = codeCount;
creatImage();
}
public CreateImageCode(int width, int height, int codeCount, int lineCount) {
this.width = width;
this.height = height;
this.codeCount = codeCount;
this.lineCount = lineCount;
creatImage();
}
//生成图片
private void creatImage() {
//字体的宽度
int fontWidth = width / codeCount;
//字体的高度
int fontHeight = height - 5;
int codeY = height - 8;
//图像buff
buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = buffImg.getGraphics();
//设置背景色
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
//设置字体
Font font = new Font("Fixedsys", Font.BOLD, fontHeight);
g.setFont(font);
//设置干扰线
for (int i = 0; i < lineCount; i++) {
int xs = random.nextInt(width);
int ys = random.nextInt(height);
int xe = xs + random.nextInt(width);
int ye = ys + random.nextInt(height);
g.setColor(getRandColor(1, 255));
g.drawLine(xs, ys, xe, ye);
}
//添加噪点
float yawpRate = 0.01f;//噪声率
int area = (int) (yawpRate * width * height);
for (int i = 0; i < area; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
buffImg.setRGB(x, y, random.nextInt(255));
}
String str1 = randomStr(codeCount);//得到随机字符
this.code = str1;
for (int i = 0; i < codeCount; i++) {
String strRand = str1.substring(i, i + 1);
g.setColor(getRandColor(1, 255));
//g.drawString(str,x,y) ---- str:为要画出来的东西x 和 y :表示要画的东西最左则字符的基线位于此图形上下文坐标系的 x,y位置处
g.drawString(strRand, i * fontWidth + 3, codeY);
}
}
//得到随机字符
private String randomStr(int n) {
String str1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567990";
String str2 = "";
int len = str1.length() - 1;
double r;
for (int i = 0; i < n; i++) {
r = (Math.random()) * len;
str2 = str2 + str1.charAt((int) r);
}
return str2;
}
//得到随机颜色
private Color getRandColor(int fc, int bc) {
if (fc > 255) fc = 255;
if (bc > 255) bc = 255;
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
//产生随机字体
private Font getFont(int size) {
Random random = new Random();
Font font[] = new Font[5];
font[0] = new Font("Ravie", Font.PLAIN, size);
font[1] = new Font("Antique Olive Compact", Font.PLAIN, size);
font[2] = new Font("Fixedsys", Font.PLAIN, size);
font[3] = new Font("Wide Latin", Font.PLAIN, size);
font[4] = new Font("Gill Sans Ultra Bold", Font.PLAIN, size);
return font[random.nextInt(5)];
}
//扭曲方法
private void shear(Graphics g, int wl, int hl, Color color) {
shearX(g, wl, hl, color);
shearY(g, wl, hl, color);
}
private void shearX(Graphics g, int wl, int hl, Color color) {
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < hl; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(0, i, wl, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + wl, i, wl, i);
}
}
}
private void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
public void write(OutputStream os) {
try {
ImageIO.write(buffImg, "png", os);
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public String getCode() {
return code.toLowerCase();
}
}

View File

@@ -0,0 +1,29 @@
package com.mir4updater.backend.utils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileUtils {
public static Logger logger = LogManager.getLogger(FileUtils.class);
public static boolean byteToFile(byte[] bytes, File file) {
// 创建文件输出流
try (FileOutputStream fos = new FileOutputStream(file)) {
// 将字节数组写入到文件中
fos.write(bytes);
logger.info("写入文件成功!" + file.getAbsoluteFile());
return true;
} catch (IOException e) {
e.printStackTrace();
logger.info(e.getMessage());
}
return false;
}
}

View File

@@ -0,0 +1,79 @@
package com.mir4updater.backend.utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class HashUtils {
public static String getFileMD5(File file) throws NoSuchAlgorithmException, IOException {
MessageDigest md = MessageDigest.getInstance("MD5");
FileInputStream fis = new FileInputStream(file);
byte[] dataBytes = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(dataBytes)) != -1) {
md.update(dataBytes, 0, bytesRead);
}
byte[] mdBytes = md.digest();
StringBuilder sb = new StringBuilder();
for (byte mdByte : mdBytes) {
sb.append(Integer.toString((mdByte & 0xff) + 0x100, 16).substring(1));
}
return sb.toString();
}
public static String calculateSHA1(File file) throws NoSuchAlgorithmException, IOException {
InputStream inputStream = new FileInputStream(file);
return calculateSHA1(inputStream);
}
public static String calculateSHA1(InputStream inputStream) throws NoSuchAlgorithmException, IOException {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
byte[] hashBytes = digest.digest();
return bytesToHex(hashBytes, true);
}
public static String calculateSHA256(File file) throws NoSuchAlgorithmException, IOException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream inputStream = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
byte[] hashBytes = digest.digest();
return bytesToHex(hashBytes);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = String.format("%02x", b & 0xFF);
/*大写*/
// String hex = String.format("%02X", b & 0xFF);
hexString.append(hex);
}
return hexString.toString();
}
private static String bytesToHex(byte[] bytes, boolean upcase) {
String format = upcase ? "%02X" : "%02x";
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = String.format(format, b & 0xFF);
hexString.append(hex);
}
return hexString.toString();
}
}

View File

@@ -0,0 +1,7 @@
package com.mir4updater.backend.utils;
public class TextUtils {
public static boolean isEmpty(CharSequence str) {
return str == null || str.length() == 0;
}
}