diff --git a/AGENTS.md b/AGENTS.md index abe7a788..2e1ae77a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ | `src/main/java/com/youlai/boot/client/` | 家属端:App 用户、通讯录、设备操作、文件 | | `src/main/java/com/youlai/boot/device/` | 设备管理:sn 设备、截图、壁纸、策略、告警、APK 安装、硬件/网络/安全/系统信息 | | `src/main/java/com/youlai/boot/open/` | 家属端开放认证:短信登录、密码登录、注册、重置密码 | -| `src/main/java/com/youlai/boot/common/config/` | `FilePath`(本地存储路径)、`WebMvcConfig`(静态资源映射) | +| `src/main/java/com/youlai/boot/common/config/` | `StorageDir`(对象存储业务目录) | **其余所有目录(`auth/`、`system/`、`framework/`、`support/`、`config/`、`module/`、`codegen/` 等)都来自上游,尽量保持不动。** @@ -137,11 +137,11 @@ xxx/ - `/api/v1/client/**`:家属端,仅接受 `clientType=client` 的令牌 - `/api/v1/open/**`:免登录(登录/注册/找回密码) - 后台接口(`/api/v1/device`、`/api/v1/app/**`、`/api/v1/developers` 等):走 Spring Security,按需 `@PreAuthorize` -- 我方新增接口的业务文件统一通过 `FileService#uploadFile(MultipartFile, String directory)` 存到 `StorageDir` 定义的固定目录(`app/icon`、`app/package-icon`、`app/package`、`app/screenshot`、`app/avatar`、`app/wallpaper`、`app/category-icon`、`app/file`),实际存储由 `storage.type`(s3 / aliyun / local)决定;不按上传日期分目录。 +- 我方新增接口的业务文件统一通过 `FileService#uploadFile(MultipartFile, String directory)` 存到 `StorageDir` 定义的固定目录(`app/icon`、`app/package-icon`、`app/package`、`app/screenshot`、`app/avatar`、`app/wallpaper`、`app/category-icon`、`app/file`、`tablet/screenshot`),实际存储由 `storage.type`(s3 / aliyun / local)决定;不按上传日期分目录。 - 上游 `FileService#uploadFile(MultipartFile)` 及其 `Aliyun/S3/Local` 三个实现保持 gitee 行为(`yyyyMMdd/` 日期目录,本地实现不算哈希),只由上游 `POST /api/v1/files` 使用,不得修改其语义。 - 头像:家属端走 `POST /api/v1/client/files/avatar`,落 `app/avatar`。管理端头像继续用上游 `POST /api/v1/files`,`system/controller/FileController` 保持 gitee 原样(不再提供 `/api/v1/files/avatar`)。 - **文件上传规则的改动边界**:涉及存储目录、对象命名、扩展名校验、URL 生成等上传规则调整时,只允许改我方新增的接口与实现(`/api/v1/client/files/**`、`/api/v1/sn/**` 等);gitee 上游已有的上传接口(`POST /api/v1/files`、`system/controller/FileController` 及其调用的 `FileService` 原方法)保持上游原样,不因我方需求改其入参、出参、校验与行为。 -- `FilePath` + `/static/{子目录}/**` 仅用于设备截图等纯本地资源,以及存量历史文件的访问。 +- 所有业务文件(安装包、图标、截图、壁纸、头像等)统一经 `FileService` 落 `StorageDir` 目录,不落本地磁盘、不提供 `/static` 访问映射。 - 新增后台菜单/表结构请补充 `sql/ttstd/` 脚本,保持与代码同步。 ## 5. 提交约定 diff --git a/sql/ttstd/sn_device_wallpaper.sql b/sql/ttstd/sn_device_wallpaper.sql index 9a6d0dec..5010d9d2 100644 --- a/sql/ttstd/sn_device_wallpaper.sql +++ b/sql/ttstd/sn_device_wallpaper.sql @@ -3,7 +3,7 @@ -- 说明:设备的壁纸改为「一图一行」的多行存储,支持显式排序(sort_no)。 -- mode 区分壁纸模式:0-单个壁纸(1 行),1-轮换壁纸(最多 7 行)。 -- sort_no 升序即轮换展示顺序;单个壁纸固定为第 0 行。 --- image_url 为可访问的完整图片 URL,如 /static/wallpaper/SN_xxx_1.jpg。 +-- image_url 为对象存储返回的完整图片 URL。 -- 迁移说明:旧表 sys_sn_device_wallpaper(单行 JSON)已被本表取代; -- 如需清理旧表请手工执行:DROP TABLE IF EXISTS `sys_sn_device_wallpaper`; -- ============================================================================= diff --git a/src/main/java/com/youlai/boot/app/category/model/document/AppCategoryDocument.java b/src/main/java/com/youlai/boot/app/category/model/document/AppCategoryDocument.java index 0acc2e36..f0ef8433 100644 --- a/src/main/java/com/youlai/boot/app/category/model/document/AppCategoryDocument.java +++ b/src/main/java/com/youlai/boot/app/category/model/document/AppCategoryDocument.java @@ -30,7 +30,7 @@ public class AppCategoryDocument { @Field("name") private String name; - @Schema(description = "分类图标(手动上传图片的访问 URL,如 /static/category_icon/{md5}.png)") + @Schema(description = "分类图标(手动上传图片的访问 URL)") @Field("icon") private String icon; diff --git a/src/main/java/com/youlai/boot/app/packages/controller/AppPackageController.java b/src/main/java/com/youlai/boot/app/packages/controller/AppPackageController.java index bc2f541d..622948f0 100644 --- a/src/main/java/com/youlai/boot/app/packages/controller/AppPackageController.java +++ b/src/main/java/com/youlai/boot/app/packages/controller/AppPackageController.java @@ -12,18 +12,11 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.OutputStream; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.List; /** @@ -96,35 +89,14 @@ public class AppPackageController { return Result.success(true); } - @Operation(summary = "下载安装包(云存储直接重定向,历史本地文件返回文件流)") + @Operation(summary = "下载安装包(重定向到对象存储访问地址)") @GetMapping("/{id}/download") public void download(@Parameter(description = "安装包ID") @PathVariable String id, HttpServletResponse response) throws IOException { - // 云存储:直接重定向到对象访问地址,不占用服务端带宽 String fileUrl = appPackageService.getFileUrl(id); - if (fileUrl != null && (fileUrl.startsWith("http://") || fileUrl.startsWith("https://"))) { - response.sendRedirect(fileUrl); - return; - } - File file = appPackageService.getPackageFile(id); - if (file == null) { + if (fileUrl == null) { response.sendError(HttpStatus.NOT_FOUND.value(), "安装包文件不存在"); return; } - String originalName = appPackageService.getOriginalName(id); - String encodedName = URLEncoder.encode(originalName, StandardCharsets.UTF_8).replace("+", "%20"); - response.reset(); - response.setContentType("application/octet-stream"); - response.setContentLengthLong(file.length()); - response.setHeader(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=\"" + encodedName + "\"; filename*=UTF-8''" + encodedName); - try (OutputStream out = response.getOutputStream(); - BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) { - byte[] buffer = new byte[8192]; - int len; - while ((len = in.read(buffer)) != -1) { - out.write(buffer, 0, len); - } - out.flush(); - } + response.sendRedirect(fileUrl); } } diff --git a/src/main/java/com/youlai/boot/app/packages/service/AppPackageService.java b/src/main/java/com/youlai/boot/app/packages/service/AppPackageService.java index 9bd9b2e9..c6f2119b 100644 --- a/src/main/java/com/youlai/boot/app/packages/service/AppPackageService.java +++ b/src/main/java/com/youlai/boot/app/packages/service/AppPackageService.java @@ -38,7 +38,7 @@ public interface AppPackageService { AppPackageVO detail(String id); /** - * 删除(同时删除本地文件) + * 删除(同时删除存储对象) */ void removeById(String id); @@ -52,29 +52,9 @@ public interface AppPackageService { /** * 获取安装包文件访问地址。 - *
- * 云存储(s3 / aliyun)返回完整 http(s) URL,Controller 可直接重定向;
- * 历史本地数据返回 {@code /static/packages/...} 相对路径。
*
* @param id 安装包ID
- * @return 访问地址,不存在返回 null
+ * @return 完整访问 URL,不存在返回 null
*/
String getFileUrl(String id);
-
- /**
- * 读取历史本地安装包文件(本地存储时代的数据)。
- * 文件不存在时返回 null,由 Controller 返回 404。
- *
- * @param id 安装包ID
- * @return 安装包文件,不存在返回 null
- */
- java.io.File getPackageFile(String id);
-
- /**
- * 获取安装包原始文件名(用于下载时的 Content-Disposition 文件名)。
- *
- * @param id 安装包ID
- * @return 原始文件名,不存在返回 null
- */
- String getOriginalName(String id);
}
diff --git a/src/main/java/com/youlai/boot/app/packages/service/impl/AppPackageServiceImpl.java b/src/main/java/com/youlai/boot/app/packages/service/impl/AppPackageServiceImpl.java
index 5bf3ccef..8734302e 100644
--- a/src/main/java/com/youlai/boot/app/packages/service/impl/AppPackageServiceImpl.java
+++ b/src/main/java/com/youlai/boot/app/packages/service/impl/AppPackageServiceImpl.java
@@ -11,7 +11,6 @@ import com.youlai.boot.app.packages.model.query.AppPackagePageQuery;
import com.youlai.boot.app.packages.model.vo.AppPackageVO;
import com.youlai.boot.app.packages.repository.AppPackageRepository;
import com.youlai.boot.app.packages.service.AppPackageService;
-import com.youlai.boot.common.config.FilePath;
import com.youlai.boot.common.config.StorageDir;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.support.file.model.FileInfo;
@@ -204,29 +203,10 @@ public class AppPackageServiceImpl implements AppPackageService {
if (doc == null) {
return;
}
- // 删除安装包对象:云端(http URL)走存储服务,历史本地文件(/static/packages/)直接删磁盘
- if (isRemoteUrl(doc.getFileUrl())) {
- deleteRemoteQuietly(doc.getFileUrl(), "安装包");
- } else if (doc.getFileName() != null) {
- FileUtil.del(FilePath.getPackagePath() + doc.getFileName());
- }
- // 删除图标对象:云端走存储服务,历史 /static 本地图标已下线映射,仅清理磁盘文件
- if (isRemoteUrl(doc.getIconUrl())) {
- deleteRemoteQuietly(doc.getIconUrl(), "图标");
- } else if (doc.getIconUrl() != null && doc.getIconUrl().startsWith("/static/app_icon/")) {
- String iconName = doc.getIconUrl().substring("/static/app_icon/".length());
- FileUtil.del(FilePath.getApkIconPath() + iconName);
- }
- // 删除截图:云端走存储服务,历史本地截图清理磁盘文件
+ deleteRemoteQuietly(doc.getFileUrl(), "安装包");
+ deleteRemoteQuietly(doc.getIconUrl(), "图标");
if (doc.getScreenshots() != null) {
- for (String shotUrl : doc.getScreenshots()) {
- if (isRemoteUrl(shotUrl)) {
- deleteRemoteQuietly(shotUrl, "截图");
- } else if (shotUrl != null && shotUrl.startsWith("/static/screenshot/")) {
- String shotName = shotUrl.substring("/static/screenshot/".length());
- FileUtil.del(FilePath.getScreenshotPath() + shotName);
- }
- }
+ doc.getScreenshots().forEach(shotUrl -> deleteRemoteQuietly(shotUrl, "截图"));
}
appPackageRepository.deleteById(id);
}
@@ -236,28 +216,6 @@ public class AppPackageServiceImpl implements AppPackageService {
appPackageRepository.updateCategories(id, categories);
}
- @Override
- public File getPackageFile(String id) {
- AppPackageDocument doc = appPackageRepository.findById(id);
- if (doc == null || doc.getFileName() == null) {
- return null;
- }
- File file = new File(FilePath.getPackagePath() + doc.getFileName());
- if (!file.exists() || !file.isFile()) {
- return null;
- }
- return file;
- }
-
- @Override
- public String getOriginalName(String id) {
- AppPackageDocument doc = appPackageRepository.findById(id);
- if (doc == null || doc.getOriginalName() == null) {
- return "package";
- }
- return doc.getOriginalName();
- }
-
@Override
public String getFileUrl(String id) {
AppPackageDocument doc = appPackageRepository.findById(id);
@@ -284,10 +242,6 @@ public class AppPackageServiceImpl implements AppPackageService {
return slash >= 0 ? path.substring(slash + 1) : path;
}
- private boolean isRemoteUrl(String url) {
- return url != null && (url.startsWith("http://") || url.startsWith("https://"));
- }
-
/**
* 删除云存储对象:失败只记日志,不阻断数据库记录删除
*/
diff --git a/src/main/java/com/youlai/boot/client/controller/ClientController.java b/src/main/java/com/youlai/boot/client/controller/ClientController.java
index f921d96f..951ea585 100644
--- a/src/main/java/com/youlai/boot/client/controller/ClientController.java
+++ b/src/main/java/com/youlai/boot/client/controller/ClientController.java
@@ -303,13 +303,10 @@ public class ClientController {
return Result.failed("设备不存在,请确认SN是否正确");
}
- // 已绑定给当前用户:直接视为已绑定
+ // 已绑定给其他用户则拒绝;绑定给当前用户时仍下发一次确认请求,
+ // 用于设备重置/密钥变更后的重新授权(设备端仍需用户同意)
if (StringUtils.hasText(deviceInfo.getSnMobile())
- && deviceInfo.getSnMobile().equals(clientUser.getMobile())) {
- return Result.failed("该设备已绑定到当前账号");
- }
- // 已绑定给其他用户则拒绝
- if (StringUtils.hasText(deviceInfo.getSnMobile())) {
+ && !deviceInfo.getSnMobile().equals(clientUser.getMobile())) {
return Result.failed("该设备已被其他用户绑定");
}
@@ -558,9 +555,7 @@ public class ClientController {
if (savedFile == null) {
return Result.failed("图片保存失败");
}
- String imageUrl = StringUtils.hasText(savedFile.getUrl())
- ? savedFile.getUrl()
- : "/static/wallpaper/" + savedFile.getFileName();
+ String imageUrl = savedFile.getUrl();
boolean sent = devicePictureService.sendPicture(sn, imageUrl, favorite != null && favorite == 1, savedFile.getFileSize());
if (!sent) {
return Result.failed("图片已保存,但推送设备失败,请检查设备是否在线");
@@ -735,10 +730,8 @@ public class ClientController {
return null;
}
String url = stored.getUrl();
- int slash = url.lastIndexOf('/');
WallpaperFileDTO dto = new WallpaperFileDTO();
- dto.setFileName(slash >= 0 ? url.substring(slash + 1) : url);
dto.setUrl(url);
dto.setFileSize(file.getSize());
dto.setMd5(md5);
diff --git a/src/main/java/com/youlai/boot/common/config/FilePath.java b/src/main/java/com/youlai/boot/common/config/FilePath.java
deleted file mode 100644
index 4a2202fc..00000000
--- a/src/main/java/com/youlai/boot/common/config/FilePath.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.youlai.boot.common.config;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-
-public class FilePath {
-
- /**
- * 有可能部署在docker中,所以需要使用绝对路径
- */
-// @Value("${file.upload-dir-unix}")
- private static final String unixUploadDir = "/home/tt/ttstd/uploads" ;
-
-// @Value("${file.upload-dir-windows}")
- private static final String windowsUploadDir = "uploadFile";
-
- private static Logger logger = LoggerFactory.getLogger(FilePath.class);
-
-
- public static final String TABLET_PATH = "tablet";
- public static final String AVATAR_PATH = "avatar";
- public static final String APK_ICON_PATH = "apkIcon";
- public static final String SCREENSHOT_PATH = "screenshot";
- public static final String WALLPAPER_PATH = "wallpaper";
- public static final String PACKAGE_PATH = "packages";
- public static final String CATEGORY_ICON_PATH = "category_icon";
-
- public static String getRootPath() {
- String osName = System.getProperty("os.name");
- logger.info("osName: {}", osName);
- String projectPath = System.getProperty("user.dir");
- logger.info("projectPath: {}", projectPath);
- if (osName.contains("Windows")) {
- return projectPath + File.separator + windowsUploadDir;
- } else {
- return projectPath + File.separator + windowsUploadDir;
- }
- }
-
- public static String getAvatarPath() {
- return getRootPath() + File.separator + TABLET_PATH + File.separator + AVATAR_PATH + File.separator;
- }
-
- /**
- * 历史应用图标目录(旧版本按 md5 落盘,现仅用于兼容存量 icon_url 与清理)
- */
- public static String getApkIconPath() {
- return getRootPath() + File.separator + TABLET_PATH + File.separator + APK_ICON_PATH + File.separator;
- }
-
- public static String getScreenshotPath() {
- return getRootPath() + File.separator + TABLET_PATH + File.separator + SCREENSHOT_PATH + File.separator;
- }
-
- /**
- * 壁纸存储绝对目录
- */
- public static String getWallpaperPath() {
- return getRootPath() + File.separator + TABLET_PATH + File.separator + WALLPAPER_PATH + File.separator;
- }
-
- public static String getPackagePath() {
- return getRootPath() + File.separator + TABLET_PATH + File.separator + PACKAGE_PATH + File.separator;
- }
-
- public static String getCategoryIconPath() {
- return getRootPath() + File.separator + TABLET_PATH + File.separator + CATEGORY_ICON_PATH + File.separator;
- }
-
- /**
- * 截图存储绝对目录
- */
- public static String getScreenshotDir() {
- return getRootPath() + File.separator + TABLET_PATH + File.separator + SCREENSHOT_PATH;
- }
-
- /**
- * 截图相对访问路径(用于拼接存储到数据库的 filePath)
- */
- public static String getScreenshotRelativePath() {
- return TABLET_PATH + "/" + SCREENSHOT_PATH + "/";
- }
-}
diff --git a/src/main/java/com/youlai/boot/common/config/StorageDir.java b/src/main/java/com/youlai/boot/common/config/StorageDir.java
index 43fec459..09071de0 100644
--- a/src/main/java/com/youlai/boot/common/config/StorageDir.java
+++ b/src/main/java/com/youlai/boot/common/config/StorageDir.java
@@ -42,6 +42,9 @@ public interface StorageDir {
/** 安装包应用截图 */
String SCREENSHOT = "app/screenshot";
+ /** 设备端上报的屏幕截图/拍照 */
+ String DEVICE_SCREENSHOT = "tablet/screenshot";
+
/** 用户头像(管理端 / 家属端) */
String AVATAR = "app/avatar";
diff --git a/src/main/java/com/youlai/boot/common/config/WebMvcConfig.java b/src/main/java/com/youlai/boot/common/config/WebMvcConfig.java
deleted file mode 100644
index 9c5c407f..00000000
--- a/src/main/java/com/youlai/boot/common/config/WebMvcConfig.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.youlai.boot.common.config;
-
-import org.springframework.context.annotation.Configuration;
-import org.springframework.http.CacheControl;
-import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
-import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * Web MVC 配置
- *
- * @author TTSTD
- * @since 2026-08-05
- */
-@Configuration
-public class WebMvcConfig implements WebMvcConfigurer {
-
- /**
- * 静态资源映射
- */
- @Override
- public void addResourceHandlers(ResourceHandlerRegistry registry) {
- // 历史应用图标静态资源映射:/static/app_icon/{md5}.png -> 磁盘 tablet/apkIcon 目录
- // 新上传图标已改存云存储,此处仅为兼容存量 app_icon / app_package 记录中的相对地址,
- // 避免映射缺失导致旧图标整片 404。文件名为内容 MD5,可安全使用长缓存。
- String legacyIconLocation = "file:" + FilePath.getApkIconPath().replace("\\", "/");
- registry.addResourceHandler("/static/app_icon/**")
- .addResourceLocations(legacyIconLocation)
- .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic().immutable());
-
- // 设备截图静态资源映射:/static/screenshot/{fileName} -> 磁盘 tablet/screenshot 目录
- // 截图内容会变化(删除/重截),且文件名 md5 可能因画面相同而重复,
- // 故不使用长缓存,避免浏览器复用旧图(表现为 304 + 图片不更新)。
- String screenshotLocation = "file:" + FilePath.getScreenshotPath().replace("\\", "/");
- registry.addResourceHandler("/static/screenshot/**")
- .addResourceLocations(screenshotLocation)
- .setCacheControl(CacheControl.noCache());
-
- // 应用安装包静态资源映射:/static/packages/{fileName} -> 磁盘 tablet/packages 目录
- // 安装包内容为内容寻址命名(含 md5),内容变更文件名必变,可安全使用长缓存。
- String packageLocation = "file:" + FilePath.getPackagePath().replace("\\", "/");
- registry.addResourceHandler("/static/packages/**")
- .addResourceLocations(packageLocation)
- .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic().immutable());
-
- // 分类图标静态资源映射:/static/category_icon/{fileName} -> 磁盘 tablet/category_icon 目录
- // 分类图标为手动上传图片,文件名内容寻址(含 md5),变更必变文件名,可安全使用长缓存。
- String categoryIconLocation = "file:" + FilePath.getCategoryIconPath().replace("\\", "/");
- registry.addResourceHandler("/static/category_icon/**")
- .addResourceLocations(categoryIconLocation)
- .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic().immutable());
-
- // 壁纸静态资源映射:/static/wallpaper/{fileName} -> 磁盘 tablet/wallpaper 目录
- // 壁纸内容会变化(更换/删除),且文件名含时间戳(同一图片重复上传文件名不同),
- // 不使用长缓存,避免设备/家属端复用旧图。
- String wallpaperLocation = "file:" + FilePath.getWallpaperPath().replace("\\", "/");
- registry.addResourceHandler("/static/wallpaper/**")
- .addResourceLocations(wallpaperLocation)
- .setCacheControl(CacheControl.noCache());
- }
-}
diff --git a/src/main/java/com/youlai/boot/device/controller/MobileController.java b/src/main/java/com/youlai/boot/device/controller/MobileController.java
index 9cf18aeb..af5ce21e 100644
--- a/src/main/java/com/youlai/boot/device/controller/MobileController.java
+++ b/src/main/java/com/youlai/boot/device/controller/MobileController.java
@@ -1,7 +1,7 @@
package com.youlai.boot.device.controller;
import com.youlai.boot.common.annotation.Log;
-import com.youlai.boot.common.config.FilePath;
+import com.youlai.boot.common.config.StorageDir;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.exception.BusinessException;
@@ -33,6 +33,8 @@ import com.youlai.boot.device.model.vo.DeviceWallpaperVO;
import com.youlai.boot.device.converter.DevicePolicyConverter;
import com.youlai.boot.device.service.*;
import com.youlai.boot.framework.security.signature.DeviceSignatureVerifier;
+import com.youlai.boot.support.file.model.FileInfo;
+import com.youlai.boot.support.file.service.FileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@@ -44,7 +46,6 @@ import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
-import java.io.File;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
@@ -76,6 +77,7 @@ public class MobileController {
private final DeviceSignatureVerifier deviceSignatureVerifier;
private final DeviceWallpaperService deviceWallpaperService;
private final DeviceAlarmService deviceAlarmService;
+ private final FileService fileService;
private final Logger logger = LoggerFactory.getLogger(MobileController.class);
@@ -114,16 +116,33 @@ public class MobileController {
return Result.failed(ResultCode.MOBILE_SIGN_INVALID, "设备签名验证失败");
}
- // 2. 注册设备(绑定即注册):SN ↔ 公钥,不存在则新建,已存在则更新公钥
+ // 2. 查已有记录:封禁与手机号冲突必须在写公钥之前校验,
+ // 否则错误手机号的请求会先把公钥换掉,导致真正绑定的设备无法再验签
SnDeviceInfo device = deviceService.lambdaQuery()
.eq(SnDeviceInfo::getSerialno, sn)
.one();
+ if (device != null) {
+ if (device.getStatus() != null && device.getStatus() == 0) {
+ return Result.failed(ResultCode.MOBILE_DEVICE_BANNED);
+ }
+ if (StringUtils.hasText(device.getSnMobile())
+ && !device.getSnMobile().equals(phone.trim())) {
+ return Result.failed("该设备已绑定其他手机号");
+ }
+ if (StringUtils.hasText(device.getPublicKey())
+ && !device.getPublicKey().equals(publicKey)) {
+ logger.info("设备公钥变更(重新授权), sn: {}, phone: {}", sn, phone.trim());
+ }
+ }
+
+ // 3. 注册设备(绑定即注册):SN ↔ 公钥 + 手机号一次写入
if (device == null) {
device = new SnDeviceInfo();
device.setSerialno(sn);
device.setStatus(1);
device.setSnModel(model);
device.setPublicKey(publicKey);
+ device.setSnMobile(phone.trim());
boolean saved = deviceService.save(device);
if (!saved) {
return Result.failed("设备注册失败");
@@ -133,21 +152,13 @@ public class MobileController {
if (model != null && !model.trim().isEmpty()) {
device.setSnModel(model);
}
+ device.setSnMobile(phone.trim());
deviceService.updateById(device);
}
- // 3. 已绑定给其他手机号则拒绝(同一设备仅允许一个家属手机号)
- if (StringUtils.hasText(device.getSnMobile())
- && !device.getSnMobile().equals(phone.trim())) {
- return Result.failed("该设备已绑定其他手机号");
- }
-
- // 4. 写入绑定关系
- device.setSnMobile(phone.trim());
- deviceService.updateById(device);
logger.info("设备确认绑定成功(绑定即注册), sn: {}, phone: {}", sn, phone.trim());
- // 5. 回推绑定成功通知到设备端(contentType=21)
+ // 4. 回推绑定成功通知到设备端(contentType=21)
deviceService.pushBindSuccess(sn, phone.trim());
Map