reghao 5 days ago
parent
commit
d63a08bb19
47 changed files with 1280 additions and 650 deletions
  1. 55 0
      oss-api/src/main/java/cn/reghao/oss/api/constant/UploadStatus.java
  2. 10 2
      oss-api/src/main/java/cn/reghao/oss/api/dto/FastUploadResult.java
  3. 2 19
      oss-api/src/main/java/cn/reghao/oss/api/dto/ObjectInfo.java
  4. 34 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/UploadDoneResult.java
  5. 3 1
      oss-api/src/main/java/cn/reghao/oss/api/dto/UploadResult.java
  6. 0 29
      oss-api/src/main/java/cn/reghao/oss/api/dto/UploadedFile.java
  7. 1 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/WebBody.java
  8. 1 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/disk/DiskStore.java
  9. 4 11
      oss-api/src/main/java/cn/reghao/oss/api/dto/media/VideoInfo.java
  10. 10 3
      oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadFileRet.java
  11. 1 3
      oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadPrepare.java
  12. 5 1
      oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadSample.java
  13. 3 21
      oss-api/src/main/java/cn/reghao/oss/api/iface/ConsoleService.java
  14. 2 6
      oss-api/src/main/java/cn/reghao/oss/api/iface/StoreService.java
  15. 29 0
      oss-api/src/main/java/cn/reghao/oss/api/util/FileUtil.java
  16. 1 1
      oss-api/src/main/java/cn/reghao/oss/api/util/OssSamplingHash.java
  17. 28 26
      oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/OssSdkController.java
  18. 6 1
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/FileMetaMapper.java
  19. 6 1
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/UploadTaskMapper.java
  20. 6 1
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/repository/ObjectRepository.java
  21. 6 13
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/DataBlock.java
  22. 24 25
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/FileMeta.java
  23. 8 1
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/UploadTask.java
  24. 51 164
      oss-mgr/src/main/java/cn/reghao/oss/mgr/rpc/ConsoleServiceImpl.java
  25. 5 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/rpc/RpcService.java
  26. 18 21
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/MetadataService.java
  27. 195 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/OssClientService.java
  28. 4 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/StoreNodeService.java
  29. 4 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/UploadChannelService.java
  30. 4 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/UserNodeService.java
  31. 1 1
      oss-mgr/src/main/resources/application-dev.yml
  32. 1 1
      oss-mgr/src/main/resources/mapper/DataBlockMapper.xml
  33. 12 2
      oss-mgr/src/main/resources/mapper/FileMetaMapper.xml
  34. 9 4
      oss-mgr/src/main/resources/mapper/UploadTaskMapper.xml
  35. 153 42
      oss-sdk/src/main/java/cn/reghao/oss/sdk/OssClient.java
  36. 2 2
      oss-store/bin/oss.yml
  37. 27 41
      oss-store/src/main/java/cn/reghao/oss/store/disk/DiskService.java
  38. 0 67
      oss-store/src/main/java/cn/reghao/oss/store/disk/FileMover.java
  39. 61 27
      oss-store/src/main/java/cn/reghao/oss/store/disk/HddFlushService.java
  40. 12 10
      oss-store/src/main/java/cn/reghao/oss/store/disk/MetadataCompensator.java
  41. 251 41
      oss-store/src/main/java/cn/reghao/oss/store/handler/OssMultipartUploadHandler.java
  42. 62 11
      oss-store/src/main/java/cn/reghao/oss/store/handler/OssRouterHandler.java
  43. 51 18
      oss-store/src/main/java/cn/reghao/oss/store/handler/OssUploadHandler.java
  44. 91 4
      oss-store/src/main/java/cn/reghao/oss/store/rpc/StoreServiceImpl.java
  45. 2 6
      oss-store/src/main/java/cn/reghao/oss/store/server/OssStoreServer.java
  46. 16 17
      oss-store/src/main/java/cn/reghao/oss/store/util/ResponseHelper.java
  47. 3 6
      oss-store/src/main/resources/application-dev.yml

+ 55 - 0
oss-api/src/main/java/cn/reghao/oss/api/constant/UploadStatus.java

@@ -0,0 +1,55 @@
+package cn.reghao.oss.api.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2026-04-29 14:35:46
+ */
+public enum UploadStatus {
+    UPLOADING(1, "上传中"),
+    FLUSHING(2, "迁移中"),
+    AVAILABLE(3, "已完成");
+
+    private final int code;
+    private final String desc;
+    private static final Map<Integer, UploadStatus> map = new HashMap<>();
+    static {
+        for (UploadStatus uploadStatus : UploadStatus.values()) {
+            map.put(uploadStatus.code, uploadStatus);
+        }
+    }
+
+    UploadStatus(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public String getName() {
+        return this.name();
+    }
+
+    /**
+     * 提供给 @ValidEnum 调用
+     *
+     * @param
+     * @return
+     * @date 2023-10-11 14:44:42
+     */
+    public int getValue() {
+        return this.code;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    public static UploadStatus getByCode(int code) {
+        return map.get(code);
+    }
+}

+ 10 - 2
oss-api/src/main/java/cn/reghao/oss/api/dto/ObjectBindDTO.java → oss-api/src/main/java/cn/reghao/oss/api/dto/FastUploadResult.java

@@ -12,14 +12,22 @@ import java.io.Serializable;
  * @date 2026-02-01 15:50:24
  */
 @NoArgsConstructor
-@AllArgsConstructor
 @Setter
 @Getter
-public class ObjectBindDTO implements Serializable {
+public class FastUploadResult implements Serializable {
     private static final long serialVersionUID = 1L;
 
     private String sha256sum;
     private String filename;
     private String objectId;
     private String objectName;
+    private long uploadBy;
+
+    public FastUploadResult(UploadResult uploadResult) {
+        this.sha256sum = uploadResult.getSha256sum();
+        this.filename = uploadResult.getFilename();
+        this.objectId = uploadResult.getObjectId();
+        this.objectName = uploadResult.getObjectName();
+        this.uploadBy = uploadResult.getUploadBy();
+    }
 }

+ 2 - 19
oss-api/src/main/java/cn/reghao/oss/api/dto/ObjectInfo.java

@@ -1,8 +1,7 @@
 package cn.reghao.oss.api.dto;
 
-import lombok.Getter;
+import lombok.Data;
 import lombok.NoArgsConstructor;
-import lombok.Setter;
 
 import java.io.Serializable;
 
@@ -11,8 +10,7 @@ import java.io.Serializable;
  * @date 2023-11-29 18:11:13
  */
 @NoArgsConstructor
-@Setter
-@Getter
+@Data
 public class ObjectInfo implements Serializable {
     private static final long serialVersionUID = 1L;
 
@@ -25,19 +23,4 @@ public class ObjectInfo implements Serializable {
     private int scope;
     private String sha256sum;
     private String updateTime;
-
-    public ObjectInfo(String objectId, String objectName, int fileType, String filename, long size, int scope, String sha256sum) {
-        this.objectId = objectId;
-        this.objectName = objectName;
-        this.fileType = fileType;
-        this.filename = filename;
-        this.size = size;
-        this.url = objectName;
-        this.scope = scope;
-        this.sha256sum = sha256sum;
-    }
-
-    public void setUrl(String url) {
-        this.url = url;
-    }
 }

+ 34 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/UploadDoneResult.java

@@ -0,0 +1,34 @@
+package cn.reghao.oss.api.dto;
+
+import cn.reghao.oss.api.constant.UploadStatus;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2026-04-30 11:13:40
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class UploadDoneResult implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String objectId;
+    private String sha256sum;
+    private String absolutePath;
+    private long size;
+    private String hostPort;
+    private int uploadStatus = UploadStatus.AVAILABLE.getCode();
+
+    public UploadDoneResult(String objectId, String sha256sum, String absolutePath, long size, String hostPort) {
+        this.objectId = objectId;
+        this.sha256sum = sha256sum;
+        this.absolutePath = absolutePath;
+        this.size = size;
+        this.hostPort = hostPort;
+    }
+}

+ 3 - 1
oss-api/src/main/java/cn/reghao/oss/api/dto/OssUploadResultDTO.java → oss-api/src/main/java/cn/reghao/oss/api/dto/UploadResult.java

@@ -13,7 +13,7 @@ import java.io.Serializable;
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class OssUploadResultDTO implements Serializable {
+public class UploadResult implements Serializable {
     private static final long serialVersionUID = 1L;
 
     private String sha256sum;
@@ -21,8 +21,10 @@ public class OssUploadResultDTO implements Serializable {
     private long size;
     private String filename;
     private String contentType;
+    private String channelPrefix;
     private String objectId;
     private String objectName;
     private long uploadBy;
     private String hostPort;
+    private int uploadStatus;
 }

+ 0 - 29
oss-api/src/main/java/cn/reghao/oss/api/dto/UploadedFile.java

@@ -1,29 +0,0 @@
-package cn.reghao.oss.api.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
-
-import java.io.Serializable;
-
-/**
- * @author reghao
- * @date 2026-02-21 00:33:38
- */
-@AllArgsConstructor
-@NoArgsConstructor
-@Getter
-@Setter
-public class UploadedFile implements Serializable {
-    private static final long serialVersionUID = 1L;
-
-    private String uploadId;
-    private String sha256sum;
-    private String channelPrefix;
-    private String filename;
-    private String contentType;
-    private long size;
-    private String absolutePath;
-    private String hostPort;
-}

+ 1 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/WebBody.java

@@ -14,6 +14,7 @@ import lombok.Setter;
 @NoArgsConstructor
 @Setter
 @Getter
+@Deprecated
 public class WebBody {
     private int code;
     private String msg;

+ 1 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/disk/DiskStore.java

@@ -12,6 +12,7 @@ import java.util.List;
  * @author reghao
  * @date 2024-10-21 09:50:23
  */
+@Deprecated
 @AllArgsConstructor
 @Getter
 public class DiskStore implements Serializable {

+ 4 - 11
oss-api/src/main/java/cn/reghao/oss/api/dto/media/VideoInfo.java

@@ -1,9 +1,6 @@
 package cn.reghao.oss.api.dto.media;
 
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
+import lombok.*;
 
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -14,12 +11,10 @@ import java.time.LocalDateTime;
  */
 @AllArgsConstructor
 @NoArgsConstructor
-@Setter
-@Getter
+@Data
 public class VideoInfo implements Serializable {
     private static final long serialVersionUID = 1L;
 
-    private String videoFileId;
     private String objectId;
     private String videoCodec;
     private Long vbitRate;
@@ -33,10 +28,8 @@ public class VideoInfo implements Serializable {
     private Integer height;
     // 单位秒
     private Integer duration;
-    private LocalDateTime createTime;
     private Long size;
 
-    public void setUrl(String url) {
-        this.url = url;
-    }
+    //private String videoFileId;
+    //private LocalDateTime createTime;
 }

+ 10 - 3
oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadFileRet.java

@@ -15,12 +15,13 @@ public class UploadFileRet implements Serializable {
     private final String uploadId;
     private boolean fastUpload;
     private String url;
-    private boolean merged;
+    //private boolean merged;
+    private boolean uploaded;
 
     public UploadFileRet(String uploadId) {
         this.uploadId = uploadId;
         this.url = null;
-        this.merged = false;
+        this.uploaded = false;
     }
 
     public UploadFileRet(String uploadId, boolean fastUpload) {
@@ -32,6 +33,12 @@ public class UploadFileRet implements Serializable {
     public UploadFileRet(String uploadId, String url) {
         this.uploadId = uploadId;
         this.url = url;
-        this.merged = true;
+        this.uploaded = false;
+    }
+
+    public UploadFileRet(String uploadId, String url, boolean uploaded) {
+        this.uploadId = uploadId;
+        this.url = url;
+        this.uploaded = true;
     }
 }

+ 1 - 3
oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadPrepare.java

@@ -27,9 +27,7 @@ public class UploadPrepare implements Serializable {
     @NotBlank
     private String filename;
     @NotNull
-    private long size;
+    private Long size;
     @NotBlank
     private String sha256sum;
-    @NotBlank
-    private String contentType;
 }

+ 5 - 1
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/UploadSample.java → oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadSample.java

@@ -1,12 +1,16 @@
-package cn.reghao.oss.mgr.model.dto;
+package cn.reghao.oss.api.dto.rest;
 
+import lombok.AllArgsConstructor;
 import lombok.Getter;
+import lombok.NoArgsConstructor;
 import lombok.Setter;
 
 /**
  * @author reghao
  * @date 2026-02-21 23:06:55
  */
+@NoArgsConstructor
+@AllArgsConstructor
 @Setter
 @Getter
 public class UploadSample {

+ 3 - 21
oss-api/src/main/java/cn/reghao/oss/api/iface/ConsoleService.java

@@ -1,6 +1,5 @@
 package cn.reghao.oss.api.iface;
 
-import cn.reghao.oss.api.dto.*;
 import cn.reghao.oss.api.dto.*;
 import cn.reghao.oss.api.dto.media.ImageInfo;
 import cn.reghao.oss.api.dto.media.VideoInfo;
@@ -14,26 +13,9 @@ import cn.reghao.oss.api.dto.media.VideoInfo;
  */
 public interface ConsoleService {
     void registerNode(StoreNodeDto storeNodeDto);
-    NodeProperties getNodeProperties(String domain);
-    ObjectChannel getChannelByCode(int owner, int channelCode);
-    Integer getChannelCodeByUrl(int owner, String url);
-    /**
-     * 本方法由 oss-console 本地调用, oss-store 不会使用
-     *
-     * @param
-     * @return
-     * @date 2025-08-16 23:08:330
-     */
-    ServerInfo getUploadStore(int channelCode);
     boolean checkExists(String sha256sum);
-    void bindOnly(ObjectBindDTO objectBindDTO);
-    void registerAndBind(OssUploadResultDTO dto);
+    void bindOnly(FastUploadResult fastUploadResult);
+    void registerAndBind(UploadResult uploadResult);
+    void updateAfterMove(UploadDoneResult uploadDoneResult);
     ObjectMeta getObjectMeta(String httpHost, String objectName);
-    boolean validateMultipart(UploadedFile uploadedFile);
-    void updatePath(String sha256sum, String path);
-    void deleteObject(String objectId);
-    void setObjectScope(String objectId, int scope);
-    ObjectInfo getObjectInfo(String objectId);
-    VideoInfo getVideoInfo(String objectId);
-    ImageInfo getImageInfo(String objectId);
 }

+ 2 - 6
oss-api/src/main/java/cn/reghao/oss/api/iface/StoreService.java

@@ -1,10 +1,6 @@
 package cn.reghao.oss.api.iface;
 
-import cn.reghao.jutil.jdk.web.db.PageList;
-import cn.reghao.oss.api.dto.ObjectInfo;
 import cn.reghao.oss.api.dto.disk.DiskVolume;
-import cn.reghao.oss.api.dto.media.AudioInfo;
-import cn.reghao.oss.api.dto.media.ConvertedImageInfo;
 import cn.reghao.oss.api.dto.media.ImageInfo;
 import cn.reghao.oss.api.dto.media.VideoInfo;
 
@@ -21,6 +17,6 @@ public interface StoreService {
     List<DiskVolume> getDiskVolumes();
     String getPartialMd5(String absolutePath, long offset, int length);
     void deleteFile(String absolutePath);
-    VideoInfo getVideoInfo(String absolutePath);
-    ImageInfo getImageInfo(String absolutePath);
+    VideoInfo getVideoInfo(String objectId, String absolutePath);
+    ImageInfo getImageInfo(String objectId, String absolutePath);
 }

+ 29 - 0
oss-api/src/main/java/cn/reghao/oss/api/util/FileUtil.java

@@ -0,0 +1,29 @@
+package cn.reghao.oss.api.util;
+
+/**
+ * @author reghao
+ * @date 2026-04-28 16:07:27
+ */
+public class FileUtil {
+    public static String getSuffix(String filename) {
+        if (filename == null) {
+            return "";
+        }
+
+        int idx = filename.lastIndexOf(".");
+        return idx == -1 ? "" : filename.substring(idx+1);
+    }
+
+    public static String getSuffixByMime(String mime) {
+        String suffix = "dat";
+        if (mime.startsWith("image")) {
+            suffix = ".jpg";
+        } else if (mime.startsWith("video")) {
+            suffix = ".mp4";
+        } else if (mime.startsWith("audio")) {
+            suffix = ".mp3";
+        }
+
+        return suffix;
+    }
+}

+ 1 - 1
oss-api/src/main/java/cn/reghao/oss/api/util/OssSamplingHash.java

@@ -64,7 +64,7 @@ public class OssSamplingHash {
         }
     }
 
-    private static String bytesToHex(byte[] hash) {
+    public static String bytesToHex(byte[] hash) {
         StringBuilder hexString = new StringBuilder();
         for (byte b : hash) {
             String hex = Integer.toHexString(0xff & b);

+ 28 - 26
oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/OssSdkController.java

@@ -1,16 +1,15 @@
 package cn.reghao.oss.mgr.controller;
 
 import cn.reghao.jutil.jdk.web.result.WebResult;
+import cn.reghao.oss.api.constant.ObjectScope;
+import cn.reghao.oss.api.dto.ObjectChannel;
 import cn.reghao.oss.api.dto.ObjectInfo;
 import cn.reghao.oss.api.dto.ObjectMeta;
 import cn.reghao.oss.api.dto.ServerInfo;
-import cn.reghao.oss.api.dto.media.ImageInfo;
 import cn.reghao.oss.api.dto.media.VideoInfo;
 import cn.reghao.oss.api.dto.rest.UploadFileRet;
 import cn.reghao.oss.api.dto.rest.UploadPrepare;
 import cn.reghao.oss.api.dto.rest.UploadPrepareRet;
-import cn.reghao.oss.api.iface.ConsoleService;
-import cn.reghao.oss.api.iface.StoreService;
 import cn.reghao.oss.api.util.SignatureUtils;
 import cn.reghao.oss.mgr.config.UserContext;
 import cn.reghao.oss.mgr.db.mapper.UploadChannelMapper;
@@ -18,14 +17,16 @@ import cn.reghao.oss.mgr.db.mapper.UserKeyMapper;
 import cn.reghao.oss.mgr.db.mapper.UserNodeMapper;
 import cn.reghao.oss.mgr.db.repository.ObjectRepository;
 import cn.reghao.oss.mgr.model.dto.FileInitRequest;
-import cn.reghao.oss.mgr.model.dto.UploadSample;
+import cn.reghao.oss.api.dto.rest.UploadSample;
 import cn.reghao.oss.mgr.model.po.StoreNode;
 import cn.reghao.oss.mgr.model.po.UploadChannel;
 import cn.reghao.oss.mgr.model.po.UserNode;
 import cn.reghao.oss.mgr.service.MetadataService;
+import cn.reghao.oss.mgr.service.OssClientService;
 import cn.reghao.oss.mgr.service.UserNodeService;
 import io.swagger.v3.oas.annotations.Operation;
 import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.*;
@@ -39,22 +40,22 @@ import java.util.*;
 @RestController
 @RequestMapping("/api/oss/sdk")
 public class OssSdkController {
+    private final OssClientService ossClientService;
     private final MetadataService metadataService;
     private final UserNodeService userNodeService;
     private final ObjectRepository objectRepository;
-    private final ConsoleService consoleService;
     private final UserKeyMapper userKeyMapper;
     private final UserNodeMapper userNodeMapper;
     private final UploadChannelMapper uploadChannelMapper;
 
-    public OssSdkController(MetadataService metadataService, UserNodeService userNodeService,
-                            ObjectRepository objectRepository, ConsoleService consoleService,
+    public OssSdkController(OssClientService ossClientService, MetadataService metadataService,
+                            UserNodeService userNodeService, ObjectRepository objectRepository,
                             UserKeyMapper userKeyMapper, UserNodeMapper userNodeMapper,
                             UploadChannelMapper uploadChannelMapper) {
+        this.ossClientService = ossClientService;
         this.metadataService = metadataService;
         this.userNodeService = userNodeService;
         this.objectRepository = objectRepository;
-        this.consoleService = consoleService;
         this.userKeyMapper = userKeyMapper;
         this.userNodeMapper = userNodeMapper;
         this.uploadChannelMapper = uploadChannelMapper;
@@ -62,28 +63,27 @@ public class OssSdkController {
 
     @Operation(summary = "获取上传对象所需的数据", description = "N")
     @PostMapping(value = "/upload_request", produces = MediaType.APPLICATION_JSON_VALUE)
-    public String requestUpload(@RequestBody FileInitRequest req) {
+    public String requestUpload(@RequestBody @Validated FileInitRequest req) {
         String objectId = req.getObjectId();
 
         // 1. 选出负载最低且空间充足的 oss-store 节点
         StoreNode targetNode = userNodeService.selectBestNode();
 
-        int channelCode = 101;
-        //channelCode = req.getChannelCode();
-        ServerInfo serverInfo = consoleService.getUploadStore(channelCode);
+        int channelCode = req.getChannelCode();
+        ServerInfo serverInfo = ossClientService.getUploadStore(channelCode);
         return WebResult.success(serverInfo);
     }
 
     @Operation(summary = "对象上传前的预检", description = "N")
     @PostMapping(value = "/prepare", produces = MediaType.APPLICATION_JSON_VALUE)
-    public String uploadPrepare(@RequestBody UploadPrepare uploadPrepare) {
+    public String uploadPrepare(@RequestBody @Validated UploadPrepare uploadPrepare) {
         UploadPrepareRet uploadPrepareRet = metadataService.prepareUpload(uploadPrepare);
         return WebResult.success(uploadPrepareRet);
     }
 
     @Operation(summary = "对象快传的二次检查", description = "N")
     @PostMapping(value = "/check_sample", produces = MediaType.APPLICATION_JSON_VALUE)
-    public String checkSample(@RequestBody UploadSample uploadSample) {
+    public String checkSample(@RequestBody @Validated UploadSample uploadSample) {
         UploadFileRet uploadFileRet = metadataService.verifyPartialHash(uploadSample);
         return WebResult.success(uploadFileRet);
     }
@@ -134,16 +134,25 @@ public class OssSdkController {
         return WebResult.failWithMsg("UserNode not found");
     }
 
+    @Operation(summary = "获取 channel", description = "N")
+    @GetMapping(value = "/channel", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getChannel(@RequestParam("channelCode") int channelCode) {
+        ObjectChannel objectChannel = ossClientService.getChannelByCode(channelCode);
+        return WebResult.success(objectChannel);
+    }
+
     @Operation(summary = "设置对象的可见范围", description = "N")
     @PostMapping(value = "/object/scope", produces = MediaType.APPLICATION_JSON_VALUE)
-    public String setObjectScope(@RequestBody FileInitRequest req) {
+    public String setObjectScope(@RequestBody @Validated FileInitRequest req) {
         String objectId = req.getObjectId();
+        int scope = ObjectScope.PUBLIC.getCode();
+        ossClientService.setObjectScope(objectId, scope);
         return WebResult.success();
     }
 
     @Operation(summary = "删除对象", description = "N")
     @PostMapping(value = "/object/delete", produces = MediaType.APPLICATION_JSON_VALUE)
-    public String deleteObject(@RequestBody FileInitRequest req) {
+    public String deleteObject(@RequestBody @Validated FileInitRequest req) {
         String objectId = req.getObjectId();
         return WebResult.success();
     }
@@ -151,21 +160,14 @@ public class OssSdkController {
     @Operation(summary = "获取对象信息", description = "N")
     @GetMapping(value = "/object/get", produces = MediaType.APPLICATION_JSON_VALUE)
     public String getObjectInfo(@RequestParam("objectId") String objectId) {
-        ObjectInfo objectInfo = consoleService.getObjectInfo(objectId);
+        ObjectInfo objectInfo = ossClientService.getObjectInfo(objectId);
         return WebResult.success(objectInfo);
     }
 
     @Operation(summary = "获取视频对象信息", description = "N")
-    @GetMapping(value = "/video/get", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(value = "/object/get/video", produces = MediaType.APPLICATION_JSON_VALUE)
     public String getVideoInfo(@RequestParam("objectId") String objectId) {
-        VideoInfo videoInfo = consoleService.getVideoInfo(objectId);
+        VideoInfo videoInfo = ossClientService.getVideoInfo(objectId);
         return WebResult.success(videoInfo);
     }
-
-    @Operation(summary = "获取图片对象信息", description = "N")
-    @GetMapping(value = "/image/get", produces = MediaType.APPLICATION_JSON_VALUE)
-    public String getImageInfo(@RequestParam("objectId") String objectId) {
-        ImageInfo imageInfo = consoleService.getImageInfo(objectId);
-        return WebResult.success(imageInfo);
-    }
 }

+ 6 - 1
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/FileMetaMapper.java

@@ -1,22 +1,27 @@
 package cn.reghao.oss.mgr.db.mapper;
 
+import cn.reghao.oss.api.dto.ObjectInfo;
 import cn.reghao.oss.api.dto.ObjectMeta;
 import cn.reghao.oss.mgr.model.po.FileMeta;
 import cn.reghao.jutil.jdk.web.db.BaseMapper;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 
+import java.util.List;
+
 /**
  * @author reghao
  * @date 2022-11-24 11:11:06
  */
 @Mapper
 public interface FileMetaMapper extends BaseMapper<FileMeta> {
+    void updateSha256sum(@Param("objectId") String objectId, @Param("sha256sum") String sha256sum);
     void updateScopeByObjectName(@Param("scope") int scope, @Param("objectName") String objectName);
     void updateScopeByObjectId(@Param("objectId") String objectId, @Param("scope") int scope);
 
-    FileMeta findBySha256sum(String sha256sum);
+    List<FileMeta> findBySha256sum(String sha256sum);
     FileMeta findByObjectId(String objectId);
     ObjectMeta findObjectMetaByName(@Param("objectName") String objectName, @Param("owner") long owner);
     ObjectMeta findObjectMetaById(@Param("objectId") String objectId, @Param("owner") long owner);
+    ObjectInfo findObjectInfoById(@Param("objectId") String objectId);
 }

+ 6 - 1
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/UploadTaskMapper.java

@@ -3,6 +3,7 @@ package cn.reghao.oss.mgr.db.mapper;
 import cn.reghao.jutil.jdk.web.db.BaseMapper;
 import cn.reghao.oss.mgr.model.po.UploadTask;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
 
@@ -12,7 +13,11 @@ import java.util.List;
  */
 @Mapper
 public interface UploadTaskMapper extends BaseMapper<UploadTask> {
-    void updateSetUploaded(String uploadId);
+    void updateTask(@Param("uploadId") String uploadId,
+                    @Param("status") int status,
+                    @Param("host") String host,
+                    @Param("httpPort") int httpPort);
+    void updateStatus(@Param("uploadId") String uploadId, @Param("status") int status);
     void deleteByUploadId(String uploadId);
 
     UploadTask findByUploadId(String uploadId);

+ 6 - 1
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/repository/ObjectRepository.java

@@ -54,6 +54,11 @@ public class ObjectRepository {
         return dataBlock;
     }
 
+    public FileMeta getFileMetaBySha256sum(String sha256sum) {
+        List<FileMeta> list = fileMetaMapper.findBySha256sum(sha256sum);
+        return list.isEmpty() ? null : list.getFirst();
+    }
+
     //@Cacheable(cacheNames = "oss:store:objectMeta", key = "#objectName+'-'+#owner", unless = "#result == null")
     @Cacheable(cacheNames = "oss:store:objectMeta", key = "#objectName", unless = "#result == null")
     public ObjectMeta getObjectMetaByName(String objectName, long owner) {
@@ -68,7 +73,7 @@ public class ObjectRepository {
     }
 
     public ObjectInfo getObjectInfo(String objectId) {
-        return null;
+        return fileMetaMapper.findObjectInfoById(objectId);
     }
 
     public FileMeta getFileMeta(String objectId) {

+ 6 - 13
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/DataBlock.java

@@ -1,8 +1,7 @@
 package cn.reghao.oss.mgr.model.po;
 
 import cn.reghao.jutil.jdk.web.db.BaseObject;
-import cn.reghao.oss.api.dto.OssUploadResultDTO;
-import cn.reghao.oss.api.dto.UploadedFile;
+import cn.reghao.oss.api.dto.UploadDoneResult;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
@@ -31,16 +30,10 @@ public class DataBlock extends BaseObject<Integer> {
     @NotNull
     private Long size;
 
-    public DataBlock(OssUploadResultDTO physicalFile) {
-        this.hostPort = physicalFile.getHostPort();
-        this.absolutePath = physicalFile.getAbsolutePath();
-        this.size = physicalFile.getSize();
-    }
-
-    public DataBlock(UploadedFile uploadedFile) {
-        this.sha256sum = uploadedFile.getSha256sum();
-        this.hostPort = uploadedFile.getHostPort();
-        this.absolutePath = uploadedFile.getAbsolutePath();
-        this.size = uploadedFile.getSize();
+    public DataBlock(UploadDoneResult uploadDoneResult) {
+        this.sha256sum = uploadDoneResult.getSha256sum();
+        this.hostPort = uploadDoneResult.getHostPort();
+        this.absolutePath = uploadDoneResult.getAbsolutePath();
+        this.size = uploadDoneResult.getSize();
     }
 }

+ 24 - 25
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/FileMeta.java

@@ -2,8 +2,8 @@ package cn.reghao.oss.mgr.model.po;
 
 import cn.reghao.jutil.jdk.web.db.BaseObject;
 import cn.reghao.oss.api.constant.ObjectScope;
-import cn.reghao.oss.api.dto.OssUploadResultDTO;
-import cn.reghao.oss.api.dto.UploadedFile;
+import cn.reghao.oss.api.dto.FastUploadResult;
+import cn.reghao.oss.api.dto.UploadResult;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
@@ -63,34 +63,33 @@ public class FileMeta extends BaseObject<Integer> {
         this.scope = scope;
     }
 
-    public FileMeta(OssUploadResultDTO dto) {
-        this.objectName = dto.getObjectName();
-        this.objectId = dto.getObjectId();
-        this.filename = dto.getFilename();
-        this.size = dto.getSize();
-        this.fileType = 1001;
-        this.contentType = dto.getContentType();
-        this.sha256sum = dto.getSha256sum();
-        this.pid = "111";
-        this.uploadBy = dto.getUploadBy();
-        this.scope = ObjectScope.PUBLIC.getCode();
+    public FileMeta(UploadResult uploadResult, int fileType, ObjectScope objectScope) {
+        this.objectName = uploadResult.getObjectName();
+        this.objectId = uploadResult.getObjectId();
+        this.filename = uploadResult.getFilename();
+        this.size = uploadResult.getSize();
+        this.fileType = fileType;
+        this.contentType = uploadResult.getContentType();
+        this.sha256sum = uploadResult.getSha256sum();
+        this.pid = "0";
+        this.uploadBy = uploadResult.getUploadBy();
+        this.scope = objectScope.getCode();
     }
 
-    public FileMeta(UploadTask uploadTask, UploadedFile uploadedFile, String objectName, int fileType) {
-        this.objectName = objectName;
-        this.objectId = uploadedFile.getUploadId();
-        this.filename = uploadedFile.getFilename();
-        this.size = uploadedFile.getSize();
-        this.fileType = fileType;
-        this.contentType = uploadedFile.getContentType();
-        this.sha256sum = uploadedFile.getSha256sum();
+    public FileMeta(FastUploadResult fastUploadResult, FileMeta fileMeta, ObjectScope objectScope) {
+        this.objectName = fastUploadResult.getObjectName();
+        this.objectId = fastUploadResult.getObjectId();
+        this.filename = fastUploadResult.getFilename();
+        this.size = fileMeta.getSize();
+        this.fileType = fileMeta.getFileType();
+        this.contentType = fileMeta.getContentType();
+        this.sha256sum = fastUploadResult.getSha256sum();
         this.pid = "0";
-        this.uploadBy = uploadTask.getUploadBy();
-        this.scope = ObjectScope.PUBLIC.getCode();
+        this.uploadBy = fastUploadResult.getUploadBy();
+        this.scope = objectScope.getCode();
     }
 
-    public FileMeta(FileMeta fileMeta, String objectId, String objectName, String filename,
-                    long uploadBy, int scope) {
+    public FileMeta(FileMeta fileMeta, String objectId, String objectName, String filename, long uploadBy, int scope) {
         this.objectName = objectName;
         this.objectId = objectId;
         this.filename = filename;

+ 8 - 1
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/UploadTask.java

@@ -9,16 +9,18 @@ import lombok.NoArgsConstructor;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Size;
+import lombok.Setter;
 
 import java.time.LocalDateTime;
 
 /**
- * 文件分片详情
+ * 上传任务
  *
  * @author reghao
  * @date 2024-10-24 16:33:16
  */
 @NoArgsConstructor
+@Setter
 @Getter
 public class UploadTask extends BaseObject<Integer> {
     @NotBlank
@@ -43,7 +45,11 @@ public class UploadTask extends BaseObject<Integer> {
     @NotNull
     private LocalDateTime expireTime;
     @NotNull
+    private Integer channelCode;
+    @NotNull
     private Long uploadBy;
+    private String host;
+    private int httpPort;
 
     public UploadTask(String uploadId, UploadPrepare uploadPrepare, long offset, int length, int splitSize, LocalDateTime expireTime) {
         this.uploadId = uploadId;
@@ -55,6 +61,7 @@ public class UploadTask extends BaseObject<Integer> {
         this.fileSize = uploadPrepare.getSize();
         this.status = 1;
         this.expireTime = expireTime;
+        this.channelCode = uploadPrepare.getChannelCode();
         this.uploadBy = UserContext.getUserId();
     }
 }

+ 51 - 164
oss-mgr/src/main/java/cn/reghao/oss/mgr/rpc/ConsoleServiceImpl.java

@@ -1,33 +1,21 @@
 package cn.reghao.oss.mgr.rpc;
 
-import cn.reghao.oss.api.constant.ObjectAction;
 import cn.reghao.oss.api.constant.ObjectScope;
 import cn.reghao.oss.api.constant.ObjectType;
 import cn.reghao.oss.api.dto.*;
-import cn.reghao.oss.api.dto.media.ImageInfo;
-import cn.reghao.oss.api.dto.media.VideoInfo;
 import cn.reghao.oss.api.iface.ConsoleService;
-import cn.reghao.oss.api.iface.StoreService;
-import cn.reghao.oss.api.util.JwtUtils;
-import cn.reghao.oss.mgr.config.UserContext;
 import cn.reghao.oss.mgr.db.mapper.DataBlockMapper;
 import cn.reghao.oss.mgr.db.mapper.FileMetaMapper;
 import cn.reghao.oss.mgr.db.mapper.UploadTaskMapper;
 import cn.reghao.oss.mgr.db.repository.ObjectRepository;
 import cn.reghao.oss.mgr.model.po.*;
-import cn.reghao.oss.mgr.service.StoreConfigService;
 import cn.reghao.oss.mgr.service.StoreNodeService;
 import cn.reghao.oss.mgr.service.UploadChannelService;
 import cn.reghao.oss.mgr.service.UserNodeService;
-import cn.reghao.oss.mgr.util.StringUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.dubbo.config.annotation.DubboService;
 import org.springframework.stereotype.Service;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
 /**
  * @author reghao
  * @date 2024-07-02 10:58:51
@@ -40,23 +28,18 @@ public class ConsoleServiceImpl implements ConsoleService {
     private final ObjectRepository objectRepository;
     private final UploadChannelService uploadChannelService;
     private final UserNodeService userNodeService;
-    private final StoreConfigService storeConfigService;
-    private final RpcService rpcService;
     private final UploadTaskMapper uploadTaskMapper;
     private final FileMetaMapper fileMetaMapper;
     private final DataBlockMapper dataBlockMapper;
 
     public ConsoleServiceImpl(StoreNodeService storeNodeService, ObjectRepository objectRepository,
                               UploadChannelService uploadChannelService, UserNodeService userNodeService,
-                              StoreConfigService storeConfigService, RpcService rpcService,
                               UploadTaskMapper uploadTaskMapper, FileMetaMapper fileMetaMapper,
                               DataBlockMapper dataBlockMapper) {
         this.storeNodeService = storeNodeService;
         this.objectRepository = objectRepository;
         this.uploadChannelService = uploadChannelService;
         this.userNodeService = userNodeService;
-        this.storeConfigService = storeConfigService;
-        this.rpcService = rpcService;
         this.uploadTaskMapper = uploadTaskMapper;
         this.fileMetaMapper = fileMetaMapper;
         this.dataBlockMapper = dataBlockMapper;
@@ -67,124 +50,37 @@ public class ConsoleServiceImpl implements ConsoleService {
         storeNodeService.addOrUpdate(storeNodeDto);
     }
 
-    @Override
-    public NodeProperties getNodeProperties(String domain) {
-        UserNode userNode = userNodeService.getUserNodeByDomain(domain);
-        if (userNode != null) {
-            String secretKey = userNode.getDomain();
-            String referer = userNode.getReferer();
-            long owner = userNode.getCreateBy();
-            return new NodeProperties(domain, secretKey, referer, (int) owner);
-        }
-
-        return null;
-    }
-
-    @Override
-    public ObjectChannel getChannelByCode(int owner, int channelCode) {
-        return uploadChannelService.getObjectChannelByChannelCode(channelCode, owner);
-    }
-
-    @Override
-    public Integer getChannelCodeByUrl(int owner, String url) {
-        List<UploadChannel> uploadChannels = uploadChannelService.getUploadChannelsByCreateBy(owner);
-        String url1 = url.replace("//", "");
-        int idx = url1.indexOf("/");
-        String objectName = url1.substring(idx+1);
-        for (UploadChannel uploadChannel : uploadChannels) {
-            if (objectName.startsWith(uploadChannel.getPrefix())) {
-                return uploadChannel.getChannelCode();
-            }
-        }
-
-        return -1;
-    }
-
-    @Override
-    public ServerInfo getUploadStore(int channelCode) {
-        long ossUser = storeConfigService.getLocalOssUser();
-        UploadChannel uploadChannel = uploadChannelService.getByChannelCodeAndCreateBy(channelCode, ossUser);
-        if (uploadChannel == null) {
-            String errMsg = String.format("channelCode %s not exist", channelCode);
-            log.error("{}", errMsg);
-            return null;
-        }
-
-        UserNode userNode = userNodeService.getUserNode(uploadChannel.getUserNodeId());
-        if (userNode == null) {
-            String errMsg = String.format("channel_code %s not associate with any store_node", uploadChannel.getId());
-            log.error("{}", errMsg);
-            return null;
-        }
-
-        String protocol = userNode.getProtocol();
-        String domain = userNode.getDomain();
-        String ossUrl = String.format("%s://%s", protocol, domain);
-        long maxSize = uploadChannel.getMaxSize();
-        long loginUser = UserContext.getUserId();
-        String channelPrefix = uploadChannel.getPrefix();
-        //String contentType = uploadChannel.getFileType();
-        // token 1h 后过期
-        long expireAt = System.currentTimeMillis() + 3600_000L;
-        Map<String, Object> map = new HashMap<>();
-        map.put("action", ObjectAction.upload.name());
-        map.put("uploadBy", loginUser);
-        map.put("expireAt", expireAt);
-        map.put("channelPrefix", channelPrefix);
-        //map.put("contentType", contentType);
-        map.put("maxSize", maxSize);
-        String uploadToken = JwtUtils.createToken(map, expireAt);
-        return new ServerInfo(ossUrl, channelCode, maxSize, uploadToken);
-    }
-
     @Override
     public boolean checkExists(String sha256sum) {
-        log.info("检查文件是否存在...");
         return objectRepository.getBySha256sum(sha256sum) != null;
     }
 
     @Override
-    public void bindOnly(ObjectBindDTO objectBindDTO) {
-        String sha256sum = objectBindDTO.getSha256sum();
-        String objectId = objectBindDTO.getObjectId();
-        String objectName = objectBindDTO.getObjectName();
-        String filename = objectBindDTO.getFilename();
-        log.info("添加已存在的文件...");
-    }
-
-    @Override
-    public void registerAndBind(OssUploadResultDTO dto) {
-        log.info("添加新文件...");
-
-        FileMeta fileMeta = new FileMeta(dto);
-        DataBlock dataBlock = new DataBlock(dto);
-        log.info("registry new object with sha256sum -> {}", dto.getSha256sum());
-        objectRepository.saveObject(fileMeta, List.of(dataBlock));
-    }
-
-    public ObjectMeta getObjectMeta(String httpHost, String objectName) {
-        UserNode userNode = userNodeService.getUserNodeByDomain(httpHost);
-        long uploadBy = userNode.getCreateBy();
-        ObjectMeta result = objectRepository.getObjectMetaByName(objectName, uploadBy);
-        //log.info("Object Instance: {}, Name: {}", System.identityHashCode(result), result.getObjectName());
-        return result;
-    }
-
-    public boolean validateMultipart(UploadedFile uploadedFile) {
-        String uploadId = uploadedFile.getUploadId();
-        String sha256sum = uploadedFile.getSha256sum();
-        UploadTask uploadTask = uploadTaskMapper.findByUploadId(uploadId);
-        if (uploadTask == null || !uploadTask.getSha256sum().equals(sha256sum)) {
-            return false;
+    public void bindOnly(FastUploadResult fastUploadResult) {
+        String sha256sum = fastUploadResult.getSha256sum();
+        FileMeta fileMeta0 = objectRepository.getFileMetaBySha256sum(sha256sum);
+        if (fileMeta0 == null) {
+            log.error("sha256sum {} not found any FileMeta", sha256sum);
+            return;
         }
 
-        String filename = uploadedFile.getFilename();
-        String suffix = StringUtil.getSuffix(filename);
-        String channelPrefix = uploadedFile.getChannelPrefix();
-        String objectName = String.format("%s%s.%s", channelPrefix, uploadId, suffix);
+        ObjectScope objectScope = ObjectScope.PUBLIC;
+        FileMeta fileMeta = new FileMeta(fastUploadResult, fileMeta0, objectScope);
+        objectRepository.saveFileMeta(fileMeta);
+    }
 
-        String contentType = uploadedFile.getContentType();
-        int fileType = ObjectType.Image.getValue();
+    @Override
+    public void registerAndBind(UploadResult uploadResult) {
+        String uploadId = uploadResult.getObjectId();
+        int status = uploadResult.getUploadStatus();
+        String hostPort = uploadResult.getHostPort();
+        String[] arr = hostPort.split(":");
+        String host = arr[0];
+        int port = Integer.parseInt(arr[1]);
+        uploadTaskMapper.updateTask(uploadId, status, host, port);
+
+        String contentType = uploadResult.getContentType();
+        int fileType = ObjectType.Any.getValue();
         if (contentType.startsWith("video")) {
             fileType = ObjectType.Video.getValue();
         } else if (contentType.startsWith("audio")) {
@@ -193,55 +89,46 @@ public class ConsoleServiceImpl implements ConsoleService {
             fileType = ObjectType.Text.getValue();
         } else if (contentType.startsWith("application")) {
             fileType = ObjectType.Other.getValue();
-        } else {
-            fileType = ObjectType.Any.getValue();
         }
 
-        FileMeta fileMeta = new FileMeta(uploadTask, uploadedFile, objectName, fileType);
-        DataBlock dataBlock = new DataBlock(uploadedFile);
-        objectRepository.saveObject(fileMeta, List.of(dataBlock));
-        uploadTaskMapper.updateSetUploaded(uploadId);
-        return true;
-    }
-
-    public void updatePath(String sha256sum, String path) {
-        dataBlockMapper.updatePath(sha256sum, path);
-    }
-
-    @Override
-    public void deleteObject(String objectId) {
-        DataBlock dataBlock = dataBlockMapper.findByObjectId(objectId);
-        String hostPort = dataBlock.getHostPort();
-
-        StoreService storeService = rpcService.getStoreService(hostPort);
-        storeService.deleteFile(dataBlock.getAbsolutePath());
+        long uploadBy = uploadResult.getUploadBy();
+        String channelPrefix = uploadResult.getChannelPrefix();
+        UploadChannel uploadChannel = uploadChannelService.getByChannelPrefixAndCreateBy(channelPrefix, uploadBy);
+        ObjectScope objectScope = ObjectScope.getByCode(uploadChannel.getScope());
 
-        objectRepository.deleteObject(objectId);
+        FileMeta fileMeta = new FileMeta(uploadResult, fileType, objectScope);
+        fileMetaMapper.save(fileMeta);
     }
 
     @Override
-    public void setObjectScope(String objectId, int scope) {
-        ObjectScope objectScope = ObjectScope.getByCode(scope);
-        fileMetaMapper.updateScopeByObjectId(objectId, objectScope.getCode());
-    }
+    public void updateAfterMove(UploadDoneResult uploadDoneResult) {
+        String uploadId = uploadDoneResult.getObjectId();
+        String sha256sum = uploadDoneResult.getSha256sum();
+
+        String objectId = uploadDoneResult.getObjectId();
+        FileMeta fileMeta = fileMetaMapper.findByObjectId(objectId);
+        if (fileMeta == null) {
+            return;
+        }
 
-    @Override
-    public ObjectInfo getObjectInfo(String objectId) {
-        return objectRepository.getObjectInfo(objectId);
-    }
+        fileMetaMapper.updateSha256sum(objectId, sha256sum);
+        DataBlock dataBlock = new DataBlock(uploadDoneResult);
+        dataBlockMapper.save(dataBlock);
 
-    @Override
-    public VideoInfo getVideoInfo(String objectId) {
-        DataBlock dataBlock = dataBlockMapper.findByObjectId(objectId);
-        String hostPort = dataBlock.getHostPort();
-        StoreService storeService = rpcService.getStoreService(hostPort);
+        UploadTask uploadTask = uploadTaskMapper.findByUploadId(uploadId);
+        if (uploadTask == null || !uploadTask.getSha256sum().equals(sha256sum)) {
+            return;
+        }
 
-        VideoInfo videoInfo = storeService.getVideoInfo(dataBlock.getAbsolutePath());
-        return videoInfo;
+        int status = uploadDoneResult.getUploadStatus();
+        uploadTaskMapper.updateStatus(uploadId, status);
     }
 
     @Override
-    public ImageInfo getImageInfo(String objectId) {
-        return null;
+    public ObjectMeta getObjectMeta(String httpHost, String objectName) {
+        UserNode userNode = userNodeService.getUserNodeByDomain(httpHost);
+        long uploadBy = userNode.getCreateBy();
+        ObjectMeta result = objectRepository.getObjectMetaByName(objectName, uploadBy);
+        return result;
     }
 }

+ 5 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/rpc/RpcService.java

@@ -33,6 +33,11 @@ public class RpcService {
         return remoteService.getService(hostPort, StoreService.class);
     }
 
+    public StoreService getStoreService(String host, int port) {
+        RemoteService<StoreService> remoteService = new RemoteService<>();
+        return remoteService.getService(host, port, StoreService.class);
+    }
+
     static class RemoteService<T> {
         public T getService(String hostPort, Class<T> clazz) {
             ReferenceConfig<?> config = CACHE.computeIfAbsent(hostPort, key -> {

+ 18 - 21
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/MetadataService.java

@@ -1,6 +1,5 @@
 package cn.reghao.oss.mgr.service;
 
-import cn.reghao.oss.api.dto.ServerInfo;
 import cn.reghao.oss.api.dto.rest.UploadFileRet;
 import cn.reghao.oss.api.dto.rest.UploadPrepare;
 import cn.reghao.oss.api.dto.rest.UploadPrepareRet;
@@ -10,9 +9,8 @@ import cn.reghao.oss.mgr.db.mapper.FileMetaMapper;
 import cn.reghao.oss.mgr.db.mapper.StoreNodeMapper;
 import cn.reghao.oss.mgr.db.mapper.UploadChannelMapper;
 import cn.reghao.oss.mgr.db.mapper.UploadTaskMapper;
-import cn.reghao.oss.mgr.db.repository.ChannelRepository;
 import cn.reghao.oss.mgr.db.repository.ObjectRepository;
-import cn.reghao.oss.mgr.model.dto.UploadSample;
+import cn.reghao.oss.api.dto.rest.UploadSample;
 import cn.reghao.oss.mgr.model.po.*;
 import cn.reghao.oss.mgr.rpc.RpcService;
 import cn.reghao.oss.mgr.util.StringUtil;
@@ -20,6 +18,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
+import java.util.List;
 import java.util.Random;
 import java.util.UUID;
 
@@ -38,7 +37,6 @@ public class MetadataService {
     private RpcService rpcService;
     private FileMetaMapper fileMetaMapper;
     private UploadChannelMapper uploadChannelMapper;
-    private ChannelRepository channelRepository;
 
     public MetadataService(ObjectRepository objectRepository, StoreNodeMapper storeNodeMapper,
                            UploadTaskMapper uploadTaskMapper, RpcService rpcService, FileMetaMapper fileMetaMapper,
@@ -100,27 +98,26 @@ public class MetadataService {
             result = serverPartMd5.equalsIgnoreCase(clientPartMd5);
             if (result) {
                 long uploadBy = UserContext.getUserId();
-                String channelPrefix = "video/playback/";
-                UploadChannel uploadChannel = uploadChannelMapper.findByCreateByAndPrefix(uploadBy, channelPrefix);
+                int channelCode = uploadTask.getChannelCode();
+                UploadChannel uploadChannel = uploadChannelMapper.findByCreateByAndChannelCode(uploadBy, channelCode);
+                String channelPrefix = uploadChannel.getPrefix();
+                List<FileMeta> fileMetaList = fileMetaMapper.findBySha256sum(sha256sum);
+                if (!fileMetaList.isEmpty()) {
+                    FileMeta fileMeta0 = fileMetaList.get(0);
+                    String objectId = uploadTask.getUploadId();
+                    String filename = uploadTask.getFilename();
+                    String suffix = StringUtil.getSuffix(filename);
+                    String objectName = String.format("%s%s.%s", channelPrefix, uploadId, suffix);
+                    int scope = uploadChannel.getScope();
 
-                FileMeta fileMeta0 = fileMetaMapper.findBySha256sum(sha256sum);
-                String objectId = uploadTask.getUploadId();
-                String filename = uploadTask.getFilename();
-                String suffix = StringUtil.getSuffix(filename);
-                String objectName = String.format("%s%s.%s", channelPrefix, uploadId, suffix);
-                int scope = uploadChannel.getScope();
-
-                FileMeta fileMeta = new FileMeta(fileMeta0, objectId, objectName, filename, uploadBy, scope);
-                fileMetaMapper.save(fileMeta);
+                    FileMeta fileMeta = new FileMeta(fileMeta0, objectId, objectName, filename, uploadBy, scope);
+                    fileMetaMapper.save(fileMeta);
+                } else {
+                    log.error("sha256sum {} not found any FileMeta", sha256sum);
+                }
             }
         }
 
         return new UploadFileRet(uploadId, result);
     }
-
-    public ServerInfo getServerInfo() {
-        int channelCode = 101;
-        UploadChannel uploadChannel = channelRepository.getByChannelCode(channelCode);
-        return null;
-    }
 }

+ 195 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/OssClientService.java

@@ -0,0 +1,195 @@
+package cn.reghao.oss.mgr.service;
+
+import cn.reghao.oss.api.constant.ObjectAction;
+import cn.reghao.oss.api.constant.ObjectScope;
+import cn.reghao.oss.api.constant.UploadStatus;
+import cn.reghao.oss.api.dto.*;
+import cn.reghao.oss.api.dto.media.VideoInfo;
+import cn.reghao.oss.api.iface.StoreService;
+import cn.reghao.oss.api.util.JwtUtils;
+import cn.reghao.oss.mgr.config.UserContext;
+import cn.reghao.oss.mgr.db.mapper.DataBlockMapper;
+import cn.reghao.oss.mgr.db.mapper.FileMetaMapper;
+import cn.reghao.oss.mgr.db.mapper.UploadTaskMapper;
+import cn.reghao.oss.mgr.db.repository.ObjectRepository;
+import cn.reghao.oss.mgr.model.po.*;
+import cn.reghao.oss.mgr.rpc.RpcService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * cn.reghao.oss.sdk.OssClient 的后端实现
+ *
+ * @author reghao
+ * @date 2026-04-30 15:45:09
+ */
+@Slf4j
+@Service
+public class OssClientService {
+    private final StoreNodeService storeNodeService;
+    private final ObjectRepository objectRepository;
+    private final UploadChannelService uploadChannelService;
+    private final UserNodeService userNodeService;
+    private final StoreConfigService storeConfigService;
+    private final RpcService rpcService;
+    private final UploadTaskMapper uploadTaskMapper;
+    private final FileMetaMapper fileMetaMapper;
+    private final DataBlockMapper dataBlockMapper;
+
+    public OssClientService(StoreNodeService storeNodeService, ObjectRepository objectRepository,
+                              UploadChannelService uploadChannelService, UserNodeService userNodeService,
+                              StoreConfigService storeConfigService, RpcService rpcService,
+                              UploadTaskMapper uploadTaskMapper, FileMetaMapper fileMetaMapper,
+                              DataBlockMapper dataBlockMapper) {
+        this.storeNodeService = storeNodeService;
+        this.objectRepository = objectRepository;
+        this.uploadChannelService = uploadChannelService;
+        this.userNodeService = userNodeService;
+        this.storeConfigService = storeConfigService;
+        this.rpcService = rpcService;
+        this.uploadTaskMapper = uploadTaskMapper;
+        this.fileMetaMapper = fileMetaMapper;
+        this.dataBlockMapper = dataBlockMapper;
+    }
+
+
+    public ObjectChannel getChannelByCode(int channelCode) {
+        long loginUser = UserContext.getUserId();
+        return uploadChannelService.getObjectChannelByChannelCode(channelCode, loginUser);
+    }
+
+    public ServerInfo getUploadStore(int channelCode) {
+        long ossUser = storeConfigService.getLocalOssUser();
+        UploadChannel uploadChannel = uploadChannelService.getByChannelCodeAndCreateBy(channelCode, ossUser);
+        if (uploadChannel == null) {
+            String errMsg = String.format("channelCode %s not exist", channelCode);
+            log.error("{}", errMsg);
+            return null;
+        }
+
+        UserNode userNode = userNodeService.getUserNode(uploadChannel.getUserNodeId());
+        if (userNode == null) {
+            String errMsg = String.format("channel_code %s not associate with any store_node", uploadChannel.getId());
+            log.error("{}", errMsg);
+            return null;
+        }
+
+        String protocol = userNode.getProtocol();
+        String domain = userNode.getDomain();
+        String ossUrl = String.format("%s://%s", protocol, domain);
+        long maxSize = uploadChannel.getMaxSize();
+        long loginUser = UserContext.getUserId();
+        String channelPrefix = uploadChannel.getPrefix();
+        //String contentType = uploadChannel.getFileType();
+        // token 1h 后过期
+        long expireAt = System.currentTimeMillis() + 3600_000L;
+        Map<String, Object> map = new HashMap<>();
+        map.put("action", ObjectAction.upload.name());
+        map.put("uploadBy", loginUser);
+        map.put("expireAt", expireAt);
+        map.put("channelPrefix", channelPrefix);
+        //map.put("contentType", contentType);
+        map.put("maxSize", maxSize);
+        String uploadToken = JwtUtils.createToken(map, expireAt);
+        return new ServerInfo(ossUrl, channelCode, maxSize, uploadToken);
+    }
+
+    public ObjectMeta getObjectMeta(String httpHost, String objectName) {
+        UserNode userNode = userNodeService.getUserNodeByDomain(httpHost);
+        long uploadBy = userNode.getCreateBy();
+        ObjectMeta result = objectRepository.getObjectMetaByName(objectName, uploadBy);
+        return result;
+    }
+
+    public void deleteObject(String objectId) {
+        DataBlock dataBlock = dataBlockMapper.findByObjectId(objectId);
+        String hostPort = dataBlock.getHostPort();
+
+        StoreService storeService = rpcService.getStoreService(hostPort);
+        storeService.deleteFile(dataBlock.getAbsolutePath());
+
+        objectRepository.deleteObject(objectId);
+    }
+
+    public void setObjectScope(String objectId, int scope) {
+        ObjectScope objectScope = ObjectScope.getByCode(scope);
+        fileMetaMapper.updateScopeByObjectId(objectId, objectScope.getCode());
+    }
+
+    public ObjectInfo getObjectInfo(String objectId) {
+        String host = "127.0.0.1";
+        int httpPort = 0;
+        String absolutePath = "";
+        DataBlock dataBlock = dataBlockMapper.findByObjectId(objectId);
+        if (dataBlock == null) {
+            UploadTask uploadTask = uploadTaskMapper.findByUploadId(objectId);
+            if (uploadTask != null && uploadTask.getStatus() == UploadStatus.FLUSHING.getCode()) {
+                host = uploadTask.getHost();
+                httpPort = uploadTask.getHttpPort();
+            } else {
+                String errorMsg = String.format("object with id {} not exist", objectId);
+                throw new RuntimeException(errorMsg);
+            }
+        } else {
+            String hostPort = dataBlock.getHostPort();
+            String[] arr = hostPort.split(":");
+            host = arr[0];
+            httpPort = Integer.parseInt(arr[1]);
+            absolutePath = dataBlock.getAbsolutePath();
+        }
+
+        StoreNode storeNode = storeNodeService.getStoreNode(host, httpPort);
+        FileMeta fileMeta = fileMetaMapper.findByObjectId(objectId);
+        String objectName = fileMeta.getObjectName();
+        int storeNodeId = storeNode.getId();
+        long uploadBy = fileMeta.getUploadBy();
+        UserNode userNode = userNodeService.getUserNode(uploadBy, storeNodeId);
+        String domain = userNode.getDomain();
+        String objectUrl = String.format("//%s/%s", domain, objectName);
+
+        ObjectInfo objectInfo = objectRepository.getObjectInfo(objectId);
+        objectInfo.setUrl(objectUrl);
+        return objectInfo;
+    }
+
+    public VideoInfo getVideoInfo(String objectId) {
+        String host = "127.0.0.1";
+        int httpPort = 0;
+        String absolutePath = "";
+        DataBlock dataBlock = dataBlockMapper.findByObjectId(objectId);
+        if (dataBlock == null) {
+            UploadTask uploadTask = uploadTaskMapper.findByUploadId(objectId);
+            if (uploadTask != null && uploadTask.getStatus() == UploadStatus.FLUSHING.getCode()) {
+                host = uploadTask.getHost();
+                httpPort = uploadTask.getHttpPort();
+            } else {
+                String errorMsg = String.format("object with id {} not exist", objectId);
+                throw new RuntimeException(errorMsg);
+            }
+        } else {
+            String hostPort = dataBlock.getHostPort();
+            String[] arr = hostPort.split(":");
+            host = arr[0];
+            httpPort = Integer.parseInt(arr[1]);
+            absolutePath = dataBlock.getAbsolutePath();
+        }
+
+        StoreNode storeNode = storeNodeService.getStoreNode(host, httpPort);
+        StoreService storeService = rpcService.getStoreService(storeNode);
+        VideoInfo videoInfo = storeService.getVideoInfo(objectId, absolutePath);
+
+        FileMeta fileMeta = fileMetaMapper.findByObjectId(objectId);
+        String objectName = fileMeta.getObjectName();
+        int storeNodeId = storeNode.getId();
+        long uploadBy = fileMeta.getUploadBy();
+        UserNode userNode = userNodeService.getUserNode(uploadBy, storeNodeId);
+        String domain = userNode.getDomain();
+        String objectUrl = String.format("//%s/%s", domain, objectName);
+        videoInfo.setUrl(objectUrl);
+
+        return videoInfo;
+    }
+}

+ 4 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/StoreNodeService.java

@@ -174,4 +174,8 @@ public class StoreNodeService {
 
         return storeVolumes;
     }
+
+    public StoreNode getStoreNode(String host, int httpPort) {
+        return storeNodeMapper.findByNodeAddrAndHttpPort(host, httpPort);
+    }
 }

+ 4 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/UploadChannelService.java

@@ -136,6 +136,10 @@ public class UploadChannelService {
         return uploadChannelMapper.findByCreateByAndChannelCode(createBy, channelCode);
     }
 
+    public UploadChannel getByChannelPrefixAndCreateBy(String channelPrefix, long createBy) {
+        return uploadChannelMapper.findByCreateByAndPrefix(createBy, channelPrefix);
+    }
+
     public ObjectChannel getObjectChannelByChannelCode(int channelCode, long createBy) {
         UploadChannel uploadChannel = uploadChannelMapper.findByCreateByAndChannelCode(createBy, channelCode);
         if (uploadChannel == null) {

+ 4 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/UserNodeService.java

@@ -121,6 +121,10 @@ public class UserNodeService {
         return userNodeMapper.findById(userNodeId);
     }
 
+    public UserNode getUserNode(long createBy, int storeNodeId) {
+        return userNodeMapper.findByCreateByAndStoreNodeId(createBy, storeNodeId);
+    }
+
     public UserNode getUserNodeByDomain(String domain) {
         return userNodeMapper.findByDomain(domain);
     }

+ 1 - 1
oss-mgr/src/main/resources/application-dev.yml

@@ -1,5 +1,5 @@
 spring:
   datasource:
-    url: jdbc:mysql://192.168.0.209/tnb_oss_tdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8
+    url: jdbc:mysql://127.0.0.1/tnb_oss_rdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8
     username: test
     password: Test_123456

+ 1 - 1
oss-mgr/src/main/resources/mapper/DataBlockMapper.xml

@@ -6,7 +6,7 @@
         insert into data_block
         (`sha256sum`,`host_port`,`absolute_path`,`size`)
         values
-        (#{sha256sum}#{hostPort},#{absolutePath},#{size})
+        (#{sha256sum},#{hostPort},#{absolutePath},#{size})
     </insert>
     <insert id="saveAll" useGeneratedKeys="true" keyProperty="id">
         insert into data_block

+ 12 - 2
oss-mgr/src/main/resources/mapper/FileMetaMapper.xml

@@ -22,14 +22,19 @@
         where object_id=#{objectId}
     </delete>
 
+    <update id="updateSha256sum">
+        update file_meta
+        set update_time=now(),sha256sum=#{sha256sum}
+        where object_id=#{objectId}
+    </update>
     <update id="updateScopeByObjectName">
         update file_meta
-        set scope=#{scope}
+        set update_time=now(),scope=#{scope}
         where object_name=#{objectName}
     </update>
     <update id="updateScopeByObjectId">
         update file_meta
-        set scope=#{scope}
+        set update_time=now(),scope=#{scope}
         where object_id=#{objectId}
     </update>
 
@@ -67,4 +72,9 @@
         on file_meta.deleted is false and file_meta.upload_by=#{owner} and file_meta.sha256sum=data_block.sha256sum
         and file_meta.object_id=#{objectId}
     </select>
+    <select id="findObjectInfoById" resultType="cn.reghao.oss.api.dto.ObjectInfo">
+        select *
+        from file_meta
+        where deleted is false and object_id=#{objectId}
+    </select>
 </mapper>

+ 9 - 4
oss-mgr/src/main/resources/mapper/UploadTaskMapper.xml

@@ -4,14 +4,19 @@
 <mapper namespace="cn.reghao.oss.mgr.db.mapper.UploadTaskMapper">
     <insert id="save" useGeneratedKeys="true" keyProperty="id">
         insert into upload_task
-        (`upload_id`,`sha256sum`,`offset`,`length`,`split_size`,`filename`,`file_size`,`status`,`expire_time`,`upload_by`)
+        (`upload_id`,`sha256sum`,`offset`,`length`,`split_size`,`filename`,`file_size`,`status`,`expire_time`,`channel_code`,`upload_by`)
         values
-        (#{uploadId},#{sha256sum},#{offset},#{length},#{splitSize},#{filename},#{fileSize},#{status},#{expireTime},#{uploadBy})
+        (#{uploadId},#{sha256sum},#{offset},#{length},#{splitSize},#{filename},#{fileSize},#{status},#{expireTime},#{channelCode},#{uploadBy})
     </insert>
 
-    <update id="updateSetUploaded">
+    <update id="updateTask">
         update upload_task
-        set `status`=2
+        set update_time=now(),`status`=#{status},`host`=#{host},`http_port`=#{httpPort}
+        where upload_id=#{uploadId}
+    </update>
+    <update id="updateStatus">
+        update upload_task
+        set update_time=now(),`status`=#{status}
         where upload_id=#{uploadId}
     </update>
 

+ 153 - 42
oss-sdk/src/main/java/cn/reghao/oss/sdk/OssClient.java

@@ -3,15 +3,13 @@ package cn.reghao.oss.sdk;
 import cn.reghao.jutil.jdk.io.FileSplitter;
 import cn.reghao.jutil.jdk.serializer.JsonConverter;
 import cn.reghao.jutil.jdk.web.result.WebResult;
-import cn.reghao.oss.api.dto.ObjectInfo;
-import cn.reghao.oss.api.dto.ServerInfo;
-import cn.reghao.oss.api.dto.UploadFilePart;
-import cn.reghao.oss.api.dto.WebBody;
-import cn.reghao.oss.api.dto.media.ImageInfo;
+import cn.reghao.oss.api.constant.ObjectScope;
+import cn.reghao.oss.api.dto.*;
 import cn.reghao.oss.api.dto.media.VideoInfo;
 import cn.reghao.oss.api.dto.rest.UploadFileRet;
 import cn.reghao.oss.api.dto.rest.UploadPrepare;
 import cn.reghao.oss.api.dto.rest.UploadPrepareRet;
+import cn.reghao.oss.api.dto.rest.UploadSample;
 import cn.reghao.oss.api.util.OssClientSigner;
 import cn.reghao.oss.api.util.OssSamplingHash;
 import com.google.gson.reflect.TypeToken;
@@ -49,11 +47,6 @@ public class OssClient {
     }
 
     public UploadPrepareRet prepareUpload(Path filePath) throws Exception {
-        String gmtDate = OssClientSigner.getGmtDate();
-        String api = "/api/oss/sdk/prepare";
-        String contentType = "application/json";
-        String authHeader = OssClientSigner.buildAuthHeader(ak, sk,"POST", api, contentType);
-
         // 1. 计算 SHA-256 (用于后端秒传校验)
         String sha256 = OssSamplingHash.calculateFullHash(filePath);
         System.out.printf("%s 的 sha256sum %s\n", filePath, sha256);
@@ -61,12 +54,18 @@ public class OssClient {
         int channelCode = 101;
         String filename0 = filePath.getFileName().toString();
         long size = filePath.toFile().length();
-        String contentType1 = "";
+        UploadPrepare uploadPrepare = new UploadPrepare(channelCode, filename0, size, sha256);
+        return prepareUpload(uploadPrepare);
+    }
 
-        UploadPrepare uploadPrepare = new UploadPrepare(channelCode, filename0, size, sha256, contentType1);
+    public UploadPrepareRet prepareUpload(UploadPrepare uploadPrepare) throws Exception {
         String jsonBody = JsonConverter.objectToJson(uploadPrepare);
 
+        String api = "/api/oss/sdk/prepare";
         String url = String.format("%s%s", endpoint, api);
+        String gmtDate = OssClientSigner.getGmtDate();
+        String contentType = "application/json";
+        String authHeader = OssClientSigner.buildAuthHeader(ak, sk,"POST", api, contentType);
         HttpRequest request = HttpRequest.newBuilder()
                 .uri(URI.create(url))
                 .header("Date", gmtDate)
@@ -90,23 +89,24 @@ public class OssClient {
     }
 
     public UploadFileRet checkSample(Path filePath, UploadPrepareRet uploadPrepareRet) throws Exception {
-        String gmtDate = OssClientSigner.getGmtDate();
-        String api = "/api/oss/sdk/check_sample";
-        String contentType = "application/json";
-        String authHeader = OssClientSigner.buildAuthHeader(ak, sk,"POST", api, contentType);
-
         String uploadId = uploadPrepareRet.getUploadId();
         long offset = uploadPrepareRet.getOffset();
         int length = uploadPrepareRet.getLength();
         String sampleMd5 = OssSamplingHash.calculateSampleMd5(filePath.toString(), offset, length);
         System.out.printf("%s 的抽样 md5 %s\n", filePath, sampleMd5);
 
-        Map<String, String> map = new HashMap<>();
-        map.put("uploadId", uploadId);
-        map.put("sampleMd5", sampleMd5);
-        String jsonBody = JsonConverter.objectToJson(map);
+        UploadSample uploadSample = new UploadSample(uploadId, sampleMd5);
+        return checkSample(uploadSample);
+    }
+
+    public UploadFileRet checkSample(UploadSample uploadSample) throws Exception {
+        String jsonBody = JsonConverter.objectToJson(uploadSample);
 
+        String api = "/api/oss/sdk/check_sample";
         String url = String.format("%s%s", endpoint, api);
+        String gmtDate = OssClientSigner.getGmtDate();
+        String contentType = "application/json";
+        String authHeader = OssClientSigner.buildAuthHeader(ak, sk,"POST", api, contentType);
         HttpRequest request = HttpRequest.newBuilder()
                 .uri(URI.create(url))
                 .header("Date", gmtDate)
@@ -134,16 +134,15 @@ public class OssClient {
         return uploadFileRet;
     }
 
-    public ServerInfo getServerInfo() throws Exception {
-        String gmtDate = OssClientSigner.getGmtDate();
+    public ServerInfo getServerInfo(int channelCode) throws Exception {
+        Map<String, Object> body = Map.of("channelCode", channelCode);
+        String jsonBody = JsonConverter.objectToJson(body);
+
         String api = "/api/oss/sdk/upload_request";
+        String url = String.format("%s%s", endpoint, api);
+        String gmtDate = OssClientSigner.getGmtDate();
         String contentType = "application/json";
         String authHeader = OssClientSigner.buildAuthHeader(ak, sk,"POST", api, contentType);
-
-        String url = String.format("%s%s", endpoint, api);
-        String objectId = "";
-        Map<String, String> body = Map.of("objectId", objectId);
-        String jsonBody = JsonConverter.objectToJson(body);
         HttpRequest request = HttpRequest.newBuilder()
                 .uri(URI.create(url))
                 .header("Date", gmtDate)
@@ -162,6 +161,26 @@ public class OssClient {
         }
     }
 
+    private String getPostResponseBody(String api, String jsonBody) throws Exception {
+        String url = String.format("%s%s", endpoint, api);
+        String gmtDate = OssClientSigner.getGmtDate();
+        String contentType = "application/json";
+        String authHeader = OssClientSigner.buildAuthHeader(ak, sk,"POST", api, contentType);
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url))
+                .header("Date", gmtDate)
+                .header("Authorization", authHeader)
+                .header("Content-Type", contentType)
+                .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
+                .build();
+        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+        if (response.statusCode() != 200) {
+            throw new RuntimeException("HTTP 请求失败: " + response.statusCode());
+        }
+
+        return response.body();
+    }
+
     /**
      * 上传文件
      * @param filePath 本地文件路径
@@ -206,6 +225,7 @@ public class OssClient {
         if (uploadPrepareRet.isExist()) {
             UploadFileRet uploadFileRet = checkSample(filePath, uploadPrepareRet);
             if (uploadFileRet.isFastUpload()) {
+                System.out.println("文件快传完成...");
                 return uploadFileRet;
             }
         }
@@ -246,7 +266,10 @@ public class OssClient {
                 failedChunkNumbers.add(chunkNumber);
             } else {
                 System.out.printf("%s:%s uploaded %s bytes\n", totalChunks, chunkNumber, currentChunkSize);
-                if (uploadFileRet.isMerged()) {
+                /*if (uploadFileRet.isMerged()) {
+                    return uploadFileRet;
+                }*/
+                if (uploadFileRet.isUploaded()) {
                     return uploadFileRet;
                 }
             }
@@ -314,12 +337,13 @@ public class OssClient {
     }
 
     public String getSignedUrl(String objectId, String action) throws Exception {
-        String gmtDate = OssClientSigner.getGmtDate();
+        String queryString = String.format("objectId=%s&action=%s", objectId, action);
         String api = "/api/oss/sdk/presign";
+        String url = String.format("%s%s?%s", endpoint, api, queryString);
+
+        String gmtDate = OssClientSigner.getGmtDate();
         String contentType = "application/json";
         String authHeader = OssClientSigner.buildAuthHeader(ak, sk,"GET", api, contentType);
-
-        String url = String.format("%s%s?objectId=%s&action=%s", endpoint, api, objectId, action);
         HttpRequest request = HttpRequest.newBuilder()
                 .uri(URI.create(url))
                 .header("Date", gmtDate)
@@ -346,38 +370,125 @@ public class OssClient {
         return null;
     }
 
+    private String getGetResponseBody(String api, String queryString) throws Exception {
+        //String queryString = String.format("objectId=%s&action=%s", objectId, action);
+        //String api = "/api/oss/sdk/presign";
+        String url = String.format("%s%s?%s", endpoint, api, queryString);
+        if (queryString == null || queryString.isBlank()) {
+            url = String.format("%s%s", endpoint, api);
+        } else {
+            url = String.format("%s%s?%s", endpoint, api, queryString);
+        }
+
+        String gmtDate = OssClientSigner.getGmtDate();
+        String contentType = "application/json";
+        String authHeader = OssClientSigner.buildAuthHeader(ak, sk,"GET", api, contentType);
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url))
+                .header("Date", gmtDate)
+                .header("Authorization", authHeader)
+                .header("Content-Type", contentType)
+                .timeout(Duration.ofSeconds(10))
+                .GET()
+                .build();
+
+        // 发送请求并获取响应
+        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+        if (response.statusCode() != 200) {
+            throw new RuntimeException("HTTP 请求失败: " + response.statusCode());
+        }
+        return response.body();
+    }
+
     public boolean headObject(String sha256sum) {
         return false;
     }
 
-    public UploadFileRet postObject(File file) throws Exception {
-        ServerInfo serverInfo = getServerInfo();
+    public UploadFileRet postObject(int channelCode, File file) throws Exception {
+        ServerInfo serverInfo = getServerInfo(channelCode);
         String uploadUrl = serverInfo.getOssUrl();
         String uploadToken = serverInfo.getToken();
         return null;
     }
 
-    public void setObjectScope(String objectId) {
+    public ObjectChannel getChannel(int channelCode) {
+        String api = "/api/oss/sdk/channel";
+        String queryString = "channelCode=" + channelCode;
+        try {
+            String body = getGetResponseBody(api, queryString);
+            Type type = new TypeToken<WebResult<ObjectChannel>>(){}.getType();
+            WebResult<ObjectChannel> webResult = JsonConverter.jsonToObject(body, type);
+            if (webResult.getCode() == 0) {
+                return webResult.getData();
+            }
+            throw new RuntimeException(webResult.getMsg());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
 
+    public void setObjectScope(String objectId, ObjectScope objectScope) {
+        Map<String, Object> body = Map.of(
+                "objectId", objectId,
+                "scope", objectScope.getCode());
+        String jsonBody = JsonConverter.objectToJson(body);
+        String api = "/api/oss/sdk/upload_request";
+        try {
+            String responseBody = getPostResponseBody(api, jsonBody);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
     }
 
     public void deleteObject(String objectId) {
 
     }
 
-    public void getObject(String objectId, String savedPath) {
+    public void getObject(String signedUrl, File targetFile) throws Exception {
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(signedUrl))
+                .GET()
+                .build();
 
+        // 直接下载到指定路径,HttpClient 会自动处理流的关闭
+        HttpResponse<Path> response = httpClient.send(request,
+                HttpResponse.BodyHandlers.ofFile(Paths.get(targetFile.getAbsolutePath())));
+        if (response.statusCode() == 200) {
+            System.out.println("下载成功,存放在: " + targetFile.getAbsolutePath());
+        } else {
+            System.err.println("下载失败,状态码: " + response.statusCode());
+        }
     }
 
     public ObjectInfo getObjectInfo(String objectId) {
-        return null;
+        String api = "/api/oss/sdk/object/get";
+        String queryString = "objectId=" + objectId;
+        try {
+            String body = getGetResponseBody(api, queryString);
+            Type type = new TypeToken<WebResult<ObjectInfo>>(){}.getType();
+            WebResult<ObjectInfo> webResult = JsonConverter.jsonToObject(body, type);
+            if (webResult.getCode() == 0) {
+                return webResult.getData();
+            }
+            throw new RuntimeException(webResult.getMsg());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
     }
     
     public VideoInfo getVideoInfo(String objectId) {
-        return null;
-    }
-
-    public ImageInfo getImageInfo(String objectId) {
-        return null;
+        String api = "/api/oss/sdk/object/get/video";
+        String queryString = "objectId=" + objectId;
+        try {
+            String body = getGetResponseBody(api, queryString);
+            Type type = new TypeToken<WebResult<VideoInfo>>(){}.getType();
+            WebResult<VideoInfo> webResult = JsonConverter.jsonToObject(body, type);
+            if (webResult.getCode() == 0) {
+                return webResult.getData();
+            }
+            throw new RuntimeException(webResult.getMsg());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
     }
 }

+ 2 - 2
oss-store/bin/oss.yml

@@ -6,8 +6,8 @@ spring:
 oss:
   port: 8020
   store-host: 127.0.0.1
-  ssd-dir: /disk/1/ssd
+  ssd-dir: /opt/disk/ssd
   hdd-dirs:
-    - /opt/disk/1
+    - /disk/1
   console-host: 127.0.0.1
   console-port: 18010

+ 27 - 41
oss-store/src/main/java/cn/reghao/oss/store/disk/DiskService.java

@@ -1,9 +1,7 @@
 package cn.reghao.oss.store.disk;
 
-import cn.reghao.oss.api.dto.disk.DiskStore;
 import cn.reghao.oss.api.dto.disk.DiskVolume;
 import cn.reghao.oss.store.config.OssStoreConfig;
-import jakarta.annotation.PostConstruct;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.io.FileUtils;
 import org.springframework.stereotype.Service;
@@ -31,7 +29,7 @@ public class DiskService {
     private final HardwareAbstractionLayer hal;
     private final FileSystem fs;
     private final OssStoreConfig ossStoreConfig;
-    private Map<String, DiskVolume> diskVolumeMap = new HashMap<>();
+    private final Map<String, DiskVolume> storeDirMap = new HashMap<>();
 
     public DiskService(OssStoreConfig ossStoreConfig) {
         SystemInfo si = new SystemInfo();
@@ -40,14 +38,6 @@ public class DiskService {
         this.ossStoreConfig = ossStoreConfig;
     }
 
-    @PostConstruct
-    public void getFileStores() {
-        this.diskVolumeMap = fs.getFileStores().stream()
-                .filter(osFileStore -> !osFileStore.getUUID().isBlank())
-                .map(this::getDiskVolume).collect(Collectors.toMap(DiskVolume::getMountPoint, k -> k));
-        getDiskStore();
-    }
-
     private DiskVolume getDiskVolume(OSFileStore osFileStore) {
         String name = osFileStore.getName();
         String diskPartition = osFileStore.getVolume();
@@ -66,10 +56,19 @@ public class DiskService {
     }
 
     public void initDisk(String ssdDir, List<String> hddDirs) throws IOException {
-        List<String> diskDirs = new ArrayList<>(hddDirs);
-        diskDirs.add(ssdDir);
-        init(diskDirs);
+        for (String hddDir : hddDirs) {
+            if (hddDir.startsWith(ssdDir)) {
+                String msg = String.format("hddDir %s 不能是 ssdDir %s 的子目录", hddDir, ssdDir);
+                throw new RuntimeException(msg);
+            }
 
+            if (ssdDir.startsWith(hddDir)) {
+                String msg = String.format("ssdDir %s 不能是 hddDir %s 的子目录", ssdDir, hddDir);
+                throw new RuntimeException(msg);
+            }
+        }
+
+        init(hddDirs);
         String ssdTmpDir = String.format("%s/tmp", ssdDir);
         createDir(ssdTmpDir);
         for (String hddDir : hddDirs) {
@@ -88,9 +87,12 @@ public class DiskService {
     }
 
     public void init(List<String> storeDirs) throws IOException {
+        Map<String, DiskVolume> diskVolumeMap = fs.getFileStores().stream()
+                .filter(osFileStore -> !osFileStore.getUUID().isBlank())
+                .map(this::getDiskVolume).collect(Collectors.toMap(DiskVolume::getMountPoint, k -> k));
         List<String> mountedDirs = new ArrayList<>(diskVolumeMap.keySet());
         // 按磁盘分区挂载点降序
-        Collections.reverse(mountedDirs);
+        mountedDirs.sort(Comparator.comparingInt(String::length).reversed());
 
         Map<String, String> map = new HashMap<>();
         for (String storeDir : storeDirs) {
@@ -101,7 +103,9 @@ public class DiskService {
                 boolean flag = false;
                 for (String mountedDir : mountedDirs) {
                     if (storeDir.startsWith(mountedDir)) {
-                        diskVolumeMap.get(mountedDir).setStoreDir(storeDir);
+                        DiskVolume diskVolume = diskVolumeMap.get(mountedDir);
+                        diskVolume.setStoreDir(storeDir);
+                        storeDirMap.put(storeDir, diskVolume);
                         flag = true;
                         break;
                     }
@@ -109,40 +113,22 @@ public class DiskService {
 
                 if (!flag) {
                     String msg = String.format("%s 没有找到相应的磁盘分区", storeDir);
-                    throw new IOException(msg);
+                    throw new RuntimeException(msg);
                 }
             } else {
                 String msg = String.format("%s 和 %s 在同一个分区 %s 上", storeDir, prevValue, logicalDisk);
-                throw new IOException(msg);
+                throw new RuntimeException(msg);
             }
         }
     }
 
-    public List<DiskStore> getDiskStore() {
-        List<DiskStore> diskStores = hal.getDiskStores().stream().map(hwDiskStore -> {
-            String model = hwDiskStore.getModel();
-            String name = hwDiskStore.getName();
-            long size = hwDiskStore.getSize();
-            List<DiskStore.DiskPartition> partitions = hwDiskStore.getPartitions().stream().map(hwPartition -> {
-                String partitionName = hwPartition.getIdentification();
-                String mountPoint = hwPartition.getMountPoint();
-                String blockId = hwPartition.getUuid();
-                String fsType = hwPartition.getType();
-                long partitionSize = hwPartition.getSize();
-                return new DiskStore.DiskPartition(partitionName, mountPoint, blockId, fsType, partitionSize);
-            }).collect(Collectors.toList());
-            return new DiskStore(model, name, size, partitions);
-        }).collect(Collectors.toList());
-        return diskStores;
-    }
-
     public List<DiskVolume> getDiskVolumes() {
-        return new ArrayList<>(diskVolumeMap.values());
+        return new ArrayList<>(storeDirMap.values());
     }
 
-    public String getSsdTempPath(String sha256sum) throws IOException {
+    public String getSsdTempPath(String objectId) throws IOException {
         String ssdRoot = ossStoreConfig.getSsdDir();
-        String filePath = String.format("%s/tmp/%s.tmp", ssdRoot, sha256sum);
+        String filePath = String.format("%s/tmp/%s.tmp", ssdRoot, objectId);
         File file = new File(filePath);
         if (!file.exists()) {
             file.createNewFile();
@@ -150,12 +136,12 @@ public class DiskService {
         return file.getAbsolutePath();
     }
 
-    public String getHddTempPath(String sha256sum) throws IOException {
+    public String getHddTempPath(String objectId) throws IOException {
         String hddRoot = ossStoreConfig.getHddDirs().get(0);
         Path finalDir = Paths.get(hddRoot, "tmp");
         Files.createDirectories(finalDir); // 如果目录不存在则创建
 
-        String filePath = String.format("%s/%s.tmp", finalDir, sha256sum);
+        String filePath = String.format("%s/%s.tmp", finalDir, objectId);
         File file = new File(filePath);
         if (!file.exists()) {
             file.createNewFile();

+ 0 - 67
oss-store/src/main/java/cn/reghao/oss/store/disk/FileMover.java

@@ -1,67 +0,0 @@
-package cn.reghao.oss.store.disk;
-
-import cn.reghao.oss.api.dto.OssUploadResultDTO;
-import cn.reghao.oss.api.iface.ConsoleService;
-import cn.reghao.oss.store.config.OssStoreConfig;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.scheduling.annotation.Async;
-import org.springframework.stereotype.Component;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
-import java.util.UUID;
-
-/**
- * @author reghao
- * @date 2026-02-01 16:24:23
- */
-@Slf4j
-@Component
-public class FileMover {
-    private final MetadataCompensator compensator;
-    private final ConsoleService consoleService;
-    private final DiskService diskService;
-    private final OssStoreConfig ossStoreConfig;
-
-    public FileMover(MetadataCompensator compensator, ConsoleService consoleService, DiskService diskService,
-                     OssStoreConfig ossStoreConfig) {
-        this.compensator = compensator;
-        this.consoleService = consoleService;
-        this.diskService = diskService;
-        this.ossStoreConfig = ossStoreConfig;
-    }
-
-    @Async // 异步执行,不阻塞上传响应
-    public void moveSsdToHdd(String tempPath, String sha256, String fileName, long size, String mime) {
-        log.info("异步搬运");
-        try {
-            String filePath = diskService.getHddTempPath(sha256);
-            Path targetPath = Path.of(filePath);
-
-            // 移动文件 (SSD -> HDD)
-            Files.move(Paths.get(tempPath), targetPath, StandardCopyOption.REPLACE_EXISTING);
-            log.info("文件已搬运至 HDD: {}", targetPath);
-
-            String nodeAddress = "";
-            int uploadBy = 1;
-            String suffix = "mp4";
-            String objectId = UUID.randomUUID().toString().replace("-", "");
-            String objectName = String.format("video/playback/%s.%s", objectId, suffix);
-            // 2. 构造同步 DTO
-            String hostPort = String.format("%s:%s", ossStoreConfig.getStoreHost(), ossStoreConfig.getPort());
-            OssUploadResultDTO dto = new OssUploadResultDTO(sha256, targetPath.toString(), size, fileName, mime, objectId, objectName, uploadBy, hostPort);
-            try {
-                // 3. 尝试通知 oss-mgr
-                consoleService.registerAndBind(dto);
-            } catch (Exception e) {
-                e.printStackTrace();
-                // 4. 通知失败,转入本地补偿
-                compensator.recordFailure(dto);
-            }
-        } catch (Exception e) {
-            log.error("物理搬运失败,文件未落盘", e);
-        }
-    }
-}

+ 61 - 27
oss-store/src/main/java/cn/reghao/oss/store/disk/HddFlushService.java

@@ -1,7 +1,7 @@
 package cn.reghao.oss.store.disk;
 
 import cn.reghao.jutil.jdk.thread.ThreadFactoryBuilder;
-import cn.reghao.oss.api.dto.UploadedFile;
+import cn.reghao.oss.api.dto.UploadDoneResult;
 import cn.reghao.oss.api.iface.ConsoleService;
 import cn.reghao.oss.store.config.OssStoreConfig;
 import lombok.extern.slf4j.Slf4j;
@@ -32,10 +32,11 @@ public class HddFlushService {
     private final ConsoleService consoleService;
     // 预留空间阈值:例如 HDD 必须保留 5GB 剩余空间,否则停止搬运
     private static final long MIN_FREE_SPACE = 5L * 1024 * 1024 * 1024;
-    private final OssStoreConfig ossStoreConfig;
     private final DiskService diskService;
+    private final MetadataCompensator compensator;
+    private final String hostPort;
 
-    public HddFlushService(ConsoleService consoleService, OssStoreConfig ossStoreConfig, DiskService diskService) {
+    public HddFlushService(OssStoreConfig ossStoreConfig, ConsoleService consoleService, DiskService diskService, MetadataCompensator compensator) {
         // 使用有界队列防止 OOM:如果 HDD 写入太慢,搬运任务堆积,直接拒绝或进入排队
         // 使用计算出的线程数创建线程池
         this.moveExecutor = new ThreadPoolExecutor(
@@ -45,54 +46,87 @@ public class HddFlushService {
                 new ThreadFactoryBuilder().setNameFormat("hdd-mover-%d").build(),
                 new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由调用线程处理,起到自然的背压作用
         );
+        this.hostPort = String.format("%s:%s", ossStoreConfig.getStoreHost(), ossStoreConfig.getPort());
         this.consoleService = consoleService;
-        this.ossStoreConfig = ossStoreConfig;
         this.diskService = diskService;
+        this.compensator = compensator;
     }
 
-    public void triggerFlush(String uploadId, String channelPrefix, String filename) {
+    /**
+     * 分片上传后的处理
+     *
+     * @param
+     * @return
+     * @date 2026-04-28 11:48:56
+     */
+    public void triggerFlush(String uploadId) {
         moveExecutor.submit(() -> {
             try {
                 String ssdPath = diskService.getSsdTempPath(uploadId);
                 String hddPath = diskService.getHddTempPath(uploadId);
 
                 File ssdFile = new File(ssdPath);
-                long fileSize = ssdFile.length();
+                long size = ssdFile.length();
                 // 1. 搬运前的空间预检
-                checkDiskSpace(fileSize, hddPath);
+                checkDiskSpace(size, hddPath);
 
                 FlushResult flushResult = moveAndChecksum(ssdPath, hddPath);
-                String sha256sum = flushResult.sha256;
+                String sha256sum = flushResult.sha256sum;
                 String contentType = flushResult.contentType;
 
-                String hostPort = String.format("%s:%s", ossStoreConfig.getStoreHost(), ossStoreConfig.getPort());
-                UploadedFile uploadedFile = new UploadedFile(uploadId, sha256sum, channelPrefix, filename, contentType, fileSize, hddPath, hostPort);
-                // 搬运成功后,更新数据库中的状态和 FileHash
-                boolean ret = consoleService.validateMultipart(uploadedFile);
-                if (ret) {
-                    Path tmpFile = Path.of(hddPath);
-                    String filePath = diskService.getHddDataPath(sha256sum);
-                    Path finalPath = Path.of(filePath);
-                    // 原子移动:这是文件系统层面的操作,极快
-                    Files.move(tmpFile, finalPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
-                    String absolutePath = finalPath.toFile().getAbsolutePath();
-                    consoleService.updatePath(sha256sum, absolutePath);
-                    log.info("{} 的 HDD 搬运完成", uploadId);
-                } else {
-                    log.error("oss-mgr 更新失败");
-                }
+                Path tmpFile = Path.of(hddPath);
+                String filePath = diskService.getHddDataPath(sha256sum);
+                Path finalPath = Path.of(filePath);
+                // hdd 磁盘内部的原子移动:这是文件系统层面的操作,极快
+                Files.move(tmpFile, finalPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
+                String absolutePath = finalPath.toFile().getAbsolutePath();
+
+                UploadDoneResult uploadDoneResult = new UploadDoneResult(uploadId, sha256sum, absolutePath, size, hostPort);
+                consoleService.updateAfterMove(uploadDoneResult);
+                log.info("{} 的 HDD 搬运完成", uploadId);
 
                 // 清理 SSD 临时文件
-                Files.deleteIfExists(Paths.get(ssdPath));
+                log.info("清理 SSD 临时文件 {}", ssdPath);
+                //Files.deleteIfExists(Paths.get(ssdPath));
             } catch (InsufficientSpaceException e) {
                 log.error("HDD 空间不足,取消搬运: {}", e.getMessage());
-                //onFailure(uploadId, "DISK_FULL");
             } catch (Exception e) {
                 log.error("HDD 搬运失败: {}", uploadId, e);
             }
         });
     }
 
+    /**
+     * 单文件上传后的处理
+     *
+     * @param
+     * @return
+     * @date 2026-04-28 11:48:43
+     */
+    public void moveSsdToHdd(String uploadId, String sha256sum, long size, String tempPath) {
+        moveExecutor.submit(() -> {
+            try {
+                String destPath = diskService.getHddDataPath(sha256sum);
+                Path targetPath = Path.of(destPath);
+
+                checkDiskSpace(size, destPath);
+                // 移动文件 (SSD -> HDD)
+                Files.move(Paths.get(tempPath), targetPath, StandardCopyOption.REPLACE_EXISTING);
+                UploadDoneResult uploadDoneResult = new UploadDoneResult(uploadId, sha256sum, destPath, size, hostPort);
+                try {
+                    // 3. 尝试通知 oss-mgr
+                    consoleService.updateAfterMove(uploadDoneResult);
+                } catch (Exception e) {
+                    log.error("{}", e.getMessage());
+                    // 4. 通知失败,转入本地补偿
+                    compensator.recordFailure(uploadDoneResult);
+                }
+            } catch (Exception e) {
+                log.error("物理搬运失败,文件未落盘", e);
+            }
+        });
+    }
+
     private void checkDiskSpace(long fileSize, String dest) throws Exception {
         File hddDir = new File(dest).getParentFile();
         long usableSpace = hddDir.getUsableSpace(); // 获取 HDD 实际可用空间
@@ -159,5 +193,5 @@ public class HddFlushService {
     }
 
     // --- 结果包装 ---
-    public record FlushResult(String sha256, String contentType) {}
+    public record FlushResult(String sha256sum, String contentType) {}
 }

+ 12 - 10
oss-store/src/main/java/cn/reghao/oss/store/disk/MetadataCompensator.java

@@ -1,7 +1,8 @@
 package cn.reghao.oss.store.disk;
 
-import cn.reghao.oss.api.dto.OssUploadResultDTO;
+import cn.reghao.oss.api.dto.UploadDoneResult;
 import cn.reghao.oss.api.iface.ConsoleService;
+import cn.reghao.oss.store.config.OssStoreConfig;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Scheduled;
@@ -23,25 +24,26 @@ import java.util.List;
 @Slf4j
 @Component
 public class MetadataCompensator {
-    private String retryLogPath = "/opt/disk/retry_sync.log";
-    private ConsoleService consoleService;
+    private final String retryLogPath;
+    private final ConsoleService consoleService;
     private final ObjectMapper objectMapper = new ObjectMapper();
 
-    public MetadataCompensator(ConsoleService consoleService) {
+    public MetadataCompensator(ConsoleService consoleService, OssStoreConfig ossStoreConfig) {
         this.consoleService = consoleService;
+        this.retryLogPath = String.format("%s/retry_sync.log", ossStoreConfig.getSsdDir());
     }
 
     /**
      * 1. 记账:同步失败时,将 DTO 序列化并追加到本地文件
      */
-    public void recordFailure(OssUploadResultDTO dto) {
+    public void recordFailure(UploadDoneResult uploadDoneResult) {
         try {
-            String jsonLine = objectMapper.writeValueAsString(dto) + System.lineSeparator();
+            String jsonLine = objectMapper.writeValueAsString(uploadDoneResult) + System.lineSeparator();
             Files.write(Paths.get(retryLogPath),
                     jsonLine.getBytes(StandardCharsets.UTF_8),
                     StandardOpenOption.CREATE,
                     StandardOpenOption.APPEND);
-            log.warn("元数据同步失败,已记录至本地重试日志: {}", dto.getSha256sum());
+            log.warn("元数据同步失败,已记录至本地重试日志: {}", uploadDoneResult.getSha256sum());
         } catch (IOException e) {
             log.error("致命错误:本地重试日志写入失败!请检查磁盘空间或权限", e);
         }
@@ -68,10 +70,10 @@ public class MetadataCompensator {
         for (String line : failedLines) {
             if (line.trim().isEmpty()) continue;
             try {
-                OssUploadResultDTO dto = objectMapper.readValue(line, OssUploadResultDTO.class);
+                UploadDoneResult uploadDoneResult = objectMapper.readValue(line, UploadDoneResult.class);
                 // 尝试重新调用 oss-mgr
-                consoleService.registerAndBind(dto);
-                log.info("补偿成功:{}", dto.getObjectName());
+                consoleService.updateAfterMove(uploadDoneResult);
+                log.info("objectId {} 补偿成功", uploadDoneResult.getObjectId());
             } catch (Exception e) {
                 // 如果依然失败,保留在队列中
                 remainingLines.add(line);

+ 251 - 41
oss-store/src/main/java/cn/reghao/oss/store/handler/OssMultipartUploadHandler.java

@@ -1,24 +1,47 @@
 package cn.reghao.oss.store.handler;
 
-import cn.reghao.oss.api.dto.WebBody;
+import cn.reghao.jutil.jdk.security.DigestUtil;
+import cn.reghao.jutil.jdk.web.result.WebResult;
+import cn.reghao.oss.api.constant.UploadStatus;
+import cn.reghao.oss.api.dto.FastUploadResult;
+import cn.reghao.oss.api.dto.UploadResult;
 import cn.reghao.oss.api.dto.rest.UploadFileRet;
+import cn.reghao.oss.api.iface.ConsoleService;
+import cn.reghao.oss.api.util.FileUtil;
+import cn.reghao.oss.api.util.OssSamplingHash;
+import cn.reghao.oss.store.config.OssStoreConfig;
 import cn.reghao.oss.store.disk.DiskService;
 import cn.reghao.oss.store.disk.HddFlushService;
 import cn.reghao.oss.store.util.ResponseHelper;
 import cn.reghao.oss.store.util.UploadProgressManager;
+import cn.reghao.oss.store.util.UploadState;
+import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.handler.codec.http.*;
 import io.netty.handler.codec.http.multipart.*;
 import io.netty.util.AttributeKey;
+import io.netty.util.ReferenceCountUtil;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.tika.Tika;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
 
 /**
+ * 处理 multipart/form-data 上传, 每个线程一个 handler
+ *
  * @author reghao
  * @date 2026-02-01 12:39:37
  */
@@ -29,19 +52,32 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
     private HttpPostMultipartRequestDecoder decoder;
 
     // 当前请求的上下文状态
-    private String currentUploadId;
-    private long currentOffset = -1;
-    private int currentChunkNumber;
-    private int totalParts = 0;
-    private long totalSize = 0;
-    private long chunkSize = 0;
-    private String filename = "";
-    private HddFlushService hddFlushService;
-    private DiskService diskService;
-
-    public OssMultipartUploadHandler(HddFlushService hddFlushService, DiskService diskService) {
+    private static final Tika tika = new Tika();
+    private final Map<String, String> formData = new HashMap<>();
+    private final OssStoreConfig ossStoreConfig;
+    private final ConsoleService consoleService;
+    private final HddFlushService hddFlushService;
+    private final DiskService diskService;
+    private FileChannel singleFileChannel;
+    private String singleFileId;
+    private final MessageDigest messageDigest;
+    // 8 字节
+    private final ByteBuf headerCollector = Unpooled.buffer(8);
+    private boolean isTypeIdentified = false;
+    private final String hostPort;
+
+    public OssMultipartUploadHandler(OssStoreConfig ossStoreConfig, ConsoleService consoleService,
+                                     HddFlushService hddFlushService, DiskService diskService) {
+        this.hostPort = String.format("%s:%s", ossStoreConfig.getStoreHost(), ossStoreConfig.getPort());
+        this.ossStoreConfig = ossStoreConfig;
+        this.consoleService = consoleService;
         this.hddFlushService = hddFlushService;
         this.diskService = diskService;
+        try {
+            this.messageDigest = MessageDigest.getInstance("SHA-256");
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("SHA-256 algorithm not found", e);
+        }
     }
 
     @Override
@@ -93,18 +129,26 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
     private void processAttribute(Attribute attr) throws IOException {
         String name = attr.getName();
         String value = attr.getValue();
-        if ("identifier".equals(name)) currentUploadId = value;
+        formData.put(name, value);
+        /*if ("identifier".equals(name)) currentUploadId = value;
         if ("chunkNumber".equals(name)) currentChunkNumber = Integer.parseInt(value);
         if ("chunkSize".equals(name)) chunkSize = Long.parseLong(value);
         if ("totalChunks".equals(name)) totalParts = Integer.parseInt(value);
-        if ("filename".equals(name)) filename = value;
+        if ("filename".equals(name)) filename = value;*/
     }
 
     // 处理 multipart/form-data 的文件
     private void processFileUpload(FileUpload fileUpload) throws IOException {
+        String currentUploadId = formData.get("identifier");
+        if (currentUploadId == null) {
+            handleSingleFileUploadStreamingly(fileUpload);
+            return;
+        }
+
         // 在非聚合模式下,即使 decoder.next() 返回了 fileUpload,
         // 也要判断它是否真的接收完毕。
         if (fileUpload.isCompleted()) {
+            long currentChunkNumber = Long.parseLong(formData.get("chunkNumber"));
             // 执行你的 SSD 写入逻辑
             handleChunkUpload(currentUploadId, currentChunkNumber, fileUpload);
         } else {
@@ -115,6 +159,90 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
         }
     }
 
+    private void handleSingleFileUploadStreamingly(FileUpload fileUpload) throws IOException {
+        // 1. 初始化文件 ID 和 Channel (仅在第一次进入时)
+        if (singleFileId == null) {
+            singleFileId = UUID.randomUUID().toString().replace("-", "");
+            formData.put("uploadId", singleFileId);
+
+            String filename = fileUpload.getFilename();
+            formData.put("filename", filename);
+
+            String ssdPath = diskService.getSsdTempPath(singleFileId);
+            RandomAccessFile raf = new RandomAccessFile(ssdPath, "rw");
+            singleFileChannel = raf.getChannel();
+            messageDigest.reset();
+        }
+
+        // 2. 获取当前已经解码出来的、尚未读取的新增字节(即获取当前新增的增量数据)
+        // 注意:不要直接用 fileUpload.getByteBuf(),那个可能包含已经写过的数据
+        ByteBuf chunk = fileUpload.getChunk((int) fileUpload.length() - (int) singleFileChannel.size());
+        if (chunk != null) {
+            // Nginx 的 client_body_buffer_size参数会显著影响 Netty 接收到的第一个 chunk 的大小
+            try {
+                if (!isTypeIdentified) {
+                    // 将新到的数据写入 collector,最多写满 8 字节
+                    int need = 8 - headerCollector.readableBytes();
+                    if (need > 0) {
+                        headerCollector.writeBytes(chunk, Math.min(need, chunk.readableBytes()));
+                        // 只要凑够了 8 字节,或者文件已经传输完毕(应对极小文件)
+                        if (headerCollector.readableBytes() == 8 || fileUpload.isCompleted()) {
+                            String sniffedType = tika.detect(toByteArray(headerCollector));
+                            formData.put("sniffedContentType", sniffedType);
+                            isTypeIdentified = true;
+                            // 释放 collector 内存
+                            headerCollector.clear();
+                        }
+                    }
+                    // 注意:这里需要重置 chunk 的 readerIndex,因为 writeBytes 移动了它
+                    // 或者在前面使用 slice()/duplicate() 保持 chunk 原封不动写磁盘
+                    chunk.readerIndex(0);
+                }
+
+                // 关键点:在移动指针前,先更新摘要
+                // 使用 nioBuffer() 不会移动 readerIndex,方便后续写入
+                ByteBuffer nioBuffer = chunk.nioBuffer();
+                // 1. 更新 SHA-256 摘要
+                messageDigest.update(nioBuffer.duplicate());
+
+                // 3. 将这部分新增字节写入磁盘
+                while (chunk.isReadable()) {
+                    // write 方法会返回实际写入字节,手动移动 readerIndex 避免死循环
+                    int written = singleFileChannel.write(chunk.nioBuffer());
+                    chunk.skipBytes(written);
+                }
+            } finally {
+                // 4. 必须手动释放这个 chunk,否则会堆外内存泄露
+                ReferenceCountUtil.release(chunk);
+            }
+        }
+
+        // 5. 检查是否完成
+        if (fileUpload.isCompleted()) {
+            singleFileChannel.force(true); // 确保落盘
+            singleFileChannel.close();
+            singleFileChannel = null;
+
+            // 3. 计算最终的十六进制摘要串
+            byte[] hashBytes = messageDigest.digest();
+            String sha256sum = OssSamplingHash.bytesToHex(hashBytes);
+            formData.put("sha256sum", sha256sum);
+            log.info("文件上传完成,SHA-256: {}", sha256sum);
+        }
+    }
+
+    private byte[] toByteArray(ByteBuf buf) {
+        // 1. 获取可读字节数
+        int length = buf.readableBytes();
+        byte[] array = new byte[length];
+
+        // 2. 将数据从 ByteBuf(可能是堆外内存)拷贝到堆内的 byte[]
+        // getBytes(index, array) 不会移动 readerIndex
+        buf.getBytes(buf.readerIndex(), array);
+
+        return array;
+    }
+
     private void handleChunkUpload(String uploadId, long offset, FileUpload fileUpload) throws IOException {
         // SSD 上的临时预分配文件路径
         String ssdPath = diskService.getSsdTempPath(uploadId);
@@ -122,57 +250,142 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
         try (RandomAccessFile raf = new RandomAccessFile(ssdPath, "rw");
              FileChannel channel = raf.getChannel()) {
 
+            long chunkSize = Long.parseLong(formData.get("chunkSize"));
             // 定位到 SSD 文件中的指定偏移量
+            int currentChunkNumber = Integer.parseInt(formData.get("chunkNumber"));
             long pos = (currentChunkNumber - 1)*chunkSize;
             channel.position(pos);
             // fileUpload.get() 可能是内存 Buffer 也可能是临时文件,Netty 自动处理
             channel.write(fileUpload.getByteBuf().nioBuffer());
 
             // 写入成功后标记位图
-            UploadProgressManager.markPart(currentUploadId, currentChunkNumber);
+            UploadProgressManager.markPart(uploadId, currentChunkNumber);
             System.out.printf("分片写入成功: ID=%s, Offset=%s, Size=%s\n", uploadId, offset, fileUpload.length());
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("{}", e.getMessage());
         }
     }
 
-    private void finalizeUpload(ChannelHandlerContext ctx) throws IOException {
+    private void finalizeUpload(ChannelHandlerContext ctx) throws Exception {
+        String channelPrefix = (String) ctx.channel().attr(AttributeKey.valueOf("channelPrefix")).get();
+        long uploadBy = (Long) ctx.channel().attr(AttributeKey.valueOf("uploadBy")).get();
+        String currentUploadId = formData.get("identifier");
+        if (currentUploadId == null) {
+            // 单文件上传
+            String uploadId = formData.get("uploadId");
+            String filename = formData.get("filename");
+            String contentType = formData.get("sniffedContentType");
+            String sha256sum = formData.get("sha256sum");
+
+            String ssdTempPath = diskService.getSsdTempPath(uploadId);
+            File tmpFile = new File(ssdTempPath);
+            long size = tmpFile.length();
+            String objectName = getObjectName(uploadId, filename, contentType, channelPrefix);
+            UploadResult uploadResult = new UploadResult();
+            uploadResult.setSha256sum(sha256sum);
+            //uploadResult.setAbsolutePath("");
+            uploadResult.setSize(size);
+            uploadResult.setFilename(filename);
+            uploadResult.setContentType(contentType);
+            uploadResult.setChannelPrefix(channelPrefix);
+            uploadResult.setObjectId(uploadId);
+            uploadResult.setObjectName(objectName);
+            uploadResult.setUploadBy(uploadBy);
+            uploadResult.setHostPort(hostPort);
+            uploadResult.setUploadStatus(UploadStatus.FLUSHING.getCode());
+
+            FastUploadResult fastUploadResult = new FastUploadResult(uploadResult);
+            if (consoleService.checkExists(uploadResult.getSha256sum())) {
+                log.info("文件 {} 触发秒传, 删除 ssd 中的临时文件 {}", uploadResult.getSha256sum(), ssdTempPath);
+                consoleService.bindOnly(fastUploadResult);
+            } else {
+                consoleService.registerAndBind(uploadResult);
+                hddFlushService.moveSsdToHdd(uploadId, sha256sum, size, ssdTempPath);
+            }
+
+            // 返回成功响应
+            String url = "";
+            UploadFileRet uploadFileRet = new UploadFileRet(uploadId, url, true);
+            String webResult = WebResult.success(uploadFileRet);
+            ResponseHelper.sendJsonResponse(ctx, webResult);
+            return;
+        }
+
         // 1. 业务逻辑处理 (Bitmap 等)
+        int totalParts = Integer.parseInt(formData.get("totalChunks"));
         if (UploadProgressManager.isComplete(currentUploadId, totalParts)) {
             log.info("文件 {} 校验通过,准备触发 HDD 异步搬运", currentUploadId);
+            String uploadId = currentUploadId;
+            String filename = formData.get("filename");
+            String ssdTempPath = diskService.getSsdTempPath(uploadId);
+            File tmpFile = new File(ssdTempPath);
+            long size = tmpFile.length();
+            String contentType = getContentType(tmpFile);
+            String objectName = getObjectName(uploadId, filename, contentType, channelPrefix);
 
-            String channelPrefix = (String) ctx.channel().attr(AttributeKey.valueOf("channelPrefix")).get();
+            UploadResult uploadResult = new UploadResult();
+            //uploadResult.setSha256sum("");
+            //uploadResult.setAbsolutePath("");
+            uploadResult.setSize(size);
+            uploadResult.setFilename(filename);
+            uploadResult.setContentType(contentType);
+            uploadResult.setChannelPrefix(channelPrefix);
+            uploadResult.setObjectId(uploadId);
+            uploadResult.setObjectName(objectName);
+            uploadResult.setUploadBy(uploadBy);
+            uploadResult.setHostPort(hostPort);
+            uploadResult.setUploadStatus(UploadStatus.FLUSHING.getCode());
+            consoleService.registerAndBind(uploadResult);
 
             // 2. 触发合并逻辑 (合并 SSD 上的碎片)
-            hddFlushService.triggerFlush(currentUploadId, channelPrefix, filename);
-
+            hddFlushService.triggerFlush(currentUploadId);
             // 3. 清理内存位图
             UploadProgressManager.remove(currentUploadId);
 
             // 4. 返回成功响应
-            UploadFileRet uploadFileRet = new UploadFileRet(currentUploadId, "");
-            WebBody webBody = new WebBody(uploadFileRet);
-            ResponseHelper.sendJsonResponse(ctx, HttpResponseStatus.OK, webBody);
-            System.out.println(currentUploadId + " 的分片已完成并已回写响应");
+            String url = "";
+            UploadFileRet uploadFileRet = new UploadFileRet(currentUploadId, url, true);
+            String webResult = WebResult.success(uploadFileRet);
+            ResponseHelper.sendJsonResponse(ctx, webResult);
+            log.info("{} 的分片上传已完成并已回写响应", currentUploadId);
         } else {
-            // 2. 创建响应
-            // 明确指定 Content-Length,否则某些客户端会一直等待 Body 结束
-            FullHttpResponse response = new DefaultFullHttpResponse(
-                    HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.EMPTY_BUFFER);
-            response.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON);
-            response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
-
-            UploadFileRet uploadFileRet = new UploadFileRet("");
-            WebBody webBody = new WebBody(uploadFileRet);
-            ResponseHelper.sendJsonResponse(ctx, HttpResponseStatus.OK, webBody);
-            System.out.println("分片 " + currentChunkNumber + " 写入完成并已回写响应");
+            UploadFileRet uploadFileRet = new UploadFileRet(currentUploadId);
+            String webResult = WebResult.success(uploadFileRet);
+            ResponseHelper.sendJsonResponse(ctx, webResult);
+
+            int currentChunkNumber = Integer.parseInt(formData.get("chunkNumber"));
+            log.info("分片 {} 写入完成并已回写响应", currentChunkNumber);
+        }
+    }
+
+    private String getObjectName(String uploadId, String filename, String contentType, String channelPrefix) {
+        String suffix = FileUtil.getSuffix(filename);
+        if (suffix.isBlank()) {
+            suffix = FileUtil.getSuffixByMime(contentType);
+        }
+
+        return String.format("%s%s.%s", channelPrefix, uploadId, suffix);
+    }
+
+    private String getContentType(File file) {
+        try {
+            // Tika 会根据文件头字节进行深度探测
+            return tika.detect(file);
+        } catch (IOException e) {
+            log.error("Tika 探测文件类型失败: {}", file.getAbsolutePath(), e);
+            return "application/octet-stream";
         }
     }
 
     private void reset() {
         cleanDecoder();
-        currentUploadId = null;
-        currentChunkNumber = -1;
+        if (messageDigest != null) {
+            messageDigest.reset();
+        }
+
+        if (headerCollector.refCnt() > 0) {
+            headerCollector.release();
+        }
     }
 
     private void cleanDecoder() {
@@ -191,10 +404,7 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
      */
     @Override
     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
-        if (decoder != null) {
-            // 如果文件没写完就断了,这里可以决定是保留残片还是删除
-            reset();
-        }
+        reset();
         super.channelInactive(ctx);
     }
 

+ 62 - 11
oss-store/src/main/java/cn/reghao/oss/store/handler/OssRouterHandler.java

@@ -4,9 +4,9 @@ import cn.reghao.oss.api.iface.ConsoleService;
 import cn.reghao.oss.api.util.JwtUtils;
 import cn.reghao.oss.store.config.OssStoreConfig;
 import cn.reghao.oss.store.disk.DiskService;
-import cn.reghao.oss.store.disk.FileMover;
 import cn.reghao.oss.store.disk.HddFlushService;
 import io.jsonwebtoken.Claims;
+import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
@@ -15,6 +15,7 @@ import io.netty.handler.codec.http.*;
 import io.netty.util.AttributeKey;
 import lombok.extern.slf4j.Slf4j;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -25,15 +26,13 @@ import java.util.List;
 public class OssRouterHandler extends SimpleChannelInboundHandler<HttpObject> {
     private final OssStoreConfig ossStoreConfig;
     private final ConsoleService consoleService;
-    private final FileMover fileMover;
     private final HddFlushService hddFlushService;
     private final DiskService diskService;
 
     public OssRouterHandler(OssStoreConfig ossStoreConfig, ConsoleService consoleService,
-                            FileMover fileMover, HddFlushService hddFlushService, DiskService diskService) {
+                            HddFlushService hddFlushService, DiskService diskService) {
         this.ossStoreConfig = ossStoreConfig;
         this.consoleService = consoleService;
-        this.fileMover = fileMover;
         this.hddFlushService = hddFlushService;
         this.diskService = diskService;
     }
@@ -45,10 +44,14 @@ public class OssRouterHandler extends SimpleChannelInboundHandler<HttpObject> {
             HttpMethod method = req.method();
             QueryStringDecoder decoder = new QueryStringDecoder(req.uri());
             String path = decoder.path();
+            log.info("{} {}", method, path);
 
             HttpHeaders headers = req.headers();
-            if (HttpMethod.POST.equals(method)) {
-                String headerValue = headers.get("Authorization");
+            if (HttpMethod.OPTIONS.equals(method)) {
+                handleOptions(ctx, req);
+                return; // 拦截并结束,不传递给后续 Handler
+            } else if (HttpMethod.POST.equals(method)) {
+                String headerValue = headers.get("authorization");
                 if (headerValue != null && headerValue.startsWith("Bearer")) {
                     String token = headerValue.replace("Bearer ", "");
                     // 校验 JWT 有效性
@@ -59,9 +62,23 @@ public class OssRouterHandler extends SimpleChannelInboundHandler<HttpObject> {
                     }
 
                     String action = String.valueOf(claims.get("action"));
-                    int uploadBy = (int) claims.get("uploadBy");
+                    long uploadBy = -1;
+                    if (claims.get("uploadBy") instanceof Integer) {
+                        int uploadByInt = (int) claims.get("uploadBy");
+                        uploadBy = uploadByInt;
+                    } else if (claims.get("uploadBy") instanceof Long) {
+                        uploadBy = (long) claims.get("uploadBy");
+                    }
+
+                    long maxSize = 0L;
+                    if (claims.get("maxSize") instanceof Integer) {
+                        int maxSizeInt = (int) claims.get("maxSize");
+                        maxSize = maxSizeInt;
+                    } else if (claims.get("maxSize") instanceof Long) {
+                        maxSize = (long) claims.get("maxSize");;
+                    }
+
                     String channelPrefix = (String) claims.get("channelPrefix");
-                    long maxSize = (long) claims.get("maxSize");
                     String contentType = (String) claims.get("contentType");
 
                     // 将上传信息注入后续的业务 Handler
@@ -74,13 +91,23 @@ public class OssRouterHandler extends SimpleChannelInboundHandler<HttpObject> {
                     sendError(ctx, HttpResponseStatus.FORBIDDEN);
                 }
 
+                String contentType = headers.get("content-type");
                 List<String> valueList = decoder.parameters().get("multipart");
-                if (!valueList.isEmpty()) {
+                if (valueList == null) {
+                    valueList = new ArrayList<>();
+                }
+
+                if (contentType.contains("multipart/form-data")) {
+                    // 浏览器模式:切换到 OssMultipartUploadHandler
+                    switchTo(ctx, "browser-uploader",
+                            new OssMultipartUploadHandler(ossStoreConfig, consoleService, hddFlushService, diskService));
+                } else if (!valueList.isEmpty()) {
                     // 分片上传:路径匹配且无分片参数
-                    switchTo(ctx, "multipart", new OssMultipartUploadHandler(hddFlushService, diskService));
+                    switchTo(ctx, "multipart",
+                            new OssMultipartUploadHandler(ossStoreConfig, consoleService, hddFlushService, diskService));
                 } else if (path.startsWith("/")) {
                     // 普通单上传:路径匹配且无分片参数
-                    switchTo(ctx,"single-file",  new OssUploadHandler(ossStoreConfig, consoleService, fileMover));
+                    switchTo(ctx,"single-file",  new OssUploadHandler(ossStoreConfig, consoleService, hddFlushService));
                 } else {
                     sendError(ctx, HttpResponseStatus.BAD_REQUEST);
                     return;
@@ -99,6 +126,30 @@ public class OssRouterHandler extends SimpleChannelInboundHandler<HttpObject> {
         }
     }
 
+    private void handleOptions(ChannelHandlerContext ctx, HttpRequest req) {
+        FullHttpResponse resp = new DefaultFullHttpResponse(
+                HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.EMPTY_BUFFER);
+        resp.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
+
+        HttpHeaders h = resp.headers();
+        String origin = req.headers().get(HttpHeaderNames.ORIGIN);
+
+        // 允许的来源 (生产环境建议从 ossStoreConfig 中读取配置)
+        h.set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, origin != null ? origin : "*");
+        // 允许的方法
+        h.set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
+        // 允许的自定义 Header (必须包含 JWT 用的 Authorization 和大文件用的 Range)
+        h.set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS,
+                "Authorization, Content-Type, Range, X-Tenant-ID, X-Requested-With");
+        // 允许携带 Cookie (如果需要)
+        h.set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
+        // 预检缓存时间 (1小时),减少频繁的 OPTIONS 请求
+        h.set(HttpHeaderNames.ACCESS_CONTROL_MAX_AGE, "3600");
+
+        // 写入响应并关闭连接(或者保持长连接)
+        ctx.writeAndFlush(resp);
+    }
+
     private void switchTo(ChannelHandlerContext ctx, String name, ChannelHandler handler) {
         ctx.pipeline().addAfter(ctx.name(), name, handler);
         ctx.pipeline().remove(this); // 路由器完成任务,从 Pipeline 中移除

+ 51 - 18
oss-store/src/main/java/cn/reghao/oss/store/handler/OssUploadHandler.java

@@ -1,10 +1,14 @@
 package cn.reghao.oss.store.handler;
 
-import cn.reghao.oss.api.dto.ObjectBindDTO;
+import cn.reghao.oss.api.dto.FastUploadResult;
+import cn.reghao.oss.api.dto.UploadResult;
+import cn.reghao.oss.api.dto.WebBody;
+import cn.reghao.oss.api.dto.rest.UploadFileRet;
 import cn.reghao.oss.api.iface.ConsoleService;
 import cn.reghao.oss.store.config.OssStoreConfig;
 import cn.reghao.oss.store.config.StorageConstants;
-import cn.reghao.oss.store.disk.FileMover;
+import cn.reghao.oss.store.disk.HddFlushService;
+import cn.reghao.oss.store.util.ResponseHelper;
 import cn.reghao.oss.store.util.UploadState;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.*;
@@ -17,6 +21,8 @@ import java.nio.charset.StandardCharsets;
 import java.util.UUID;
 
 /**
+ * sdk 使用的单文件流式上传
+ *
  * @author reghao
  * @date 2026-02-01 12:39:37
  */
@@ -25,16 +31,18 @@ public class OssUploadHandler extends SimpleChannelInboundHandler<HttpObject> {
     private static final AttributeKey<UploadState> STATE_KEY = AttributeKey.valueOf("uploadState");
     private static final AttributeKey<Boolean> SKIP_BODY = AttributeKey.valueOf("skipBody");
     private final String ssdTemp;
+    private final OssStoreConfig ossStoreConfig;
     private final ConsoleService consoleService;
-    private final FileMover fileMover;
+    private final HddFlushService hddFlushService;
     // 每一个 Channel (连接) 对应一个 Handler 实例,所以可以用局部变量记录
     private long readingBytes = 0;
     private boolean isOverLimit = false;
 
-    public OssUploadHandler(OssStoreConfig ossStoreConfig, ConsoleService consoleService, FileMover fileMover) {
+    public OssUploadHandler(OssStoreConfig ossStoreConfig, ConsoleService consoleService, HddFlushService hddFlushService) {
+        this.ossStoreConfig = ossStoreConfig;
         this.ssdTemp = ossStoreConfig.getSsdDir();
         this.consoleService = consoleService;
-        this.fileMover = fileMover;
+        this.hddFlushService = hddFlushService;
     }
 
     @Override
@@ -69,19 +77,21 @@ public class OssUploadHandler extends SimpleChannelInboundHandler<HttpObject> {
                 return;
             }
 
-            String clientSha256 = request.headers().get("X-File-SHA256");
             String objectId = UUID.randomUUID().toString().replace("-", "");
+            String clientSha256 = request.headers().get("X-File-SHA256", objectId);
             String filename = request.headers().get("X-File-Name", objectId);
 
             String channelPrefix = (String) ctx.channel().attr(AttributeKey.valueOf("channelPrefix")).get();
             log.info("channelPrefix -> {}", channelPrefix);
 
             String suffix = "mp4";
-            String objectName = String.format("video/playback/%s.%s", objectId, suffix);
-            ObjectBindDTO objectBindDTO = new ObjectBindDTO(clientSha256, filename, objectId, objectName);
+            String objectName = String.format("%s/%s.%s", channelPrefix, objectId, suffix);
+            long uploadBy = (Long) ctx.channel().attr(AttributeKey.valueOf("uploadBy")).get();
+            //FastUploadResult fastUploadResult = new FastUploadResult(clientSha256, filename, objectId, objectName, uploadBy);
+            FastUploadResult fastUploadResult = new FastUploadResult();
             if (clientSha256 != null && consoleService.checkExists(clientSha256)) {
                 log.info("触发秒传: {}", clientSha256);
-                consoleService.bindOnly(objectBindDTO);
+                consoleService.bindOnly(fastUploadResult);
                 sendResponse(ctx, "{\"status\":\"success\", \"msg\":\"Fast Uploaded\"}");
                 ctx.channel().attr(SKIP_BODY).set(true);
                 return;
@@ -140,12 +150,37 @@ public class OssUploadHandler extends SimpleChannelInboundHandler<HttpObject> {
 
         if (content instanceof LastHttpContent && state != null) {
             state.close();
-            String detectedMime = state.getMimeType(); // 获取识别到的类型
-            String sha256 = state.getSha256();
-            fileMover.moveSsdToHdd(state.getTempPath(), sha256, state.getFilename(), state.getTotalSize(), detectedMime);
-            String jsonPayload = "{\"status\":\"success\", \"sha256\":\"" + sha256 + "\"}";
-            String jsonPayload1 = "{\"code\":0,\"msg\":\"success\", \"data\":{\"sha256\":\"" + sha256 + "\"}}";
-            sendResponse(ctx, jsonPayload);
+            String channelPrefix = (String) ctx.channel().attr(AttributeKey.valueOf("channelPrefix")).get();
+            long uploadBy = (Long) ctx.channel().attr(AttributeKey.valueOf("uploadBy")).get();
+
+            String contentType = state.getMimeType(); // 获取识别到的类型
+            String sha256sum = state.getSha256();
+            String ssdTempPath = state.getTempPath();
+            String filename = state.getFilename();
+            long size = state.getTotalSize();
+            String uploadId = state.getObjectId();
+            String objectName = String.format("%s/%s", channelPrefix, uploadId);
+
+            String hostPort = String.format("%s:%s", ossStoreConfig.getStoreHost(), ossStoreConfig.getPort());
+            UploadResult uploadResult = new UploadResult();
+            uploadResult.setSha256sum(sha256sum);
+            //uploadResult.setAbsolutePath("");
+            uploadResult.setSize(size);
+            uploadResult.setFilename(filename);
+            uploadResult.setContentType(contentType);
+            uploadResult.setChannelPrefix(channelPrefix);
+            uploadResult.setObjectId(uploadId);
+            uploadResult.setObjectName(objectName);
+            uploadResult.setUploadBy(uploadBy);
+            uploadResult.setHostPort(hostPort);
+
+            consoleService.registerAndBind(uploadResult);
+            hddFlushService.moveSsdToHdd(uploadId, sha256sum, size, ssdTempPath);
+
+            String url = "";
+            UploadFileRet uploadFileRet = new UploadFileRet(uploadId, url);
+            WebBody webBody = new WebBody(uploadFileRet);
+            ResponseHelper.sendJsonResponse(ctx, HttpResponseStatus.OK, webBody);
         }
     }
 
@@ -160,9 +195,7 @@ public class OssUploadHandler extends SimpleChannelInboundHandler<HttpObject> {
 
     @Override
     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
-        if (!(cause instanceof java.io.IOException)) {
-            log.error("非 IO 异常: ", cause);
-        }
+        log.error("异常: ", cause);
         ctx.close();
     }
 }

+ 91 - 4
oss-store/src/main/java/cn/reghao/oss/store/rpc/StoreServiceImpl.java

@@ -1,11 +1,16 @@
 package cn.reghao.oss.store.rpc;
 
-import cn.reghao.oss.api.dto.ObjectInfo;
+import cn.reghao.jutil.jdk.media.model.MediaQuality;
+import cn.reghao.jutil.jdk.media.model.MediaResolution;
 import cn.reghao.oss.api.dto.disk.DiskVolume;
 import cn.reghao.oss.api.dto.media.ImageInfo;
 import cn.reghao.oss.api.dto.media.VideoInfo;
 import cn.reghao.oss.api.iface.StoreService;
 import cn.reghao.oss.api.util.OssSamplingHash;
+import cn.reghao.oss.store.disk.DiskService;
+import cn.reghao.oss.store.media.VideoMetadataValidator;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.io.FileUtils;
 import org.apache.dubbo.config.annotation.DubboService;
 import org.springframework.stereotype.Service;
@@ -21,9 +26,16 @@ import java.util.List;
  * @author reghao
  * @date 2023-08-01 14:54:23
  */
+@Slf4j
 @DubboService
 @Service
 public class StoreServiceImpl implements StoreService {
+    private DiskService diskService;
+
+    public StoreServiceImpl(DiskService diskService) {
+        this.diskService = diskService;
+    }
+
     @Override
     public List<DiskVolume> getDiskVolumes() {
         return Collections.emptyList();
@@ -44,12 +56,87 @@ public class StoreServiceImpl implements StoreService {
     }
 
     @Override
-    public VideoInfo getVideoInfo(String absolutePath) {
-        return null;
+    public VideoInfo getVideoInfo(String objectId, String absolutePath) {
+        try {
+            File file = new File(absolutePath);
+            if (!file.exists()) {
+                String tmpPath = diskService.getSsdTempPath(objectId);
+                file = new File(tmpPath);
+                if (!file.exists()) {
+                    String errorMsg = String.format("file with objectId %s not exist", objectId);
+                    throw new RuntimeException(errorMsg);
+                }
+            }
+
+            log.info("{}", file.getAbsolutePath());
+            JsonNode jsonNode = VideoMetadataValidator.getFullMetadata(file.getAbsolutePath());
+            VideoInfo videoInfo = parseVideoInfo(jsonNode);
+            videoInfo.setObjectId(objectId);
+            videoInfo.setUrlType("mp4");
+            videoInfo.setUrl("");
+
+            MediaResolution mediaResolution = MediaQuality.getQuality(videoInfo.getWidth(), videoInfo.getHeight());
+            videoInfo.setQuality(mediaResolution.getQualityStr());
+            return videoInfo;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public VideoInfo parseVideoInfo(JsonNode node) {
+        VideoInfo videoInfo = new VideoInfo();
+        // 1. 处理 format 层级的数据
+        JsonNode format = node.get("format");
+        if (format != null) {
+            videoInfo.setFormatName(format.path("format_name").asText());
+            videoInfo.setSize(format.path("size").asLong());
+            // duration 是字符串 "7.105011",转换为 Integer(秒)
+            videoInfo.setDuration((int) Math.round(format.path("duration").asDouble()));
+        }
+
+        // 2. 处理 streams 层级的数据
+        JsonNode streams = node.get("streams");
+        if (streams != null && streams.isArray()) {
+            for (JsonNode stream : streams) {
+                String codecType = stream.path("codec_type").asText();
+                if ("video".equals(codecType)) {
+                    // 填充视频流信息
+                    videoInfo.setVideoCodec(stream.path("codec_name").asText());
+                    videoInfo.setVbitRate(stream.path("bit_rate").asLong());
+                    videoInfo.setWidth(stream.path("width").asInt());
+                    videoInfo.setHeight(stream.path("height").asInt());
+                } else if ("audio".equals(codecType)) {
+                    // 填充音频流信息
+                    videoInfo.setAudioCodec(stream.path("codec_name").asText());
+                    videoInfo.setAbitRate(stream.path("bit_rate").asLong());
+                }
+            }
+        }
+
+        // 3. 设置业务字段(这些字段不在 JSON 中,需根据你的业务逻辑填充)
+        //videoInfo.setCreateTime(LocalDateTime.now());
+        // videoInfo.setVideoFileId(...);
+        // videoInfo.setQuality(...);
+        // videoInfo.setUrl(...);
+        return videoInfo;
     }
 
     @Override
-    public ImageInfo getImageInfo(String absolutePath) {
+    public ImageInfo getImageInfo(String objectId, String absolutePath) {
+        try {
+            File file = new File(absolutePath);
+            if (!file.exists()) {
+                String tmpPath = diskService.getSsdTempPath(objectId);
+                file = new File(tmpPath);
+                if (!file.exists()) {
+                    String errorMsg = String.format("file with objectId %s not exist", objectId);
+                    throw new RuntimeException(errorMsg);
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
         return null;
     }
 }

+ 2 - 6
oss-store/src/main/java/cn/reghao/oss/store/server/OssStoreServer.java

@@ -3,7 +3,6 @@ package cn.reghao.oss.store.server;
 import cn.reghao.oss.api.iface.ConsoleService;
 import cn.reghao.oss.store.config.OssStoreConfig;
 import cn.reghao.oss.store.disk.DiskService;
-import cn.reghao.oss.store.disk.FileMover;
 import cn.reghao.oss.store.disk.HddFlushService;
 import cn.reghao.oss.store.handler.CorsResponseFilter;
 import cn.reghao.oss.store.handler.OssRouterHandler;
@@ -36,17 +35,14 @@ public class OssStoreServer implements DisposableBean {
     private final int port;
     private OssStoreConfig ossStoreConfig;
     private ConsoleService consoleService;
-    private FileMover fileMover;
     private HddFlushService hddFlushService;
     private DiskService diskService;
 
     public OssStoreServer(OssStoreConfig ossStoreConfig, ConsoleService consoleService,
-                          FileMover fileMover, HddFlushService hddFlushService,
-                          DiskService diskService) {
+                          HddFlushService hddFlushService, DiskService diskService) {
         this.port = ossStoreConfig.getPort();
         this.ossStoreConfig = ossStoreConfig;
         this.consoleService = consoleService;
-        this.fileMover = fileMover;
         this.hddFlushService = hddFlushService;
         this.diskService = diskService;
     }
@@ -86,7 +82,7 @@ public class OssStoreServer implements DisposableBean {
                                 //p.addLast(new HttpObjectAggregator(65536)); // 聚合 Header, 流式处理不需要聚合
                                 p.addLast(new ChunkedWriteHandler());      // 支持大文件下载
                                 //p.addLast(ossUploadHandler);
-                                p.addLast(new OssRouterHandler(ossStoreConfig, consoleService, fileMover, hddFlushService, diskService));
+                                p.addLast(new OssRouterHandler(ossStoreConfig, consoleService, hddFlushService, diskService));
                                 //p.addLast(new OssMultipartUploadHandler(ossStoreConfig));
                                 //p.addLast(diskGroup, ossDownloadHandler);
                             }

+ 16 - 17
oss-store/src/main/java/cn/reghao/oss/store/util/ResponseHelper.java

@@ -4,11 +4,13 @@ import cn.reghao.oss.api.dto.WebBody;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufOutputStream;
+import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.http.*;
 import lombok.extern.slf4j.Slf4j;
 
 import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
 
 /**
  * @author reghao
@@ -27,26 +29,10 @@ public class ResponseHelper {
                 mapper.writeValue((OutputStream)os, webBody);
             }
 
-            // 3. 构建 Http 响应
-            FullHttpResponse response = new DefaultFullHttpResponse(
-                    HttpVersion.HTTP_1_1, status, content);
-
-            // 4. 设置 Header
+            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content);
             response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8");
             // 明确指定 Content-Length,否则某些客户端会一直等待 Body 结束
             response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
-
-            // 3. 检查是否需要保持长连接 (Keep-Alive)
-            // 如果不设置这个,某些客户端在发完第一个分片后,连接就会被 Netty 端的默认逻辑关闭
-            // 导致第二个分片发不出来(没响应)
-        /*if (HttpUtil.isKeepAlive(currentRequest)) { // 需要在 channelRead0 记录下当前的 HttpRequest
-            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
-            ctx.writeAndFlush(response);
-        } else {
-            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
-        }*/
-            // 5. 写入并刷盘,完成后自动释放连接
-            //ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
             ctx.writeAndFlush(response);
 
         } catch (Exception e) {
@@ -54,4 +40,17 @@ public class ResponseHelper {
             log.error("JSON 序列化失败", e);
         }
     }
+
+    public static void sendJsonResponse(ChannelHandlerContext ctx, String jsonBody) {
+        FullHttpResponse response = new DefaultFullHttpResponse(
+                HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
+                Unpooled.copiedBuffer(jsonBody, StandardCharsets.UTF_8));
+
+        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8");
+        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
+
+        // 如果是秒传,建议不要直接加 .addListener(ChannelFutureListener.CLOSE)
+        // 除非你前端配合在收到响应后立即断开上传流
+        ctx.writeAndFlush(response);
+    }
 }

+ 3 - 6
oss-store/src/main/resources/application-dev.yml

@@ -6,11 +6,8 @@ spring:
 oss:
   port: 8020
   store-host: 127.0.0.1
-  ssd-dir: /disk/1/ssd
+  ssd-dir: /opt/disk/ssd
   hdd-dirs:
-    - /opt/disk/1
+    - /disk/1
   console-host: 127.0.0.1
-  console-port: 18010
-logging:
-  level:
-    org.apache.dubbo: OFF
+  console-port: 18010