reghao hace 4 semanas
padre
commit
0b2b854a93
Se han modificado 100 ficheros con 5111 adiciones y 2 borrados
  1. 19 2
      oss-api/pom.xml
  2. 55 0
      oss-api/src/main/java/cn/reghao/oss/api/constant/ObjectAction.java
  3. 56 0
      oss-api/src/main/java/cn/reghao/oss/api/constant/ObjectScope.java
  4. 39 0
      oss-api/src/main/java/cn/reghao/oss/api/constant/ObjectSize.java
  5. 67 0
      oss-api/src/main/java/cn/reghao/oss/api/constant/ObjectType.java
  6. 46 0
      oss-api/src/main/java/cn/reghao/oss/api/constant/UploadChannelType.java
  7. 21 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/NodeProperties.java
  8. 25 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/ObjectBindDTO.java
  9. 27 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/ObjectChannel.java
  10. 43 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/ObjectInfo.java
  11. 26 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/ObjectMeta.java
  12. 29 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/OssStoreToken.java
  13. 28 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/OssUploadResultDTO.java
  14. 22 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/PhysicalFile.java
  15. 17 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/SelectOption.java
  16. 21 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/ServerInfo.java
  17. 28 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/StoreNodeDto.java
  18. 36 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/UploadFilePart.java
  19. 29 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/UploadedFile.java
  20. 27 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/WebBody.java
  21. 36 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/disk/DiskStore.java
  22. 47 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/disk/DiskVolume.java
  23. 32 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/media/AudioInfo.java
  24. 21 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/media/AudioUrl.java
  25. 29 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/media/ConvertedImageInfo.java
  26. 33 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/media/ImageInfo.java
  27. 24 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/media/ImageUrlDto.java
  28. 42 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/media/VideoInfo.java
  29. 24 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/media/VideoUrlDto.java
  30. 36 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadFilePart.java
  31. 37 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadFileRet.java
  32. 35 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadPrepare.java
  33. 32 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadPrepareRet.java
  34. 39 0
      oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadedPart.java
  35. 39 0
      oss-api/src/main/java/cn/reghao/oss/api/iface/ConsoleService.java
  36. 26 0
      oss-api/src/main/java/cn/reghao/oss/api/iface/StoreService.java
  37. 56 0
      oss-api/src/main/java/cn/reghao/oss/api/util/JwtUtils.java
  38. 64 0
      oss-api/src/main/java/cn/reghao/oss/api/util/OssClientSigner.java
  39. 95 0
      oss-api/src/main/java/cn/reghao/oss/api/util/OssSamplingHash.java
  40. 40 0
      oss-api/src/main/java/cn/reghao/oss/api/util/SignatureUtils.java
  41. 7 0
      oss-mgr/Dockerfile
  42. 158 0
      oss-mgr/pom.xml
  43. 11 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/OssMgrApplication.java
  44. 37 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/config/BeanConfig.java
  45. 68 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/config/CacheConfig.java
  46. 26 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/config/FilterConfig.java
  47. 97 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/config/OssAuthFilter.java
  48. 25 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/config/SpringLifecycle.java
  49. 35 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/config/UserContext.java
  50. 68 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/config/mybatis/DataSourceConfig.java
  51. 83 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/config/mybatis/PageListInterceptor.java
  52. 171 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/OssSdkController.java
  53. 64 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/StoreNodeController.java
  54. 110 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/UploadChannelController.java
  55. 45 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/UserKeyController.java
  56. 69 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/UserNodeController.java
  57. 17 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/DataBlockMapper.java
  58. 22 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/FileMetaMapper.java
  59. 17 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/StoreNodeMapper.java
  60. 16 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/StoreVolumeMapper.java
  61. 25 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/UploadChannelMapper.java
  62. 20 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/UploadTaskMapper.java
  63. 17 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/UserKeyMapper.java
  64. 28 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/UserNodeMapper.java
  65. 39 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/repository/ChannelRepository.java
  66. 77 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/repository/ObjectRepository.java
  67. 56 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/repository/StoreRepository.java
  68. 15 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/FileInitRequest.java
  69. 22 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/KeyAuthDto.java
  70. 26 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/NodeAddDto.java
  71. 28 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/NodeUpdateDto.java
  72. 35 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/UploadChannelDto.java
  73. 15 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/UploadSample.java
  74. 46 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/DataBlock.java
  75. 105 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/FileMeta.java
  76. 37 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/StoreNode.java
  77. 63 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/StoreVolume.java
  78. 75 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/UploadChannel.java
  79. 60 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/UploadTask.java
  80. 36 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/UserKey.java
  81. 51 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/UserNode.java
  82. 17 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/vo/AddChannelAttr.java
  83. 33 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/vo/StoreNodeInfo.java
  84. 247 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/rpc/ConsoleServiceImpl.java
  85. 99 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/rpc/RpcService.java
  86. 126 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/MetadataService.java
  87. 22 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/StoreConfigService.java
  88. 177 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/StoreNodeService.java
  89. 41 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/TaskService.java
  90. 158 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/UploadChannelService.java
  91. 56 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/UserKeyService.java
  92. 167 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/UserNodeService.java
  93. 228 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/util/ServletUtil.java
  94. 16 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/util/StringUtil.java
  95. 5 0
      oss-mgr/src/main/resources/application-dev.yml
  96. 5 0
      oss-mgr/src/main/resources/application-test.yml
  97. 59 0
      oss-mgr/src/main/resources/application.yml
  98. 77 0
      oss-mgr/src/main/resources/logback-spring.xml
  99. 38 0
      oss-mgr/src/main/resources/mapper/DataBlockMapper.xml
  100. 70 0
      oss-mgr/src/main/resources/mapper/FileMetaMapper.xml

+ 19 - 2
oss-api/pom.xml

@@ -13,12 +13,29 @@
     <version>1.0.0-SNAPSHOT</version>
 
     <properties>
-        <maven.compiler.source>17</maven.compiler.source>
-        <maven.compiler.target>17</maven.compiler.target>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     </properties>
 
     <dependencies>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-api</artifactId>
+            <version>0.13.0</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-impl</artifactId>
+            <version>0.13.0</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
+            <version>0.13.0</version>
+            <scope>runtime</scope>
+        </dependency>
     </dependencies>
 </project>

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

@@ -0,0 +1,55 @@
+package cn.reghao.oss.api.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2023-10-18 13:15:45
+ */
+public enum ObjectAction {
+    upload(1, "上传"),
+    download(2, "下载"),
+    access(3, "访问"),
+    delete(4, "删除"),
+    all(5, "all");
+
+    private final int code;
+    private final String desc;
+
+    private static Map<Integer, String> descMap = new HashMap<>();
+    static {
+        for (ObjectAction scope : ObjectAction.values()) {
+            descMap.put(scope.code, scope.desc);
+        }
+    }
+
+    ObjectAction(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;
+    }
+
+    // TODO 第一次调用时会初始化 descMap
+    public static String getDescByCode(int code) {
+        return descMap.get(code);
+    }
+}

+ 56 - 0
oss-api/src/main/java/cn/reghao/oss/api/constant/ObjectScope.java

@@ -0,0 +1,56 @@
+package cn.reghao.oss.api.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 描述资源的可见范围
+ *
+ * @author reghao
+ * @date 2023-05-19 18:15:45
+ */
+public enum ObjectScope {
+    PRIVATE(1, "本人可见"),
+    PUBLIC(2, "所有人可见");
+
+    private final int code;
+    private final String desc;
+    private static final Map<Integer, ObjectScope> map = new HashMap<>();
+    static {
+        for (ObjectScope scope : ObjectScope.values()) {
+            map.put(scope.code, scope);
+        }
+    }
+
+    ObjectScope(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 ObjectScope getByCode(int code) {
+        return map.get(code);
+    }
+}

+ 39 - 0
oss-api/src/main/java/cn/reghao/oss/api/constant/ObjectSize.java

@@ -0,0 +1,39 @@
+package cn.reghao.oss.api.constant;
+
+/**
+ * @author reghao
+ * @date 2024-10-23 10:49:04
+ */
+public enum ObjectSize {
+    mb10(1024L*1024*10, "10MB"),
+    mb100(1024L*1024*100, "100MB"),
+    gb1(1024L*1024*1024, "1GB"),
+    gb10(1024L*1024*1024*10, "10GB"),
+    gb20(1024L*1024*1024*20, "20GB");
+
+    private final long size;
+    private final String desc;
+    ObjectSize(long size, String desc) {
+        this.size = size;
+        this.desc = desc;
+    }
+
+    /**
+     * 提供给 @ValidEnum 调用
+     *
+     * @param
+     * @return
+     * @date 2023-10-11 14:44:42
+     */
+    public long getValue() {
+        return this.size;
+    }
+
+    public long getSize() {
+        return size;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+}

+ 67 - 0
oss-api/src/main/java/cn/reghao/oss/api/constant/ObjectType.java

@@ -0,0 +1,67 @@
+package cn.reghao.oss.api.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2023-06-13 15:09:09
+ */
+public enum ObjectType {
+    Dir(1000, "Dir"),
+    Image(1001, "Image"),
+    Video(1002, "Video"),
+    Audio(1003, "Audio"),
+    Text(1004, "Text"),
+    Other(1005, "Application"),
+    Any(1006, "*/*");
+
+    private final int code;
+    // contentType 的值不能改变, 代码中会使用它作为前缀和上传文件的 content-type 进行比较
+    private final String contentType;
+    ObjectType(int code, String contentType) {
+        this.code = code;
+        this.contentType = contentType;
+    }
+
+    private static Map<Integer, ObjectType> map = new HashMap<>();
+    static {
+        for (ObjectType type : ObjectType.values()) {
+            map.put(type.code, type);
+        }
+    }
+
+    private static Map<Integer, String> descMap = new HashMap<>();
+    static {
+        for (ObjectType objectType : ObjectType.values()) {
+            descMap.put(objectType.code, objectType.contentType);
+        }
+    }
+
+    public static ObjectType getByCode(int code) {
+        return map.get(code);
+    }
+
+    /**
+     * 提供给 @ValidEnum 调用
+     *
+     * @param
+     * @return
+     * @date 2023-10-11 14:44:42
+     */
+    public int getValue() {
+        return this.code;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDesc() {
+        return contentType;
+    }
+
+    public static String getDescByCode(int code) {
+        return descMap.get(code);
+    }
+}

+ 46 - 0
oss-api/src/main/java/cn/reghao/oss/api/constant/UploadChannelType.java

@@ -0,0 +1,46 @@
+package cn.reghao.oss.api.constant;
+
+/**
+ * @author reghao
+ * @date 2025-10-19 17:24:54
+ */
+public enum UploadChannelType {
+    videoChannel(101, "video/playback/", ObjectType.Video.getCode(), ObjectSize.mb10.getSize(), ObjectScope.PUBLIC.getCode()),
+    imageChannel(102, "image/i/", ObjectType.Image.getCode(), ObjectSize.mb100.getSize(), ObjectScope.PUBLIC.getCode()),
+    photoChannel(103, "image/p/", ObjectType.Image.getCode(), ObjectSize.gb10.getSize(), ObjectScope.PUBLIC.getCode()),
+    fileChannel(104, "file/", ObjectType.Any.getCode(), ObjectSize.gb10.getSize(), ObjectScope.PRIVATE.getCode()),
+    camChannel(105, "video/cam/", ObjectType.Video.getCode(), ObjectSize.gb10.getSize(), ObjectScope.PRIVATE.getCode());
+
+    private final int channelCode;
+    private final String channelPrefix;
+    private final int fileType;
+    private final long maxSize;
+    private final int scope;
+    UploadChannelType(int channelCode, String channelPrefix, int fileType, long maxSize, int scope) {
+        this.channelCode = channelCode;
+        this.channelPrefix = channelPrefix;
+        this.fileType = fileType;
+        this.maxSize = maxSize;
+        this.scope = scope;
+    }
+
+    public int getChannelCode() {
+        return channelCode;
+    }
+
+    public String getChannelPrefix() {
+        return channelPrefix;
+    }
+
+    public int getFileType() {
+        return fileType;
+    }
+
+    public long getMaxSize() {
+        return maxSize;
+    }
+
+    public int getScope() {
+        return scope;
+    }
+}

+ 21 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/NodeProperties.java

@@ -0,0 +1,21 @@
+package cn.reghao.oss.api.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2024-03-01 09:22:17
+ */
+@AllArgsConstructor
+@Getter
+public class NodeProperties implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String domain;
+    private String secretKey;
+    private String referer;
+    private int owner;
+}

+ 25 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/ObjectBindDTO.java

@@ -0,0 +1,25 @@
+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-01 15:50:24
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Setter
+@Getter
+public class ObjectBindDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String sha256sum;
+    private String filename;
+    private String objectId;
+    private String objectName;
+}

+ 27 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/ObjectChannel.java

@@ -0,0 +1,27 @@
+package cn.reghao.oss.api.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2024-02-26 10:08:12
+ */
+@AllArgsConstructor
+@Getter
+public class ObjectChannel implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private int id;
+    private int channelCode;
+    private String prefix;
+    private long maxSize;
+    private int fileType;
+    private boolean setUrl;
+    private boolean setCallback;
+    private int scope;
+    private String domain;
+    private int createBy;
+}

+ 43 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/ObjectInfo.java

@@ -0,0 +1,43 @@
+package cn.reghao.oss.api.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2023-11-29 18:11:13
+ */
+@NoArgsConstructor
+@Setter
+@Getter
+public class ObjectInfo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String objectId;
+    private String objectName;
+    private int fileType;
+    private String filename;
+    private long size;
+    private String url;
+    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;
+    }
+}

+ 26 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/ObjectMeta.java

@@ -0,0 +1,26 @@
+package cn.reghao.oss.api.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2023-04-30 12:17:28
+ */
+@Setter
+@Getter
+public class ObjectMeta implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private int id;
+    private String objectName;
+    private String objectId;
+    private String absolutePath;
+    private String filename;
+    private long size;
+    private String contentType;
+    private int scope;
+    private int uploadBy;
+}

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

@@ -0,0 +1,29 @@
+package cn.reghao.oss.api.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * @author reghao
+ * @date 2026-02-22 15:55:30
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Setter
+@Getter
+public class OssStoreToken {
+    private String action;
+    private int accessBy;
+    private long expireAt;
+    private String channelPrefix;
+    private String contentType;
+    private long maxSize;
+
+    public OssStoreToken(String action, int accessBy, long expireAt) {
+        this.action = action;
+        this.accessBy = accessBy;
+        this.expireAt = expireAt;
+    }
+}

+ 28 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/OssUploadResultDTO.java

@@ -0,0 +1,28 @@
+package cn.reghao.oss.api.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2026-02-01 15:57:23
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class OssUploadResultDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String sha256sum;
+    private String absolutePath;
+    private long size;
+    private String filename;
+    private String contentType;
+    private String objectId;
+    private String objectName;
+    private long uploadBy;
+    private String hostPort;
+}

+ 22 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/PhysicalFile.java

@@ -0,0 +1,22 @@
+package cn.reghao.oss.api.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author reghao
+ * @date 2026-02-01 15:50:05
+ */
+@Setter
+@Getter
+public class PhysicalFile {
+    private String sha256Sum;
+    private String absolutePath;
+    private long size;
+    private String filename;
+    private String contentType;
+    private String objectId;
+    private String objectName;
+    private int uploadBy;
+    private String nodeAddress;
+}

+ 17 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/SelectOption.java

@@ -0,0 +1,17 @@
+package cn.reghao.oss.api.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 前端 el-select 标签中 el-option 的值
+ *
+ * @author reghao
+ * @date 2024-09-14 09:11:32
+ */
+@AllArgsConstructor
+@Getter
+public class SelectOption {
+    private String label;
+    private String value;
+}

+ 21 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/ServerInfo.java

@@ -0,0 +1,21 @@
+package cn.reghao.oss.api.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2023-08-23 11:41:55
+ */
+@AllArgsConstructor
+@Getter
+public class ServerInfo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String ossUrl;
+    private int channelCode;
+    private long maxSize;
+    private String token;
+}

+ 28 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/StoreNodeDto.java

@@ -0,0 +1,28 @@
+package cn.reghao.oss.api.dto;
+
+import cn.reghao.oss.api.dto.disk.DiskVolume;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2024-03-04 17:12:14
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Setter
+@Getter
+public class StoreNodeDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String nodeAddr;
+    private int httpPort;
+    private int rpcPort;
+    private List<DiskVolume> diskVolumes = new ArrayList<>();
+}

+ 36 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/UploadFilePart.java

@@ -0,0 +1,36 @@
+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 2022-04-25 10:42:38
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class UploadFilePart implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private int channelCode;
+    // 文件标识
+    private String identifier;
+    private String filename;
+    private String relativePath;
+    // 文件大小
+    private long totalSize;
+    // 分片文件大小
+    private long chunkSize;
+    // 分片文件数量
+    private long totalChunks;
+    // 当前分片文件索引
+    private int chunkNumber;
+    // 当前分片文件大小
+    private int currentChunkSize;
+}

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

@@ -0,0 +1,29 @@
+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;
+}

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

@@ -0,0 +1,27 @@
+package cn.reghao.oss.api.dto;
+
+import cn.reghao.oss.api.dto.rest.UploadFileRet;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * @author reghao
+ * @date 2026-02-21 11:37:38
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Setter
+@Getter
+public class WebBody {
+    private int code;
+    private String msg;
+    private UploadFileRet uploadFileRet;
+
+    public WebBody(UploadFileRet uploadFileRet) {
+        this.code = 0;
+        this.msg = "success";
+        this.uploadFileRet = uploadFileRet;
+    }
+}

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

@@ -0,0 +1,36 @@
+package cn.reghao.oss.api.dto.disk;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 代表一块磁盘
+ *
+ * @author reghao
+ * @date 2024-10-21 09:50:23
+ */
+@AllArgsConstructor
+@Getter
+public class DiskStore implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String model;
+    private String name;
+    private long size;
+    private List<DiskPartition> partitions;
+
+    @AllArgsConstructor
+    @Getter
+    public static class DiskPartition implements Serializable {
+        private static final long serialVersionUID = 1L;
+
+        private String partitionName;
+        private String mountPoint;
+        private String blockId;
+        private String fsType;
+        private long partitionSize;
+    }
+}

+ 47 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/disk/DiskVolume.java

@@ -0,0 +1,47 @@
+package cn.reghao.oss.api.dto.disk;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * 一个磁盘分区及其下面的存储目录
+ *
+ * @author reghao
+ * @date 2024-10-21 11:29:28
+ */
+@AllArgsConstructor
+@Setter
+@Getter
+public class DiskVolume implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String name;
+    // 磁盘分区
+    private String volume;
+    // 磁盘分区挂载的目录
+    private String mountPoint;
+    private String fsType;
+    private String blockId;
+    private long totalSpace;
+    private long availSpace;
+    private long totalInode;
+    private long availInode;
+    // 实际存储文件的目录(在挂载目录下)
+    private String storeDir;
+
+    public DiskVolume(String name, String volume, String mountPoint, String fsType, String blockId,
+                      long totalSpace, long availSpace, long totalInode, long availInode) {
+        this.name = name;
+        this.volume = volume;
+        this.mountPoint = mountPoint;
+        this.fsType = fsType;
+        this.blockId = blockId;
+        this.totalSpace = totalSpace;
+        this.availSpace = availSpace;
+        this.totalInode = totalInode;
+        this.availInode = availInode;
+    }
+}

+ 32 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/media/AudioInfo.java

@@ -0,0 +1,32 @@
+package cn.reghao.oss.api.dto.media;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2023-08-28 17:00:56
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Setter
+@Getter
+public class AudioInfo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String audioFileId;
+    private String objectId;
+    private int duration;
+    private String codec;
+    private Long bitRate;
+    private String url;
+    private Long size;
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+}

+ 21 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/media/AudioUrl.java

@@ -0,0 +1,21 @@
+package cn.reghao.oss.api.dto.media;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2023-10-12 15:07:44
+ */
+@Setter
+@Getter
+public class AudioUrl implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String audioFileId;
+    private String codec;
+    private Long bitRate;
+    private String url;
+}

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

@@ -0,0 +1,29 @@
+package cn.reghao.oss.api.dto.media;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2024-03-08 17:07:23
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Setter
+@Getter
+public class ConvertedImageInfo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String imageFileId;
+    private String objectId;
+    private String format;
+    private String url;
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+}

+ 33 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/media/ImageInfo.java

@@ -0,0 +1,33 @@
+package cn.reghao.oss.api.dto.media;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2024-03-08 10:56:27
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Setter
+@Getter
+public class ImageInfo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    // 原始文件的 objectId
+    private String imageFileId;
+    private String objectId;
+    private String format;
+    private String url;
+    private Integer width;
+    private Integer height;
+    private Long size;
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+}

+ 24 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/media/ImageUrlDto.java

@@ -0,0 +1,24 @@
+package cn.reghao.oss.api.dto.media;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2023-08-28 15:50:38
+ */
+@Setter
+@Getter
+public class ImageUrlDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String imageFileId;
+    private String originalUrl;
+    private String thumbnailUrl;
+
+    public ImageUrlDto(String imageFileId) {
+        this.imageFileId = imageFileId;
+    }
+}

+ 42 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/media/VideoInfo.java

@@ -0,0 +1,42 @@
+package cn.reghao.oss.api.dto.media;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * @author reghao
+ * @date 2023-01-11 10:41:53
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Setter
+@Getter
+public class VideoInfo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String videoFileId;
+    private String objectId;
+    private String videoCodec;
+    private Long vbitRate;
+    private String audioCodec;
+    private Long abitRate;
+    private String formatName;
+    private String urlType;
+    private String url;
+    private String quality;
+    private Integer width;
+    private Integer height;
+    // 单位秒
+    private Integer duration;
+    private LocalDateTime createTime;
+    private Long size;
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+}

+ 24 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/media/VideoUrlDto.java

@@ -0,0 +1,24 @@
+package cn.reghao.oss.api.dto.media;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2023-01-10 09:51:53
+ */
+@Getter
+@Setter
+public class VideoUrlDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String objectId;
+    private int channelCode;
+    private String type;
+    private String url;
+    private int width;
+    private int height;
+    private String quality;
+}

+ 36 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadFilePart.java

@@ -0,0 +1,36 @@
+package cn.reghao.oss.api.dto.rest;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2022-04-25 10:42:38
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class UploadFilePart implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private int channelCode;
+    // 文件标识
+    private String identifier;
+    private String filename;
+    private String relativePath;
+    // 文件大小
+    private long totalSize;
+    // 分片文件大小
+    private long chunkSize;
+    // 分片文件数量
+    private long totalChunks;
+    // 当前分片文件索引
+    private int chunkNumber;
+    // 当前分片文件大小
+    private int currentChunkSize;
+}

+ 37 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadFileRet.java

@@ -0,0 +1,37 @@
+package cn.reghao.oss.api.dto.rest;
+
+import lombok.Getter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2023-05-23 11:11:32
+ */
+@Getter
+public class UploadFileRet implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private final String uploadId;
+    private boolean fastUpload;
+    private String url;
+    private boolean merged;
+
+    public UploadFileRet(String uploadId) {
+        this.uploadId = uploadId;
+        this.url = null;
+        this.merged = false;
+    }
+
+    public UploadFileRet(String uploadId, boolean fastUpload) {
+        this.uploadId = uploadId;
+        this.url = null;
+        this.fastUpload = fastUpload;
+    }
+
+    public UploadFileRet(String uploadId, String url) {
+        this.uploadId = uploadId;
+        this.url = url;
+        this.merged = true;
+    }
+}

+ 35 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadPrepare.java

@@ -0,0 +1,35 @@
+package cn.reghao.oss.api.dto.rest;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * 分片文件
+ *
+ * @author reghao
+ * @date 2021-11-23 10:23:00
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Setter
+@Getter
+public class UploadPrepare implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @NotNull
+    private int channelCode;
+    @NotBlank
+    private String filename;
+    @NotNull
+    private long size;
+    @NotBlank
+    private String sha256sum;
+    @NotBlank
+    private String contentType;
+}

+ 32 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadPrepareRet.java

@@ -0,0 +1,32 @@
+package cn.reghao.oss.api.dto.rest;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2022-04-21 09:27:55
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+public class UploadPrepareRet implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String uploadId;
+    private int splitSize;
+    private boolean exist;
+    private long offset;
+    private int length;
+
+    public UploadPrepareRet(String uploadId, int splitSize) {
+        this.uploadId = uploadId;
+        this.splitSize = splitSize;
+        this.exist = false;
+        this.offset = 0;
+        this.length = 0;
+    }
+}

+ 39 - 0
oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadedPart.java

@@ -0,0 +1,39 @@
+package cn.reghao.oss.api.dto.rest;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2023-05-24 09:23:25
+ */
+@Setter
+@Getter
+public class UploadedPart implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private boolean needMerge;
+    private List<Integer> uploaded;
+    private boolean skipUpload;
+    private String uploadId;
+    private String url;
+
+    public UploadedPart(String uploadId, String url) {
+        this.uploadId = uploadId;
+        this.url = url;
+        this.skipUpload = true;
+    }
+
+    public UploadedPart() {
+        this.skipUpload = false;
+        this.uploaded = Collections.emptyList();
+    }
+
+    public void setUploaded(List<Integer> uploaded) {
+        this.uploaded = uploaded;
+    }
+}

+ 39 - 0
oss-api/src/main/java/cn/reghao/oss/api/iface/ConsoleService.java

@@ -0,0 +1,39 @@
+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;
+
+/**
+ * 获取由 oss-console 管理的 StoreNode 配置
+ * 由 oss-console 实现, oss-store 调用
+ *
+ * @author reghao
+ * @date 2024-07-02 10:55:56
+ */
+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);
+    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);
+}

+ 26 - 0
oss-api/src/main/java/cn/reghao/oss/api/iface/StoreService.java

@@ -0,0 +1,26 @@
+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;
+
+import java.util.List;
+
+/**
+ * 对 oss-store 进行操作
+ * 由 oss-store 实现, oss-console 调用
+ *
+ * @author reghao
+ * @date 2023-08-01 14:51:50
+ */
+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);
+}

+ 56 - 0
oss-api/src/main/java/cn/reghao/oss/api/util/JwtUtils.java

@@ -0,0 +1,56 @@
+package cn.reghao.oss.api.util;
+
+import io.jsonwebtoken.*;
+import io.jsonwebtoken.security.Keys;
+import javax.crypto.SecretKey;
+import java.util.Date;
+import java.util.Map;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+
+/**
+ * @author reghao
+ * @date 2026-02-18 16:07:44
+ */
+public class JwtUtils {
+    // 256-bit 密钥:21TB 系统建议通过配置中心下发,不要硬编码
+    private static final String SECRET_STR = "your-21tb-storage-system-super-secret-key-001";
+    private static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET_STR.getBytes(StandardCharsets.UTF_8));
+
+    // 预构建解析器以提升性能(在高并发 Store 节点写入时非常重要)
+    private static final JwtParser PARSER = Jwts.parser()
+            .verifyWith(KEY)
+            .build();
+
+    /**
+     * 签发 Token
+     * 0.13.0 中 builder().claims(map) 会将 map 中的内容放入 Payload
+     */
+    public static String createToken(Map<String, Object> claims, long expireMillis) {
+        long now = System.currentTimeMillis();
+
+        return Jwts.builder()
+                .claims(claims)                    // 自定义载荷 (tenantId, fileHash 等)
+                .id(UUID.randomUUID().toString()) // 建议加上 jti,防止重放攻击
+                .issuedAt(new Date(now))           // 签发时间
+                .expiration(new Date(now + expireMillis)) // 过期时间
+                .signWith(KEY)                     // 0.13.0 自动选择算法(通常是 HS256)
+                .compact();
+    }
+
+    /**
+     * 解析并校验 Token
+     */
+    public static Claims parseToken(String token) {
+        try {
+            // parseSignedClaims 处理带签名的 JWT (JWS)
+            return PARSER.parseSignedClaims(token).getPayload();
+        } catch (ExpiredJwtException e) {
+            // 21TB 大文件上传时,如果 Token 在传输中途过期,Store 需拒绝后续分片
+            return null;
+        } catch (JwtException | IllegalArgumentException e) {
+            // 签名非法或 Token 格式错误
+            return null;
+        }
+    }
+}

+ 64 - 0
oss-api/src/main/java/cn/reghao/oss/api/util/OssClientSigner.java

@@ -0,0 +1,64 @@
+package cn.reghao.oss.api.util;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.Base64;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * @author reghao
+ * @date 2026-02-22 17:52:20
+ */
+public class OssClientSigner {
+    /**
+     * 构建 Authorization 请求头
+     * @param method   请求方法 (GET, PUT, POST)
+     * @param url 资源路径 (例如 /bucket/file.jpg)
+     * @param contentType Content-Type (可为空)
+     * @return 完整的 Authorization 字符串
+     */
+    public static String buildAuthHeader(String ak, String sk, String method, String url, String contentType) throws Exception {
+        String signature = getSignature(ak, sk, method, url, contentType);
+        // 4. 返回 Header 格式
+        return "OSS " + ak + ":" + signature;
+    }
+
+    private static String getSignature(String ak, String sk, String method, String url, String contentType) {
+        // 1. 获取标准 HTTP GMT 时间
+        String dateStr = getGmtDate();
+        // 2. 构造 StringToSign (需与后端严格对齐)
+        String stringToSign = getStringToSign(method, url, contentType, dateStr);
+        // 3. HMAC-SHA256 加密
+        String signature = calculateHmac(sk, stringToSign);
+        return signature;
+    }
+
+    public static String getStringToSign(String method, String url, String contentType, String dateStr) {
+        String md5 = "";
+        String stringToSign0 = method + "\n" + md5 + "\n" + contentType + "\n" + dateStr + "\n" + url;
+        return stringToSign0;
+    }
+
+    public static String calculateHmac(String sk, String stringToSign) {
+        try {
+            SecretKeySpec signingKey = new SecretKeySpec(sk.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+            Mac mac = Mac.getInstance("HmacSHA256");
+            mac.init(signingKey);
+            byte[] rawHmac = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
+            return Base64.getEncoder().encodeToString(rawHmac);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to calculate HMAC", e);
+        }
+    }
+
+    private final static SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
+    // 获取当前的 GMT 时间供请求头使用
+    public static String getGmtDate() {
+        sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
+        return sdf.format(new Date());
+    }
+}

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

@@ -0,0 +1,95 @@
+package cn.reghao.oss.api.util;
+
+import java.io.File;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.security.MessageDigest;
+import java.util.HexFormat;
+
+/**
+ * @author reghao
+ * @date 2026-02-02 09:47:25
+ */
+public class OssSamplingHash {
+    private static final int SAMPLE_SIZE = 2 * 1024 * 1024; // 每段取 2MB
+
+    public static String calculateSampleHash(Path path) throws Exception {
+        long fileSize = path.toFile().length();
+
+        // 如果文件太小(小于 6MB),直接全量计算
+        if (fileSize <= SAMPLE_SIZE * 3) {
+            return calculateFullHash(path);
+        }
+
+        MessageDigest digest = MessageDigest.getInstance("SHA-256");
+        try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
+            // 1. 抽取头部
+            readAndUpdate(channel, 0, SAMPLE_SIZE, digest);
+
+            // 2. 抽取中间
+            readAndUpdate(channel, fileSize / 2, SAMPLE_SIZE, digest);
+
+            // 3. 抽取尾部
+            readAndUpdate(channel, fileSize - SAMPLE_SIZE, SAMPLE_SIZE, digest);
+        }
+
+        // 4. 关键:混合文件大小,防止不同大小的文件但在这些位置数据相同
+        digest.update(ByteBuffer.allocate(8).putLong(fileSize).flip());
+        return HexFormat.of().formatHex(digest.digest());
+    }
+
+    public static String calculateSampleMd5(String absolutePath, long offset, int length) {
+        File file = new File(absolutePath);
+
+        if (!file.exists()) return null;
+
+        try (RandomAccessFile raf = new RandomAccessFile(file, "r");
+             FileChannel channel = raf.getChannel()) {
+
+            // 1. 定位并读取指定区间的数据到内存
+            ByteBuffer buffer = ByteBuffer.allocate(length);
+            channel.read(buffer, offset);
+            buffer.flip();
+
+            // 2. 计算该区段的哈希 (为了速度,局部哈希可以用 MD5)
+            MessageDigest md5 = MessageDigest.getInstance("MD5");
+            md5.update(buffer);
+            return bytesToHex(md5.digest());
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static String bytesToHex(byte[] hash) {
+        StringBuilder hexString = new StringBuilder();
+        for (byte b : hash) {
+            String hex = Integer.toHexString(0xff & b);
+            if (hex.length() == 1) hexString.append('0');
+            hexString.append(hex);
+        }
+        return hexString.toString();
+    }
+
+    private static void readAndUpdate(FileChannel channel, long position, int size, MessageDigest digest) throws Exception {
+        ByteBuffer buffer = ByteBuffer.allocate(size);
+        channel.read(buffer, position);
+        buffer.flip();
+        digest.update(buffer);
+    }
+
+    public static String calculateFullHash(Path path) throws Exception {
+        MessageDigest digest = MessageDigest.getInstance("SHA-256");
+        try (var is = Files.newInputStream(path)) {
+            byte[] buffer = new byte[8192];
+            int read;
+            while ((read = is.read(buffer)) != -1) {
+                digest.update(buffer, 0, read);
+            }
+        }
+        return HexFormat.of().formatHex(digest.digest());
+    }
+}

+ 40 - 0
oss-api/src/main/java/cn/reghao/oss/api/util/SignatureUtils.java

@@ -0,0 +1,40 @@
+package cn.reghao.oss.api.util;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * @author reghao
+ * @date 2026-02-02 16:39:46
+ */
+public class SignatureUtils {
+    private static final String HMAC_ALGO = "HmacSHA256";
+    private static final String SECRET_KEY = "your-very-secret-key-do-not-leak";
+
+    /**
+     * 生成签名
+     * @param accessKeyId 租户ID
+     * @param expires 过期时间戳(秒)
+     * @param objectId 物理文件路径/对象ID
+     * @param secretKey 租户对应的密钥
+     */
+    public static String calculateSignature(String accessKeyId, long expires, String objectId) {
+        try {
+            // 待签名字符串:严格对应 oss-store 校验的顺序
+            String stringToSign = accessKeyId + "\n" + expires + "\n" + objectId;
+
+            Mac hmacSha256 = Mac.getInstance(HMAC_ALGO);
+            SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), HMAC_ALGO);
+            hmacSha256.init(secretKeySpec);
+
+            byte[] hash = hmacSha256.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
+
+            // 使用 URL 安全的 Base64 编码 (去掉末尾的 = 填充)
+            return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
+        } catch (Exception e) {
+            throw new RuntimeException("签名生成失败", e);
+        }
+    }
+}

+ 7 - 0
oss-mgr/Dockerfile

@@ -0,0 +1,7 @@
+FROM registry.cn-chengdu.aliyuncs.com/reghao/eclipse-temurin:21-jre-alpine
+
+WORKDIR /app
+RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
+COPY target/oss-mgr.jar /app/oss-mgr.jar
+
+ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app/oss-mgr.jar"]

+ 158 - 0
oss-mgr/pom.xml

@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>cn.reghao.oss</groupId>
+        <artifactId>oss</artifactId>
+        <version>1.0.0</version>
+    </parent>
+
+    <artifactId>oss-mgr</artifactId>
+    <version>1.0.0</version>
+    <packaging>jar</packaging>
+
+    <properties>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${springboot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.reghao.oss</groupId>
+            <artifactId>oss-api</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+            <version>8.0.31</version>
+        </dependency>
+        <dependency>
+            <groupId>com.zaxxer</groupId>
+            <artifactId>HikariCP</artifactId>
+            <version>3.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-starter</artifactId>
+            <version>3.0.4</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-cache</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.dubbo</groupId>
+            <artifactId>dubbo-spring-boot-starter</artifactId>
+            <version>3.3.6</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-ui</artifactId>
+            <version>1.7.0</version>
+        </dependency>
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <id>dev</id>
+            <properties>
+                <profile.active>dev</profile.active>
+            </properties>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+        </profile>
+        <profile>
+            <id>test</id>
+            <properties>
+                <profile.active>test</profile.active>
+            </properties>
+        </profile>
+    </profiles>
+
+    <build>
+        <finalName>oss-mgr</finalName>
+        <extensions>
+            <extension>
+                <groupId>kr.motd.maven</groupId>
+                <artifactId>os-maven-plugin</artifactId>
+                <version>1.5.0.Final</version>
+            </extension>
+        </extensions>
+
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+                <includes>
+                    <include>application.yml</include>
+                    <include>application-${profile.active}.yml</include>
+                    <include>mapper/**</include>
+                    <include>*.xml</include>
+                </includes>
+            </resource>
+        </resources>
+
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.13.0</version>
+                <configuration>
+                    <compilerArgs>
+                        <arg>-parameters</arg>
+                    </compilerArgs>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${springboot.version}</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 11 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/OssMgrApplication.java

@@ -0,0 +1,11 @@
+package cn.reghao.oss.mgr;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class OssMgrApplication  {
+    public static void main(String[] args) {
+        SpringApplication.run(OssMgrApplication.class, args);
+    }
+}

+ 37 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/config/BeanConfig.java

@@ -0,0 +1,37 @@
+package cn.reghao.oss.mgr.config;
+
+import cn.reghao.jutil.jdk.converter.ByteConverter;
+import cn.reghao.jutil.jdk.http.WebClient;
+import cn.reghao.jutil.jdk.http.WebRequest;
+import cn.reghao.jutil.jdk.shell.ShellExecutor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * @author reghao
+ * @date 2023-08-02 09:14:17
+ */
+@Configuration
+public class BeanConfig {
+    @Bean
+    public RestTemplate restTemplate(){
+        return new RestTemplate();
+    }
+
+    @Bean
+    public WebRequest webRequest() {
+        //return new DefaultWebRequest();
+        return new WebClient();
+    }
+
+    @Bean
+    public ShellExecutor shellExecutor() {
+        return new ShellExecutor();
+    }
+
+    @Bean
+    public ByteConverter byteConverter() {
+        return new ByteConverter();
+    }
+}

+ 68 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/config/CacheConfig.java

@@ -0,0 +1,68 @@
+package cn.reghao.oss.mgr.config;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+
+import org.springframework.cache.caffeine.CaffeineCacheManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 缓存配置
+ *
+ * @author reghao
+ * @date 2023-05-22 16:31:04
+ */
+@EnableCaching
+@Configuration
+public class CacheConfig {
+    /**
+     * 为 Spring Cache 的相关注解提供一个缓存
+     *
+     * @param
+     * @return
+     * @date 2024-03-04 16:37:05
+     */
+    @Bean
+    public CacheManager cacheManager() {
+        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
+        Caffeine<Object, Object> caffeineCache = Caffeine.newBuilder()
+                .initialCapacity(1000)
+                .maximumSize(10_000)
+                .expireAfterAccess(365, TimeUnit.DAYS);
+        cacheManager.setCaffeine(caffeineCache);
+        return cacheManager;
+    }
+
+    /**
+     * 提供一个全局的独立缓存
+     *
+     * @param
+     * @return
+     * @date 2024-03-04 16:37:38
+     */
+    @Bean("caffeineCache")
+    public Cache<String, Object> caffeineCache() {
+        return Caffeine.newBuilder()
+                .initialCapacity(1000)
+                .maximumSize(10_000)
+                .expireAfterAccess(365, TimeUnit.DAYS)
+                .build();
+    }
+
+    @Bean
+    public Cache<String, String> caffeineCache1() {
+        return Caffeine.newBuilder()
+                // 设置最后一次写入或访问后经过固定时间过期
+                .expireAfterWrite(30, TimeUnit.DAYS)
+                // 初始的缓存空间大小
+                .initialCapacity(10_000)
+                // 缓存的最大条数
+                .maximumSize(100_000)
+                .build();
+    }
+}

+ 26 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/config/FilterConfig.java

@@ -0,0 +1,26 @@
+package cn.reghao.oss.mgr.config;
+
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author reghao
+ * @date 2026-02-22 18:07:17
+ */
+@Configuration
+public class FilterConfig {
+    @Bean
+    public FilterRegistrationBean<OssAuthFilter> authFilterRegistration(OssAuthFilter ossAuthFilter) {
+        FilterRegistrationBean<OssAuthFilter> registration = new FilterRegistrationBean<>();
+        registration.setFilter(ossAuthFilter);
+
+        // 只拦截对象操作相关的路径
+        registration.addUrlPatterns("/api/oss/sdk/*", "/api/oss/mgr/*");
+
+        // 设置执行顺序,数越小越靠前
+        registration.setOrder(1);
+        registration.setName("ossAuthFilter");
+        return registration;
+    }
+}

+ 97 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/config/OssAuthFilter.java

@@ -0,0 +1,97 @@
+package cn.reghao.oss.mgr.config;
+
+import cn.reghao.oss.api.util.OssClientSigner;
+import cn.reghao.oss.mgr.db.mapper.UserKeyMapper;
+import cn.reghao.oss.mgr.model.po.UserKey;
+import com.github.benmanes.caffeine.cache.Cache;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * @author reghao
+ * @date 2026-02-22 17:57:45
+ */
+@Component
+public class OssAuthFilter extends OncePerRequestFilter {
+    private final Cache<String, Object> credentialCache;
+    private final UserKeyMapper userKeyMapper;
+
+    public OssAuthFilter(Cache<String, Object> credentialCache, UserKeyMapper userKeyMapper) {
+        this.credentialCache = credentialCache;
+        this.userKeyMapper = userKeyMapper;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws IOException, ServletException {
+        String authHeader = request.getHeader("Authorization");
+        String dateHeader = request.getHeader("Date");
+
+        // 1. 时间戳校验(防重放)
+        if (!validateDate(dateHeader)) {
+            response.sendError(403, "RequestTimeTooSkewed");
+            return;
+        }
+
+        // 2. 解析 AK 和 Signature
+        String ak = parseAk(authHeader);
+        String clientSig = parseSignature(authHeader);
+
+        // 3. 获取 SK(先查缓存,再查库)
+        UserKey userKey = (UserKey) credentialCache.get(ak, userKeyMapper::findByAccessKeyId);
+        String sk = userKey.getAccessKeySecret();
+
+        // 4. 重构 StringToSign 并比对
+        String method = request.getMethod();
+        String md5 = request.getHeader("Content-MD5");
+        if (md5 == null) md5 = "";
+        String contentType = request.getContentType();
+        if (contentType == null) contentType = "";
+        String resource = request.getRequestURI();
+        String stringToSign = OssClientSigner.getStringToSign(method, resource, contentType, dateHeader);
+
+        String serverSig = OssClientSigner.calculateHmac(sk, stringToSign);
+        if (serverSig.equals(clientSig)) {
+            long userId = userKey.getCreateBy();
+            // 使用 try-with-resources 自动管理生命周期
+            try (UserContext.UserResource ignored = UserContext.set(userId)) {
+                // 此时 UserContext.getUserId() 有值
+                filterChain.doFilter(request, response);
+            } // 代码运行到这里,会自动触发 UserResource.close(),执行 ThreadLocal.remove()
+        } else {
+            response.sendError(403, "SignatureDoesNotMatch");
+        }
+    }
+
+    private boolean validateDate(String dateHeader) {
+        if (dateHeader == null || dateHeader.isEmpty()) return false;
+        try {
+            // 解析 HTTP GMT 格式时间: Sun, 22 Feb 2026 18:22:28 GMT
+            DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME;
+            ZonedDateTime requestDate = ZonedDateTime.parse(dateHeader, formatter);
+            long requestTimeMillis = requestDate.toInstant().toEpochMilli();
+            long currentTimeMillis = System.currentTimeMillis();
+
+            // 允许 15 分钟内的误差
+            return Math.abs(currentTimeMillis - requestTimeMillis) < 15 * 60 * 1000;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private String parseAk(String authHeader) {
+        return authHeader.substring(4).split(":")[0];
+    }
+
+    private String parseSignature(String authHeader) {
+        return authHeader.substring(4).split(":")[1];
+    }
+}

+ 25 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/config/SpringLifecycle.java

@@ -0,0 +1,25 @@
+package cn.reghao.oss.mgr.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author reghao
+ * @date 2022-03-23 09:22:01
+ */
+@Slf4j
+@Component
+public class SpringLifecycle implements ApplicationRunner, DisposableBean {
+    @Override
+    public void run(ApplicationArguments args) {
+        log.info("oss-mgr started...");
+    }
+
+    @Override
+    public void destroy() {
+        log.info("oss-mgr shutdown...");
+    }
+}

+ 35 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/config/UserContext.java

@@ -0,0 +1,35 @@
+package cn.reghao.oss.mgr.config;
+
+/**
+ * @author reghao
+ * @date 2023-06-02 10:48:59
+ */
+public class UserContext {
+    private static final ThreadLocal<Long> USER_ID_HOLDER = new ThreadLocal<>();
+
+    /**
+     * 设置用户 ID 并返回一个可自动关闭的处理类
+     */
+    public static UserResource set(Long userId) {
+        USER_ID_HOLDER.set(userId);
+        return new UserResource();
+    }
+
+    public static Long getUserId() {
+        return USER_ID_HOLDER.get();
+    }
+
+    private static void clear() {
+        USER_ID_HOLDER.remove();
+    }
+
+    /**
+     * 内部类,实现自动清理逻辑
+     */
+    public static class UserResource implements AutoCloseable {
+        @Override
+        public void close() {
+            UserContext.clear();
+        }
+    }
+}

+ 68 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/config/mybatis/DataSourceConfig.java

@@ -0,0 +1,68 @@
+package cn.reghao.oss.mgr.config.mybatis;
+
+import org.apache.ibatis.plugin.Interceptor;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.mybatis.spring.SqlSessionTemplate;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.transaction.PlatformTransactionManager;
+
+import javax.sql.DataSource;
+
+/**
+ * MyBatis 初始化配置
+ * 若配置了多数据源,则不启用此配置
+ *
+ * @author reghao
+ * @date 2021-04-26 17:48:29
+ */
+@Configuration
+public class DataSourceConfig {
+    private final DataSource dataSource;
+
+    public DataSourceConfig(DataSource dataSource) {
+        this.dataSource = dataSource;
+    }
+
+    @Bean
+    public SqlSessionFactory sqlSessionFactory() throws Exception {
+        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
+        factoryBean.setDataSource(dataSource);
+        factoryBean.setPlugins(new Interceptor[]{pageListInterceptor()});
+
+        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
+        configuration.setMapUnderscoreToCamelCase(true);
+        configuration.setDefaultFetchSize(100);
+        configuration.setDefaultStatementTimeout(300);
+        factoryBean.setConfiguration(configuration);
+
+        String location = "classpath*:mapper/**.xml";
+        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(location));
+        return factoryBean.getObject();
+    }
+
+    /**
+     * 配置 mybatis 的分页拦截器
+     *
+     * @param
+     * @return
+     * @date 2021-12-21 下午5:23
+     */
+    @Bean
+    public PageListInterceptor pageListInterceptor() {
+        return new PageListInterceptor();
+    }
+
+    @Bean(value = "sqlSession")
+    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
+        return new SqlSessionTemplate(sqlSessionFactory);
+    }
+
+    @Bean(value = "transactionManager")
+    public PlatformTransactionManager annotationDrivenTransactionManager() {
+        return new DataSourceTransactionManager(dataSource);
+    }
+}

+ 83 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/config/mybatis/PageListInterceptor.java

@@ -0,0 +1,83 @@
+package cn.reghao.oss.mgr.config.mybatis;
+
+import cn.reghao.jutil.jdk.web.db.Page;
+import org.apache.ibatis.binding.MapperMethod;
+import org.apache.ibatis.executor.parameter.ParameterHandler;
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.plugin.*;
+import org.apache.ibatis.reflection.MetaObject;
+import org.apache.ibatis.reflection.SystemMetaObject;
+
+import java.sql.Connection;
+import java.util.Properties;
+
+/**
+ * MyBatis 分页拦截器
+ *
+ * @author reghao
+ * @date 2021-04-26 17:40:32
+ */
+@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class})})
+public class PageListInterceptor implements Interceptor {
+    private final String methodSuffix = "ByPage";
+    @Deprecated
+    private int page;
+    @Deprecated
+    private int size;
+    private String dbType;
+
+    @Override
+    public Object intercept(Invocation invocation) throws Throwable {
+        StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
+        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
+        while(metaObject.hasGetter("h")){
+            Object object = metaObject.getValue("h");
+            metaObject = SystemMetaObject.forObject(object);
+        }
+
+        while(metaObject.hasGetter("target")){
+            Object object = metaObject.getValue("target");
+            metaObject = SystemMetaObject.forObject(object);
+        }
+
+        MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("delegate.mappedStatement");
+        String mapId = mappedStatement.getId();
+        if (mapId.matches(String.format(".+%s$", methodSuffix))){
+            ParameterHandler parameterHandler = (ParameterHandler)metaObject.getValue("delegate.parameterHandler");
+            Object object = parameterHandler.getParameterObject();
+            Page page;
+            if (object instanceof Page) {
+                page = (Page) object;
+            } else if (object instanceof MapperMethod.ParamMap) {
+                MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) object;
+                Object param1 = paramMap.get("param1");
+                if (param1 instanceof Page) {
+                    page = (Page) param1;
+                } else {
+                    throw new Exception("byPage 方法的第一个参数不是 Page 类型");
+                }
+            } else {
+                throw new Exception("没有 Page 类型的参数");
+            }
+
+            String sql = (String) metaObject.getValue("delegate.boundSql.sql");
+            //sql += " limit "+(page-1)*size +","+size;
+            sql += String.format(" limit %s,%s", (page.getPage()-1)*page.getSize(), page.getSize());
+            metaObject.setValue("delegate.boundSql.sql", sql);
+        }
+        return invocation.proceed();
+    }
+
+    @Override
+    public Object plugin(Object target) {
+        return Plugin.wrap(target, this);
+    }
+
+    @Override
+    public void setProperties(Properties properties) {
+        String limit = properties.getProperty("limit","10");
+        this.page = Integer.parseInt(limit);
+        this.dbType = properties.getProperty("dbType", "mysql");
+    }
+}

+ 171 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/OssSdkController.java

@@ -0,0 +1,171 @@
+package cn.reghao.oss.mgr.controller;
+
+import cn.reghao.jutil.jdk.web.result.WebResult;
+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;
+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.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.UserNodeService;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * oss 对外提供的接口
+ *
+ * @author reghao
+ * @date 2026-02-01 15:44:22
+ */
+@RestController
+@RequestMapping("/api/oss/sdk")
+public class OssSdkController {
+    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,
+                            UserKeyMapper userKeyMapper, UserNodeMapper userNodeMapper,
+                            UploadChannelMapper uploadChannelMapper) {
+        this.metadataService = metadataService;
+        this.userNodeService = userNodeService;
+        this.objectRepository = objectRepository;
+        this.consoleService = consoleService;
+        this.userKeyMapper = userKeyMapper;
+        this.userNodeMapper = userNodeMapper;
+        this.uploadChannelMapper = uploadChannelMapper;
+    }
+
+    @Operation(summary = "获取上传对象所需的数据", description = "N")
+    @PostMapping(value = "/upload_request", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String requestUpload(@RequestBody FileInitRequest req) {
+        String objectId = req.getObjectId();
+
+        // 1. 选出负载最低且空间充足的 oss-store 节点
+        StoreNode targetNode = userNodeService.selectBestNode();
+
+        int channelCode = 101;
+        //channelCode = req.getChannelCode();
+        ServerInfo serverInfo = consoleService.getUploadStore(channelCode);
+        return WebResult.success(serverInfo);
+    }
+
+    @Operation(summary = "对象上传前的预检", description = "N")
+    @PostMapping(value = "/prepare", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String uploadPrepare(@RequestBody 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) {
+        UploadFileRet uploadFileRet = metadataService.verifyPartialHash(uploadSample);
+        return WebResult.success(uploadFileRet);
+    }
+
+    @Operation(summary = "获取访问对象所需的签名 URL", description = "N")
+    @GetMapping(value = "/presign", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getPresignedUrl(@RequestParam("objectId") String objectId, @RequestParam("action") String action) {
+        long loginUser = UserContext.getUserId();
+        ObjectMeta objectMeta = objectRepository.getObjectMetaById(objectId, loginUser);
+        if (objectMeta == null) {
+            return WebResult.failWithMsg("not exist");
+        }
+
+        // 24h
+        int durationInMinutes = 1440;
+        // 2. 计算过期时间(当前时间 + 持续分钟数)
+        long expires = (System.currentTimeMillis() / 1000) + (durationInMinutes * 60L);
+
+        String accessKeyId = userKeyMapper.findByCreateBy(loginUser).getAccessKeyId();
+        // 生成一个短随机数,例如 UUID 的前 8 位或随机 16 进制字符串
+        // 避免重放攻击, 使 url 只能访问一次
+        String nonce = UUID.randomUUID().toString().substring(0, 8);
+
+        // 3. 调用工具类生成签名
+        String sign = SignatureUtils.calculateSignature(accessKeyId, expires, objectId);
+
+        String objectName = objectMeta.getObjectName();
+        List<UploadChannel> uploadChannelList = uploadChannelMapper.findByCreateBy(loginUser);
+        Collections.reverse(uploadChannelList);
+        for (UploadChannel uploadChannel : uploadChannelList) {
+            String prefix = uploadChannel.getPrefix();
+            if (objectName.startsWith(prefix)) {
+                int userNodeId = uploadChannel.getUserNodeId();
+                UserNode userNode = userNodeMapper.findById(userNodeId);
+
+                String protocol = userNode.getProtocol();
+                String domain = userNode.getDomain();
+                String ossUrl = String.format("%s://%s", protocol, domain);
+                String signedParams = String.format("ak=%s&t=%s&nonce=%s&sign=%s", accessKeyId, expires, nonce, sign);;
+
+                // 4. 拼接最终的公网访问 URL
+                // 格式:域名 + objectId + 参数
+                String signedUrl = String.format("%s/%s?%s", ossUrl, objectName, signedParams);
+                return WebResult.success(signedUrl);
+            }
+        }
+
+        return WebResult.failWithMsg("UserNode not found");
+    }
+
+    @Operation(summary = "设置对象的可见范围", description = "N")
+    @PostMapping(value = "/object/scope", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String setObjectScope(@RequestBody FileInitRequest req) {
+        String objectId = req.getObjectId();
+        return WebResult.success();
+    }
+
+    @Operation(summary = "删除对象", description = "N")
+    @PostMapping(value = "/object/delete", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteObject(@RequestBody FileInitRequest req) {
+        String objectId = req.getObjectId();
+        return WebResult.success();
+    }
+
+    @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);
+        return WebResult.success(objectInfo);
+    }
+
+    @Operation(summary = "获取视频对象信息", description = "N")
+    @GetMapping(value = "/video/get", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getVideoInfo(@RequestParam("objectId") String objectId) {
+        VideoInfo videoInfo = consoleService.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);
+    }
+}

+ 64 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/StoreNodeController.java

@@ -0,0 +1,64 @@
+package cn.reghao.oss.mgr.controller;
+
+import cn.reghao.jutil.jdk.web.result.Result;
+import cn.reghao.jutil.jdk.web.result.WebResult;
+import cn.reghao.oss.api.dto.SelectOption;
+import cn.reghao.oss.mgr.model.po.StoreVolume;
+import cn.reghao.oss.mgr.model.vo.StoreNodeInfo;
+import cn.reghao.oss.mgr.service.StoreNodeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2024-02-23 11:29:14
+ */
+@Tag(name = "存储节点接口")
+@RestController
+@RequestMapping("/api/oss/store")
+public class StoreNodeController {
+    private final StoreNodeService storeNodeService;
+
+    public StoreNodeController(StoreNodeService storeNodeService) {
+        this.storeNodeService = storeNodeService;
+    }
+
+    @Operation(summary = "存储节点列表", description = "N")
+    @GetMapping(value = "/list", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String storeNodePage() {
+        List<StoreNodeInfo> list = storeNodeService.getByPage();
+        return WebResult.success(list);
+    }
+
+    @Operation(summary = "存储节点磁盘列表", description = "N")
+    @GetMapping(value = "/disk", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String storeNodePage(@RequestParam("storeNodeId") Integer storeNodeId) {
+        List<StoreVolume> list = storeNodeService.getStoreDisks(storeNodeId);
+        return WebResult.success(list);
+    }
+
+    @Operation(summary = "存储节点 kv 列表", description = "N")
+    @GetMapping(value = "/kv", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String storeNodeKeyValue() {
+        List<SelectOption> storeNodes = storeNodeService.getNodeKeyValues();
+        return WebResult.success(storeNodes);
+    }
+
+    @Operation(summary = "设置存储节点的状态", description = "N")
+    @PostMapping(value = "/update_status/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String updateStoreNodeStatus(@PathVariable("id") Integer storeNodeId) {
+        Result result = storeNodeService.updateStatus(storeNodeId);
+        return WebResult.result(result);
+    }
+
+    @Operation(summary = "删除存储节点", description = "N")
+    @PostMapping(value = "/delete/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteStoreNode(@PathVariable("id") Integer storeNodeId) {
+        Result result = storeNodeService.delete(storeNodeId);
+        return WebResult.result(result);
+    }
+}

+ 110 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/UploadChannelController.java

@@ -0,0 +1,110 @@
+package cn.reghao.oss.mgr.controller;
+
+import cn.reghao.jutil.jdk.web.result.Result;
+import cn.reghao.jutil.jdk.web.result.WebResult;
+import cn.reghao.oss.api.constant.ObjectScope;
+import cn.reghao.oss.api.constant.ObjectSize;
+import cn.reghao.oss.api.constant.ObjectType;
+import cn.reghao.oss.api.dto.SelectOption;
+import cn.reghao.oss.mgr.config.UserContext;
+import cn.reghao.oss.mgr.model.dto.UploadChannelDto;
+import cn.reghao.oss.mgr.model.po.UploadChannel;
+import cn.reghao.oss.mgr.model.po.UserNode;
+import cn.reghao.oss.mgr.model.vo.AddChannelAttr;
+import cn.reghao.oss.mgr.service.UploadChannelService;
+import cn.reghao.oss.mgr.service.UserNodeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2024-02-23 15:05:30
+ */
+@Slf4j
+@Tag(name = "上传通道接口")
+@RestController
+@RequestMapping("/api/oss/channel")
+public class UploadChannelController {
+    private final UserNodeService userNodeService;
+    private final UploadChannelService uploadChannelService;
+
+    public UploadChannelController(UserNodeService userNodeService, UploadChannelService uploadChannelService) {
+        this.userNodeService = userNodeService;
+        this.uploadChannelService = uploadChannelService;
+    }
+
+    @Operation(summary = "上传通道列表页面", description = "N")
+    @GetMapping(value = "/list", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String index(@RequestParam("userNodeId") Integer userNodeId) {
+        long loginUser = UserContext.getUserId();
+        List<UserNode> userNodes = userNodeService.getUserNodes(loginUser);
+        if (userNodes.isEmpty()) {
+            String errMsg = "没有可用节点";
+            WebResult.failWithMsg(errMsg);
+        }
+
+        List<UploadChannel> list = uploadChannelService.getChannelsByUserNode(userNodeId);
+        return WebResult.success(list);
+    }
+
+    @Operation(summary = "上传通道属性", description = "N")
+    @GetMapping(value = "/attr", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String addChannelPage() {
+        List<SelectOption> objectTypes = new ArrayList<>();
+        for (ObjectType objectType : ObjectType.values()) {
+            if (objectType.getCode() == ObjectType.Dir.getCode()) {
+                continue;
+            }
+            objectTypes.add(new SelectOption(objectType.name(), objectType.getCode()+""));
+        }
+
+        List<SelectOption> objectScopes = new ArrayList<>();
+        for (ObjectScope objectScope : ObjectScope.values()) {
+            objectScopes.add(new SelectOption(objectScope.name(), objectScope.getCode()+""));
+        }
+
+        List<SelectOption> sizeList = Arrays.stream(ObjectSize.values())
+                .map(objectSize -> new SelectOption(objectSize.getDesc(), objectSize.getSize()+""))
+                .collect(Collectors.toList());
+
+        AddChannelAttr addChannelAttr = new AddChannelAttr(objectTypes, objectScopes, sizeList);
+        return WebResult.success(addChannelAttr);
+    }
+
+    @Operation(summary = "初始化上传通道", description = "N")
+    @PostMapping(value = "/init", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String initUploadChannel() throws Exception {
+        uploadChannelService.initUploadChannel();
+        return WebResult.success();
+    }
+
+    @Operation(summary = "添加上传通道", description = "N")
+    @PostMapping(value = "/add", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String addUploadChannel(@RequestBody @Validated UploadChannelDto uploadChannelDto) throws Exception {
+        Result result = uploadChannelService.addObjectChannel(uploadChannelDto);
+        return WebResult.result(result);
+    }
+
+    @Operation(summary = "设置通道状态", description = "N")
+    @PostMapping(value = "/channel_status/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String setChannelStatus(@PathVariable("id") Integer id) {
+        uploadChannelService.updateChannelStatus(id);
+        return WebResult.success();
+    }
+
+    @Operation(summary = "删除通道", description = "N")
+    @PostMapping(value = "/delete/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteUploadChannel(@PathVariable("id") Integer id) throws Exception {
+        Result result = uploadChannelService.deleteChannel(id);
+        return WebResult.result(result);
+    }
+}

+ 45 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/UserKeyController.java

@@ -0,0 +1,45 @@
+package cn.reghao.oss.mgr.controller;
+
+import cn.reghao.jutil.jdk.web.result.WebResult;
+import cn.reghao.oss.mgr.model.po.UserKey;
+import cn.reghao.oss.mgr.service.StoreConfigService;
+import cn.reghao.oss.mgr.service.UserKeyService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2024-02-27 10:24:31
+ */
+@Tag(name = "oss key 接口")
+@RestController
+@RequestMapping("/api/oss/key")
+public class UserKeyController {
+    private final UserKeyService userKeyService;
+    private final StoreConfigService storeConfigService;
+
+    public UserKeyController(UserKeyService userKeyService, StoreConfigService storeConfigService) {
+        this.userKeyService = userKeyService;
+        this.storeConfigService = storeConfigService;
+    }
+
+    @Operation(summary = "oss key 页面", description = "N")
+    @GetMapping(value = "/list", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String userKeyPage() {
+        long ossUser = storeConfigService.getOssUser();
+        List<UserKey> list = userKeyService.getUserKeys(ossUser);
+        return WebResult.success(list);
+    }
+
+    @Operation(summary = "重新生成  oss key", description = "N")
+    @PostMapping(value = "/regenerate", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String regenerate() {
+        long ossUser = storeConfigService.getOssUser();
+        userKeyService.regenerate(ossUser);
+        return WebResult.success();
+    }
+}

+ 69 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/UserNodeController.java

@@ -0,0 +1,69 @@
+package cn.reghao.oss.mgr.controller;
+
+import cn.reghao.jutil.jdk.web.result.Result;
+import cn.reghao.jutil.jdk.web.result.WebResult;
+import cn.reghao.oss.api.dto.SelectOption;
+import cn.reghao.oss.mgr.config.UserContext;
+import cn.reghao.oss.mgr.model.dto.NodeAddDto;
+import cn.reghao.oss.mgr.model.dto.NodeUpdateDto;
+import cn.reghao.oss.mgr.model.po.UserNode;
+import cn.reghao.oss.mgr.service.UserNodeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2019-08-30 18:49:15
+ */
+@Tag(name = "用户节点接口")
+@RestController
+@RequestMapping("/api/oss/my")
+public class UserNodeController {
+    private final UserNodeService userNodeService;
+
+    public UserNodeController(UserNodeService userNodeService) {
+        this.userNodeService = userNodeService;
+    }
+
+    @Operation(summary = "用户节点列表", description = "N")
+    @GetMapping(value = "/list", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String userNodesPage() {
+        long loginUser = UserContext.getUserId();
+        List<UserNode> list = userNodeService.getUserNodes(loginUser);
+        return WebResult.success(list);
+    }
+
+    @Operation(summary = "用户节点 kv 列表", description = "N")
+    @GetMapping(value = "/kv", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String userNodeKeyValue() {
+        long loginUser = UserContext.getUserId();
+        List<SelectOption> storeNodes = userNodeService.getNodeKeyValues(loginUser);
+        return WebResult.success(storeNodes);
+    }
+
+    @Operation(summary = "添加用户节点", description = "N")
+    @PostMapping(value = "/add", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String addUserNode(@RequestBody @Validated NodeAddDto nodeAddDto) {
+        Result result = userNodeService.add(nodeAddDto);
+        return WebResult.result(result);
+    }
+
+    @Operation(summary = "更新用户节点", description = "N")
+    @PostMapping(value = "/update", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String updateNode(@RequestBody @Validated NodeUpdateDto nodeUpdateDto) {
+        userNodeService.updateUserNode(nodeUpdateDto);
+        return WebResult.success();
+    }
+
+    @Operation(summary = "删除用户节点", description = "N")
+    @PostMapping(value = "/delete/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteUserNode(@PathVariable("id") Integer userNodeId) {
+        Result result = userNodeService.delete(userNodeId);
+        return WebResult.result(result);
+    }
+}

+ 17 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/DataBlockMapper.java

@@ -0,0 +1,17 @@
+package cn.reghao.oss.mgr.db.mapper;
+
+import cn.reghao.jutil.jdk.web.db.BaseMapper;
+import cn.reghao.oss.mgr.model.po.DataBlock;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * @author reghao
+ * @date 2022-11-24 11:11:24
+ */
+@Mapper
+public interface DataBlockMapper extends BaseMapper<DataBlock> {
+    void updatePath(@Param("sha256sum") String sha256sum, @Param("path") String path);
+    DataBlock findBySha256sum(String sha256sum);
+    DataBlock findByObjectId(String objectId);
+}

+ 22 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/FileMetaMapper.java

@@ -0,0 +1,22 @@
+package cn.reghao.oss.mgr.db.mapper;
+
+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;
+
+/**
+ * @author reghao
+ * @date 2022-11-24 11:11:06
+ */
+@Mapper
+public interface FileMetaMapper extends BaseMapper<FileMeta> {
+    void updateScopeByObjectName(@Param("scope") int scope, @Param("objectName") String objectName);
+    void updateScopeByObjectId(@Param("objectId") String objectId, @Param("scope") int scope);
+
+    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);
+}

+ 17 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/StoreNodeMapper.java

@@ -0,0 +1,17 @@
+package cn.reghao.oss.mgr.db.mapper;
+
+import cn.reghao.jutil.jdk.web.db.BaseMapper;
+import cn.reghao.oss.mgr.model.po.StoreNode;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * @author reghao
+ * @date 2025-10-16 14:12:42
+ */
+@Mapper
+public interface StoreNodeMapper extends BaseMapper<StoreNode> {
+    StoreNode findByNodeAddrAndHttpPort(@Param("nodeAddr") String nodeAddr, @Param("httpPort") int httpPort);
+    StoreNode findById(int id);
+    StoreNode findFirstStore();
+}

+ 16 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/StoreVolumeMapper.java

@@ -0,0 +1,16 @@
+package cn.reghao.oss.mgr.db.mapper;
+
+import cn.reghao.jutil.jdk.web.db.BaseMapper;
+import cn.reghao.oss.mgr.model.po.StoreVolume;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2025-10-16 16:22:12
+ */
+@Mapper
+public interface StoreVolumeMapper extends BaseMapper<StoreVolume> {
+    List<StoreVolume> findByStoreNodeId(int storeNodeId);
+}

+ 25 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/UploadChannelMapper.java

@@ -0,0 +1,25 @@
+package cn.reghao.oss.mgr.db.mapper;
+
+import cn.reghao.jutil.jdk.web.db.BaseMapper;
+import cn.reghao.oss.mgr.model.po.UploadChannel;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2025-10-16 14:13:04
+ */
+@Mapper
+public interface UploadChannelMapper extends BaseMapper<UploadChannel> {
+    void deleteById(int id);
+
+    int countByCreateBy(long createBy);
+    List<UploadChannel> findByCreateBy(long createBy);
+    UploadChannel findById(int id);
+    List<UploadChannel> findByCreateByAndUserNodeId(@Param("createBy") long createBy, @Param("userNodeId") int userNodeId);
+    UploadChannel findByCreateByAndChannelCode(@Param("createBy") long createBy, @Param("channelCode") int channelCode);
+    UploadChannel findByCreateByAndPrefix(@Param("createBy") long createBy, @Param("prefix") String prefix);
+    UploadChannel findByCreateByAndName(@Param("createBy") long createBy, @Param("name") String name);
+}

+ 20 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/UploadTaskMapper.java

@@ -0,0 +1,20 @@
+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 java.util.List;
+
+/**
+ * @author reghao
+ * @date 2024-10-24 16:35:48
+ */
+@Mapper
+public interface UploadTaskMapper extends BaseMapper<UploadTask> {
+    void updateSetUploaded(String uploadId);
+    void deleteByUploadId(String uploadId);
+
+    UploadTask findByUploadId(String uploadId);
+    List<UploadTask> findByExpired();
+}

+ 17 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/UserKeyMapper.java

@@ -0,0 +1,17 @@
+package cn.reghao.oss.mgr.db.mapper;
+
+import cn.reghao.jutil.jdk.web.db.BaseMapper;
+import cn.reghao.oss.mgr.model.po.UserKey;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author reghao
+ * @date 2025-10-16 14:13:12
+ */
+@Mapper
+public interface UserKeyMapper extends BaseMapper<UserKey> {
+    void updateByCreateBy(UserKey userKey);
+
+    UserKey findByCreateBy(long createBy);
+    UserKey findByAccessKeyId(String  accessKeyId);
+}

+ 28 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/UserNodeMapper.java

@@ -0,0 +1,28 @@
+package cn.reghao.oss.mgr.db.mapper;
+
+import cn.reghao.jutil.jdk.web.db.BaseMapper;
+import cn.reghao.oss.mgr.model.dto.NodeUpdateDto;
+import cn.reghao.oss.mgr.model.po.UserNode;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2025-10-16 14:13:20
+ */
+@Mapper
+public interface UserNodeMapper extends BaseMapper<UserNode> {
+    /*void updateUserNode(@Param("userNodeId") int userNodeId,
+                        @Param("domain") int domain,
+                        @Param("referer") int referer,
+                        @Param("secretKey") int secretKey);*/
+    void updateUserNode(NodeUpdateDto nodeUpdateDto);
+
+    int countByStoreNodeId(int storeNodeId);
+    UserNode findByCreateByAndStoreNodeId(@Param("createBy") long createBy, @Param("storeNodeId") int storeNodeId);
+    List<UserNode> findByCreateBy(long createBy);
+    UserNode findByDomain(String domain);
+    UserNode findById(int id);
+}

+ 39 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/repository/ChannelRepository.java

@@ -0,0 +1,39 @@
+package cn.reghao.oss.mgr.db.repository;
+
+import cn.reghao.oss.mgr.config.UserContext;
+import cn.reghao.oss.mgr.db.mapper.UploadChannelMapper;
+import cn.reghao.oss.mgr.model.po.UploadChannel;
+import cn.reghao.oss.mgr.service.StoreConfigService;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * @author reghao
+ * @date 2025-10-19 22:07:47
+ */
+@Repository
+public class ChannelRepository {
+    private final UploadChannelMapper uploadChannelMapper;
+    private final StoreConfigService storeConfigService;
+
+    public ChannelRepository(UploadChannelMapper uploadChannelMapper, StoreConfigService storeConfigService) {
+        this.uploadChannelMapper = uploadChannelMapper;
+        this.storeConfigService = storeConfigService;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void createUploadChannel(UploadChannel uploadChannel) throws Exception {
+        uploadChannelMapper.save(uploadChannel);
+
+        /*int userNodeId = uploadChannel.getUserNodeId();
+        String prefix = uploadChannel.getPrefix();
+        int scope = uploadChannel.getScope();
+        long ossUser = storeConfigService.getLocalOssUser();
+        storeServiceWrapper.createChannel(userNodeId, prefix, scope, ossUser);*/
+    }
+
+    public UploadChannel getByChannelCode(int channelCode) {
+        long loginUser = UserContext.getUserId();
+        return uploadChannelMapper.findByCreateByAndChannelCode(loginUser, channelCode);
+    }
+}

+ 77 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/repository/ObjectRepository.java

@@ -0,0 +1,77 @@
+package cn.reghao.oss.mgr.db.repository;
+
+import cn.reghao.oss.api.dto.ObjectInfo;
+import cn.reghao.oss.api.dto.ObjectMeta;
+import cn.reghao.oss.mgr.db.mapper.DataBlockMapper;
+import cn.reghao.oss.mgr.db.mapper.FileMetaMapper;
+import cn.reghao.oss.mgr.model.po.DataBlock;
+import cn.reghao.oss.mgr.model.po.FileMeta;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2023-05-22 16:48:40
+ */
+@Slf4j
+@Service
+public class ObjectRepository {
+    private final FileMetaMapper fileMetaMapper;
+    private final DataBlockMapper dataBlockMapper;
+
+    public ObjectRepository(FileMetaMapper fileMetaMapper, DataBlockMapper dataBlockMapper) {
+        this.fileMetaMapper = fileMetaMapper;
+        this.dataBlockMapper = dataBlockMapper;
+    }
+
+    public void saveFileMeta(FileMeta fileMeta) {
+        fileMetaMapper.save(fileMeta);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void saveObject(FileMeta fileMeta, List<DataBlock> list) {
+        fileMetaMapper.save(fileMeta);
+        dataBlockMapper.saveAll(list);
+    }
+
+    @CacheEvict(cacheNames = "oss:store:objectMeta", key = "#objectName")
+    public void updateObjectScope(int scope, String objectName) {
+        fileMetaMapper.updateScopeByObjectName(scope, objectName);
+        log.info("evict {}", objectName);
+    }
+
+    public void deleteObject(String objectId) {
+
+    }
+
+    public DataBlock getBySha256sum(String sha256sum) {
+        DataBlock dataBlock = dataBlockMapper.findBySha256sum(sha256sum);
+        return dataBlock;
+    }
+
+    //@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) {
+        log.info("cache miss {}", objectName);
+        ObjectMeta objectMeta = fileMetaMapper.findObjectMetaByName(objectName, owner);
+        return objectMeta;
+    }
+
+    public ObjectMeta getObjectMetaById(String objectId, long loginUser) {
+        ObjectMeta objectMeta = fileMetaMapper.findObjectMetaById(objectId, loginUser);
+        return objectMeta;
+    }
+
+    public ObjectInfo getObjectInfo(String objectId) {
+        return null;
+    }
+
+    public FileMeta getFileMeta(String objectId) {
+        return fileMetaMapper.findByObjectId(objectId);
+    }
+}

+ 56 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/repository/StoreRepository.java

@@ -0,0 +1,56 @@
+package cn.reghao.oss.mgr.db.repository;
+
+import cn.reghao.oss.api.dto.StoreNodeDto;
+import cn.reghao.oss.mgr.db.mapper.StoreNodeMapper;
+import cn.reghao.oss.mgr.db.mapper.StoreVolumeMapper;
+import cn.reghao.oss.mgr.db.mapper.UploadChannelMapper;
+import cn.reghao.oss.mgr.model.po.StoreNode;
+import cn.reghao.oss.mgr.model.po.StoreVolume;
+import cn.reghao.oss.mgr.model.po.UploadChannel;
+import cn.reghao.oss.mgr.service.StoreConfigService;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2025-10-17 09:14:47
+ */
+@Repository
+public class StoreRepository {
+    private final StoreNodeMapper storeNodeMapper;
+    private final StoreVolumeMapper storeVolumeMapper;
+    private final UploadChannelMapper uploadChannelMapper;
+    private final StoreConfigService storeConfigService;
+
+    public StoreRepository(StoreNodeMapper storeNodeMapper, StoreVolumeMapper storeVolumeMapper,
+                           UploadChannelMapper uploadChannelMapper, StoreConfigService storeConfigService) {
+        this.storeNodeMapper = storeNodeMapper;
+        this.storeVolumeMapper = storeVolumeMapper;
+        this.uploadChannelMapper = uploadChannelMapper;
+        this.storeConfigService = storeConfigService;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public int saveStoreNode(StoreNodeDto storeNodeDto) {
+        StoreNode storeNode = new StoreNode(storeNodeDto);
+        storeNodeMapper.save(storeNode);
+
+        int storeNodeId = storeNode.getId();
+        List<StoreVolume> storeVolumes = storeNodeDto.getDiskVolumes().stream()
+                .map(diskVolume -> new StoreVolume(storeNodeId, diskVolume))
+                .collect(Collectors.toList());
+        if (!storeVolumes.isEmpty()) {
+            storeVolumeMapper.saveAll(storeVolumes);
+        }
+        return storeNode.getId();
+    }
+
+    @Cacheable(cacheNames = "tnb:file:channel", key = "#ossUser + '_' + #channelName", unless = "#result == null")
+    public UploadChannel getUploadChannel(int ossUser, String channelName) {
+        return uploadChannelMapper.findByCreateByAndName(ossUser, channelName);
+    }
+}

+ 15 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/FileInitRequest.java

@@ -0,0 +1,15 @@
+package cn.reghao.oss.mgr.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author reghao
+ * @date 2026-02-02 22:12:53
+ */
+@Setter
+@Getter
+public class FileInitRequest {
+    private String objectId;
+    private int channelCode;
+}

+ 22 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/KeyAuthDto.java

@@ -0,0 +1,22 @@
+package cn.reghao.oss.mgr.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+/**
+ * @author reghao
+ * @date 2024-02-27 11:21:07
+ */
+@Setter
+@Getter
+public class KeyAuthDto {
+    @NotBlank
+    @Size(min = 8, max = 8, message = "accessKeyId 长度应为 8 个字符")
+    private String accessKeyId;
+    @NotBlank
+    @Size(min = 20, max = 20, message = "accessKeySecret 长度应为 20 字符")
+    private String accessKeySecret;
+}

+ 26 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/NodeAddDto.java

@@ -0,0 +1,26 @@
+package cn.reghao.oss.mgr.model.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Getter;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+
+/**
+ * @author reghao
+ * @date 2024-02-27 09:42:44
+ */
+@Setter
+@Getter
+public class NodeAddDto {
+    @NotNull
+    private Integer storeNodeId;
+    @Pattern(regexp = "^[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,6}$", message = "域名格式不正确")
+    @Size(max = 128, message = "域名长度应小于 128 个字符")
+    private String domain;
+    @NotBlank
+    @Size(max = 5, message = "协议应该是 http 或 https")
+    private String protocol;
+}

+ 28 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/NodeUpdateDto.java

@@ -0,0 +1,28 @@
+package cn.reghao.oss.mgr.model.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Getter;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+
+/**
+ * @author reghao
+ * @date 2025-10-20 16:42:44
+ */
+@Setter
+@Getter
+public class NodeUpdateDto {
+    @NotNull
+    private Integer userNodeId;
+    @Pattern(regexp = "^[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,6}$", message = "域名格式不正确")
+    @Size(max = 128, message = "域名长度应小于 128 个字符")
+    private String domain;
+    @NotBlank
+    @Size(max = 5, message = "协议应该是 http 或 https")
+    private String protocol;
+    private String referer;
+    private String secretKey;
+}

+ 35 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/UploadChannelDto.java

@@ -0,0 +1,35 @@
+package cn.reghao.oss.mgr.model.dto;
+
+import cn.reghao.jutil.jdk.web.validator.ValidEnum;
+import cn.reghao.oss.api.constant.ObjectSize;
+import cn.reghao.oss.api.constant.ObjectScope;
+import cn.reghao.oss.api.constant.ObjectType;
+import lombok.Getter;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+
+/**
+ * @author reghao
+ * @date 2021-06-03 16:15:23
+ */
+@Setter
+@Getter
+public class UploadChannelDto {
+    @Pattern(regexp = "([^\\\\:*<>|\"?\\r\\n\\s/]+/)*([^\\\\:*<>|\"?\\r\\n\\s/]+)/$", message = "通道前缀不匹配")
+    @Size(min = 2, max = 128, message = "通道前缀长度应在 2 ~ 128 个字符之间")
+    private String channelPrefix;
+    @NotNull
+    @Size(min = 2, max = 128, message = "通道名长度应在 2 ~ 128 个字符之间")
+    private String channelName;
+    @ValidEnum(value = ObjectSize.class, message = "通道允许上传的文件大小不合法")
+    private Long maxSize;
+    @ValidEnum(value = ObjectType.class, message = "通道允许上传的文件类型不合法")
+    private Integer objectType;
+    @ValidEnum(value = ObjectScope.class, message = "通道作用域不合法")
+    private Integer scope;
+    @NotNull
+    private Integer userNodeId;
+}

+ 15 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/dto/UploadSample.java

@@ -0,0 +1,15 @@
+package cn.reghao.oss.mgr.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author reghao
+ * @date 2026-02-21 23:06:55
+ */
+@Setter
+@Getter
+public class UploadSample {
+    private String uploadId;
+    private String sampleMd5;
+}

+ 46 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/DataBlock.java

@@ -0,0 +1,46 @@
+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 lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+/**
+ * @author reghao
+ * @date 2022-11-24 10:25:18
+ */
+@NoArgsConstructor
+@Setter
+@Getter
+public class DataBlock extends BaseObject<Integer> {
+    @NotBlank
+    @Size(max = 255)
+    private String sha256sum;
+    @NotBlank
+    @Size(max = 255)
+    private String hostPort;
+    @NotBlank
+    @Size(max = 255)
+    private String absolutePath;
+    @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();
+    }
+}

+ 105 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/FileMeta.java

@@ -0,0 +1,105 @@
+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 lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+/**
+ * 文件元数据
+ *
+ * @author reghao
+ * @date 2022-11-21 10:53:10
+ */
+@NoArgsConstructor
+@Setter
+@Getter
+public class FileMeta extends BaseObject<Integer> {
+    @NotBlank
+    @Size(max = 255)
+    private String objectName;
+    @NotBlank
+    @Size(max = 255)
+    private String objectId;
+    @NotBlank
+    @Size(max = 255)
+    private String pid;
+    @NotBlank
+    @Size(max = 255)
+    private String sha256sum;
+    @NotBlank
+    @Size(max = 255)
+    private String filename;
+    @NotNull
+    private Long size;
+    @NotNull
+    private Integer fileType;
+    @NotBlank
+    @Size(max = 255)
+    private String contentType;
+    @NotNull
+    private Integer scope;
+    @NotNull
+    private Long uploadBy;
+
+    // 目录对象
+    public FileMeta(long owner, String objectName, String objectId, String filename, String pid, int scope) {
+        this.objectName = objectName;
+        this.objectId = objectId;
+        this.filename = filename;
+        this.size = 0L;
+        this.fileType = 1000;
+        this.contentType = "0";
+        this.sha256sum = "0";
+        this.pid = pid;
+        this.uploadBy = owner;
+        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(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();
+        this.pid = "0";
+        this.uploadBy = uploadTask.getUploadBy();
+        this.scope = ObjectScope.PUBLIC.getCode();
+    }
+
+    public FileMeta(FileMeta fileMeta, String objectId, String objectName, String filename,
+                    long uploadBy, int scope) {
+        this.objectName = objectName;
+        this.objectId = objectId;
+        this.filename = filename;
+        this.size = fileMeta.getSize();
+        this.fileType = fileMeta.getFileType();
+        this.contentType = fileMeta.getContentType();
+        this.sha256sum = fileMeta.getSha256sum();
+        this.pid = "0";
+        this.uploadBy = uploadBy;
+        this.scope = scope;
+    }
+}

+ 37 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/StoreNode.java

@@ -0,0 +1,37 @@
+package cn.reghao.oss.mgr.model.po;
+
+import cn.reghao.jutil.jdk.web.db.BaseObject;
+import cn.reghao.oss.api.dto.StoreNodeDto;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+
+/**
+ * @author reghao
+ * @date 2024-02-23 11:29:14
+ */
+@NoArgsConstructor
+@Getter
+@Setter
+public class StoreNode extends BaseObject<Integer> {
+    @NotBlank
+    @Pattern(regexp = "^((2((5[0-5])|([0-4]\\d)))|([0-1]?\\d{1,2}))(\\.((2((5[0-5])|([0-4]\\d)))|([0-1]?\\d{1,2}))){3}$", message = "节点地址格式不正确")
+    private String nodeAddr;
+    @NotNull
+    private Integer httpPort;
+    @NotNull
+    private Integer rpcPort;
+    @NotNull
+    private Boolean enabled;
+
+    public StoreNode(StoreNodeDto storeNodeDto) {
+        this.nodeAddr = storeNodeDto.getNodeAddr();
+        this.httpPort = storeNodeDto.getHttpPort();
+        this.rpcPort = storeNodeDto.getRpcPort();
+        this.enabled = false;
+    }
+}

+ 63 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/StoreVolume.java

@@ -0,0 +1,63 @@
+package cn.reghao.oss.mgr.model.po;
+
+import cn.reghao.jutil.jdk.web.db.BaseObject;
+import cn.reghao.oss.api.dto.disk.DiskVolume;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+/**
+ * @author reghao
+ * @date 2024-03-04 17:08:20
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Setter
+@Getter
+public class StoreVolume extends BaseObject<Integer> {
+    @NotBlank
+    private String storeDir;
+    @NotNull
+    private Integer storeNodeId;
+    @NotBlank
+    private String name;
+    @NotBlank
+    private String volume;
+    @NotBlank
+    private String mountPoint;
+    @NotBlank
+    private String fsType;
+    @NotBlank
+    private String blockId;
+    @NotNull
+    private Long totalSpace;
+    @NotNull
+    private Long availSpace;
+    @NotNull
+    private Long totalInode;
+    @NotNull
+    private Long availInode;
+
+    private String usedSpaceStr;
+    private double percentSpace;
+    private long usedInode;
+    private double percentInode;
+
+    public StoreVolume(int storeNodeId, DiskVolume diskVolume) {
+        this.storeDir = diskVolume.getStoreDir();
+        this.storeNodeId = storeNodeId;
+        this.name = diskVolume.getName();
+        this.volume = diskVolume.getVolume();
+        this.mountPoint = diskVolume.getMountPoint();
+        this.fsType = diskVolume.getFsType();
+        this.blockId = diskVolume.getBlockId();
+        this.totalSpace = diskVolume.getTotalSpace();
+        this.availSpace = diskVolume.getAvailSpace();
+        this.totalInode = diskVolume.getTotalInode();
+        this.availInode = diskVolume.getAvailInode();
+    }
+}

+ 75 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/UploadChannel.java

@@ -0,0 +1,75 @@
+package cn.reghao.oss.mgr.model.po;
+
+import cn.reghao.jutil.jdk.web.db.BaseObject;
+import cn.reghao.oss.api.constant.UploadChannelType;
+import cn.reghao.oss.mgr.model.dto.UploadChannelDto;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+/**
+ * @author reghao
+ * @date 2024-02-23 15:05:30
+ */
+@NoArgsConstructor
+@Getter
+@Setter
+public class UploadChannel extends BaseObject<Integer> {
+    @NotNull
+    private Integer userNodeId;
+    @NotNull
+    private Integer channelCode;
+    @NotBlank
+    private String name;
+    @NotBlank
+    private String prefix;
+    @NotNull
+    private Long maxSize;
+    @NotNull
+    private Integer fileType;
+    @NotNull
+    private Integer scope;
+    @NotNull
+    private Boolean setUrl;
+    @NotNull
+    private Boolean setCallback;
+    @NotNull
+    private Boolean enabled;
+    @NotNull
+    private Long createBy;
+
+    private String maxSizeStr;
+    private String fileTypeStr;
+    private String scopeStr;
+
+    public UploadChannel(int channelCode, UploadChannelDto uploadChannelDto, long ossUser) {
+        this.userNodeId = uploadChannelDto.getUserNodeId();
+        this.channelCode = channelCode;
+        this.name = uploadChannelDto.getChannelName();
+        this.prefix = uploadChannelDto.getChannelPrefix();
+        this.maxSize = uploadChannelDto.getMaxSize();
+        this.fileType = uploadChannelDto.getObjectType();
+        this.scope = uploadChannelDto.getScope();
+        this.setUrl = false;
+        this.setCallback = false;
+        this.enabled = true;
+        this.createBy = ossUser;
+    }
+
+    public UploadChannel(int userNodeId, UploadChannelType uploadChannelType, long ossUser) {
+        this.userNodeId = userNodeId;
+        this.channelCode = uploadChannelType.getChannelCode();
+        this.name = uploadChannelType.name();
+        this.prefix = uploadChannelType.getChannelPrefix();
+        this.maxSize = uploadChannelType.getMaxSize();
+        this.fileType = uploadChannelType.getFileType();
+        this.scope = uploadChannelType.getScope();
+        this.setUrl = false;
+        this.setCallback = false;
+        this.enabled = true;
+        this.createBy = ossUser;
+    }
+}

+ 60 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/UploadTask.java

@@ -0,0 +1,60 @@
+package cn.reghao.oss.mgr.model.po;
+
+import cn.reghao.jutil.jdk.web.db.BaseObject;
+import cn.reghao.oss.api.dto.rest.UploadPrepare;
+import cn.reghao.oss.mgr.config.UserContext;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+import java.time.LocalDateTime;
+
+/**
+ * 文件分片详情
+ *
+ * @author reghao
+ * @date 2024-10-24 16:33:16
+ */
+@NoArgsConstructor
+@Getter
+public class UploadTask extends BaseObject<Integer> {
+    @NotBlank
+    @Size(max = 255)
+    private String uploadId;
+    @NotBlank
+    @Size(max = 255)
+    private String sha256sum;
+    @NotNull
+    private Long offset;
+    @NotNull
+    private Integer length;
+    @NotNull
+    private Integer splitSize;
+    @NotBlank
+    @Size(max = 255)
+    private String filename;
+    @NotNull
+    private Long fileSize;
+    @NotNull
+    private Integer status;
+    @NotNull
+    private LocalDateTime expireTime;
+    @NotNull
+    private Long uploadBy;
+
+    public UploadTask(String uploadId, UploadPrepare uploadPrepare, long offset, int length, int splitSize, LocalDateTime expireTime) {
+        this.uploadId = uploadId;
+        this.sha256sum = uploadPrepare.getSha256sum();
+        this.offset = offset;
+        this.length = length;
+        this.splitSize = splitSize;
+        this.filename = uploadPrepare.getFilename();
+        this.fileSize = uploadPrepare.getSize();
+        this.status = 1;
+        this.expireTime = expireTime;
+        this.uploadBy = UserContext.getUserId();
+    }
+}

+ 36 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/UserKey.java

@@ -0,0 +1,36 @@
+package cn.reghao.oss.mgr.model.po;
+
+import cn.reghao.jutil.jdk.web.db.BaseObject;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+/**
+ * @author reghao
+ * @date 2024-02-27 10:32:53
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+public class UserKey extends BaseObject<Integer> {
+    @NotBlank
+    @Size(min = 8, max = 8)
+    private String accessKeyId;
+    @NotBlank
+    @Size(min = 20, max = 20)
+    private String accessKeySecret;
+    @NotNull
+    private Long createBy;
+
+    public UserKey(String accessKeyId, String accessKeySecret, long createBy) {
+        this.accessKeyId = accessKeyId;
+        this.accessKeySecret = accessKeySecret;
+        this.createBy = createBy;
+    }
+}

+ 51 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/UserNode.java

@@ -0,0 +1,51 @@
+package cn.reghao.oss.mgr.model.po;
+
+import cn.reghao.jutil.jdk.web.db.BaseObject;
+import cn.reghao.oss.mgr.model.dto.NodeAddDto;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+
+/**
+ * @author reghao
+ * @date 2024-02-27 09:45:11
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+public class UserNode extends BaseObject<Integer> {
+    @NotNull
+    private Integer storeNodeId;
+    @NotBlank
+    @Pattern(regexp = "^[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,6}$", message = "域名格式不正确")
+    private String domain;
+    private String protocol;
+    private String secretKey;
+    private String referer;
+    @NotNull
+    private Long createBy;
+
+    private String storeNodeAddr;
+
+    public UserNode(NodeAddDto nodeAddDto, long createBy) {
+        this.storeNodeId = nodeAddDto.getStoreNodeId();
+        this.domain = nodeAddDto.getDomain();
+        this.protocol = nodeAddDto.getProtocol();
+        this.createBy = createBy;
+    }
+
+    public UserNode(int storeNodeId, String domain, String secretKey, String referer, long createBy) {
+        this.storeNodeId = storeNodeId;
+        this.domain = domain;
+        this.protocol = "http";
+        this.secretKey = secretKey;
+        this.referer = referer;
+        this.createBy = createBy;
+    }
+}

+ 17 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/vo/AddChannelAttr.java

@@ -0,0 +1,17 @@
+package cn.reghao.oss.mgr.model.vo;
+
+import cn.reghao.oss.api.dto.SelectOption;
+import lombok.AllArgsConstructor;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2025-10-18 20:52:03
+ */
+@AllArgsConstructor
+public class AddChannelAttr {
+    List<SelectOption> objectTypes;
+    List<SelectOption> objectScopes;
+    List<SelectOption> sizeList;
+}

+ 33 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/vo/StoreNodeInfo.java

@@ -0,0 +1,33 @@
+package cn.reghao.oss.mgr.model.vo;
+
+import cn.reghao.oss.mgr.model.po.StoreNode;
+import lombok.Getter;
+
+/**
+ * @author reghao
+ * @date 2024-08-23 17:47:49
+ */
+@Getter
+public class StoreNodeInfo {
+    private int id;
+    private String nodeAddr;
+    private int httpPort;
+    private int rpcPort;
+    private String total;
+    private String used;
+    private double percent;
+    private String status;
+    private boolean enabled;
+
+    public StoreNodeInfo(StoreNode storeNode, String total, String used, double percent) {
+        this.id = storeNode.getId();
+        this.nodeAddr = storeNode.getNodeAddr();
+        this.httpPort = storeNode.getHttpPort();
+        this.rpcPort = storeNode.getRpcPort();
+        this.total = total;
+        this.used = used;
+        this.percent = percent;
+        this.status = storeNode.getEnabled() ? "可读写" : "只读";
+        this.enabled = storeNode.getEnabled();
+    }
+}

+ 247 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/rpc/ConsoleServiceImpl.java

@@ -0,0 +1,247 @@
+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
+ */
+@Slf4j
+@DubboService
+@Service
+public class ConsoleServiceImpl implements ConsoleService {
+    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 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;
+    }
+
+    @Override
+    public void registerNode(StoreNodeDto storeNodeDto) {
+        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;
+        }
+
+        String filename = uploadedFile.getFilename();
+        String suffix = StringUtil.getSuffix(filename);
+        String channelPrefix = uploadedFile.getChannelPrefix();
+        String objectName = String.format("%s%s.%s", channelPrefix, uploadId, suffix);
+
+        String contentType = uploadedFile.getContentType();
+        int fileType = ObjectType.Image.getValue();
+        if (contentType.startsWith("video")) {
+            fileType = ObjectType.Video.getValue();
+        } else if (contentType.startsWith("audio")) {
+            fileType = ObjectType.Audio.getValue();
+        } else if (contentType.startsWith("text")) {
+            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());
+
+        objectRepository.deleteObject(objectId);
+    }
+
+    @Override
+    public void setObjectScope(String objectId, int scope) {
+        ObjectScope objectScope = ObjectScope.getByCode(scope);
+        fileMetaMapper.updateScopeByObjectId(objectId, objectScope.getCode());
+    }
+
+    @Override
+    public ObjectInfo getObjectInfo(String objectId) {
+        return objectRepository.getObjectInfo(objectId);
+    }
+
+    @Override
+    public VideoInfo getVideoInfo(String objectId) {
+        DataBlock dataBlock = dataBlockMapper.findByObjectId(objectId);
+        String hostPort = dataBlock.getHostPort();
+        StoreService storeService = rpcService.getStoreService(hostPort);
+
+        VideoInfo videoInfo = storeService.getVideoInfo(dataBlock.getAbsolutePath());
+        return videoInfo;
+    }
+
+    @Override
+    public ImageInfo getImageInfo(String objectId) {
+        return null;
+    }
+}

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

@@ -0,0 +1,99 @@
+package cn.reghao.oss.mgr.rpc;
+
+import cn.reghao.oss.api.iface.StoreService;
+import cn.reghao.oss.mgr.db.mapper.DataBlockMapper;
+import cn.reghao.oss.mgr.model.po.DataBlock;
+import cn.reghao.oss.mgr.model.po.StoreNode;
+import org.apache.dubbo.config.ApplicationConfig;
+import org.apache.dubbo.config.ConsumerConfig;
+import org.apache.dubbo.config.ReferenceConfig;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author reghao
+ * @date 2024-07-05 10:44:51
+ */
+@Service
+public class RpcService {
+    private static final ConcurrentHashMap<String, ReferenceConfig<?>> CACHE = new ConcurrentHashMap<>();
+
+    public StoreService getStoreService(StoreNode storeNode) {
+        RemoteService<StoreService> remoteService = new RemoteService<>();
+        String host = storeNode.getNodeAddr();
+        int port = storeNode.getRpcPort();
+        return remoteService.getService(host, port, StoreService.class);
+    }
+
+    public StoreService getStoreService(String hostPort) {
+        RemoteService<StoreService> remoteService = new RemoteService<>();
+        return remoteService.getService(hostPort, StoreService.class);
+    }
+
+    static class RemoteService<T> {
+        public T getService(String hostPort, Class<T> clazz) {
+            ReferenceConfig<?> config = CACHE.computeIfAbsent(hostPort, key -> {
+                String[] arr = hostPort.split(":");
+                String host = arr[0];
+                int port = Integer.parseInt(arr[1]);
+
+                String serviceName = "remote-service";
+                String dubboUrl = String.format("dubbo://%s:%s/%s", host, port, clazz.getName());
+
+                // 当前应用配置
+                ApplicationConfig application = new ApplicationConfig();
+                application.setName(serviceName);
+
+                ConsumerConfig consumerConfig = new ConsumerConfig();
+                consumerConfig.setTimeout(10_000);
+
+                // 注意:ReferenceConfig为重对象,内部封装了与注册中心的连接,以及与服务提供方的连接
+                // 引用远程服务
+                // TODO 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
+                ReferenceConfig<T> reference = new ReferenceConfig<>();
+                reference.setApplication(application);
+                reference.setInterface(clazz);
+                reference.setUrl(dubboUrl);
+                reference.setConsumer(consumerConfig);
+                reference.setCheck(false);
+                reference.setScope("remote"); // 明确是远程调用
+                // 显式调用 get() 会完成初始化并创建 Invoker
+                reference.get();
+                return reference;
+            });
+
+            return (T) config.get();
+        }
+
+        public T getService(String host, int port, Class<T> clazz) {
+            String serviceName = "remote-service";
+            String dubboUrl = String.format("dubbo://%s:%s/%s", host, port, clazz.getName());
+
+            // 当前应用配置
+            ApplicationConfig application = new ApplicationConfig();
+            application.setName(serviceName);
+
+            ConsumerConfig consumerConfig = new ConsumerConfig();
+            consumerConfig.setTimeout(10_000);
+
+            // 注意:ReferenceConfig为重对象,内部封装了与注册中心的连接,以及与服务提供方的连接
+            // 引用远程服务
+            // TODO 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
+            ReferenceConfig<T> reference = new ReferenceConfig<>();
+            reference.setApplication(application);
+            reference.setInterface(clazz);
+            reference.setUrl(dubboUrl);
+            reference.setConsumer(consumerConfig);
+            return reference.get();
+        }
+    }
+
+    // 记得在应用销毁时清理,防止内存泄漏
+    public static void destroy() {
+        CACHE.values().forEach(ReferenceConfig::destroy);
+        CACHE.clear();
+    }
+}

+ 126 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/MetadataService.java

@@ -0,0 +1,126 @@
+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;
+import cn.reghao.oss.api.iface.StoreService;
+import cn.reghao.oss.mgr.config.UserContext;
+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.mgr.model.po.*;
+import cn.reghao.oss.mgr.rpc.RpcService;
+import cn.reghao.oss.mgr.util.StringUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.Random;
+import java.util.UUID;
+
+/**
+ * @author reghao
+ * @date 2026-02-01 15:49:10
+ */
+@Slf4j
+@Service
+public class MetadataService {
+    private static final int CHUNK_SIZE = 1024 * 1024; // 采样块大小:1MB
+    private static final int SPLIT_SIZE = 1024 * 1024 * 10; // 分片大小:10MB
+    private final ObjectRepository objectRepository;
+    private StoreNodeMapper storeNodeMapper;
+    private UploadTaskMapper uploadTaskMapper;
+    private RpcService rpcService;
+    private FileMetaMapper fileMetaMapper;
+    private UploadChannelMapper uploadChannelMapper;
+    private ChannelRepository channelRepository;
+
+    public MetadataService(ObjectRepository objectRepository, StoreNodeMapper storeNodeMapper,
+                           UploadTaskMapper uploadTaskMapper, RpcService rpcService, FileMetaMapper fileMetaMapper,
+                           UploadChannelMapper uploadChannelMapper) {
+        this.objectRepository = objectRepository;
+        this.storeNodeMapper = storeNodeMapper;
+        this.uploadTaskMapper = uploadTaskMapper;
+        this.rpcService = rpcService;
+        this.fileMetaMapper = fileMetaMapper;
+        this.uploadChannelMapper = uploadChannelMapper;
+    }
+
+    public UploadPrepareRet prepareUpload(UploadPrepare uploadPrepare) {
+        String uploadId = UUID.randomUUID().toString().replace("-", "");
+        long size = uploadPrepare.getSize();
+        String sha256sum = uploadPrepare.getSha256sum();
+        DataBlock dataBlock = objectRepository.getBySha256sum(sha256sum);
+        UploadPrepareRet uploadPrepareRet;
+        long randomOffset = 0;
+        if (dataBlock != null) {
+            Random random = new Random(size);
+            randomOffset = (long) (random.nextDouble() * (size - CHUNK_SIZE));
+            boolean exist = true;
+            uploadPrepareRet = new UploadPrepareRet(uploadId, SPLIT_SIZE, exist, randomOffset, CHUNK_SIZE);
+        } else {
+            long splitNumber = size/SPLIT_SIZE + (size%SPLIT_SIZE != 0 ? 1 : 0);
+            uploadPrepareRet = new UploadPrepareRet(uploadId, SPLIT_SIZE);
+        }
+
+        LocalDateTime expireTime = LocalDateTime.now().plusHours(24);
+        UploadTask uploadTask = new UploadTask(uploadId, uploadPrepare, randomOffset, CHUNK_SIZE, SPLIT_SIZE, expireTime);
+        uploadTaskMapper.save(uploadTask);
+        return uploadPrepareRet;
+    }
+
+    public UploadFileRet verifyPartialHash(UploadSample uploadSample) {
+        String uploadId = uploadSample.getUploadId();
+        String clientPartMd5 = uploadSample.getSampleMd5();
+
+        UploadTask uploadTask = uploadTaskMapper.findByUploadId(uploadId);
+        boolean result = false;
+        if (uploadTask == null) {
+            return new UploadFileRet(uploadId, result);
+        }
+
+        long offset = uploadTask.getOffset();
+        int length = uploadTask.getLength();
+        String sha256sum = uploadTask.getSha256sum();
+        DataBlock dataBlock = objectRepository.getBySha256sum(sha256sum);
+        String hostPort = dataBlock.getHostPort();
+        String[] arr = hostPort.split(":");
+        String host = arr[0];
+        int port = Integer.parseInt(arr[1]);
+        StoreNode storeNode = storeNodeMapper.findByNodeAddrAndHttpPort(host, port);
+        if (storeNode != null) {
+            StoreService storeService = rpcService.getStoreService(storeNode);
+            String absolutePath = dataBlock.getAbsolutePath();
+            String serverPartMd5 = storeService.getPartialMd5(absolutePath, offset, length);
+            result = serverPartMd5.equalsIgnoreCase(clientPartMd5);
+            if (result) {
+                long uploadBy = UserContext.getUserId();
+                String channelPrefix = "video/playback/";
+                UploadChannel uploadChannel = uploadChannelMapper.findByCreateByAndPrefix(uploadBy, channelPrefix);
+
+                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);
+            }
+        }
+
+        return new UploadFileRet(uploadId, result);
+    }
+
+    public ServerInfo getServerInfo() {
+        int channelCode = 101;
+        UploadChannel uploadChannel = channelRepository.getByChannelCode(channelCode);
+        return null;
+    }
+}

+ 22 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/StoreConfigService.java

@@ -0,0 +1,22 @@
+package cn.reghao.oss.mgr.service;
+
+import cn.reghao.oss.mgr.config.UserContext;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author reghao
+ * @date 2024-09-09 11:35:55
+ */
+@Service
+public class StoreConfigService {
+    public long getLocalOssUser() {
+        long loginUser = UserContext.getUserId();
+        return loginUser;
+    }
+
+    @Cacheable(cacheNames = "tnb:file:oss_user", key = "'local_oss_user'", unless = "#result == null")
+    public Integer getOssUser() {
+        return 0;
+    }
+}

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

@@ -0,0 +1,177 @@
+package cn.reghao.oss.mgr.service;
+
+import cn.reghao.jutil.jdk.converter.ByteConverter;
+import cn.reghao.jutil.jdk.converter.ByteType;
+import cn.reghao.jutil.jdk.math.Calculator;
+import cn.reghao.jutil.jdk.web.result.Result;
+import cn.reghao.oss.api.dto.SelectOption;
+import cn.reghao.oss.api.dto.StoreNodeDto;
+import cn.reghao.oss.api.dto.disk.DiskVolume;
+import cn.reghao.oss.mgr.db.mapper.StoreNodeMapper;
+import cn.reghao.oss.mgr.db.mapper.StoreVolumeMapper;
+import cn.reghao.oss.mgr.db.mapper.UserNodeMapper;
+import cn.reghao.oss.mgr.db.repository.StoreRepository;
+import cn.reghao.oss.mgr.model.po.StoreNode;
+import cn.reghao.oss.mgr.model.po.StoreVolume;
+import cn.reghao.oss.mgr.model.vo.StoreNodeInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2024-02-23 13:01:38
+ */
+@Slf4j
+@Service
+public class StoreNodeService {
+    private final ByteConverter byteConverter;
+    private final StoreNodeMapper storeNodeMapper;
+    private final StoreVolumeMapper storeVolumeMapper;
+    private final UserNodeMapper userNodeMapper;
+    private final StoreRepository storeRepository;
+    private final UserNodeService userNodeService;
+    private final UploadChannelService uploadChannelService;
+
+    public StoreNodeService(ByteConverter byteConverter, StoreNodeMapper storeNodeMapper,
+                            StoreVolumeMapper storeVolumeMapper, UserNodeMapper userNodeMapper,
+                            StoreRepository storeRepository, UserNodeService userNodeService,
+                            UploadChannelService uploadChannelService) {
+        this.byteConverter = byteConverter;
+        this.storeNodeMapper = storeNodeMapper;
+        this.storeVolumeMapper = storeVolumeMapper;
+        this.userNodeMapper = userNodeMapper;
+        this.storeRepository = storeRepository;
+        this.userNodeService = userNodeService;
+        this.uploadChannelService = uploadChannelService;
+    }
+
+    public void addOrUpdate(StoreNodeDto storeNodeDto) {
+        String nodeAddr = storeNodeDto.getNodeAddr();
+        int httpPort = storeNodeDto.getHttpPort();
+        StoreNode storeNode = storeNodeMapper.findByNodeAddrAndHttpPort(nodeAddr, httpPort);
+        if (storeNode == null) {
+            storeRepository.saveStoreNode(storeNodeDto);
+        } else {
+            int storeNodeId = storeNode.getId();
+            List<StoreVolume> storeVolumes = storeNodeDto.getDiskVolumes().stream()
+                    .map(diskVolume -> new StoreVolume(storeNodeId, diskVolume))
+                    .collect(Collectors.toList());
+            //storeVolumeMapper.saveAll(storeVolumes);
+        }
+    }
+
+    private void init() {
+        // 没有 UserNode
+        int total = userNodeMapper.findAll().size();
+        if (total == 0) {
+            userNodeService.initUserNode();
+        }
+        try {
+            uploadChannelService.initUploadChannel();
+        } catch (Exception e) {
+            log.error("初始化 UploadChannel 异常: {}", e.getMessage());
+        }
+    }
+
+    public Result updateStatus(int storeNodeId) {
+        StoreNode storeNode = storeNodeMapper.findById(storeNodeId);
+        if (storeNode != null) {
+            boolean enabled = storeNode.getEnabled();
+            storeNode.setEnabled(!enabled);
+            storeNodeMapper.save(storeNode);
+            return Result.success();
+        }
+
+        return Result.fail("存储节点不存在");
+    }
+
+    public Result delete(int storeNodeId) {
+        StoreNode storeNode = storeNodeMapper.findById(storeNodeId);
+        if (storeNode == null) {
+            return Result.fail("node not exist");
+        }
+
+        int total = userNodeMapper.countByStoreNodeId(storeNodeId);
+        if (total > 0) {
+            return Result.fail("someone uses the node");
+        }
+
+        storeNodeMapper.delete(storeNode);
+        return Result.success();
+    }
+
+    public List<StoreNodeInfo> getByPage() {
+        List<StoreNode> page = storeNodeMapper.findAll();
+        List<StoreNodeInfo> list =  page.stream().map(storeNode -> {
+            List<StoreVolume> storeVolumeList = storeVolumeMapper.findByStoreNodeId(storeNode.getId());
+            long total = 0L;
+            long avail = 0L;
+            for (StoreVolume storeVolume : storeVolumeList) {
+                total += storeVolume.getTotalSpace();
+                avail += storeVolume.getAvailSpace();
+            }
+            long used = total - avail;
+
+            String totalStr = byteConverter.convert(ByteType.Bytes, total);
+            String usedStr = byteConverter.convert(ByteType.Bytes, used);
+            double percent = 0;
+            if (avail != 0) {
+                percent = Calculator.getPercentage(total, avail);
+            }
+            return new StoreNodeInfo(storeNode, totalStr, usedStr, percent);
+        }).collect(Collectors.toList());
+
+        return list;
+    }
+
+    public StoreNode getByStoreNodeId(int id) {
+        return storeNodeMapper.findById(id);
+    }
+
+    public List<SelectOption> getNodeKeyValues() {
+        return storeNodeMapper.findAll().stream()
+                .map(storeNode -> {
+                    int storeNodeId = storeNode.getId();
+                    String nodeAddr = storeNode.getNodeAddr();
+                    return new SelectOption(nodeAddr, storeNodeId+"");
+                }).collect(Collectors.toList());
+    }
+
+    public List<StoreVolume> getStoreDisks(int storeNodeId) {
+        StoreNode storeNode = storeNodeMapper.findById(storeNodeId);
+        if (storeNode == null) {
+            return null;
+        }
+
+        //List<DiskVolume> list = storeServiceWrapper.getDiskVolumes(storeNodeId);
+        List<DiskVolume> list = new ArrayList<>();
+        List<StoreVolume> storeVolumes = list.stream()
+                .map(diskVolume -> new StoreVolume(storeNodeId, diskVolume))
+                .collect(Collectors.toList());
+        //storeVolumeMapper.saveAll(storeVolumes);
+
+        storeVolumes.forEach(storeVolume -> {
+            long total = storeVolume.getTotalSpace();
+            long avail = storeVolume.getAvailSpace();
+            long used = total - avail;
+            double percent = Calculator.getPercentage(total, avail);
+            String totalStr = byteConverter.convert(ByteType.Bytes, total);
+            String usedStr = byteConverter.convert(ByteType.Bytes, used);
+            storeVolume.setUsedSpaceStr(usedStr);
+            storeVolume.setPercentSpace(percent);
+
+            long totalInode = storeVolume.getTotalInode();
+            long availInode = storeVolume.getAvailInode();
+            long usedInode = totalInode - availInode;
+            double percentInode = Calculator.getPercentage(totalInode, availInode);
+            storeVolume.setUsedInode(usedInode);
+            storeVolume.setPercentInode(percentInode);
+        });
+
+        return storeVolumes;
+    }
+}

+ 41 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/TaskService.java

@@ -0,0 +1,41 @@
+package cn.reghao.oss.mgr.service;
+
+import cn.reghao.oss.mgr.db.mapper.UploadTaskMapper;
+import cn.reghao.oss.mgr.model.po.UploadTask;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2026-02-22 23:23:35
+ */
+@EnableScheduling
+@Slf4j
+@Service
+public class TaskService {
+    private final UploadTaskMapper uploadTaskMapper;
+
+    public TaskService(UploadTaskMapper uploadTaskMapper) {
+        this.uploadTaskMapper = uploadTaskMapper;
+    }
+
+    // 每天凌晨 4:00 执行清理任务
+    @Scheduled(cron = "0 0 4 * * ?")
+    @Transactional
+    public void cleanExpiredTasks() {
+        log.info("执行过期 UploadTask 清理任务...");
+        // 1. 找出过期的任务
+        List<UploadTask> expiredTasks = uploadTaskMapper.findByExpired();
+        for (UploadTask task : expiredTasks) {
+            // 2. 物理删除 SSD 上的残余文件块(重要!防止 SSD 满)
+            //FileUtil.deleteQuietly(task.getSsdPath());
+            // 3. 逻辑删除数据库记录
+            //jdbcTemplate.update("DELETE FROM upload_tasks WHERE upload_id = ?", task.getUploadId());
+        }
+    }
+}

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

@@ -0,0 +1,158 @@
+package cn.reghao.oss.mgr.service;
+
+import cn.reghao.jutil.jdk.converter.ByteConverter;
+import cn.reghao.jutil.jdk.converter.ByteType;
+import cn.reghao.jutil.jdk.web.result.Result;
+import cn.reghao.jutil.jdk.web.result.ResultStatus;
+import cn.reghao.oss.api.dto.ObjectChannel;
+import cn.reghao.oss.api.constant.ObjectScope;
+import cn.reghao.oss.api.constant.ObjectType;
+import cn.reghao.oss.mgr.db.mapper.UploadChannelMapper;
+import cn.reghao.oss.mgr.db.repository.ChannelRepository;
+import cn.reghao.oss.api.constant.UploadChannelType;
+import cn.reghao.oss.mgr.model.dto.UploadChannelDto;
+import cn.reghao.oss.mgr.model.po.UploadChannel;
+import cn.reghao.oss.mgr.model.po.UserNode;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2024-02-23 15:14:53
+ */
+@Service
+public class UploadChannelService {
+    private final ByteConverter byteConverter;
+    private final UploadChannelMapper uploadChannelMapper;
+    private final UserNodeService userNodeService;
+    private final StoreConfigService storeConfigService;
+    private final ChannelRepository channelRepository;
+
+    public UploadChannelService(ByteConverter byteConverter, UploadChannelMapper uploadChannelMapper,
+                                UserNodeService userNodeService, StoreConfigService storeConfigService,
+                                ChannelRepository channelRepository) {
+        this.byteConverter = byteConverter;
+        this.uploadChannelMapper = uploadChannelMapper;
+        this.userNodeService = userNodeService;
+        this.storeConfigService = storeConfigService;
+        this.channelRepository = channelRepository;
+    }
+
+    public synchronized Result addObjectChannel(UploadChannelDto uploadChannelDto) throws Exception {
+        long createBy = storeConfigService.getLocalOssUser();
+        int userNodeId = uploadChannelDto.getUserNodeId();
+        UserNode userNode = userNodeService.getUserNode(userNodeId);
+        if (userNode == null) {
+            return Result.fail(String.format("store_node with id %s not exist", userNodeId));
+        }
+
+        String channelPrefix = uploadChannelDto.getChannelPrefix();
+        UploadChannel uploadChannel = uploadChannelMapper.findByCreateByAndPrefix(createBy, channelPrefix);
+        if (uploadChannel != null) {
+            return Result.fail(String.format("channel_prefix %s exist", channelPrefix));
+        }
+
+        int channelCode = getNextChannelCode(createBy);
+        uploadChannel = new UploadChannel(channelCode, uploadChannelDto, (int) createBy);
+        channelRepository.createUploadChannel(uploadChannel);
+        return Result.success();
+    }
+
+    /**
+     * 初始化系统中使用的 UploadChannel
+     *
+     * @param
+     * @return
+     * @date 2025-10-21 09:55:23
+     */
+    public void initUploadChannel() throws Exception {
+        long ossUser = storeConfigService.getLocalOssUser();
+        List<UserNode> list = userNodeService.getUserNodes(ossUser);
+        if (list.size() == 1 && uploadChannelMapper.findByCreateBy(ossUser).isEmpty()) {
+            int userNodeId = list.get(0).getId();
+            for (UploadChannelType uploadChannelType : UploadChannelType.values()) {
+                UploadChannel uploadChannel = new UploadChannel(userNodeId, uploadChannelType, ossUser);
+                channelRepository.createUploadChannel(uploadChannel);
+            }
+        }
+    }
+
+    private int getNextChannelCode(long createBy) {
+        int total = uploadChannelMapper.countByCreateBy(createBy);
+        return 101 + total;
+    }
+
+    public void updateChannelStatus(int id) {
+        UploadChannel uploadChannel = uploadChannelMapper.findById(id);
+        if (uploadChannel != null) {
+            boolean enabled = uploadChannel.getEnabled();
+            uploadChannel.setEnabled(!enabled);
+            uploadChannelMapper.save(uploadChannel);
+        }
+    }
+
+    public Result deleteChannel(int id) throws Exception {
+        UploadChannel uploadChannel = uploadChannelMapper.findById(id);
+        if (uploadChannel == null) {
+            String errMsg = String.format("通道不存在");
+            return Result.result(ResultStatus.FAIL, errMsg);
+        }
+
+        int channelCode = uploadChannel.getId();
+        String prefix = uploadChannel.getPrefix();
+        long ossUser = storeConfigService.getLocalOssUser();
+        /*Integer total = storeServiceWrapper.countChannelObjects(channelCode, prefix, ossUser);
+        if (total != null && total == 0) {
+            storeServiceWrapper.deleteByObjectName(channelCode, prefix, ossUser);
+            uploadChannelMapper.deleteById(id);
+            return Result.result(ResultStatus.SUCCESS);
+        }*/
+
+        String errMsg = String.format("%s 通道中尚有对象存在", prefix);
+        return Result.result(ResultStatus.FAIL, errMsg);
+    }
+
+    public List<UploadChannel> getChannelsByUserNode(int userNodeId) {
+        long createBy = storeConfigService.getLocalOssUser();
+        List<UploadChannel> list = uploadChannelMapper.findByCreateByAndUserNodeId(createBy, userNodeId);
+        list.forEach(uploadChannel -> {
+            String maxSizeStr = byteConverter.convert(ByteType.Bytes, uploadChannel.getMaxSize());
+            uploadChannel.setMaxSizeStr(maxSizeStr);
+            String fileTypeStr = ObjectType.getDescByCode(uploadChannel.getFileType());
+            uploadChannel.setFileTypeStr(fileTypeStr);
+            String scopeStr = ObjectScope.getByCode(uploadChannel.getScope()).name();
+            uploadChannel.setScopeStr(scopeStr);
+        });
+
+        return list;
+    }
+
+    public List<UploadChannel> getUploadChannelsByCreateBy(long createBy) {
+        return uploadChannelMapper.findByCreateBy(createBy);
+    }
+
+    public UploadChannel getByChannelCodeAndCreateBy(int channelCode, long createBy) {
+        return uploadChannelMapper.findByCreateByAndChannelCode(createBy, channelCode);
+    }
+
+    public ObjectChannel getObjectChannelByChannelCode(int channelCode, long createBy) {
+        UploadChannel uploadChannel = uploadChannelMapper.findByCreateByAndChannelCode(createBy, channelCode);
+        if (uploadChannel == null) {
+            return null;
+        }
+
+        int id = uploadChannel.getId();
+        String channelPrefix = uploadChannel.getPrefix();
+        long maxSize = uploadChannel.getMaxSize();
+        int fileType = uploadChannel.getFileType();
+        boolean setUrl = uploadChannel.getSetUrl();
+        boolean setCallback = uploadChannel.getSetCallback();
+        int scope = uploadChannel.getScope();
+
+        UserNode userNode = userNodeService.getUserNode(uploadChannel.getUserNodeId());
+        String domain = userNode.getDomain();
+        return new ObjectChannel(id, channelCode, channelPrefix, maxSize, fileType,
+                setUrl, setCallback, scope, domain, (int) createBy);
+    }
+}

+ 56 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/UserKeyService.java

@@ -0,0 +1,56 @@
+package cn.reghao.oss.mgr.service;
+
+import cn.reghao.jutil.jdk.web.result.Result;
+import cn.reghao.jutil.jdk.security.RandomString;
+import cn.reghao.oss.mgr.db.mapper.UserKeyMapper;
+import cn.reghao.oss.mgr.model.dto.KeyAuthDto;
+import cn.reghao.oss.mgr.model.po.UserKey;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2024-02-27 10:25:25
+ */
+@Service
+public class UserKeyService {
+    private final String secretKey = "ossconsole.reghao.cn";
+    private final UserKeyMapper userKeyMapper;
+
+    public UserKeyService(UserKeyMapper userKeyMapper) {
+        this.userKeyMapper = userKeyMapper;
+    }
+
+    public Result auth(KeyAuthDto keyAuthDto) {
+        return Result.fail("secret not matched");
+    }
+
+    private String getTokenByUserId(long loginUser) {
+        return null;
+    }
+
+    public String getUserFromToken(String token) {
+        return null;
+    }
+
+    public void regenerate(long ossUser) {
+    }
+
+    public List<UserKey> getUserKeys(long ossUser) {
+        UserKey userKey = userKeyMapper.findByCreateBy(ossUser);
+        if (userKey == null) {
+            userKey = create(ossUser);
+        }
+
+        return List.of(userKey);
+    }
+
+    private UserKey create(long ossUser) {
+        String accessKeyId = RandomString.getString(8);
+        String accessKeySecret = RandomString.getString(20);
+        UserKey userKey = new UserKey(accessKeyId, accessKeySecret, ossUser);
+        userKeyMapper.save(userKey);
+        return userKey;
+    }
+}

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

@@ -0,0 +1,167 @@
+package cn.reghao.oss.mgr.service;
+
+import cn.reghao.jutil.jdk.web.result.Result;
+import cn.reghao.oss.api.dto.SelectOption;
+import cn.reghao.oss.mgr.db.mapper.StoreNodeMapper;
+import cn.reghao.oss.mgr.db.mapper.UploadChannelMapper;
+import cn.reghao.oss.mgr.db.mapper.UserNodeMapper;
+import cn.reghao.oss.mgr.model.dto.NodeAddDto;
+import cn.reghao.oss.mgr.model.dto.NodeUpdateDto;
+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 lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2024-02-27 09:49:56
+ */
+@Slf4j
+@Service
+public class UserNodeService {
+    private final UserNodeMapper userNodeMapper;
+    private final StoreNodeMapper storeNodeMapper;
+    private final UploadChannelMapper uploadChannelMapper;
+    private final StoreConfigService storeConfigService;
+
+    public UserNodeService(UserNodeMapper userNodeMapper, StoreNodeMapper storeNodeMapper,
+                           UploadChannelMapper uploadChannelMapper, StoreConfigService storeConfigService) {
+        this.userNodeMapper = userNodeMapper;
+        this.storeNodeMapper = storeNodeMapper;
+        this.uploadChannelMapper = uploadChannelMapper;
+        this.storeConfigService = storeConfigService;
+    }
+
+    public Result add(NodeAddDto nodeAddDto) {
+        int storeNodeId = nodeAddDto.getStoreNodeId();
+        long ossUser = storeConfigService.getLocalOssUser();
+        UserNode userNode = userNodeMapper.findByCreateByAndStoreNodeId(ossUser, storeNodeId);
+        if (userNode != null) {
+            return Result.fail("UserNode exist");
+        }
+
+        StoreNode storeNode = storeNodeMapper.findById(storeNodeId);
+        if (storeNode == null) {
+            return Result.success("StoreNode not exist");
+        }
+
+        userNode = new UserNode(nodeAddDto, ossUser);
+        userNodeMapper.save(userNode);
+        return Result.success("node added");
+    }
+
+    /**
+     * 自动初始化 UserNode
+     *
+     * @param
+     * @return
+     * @date 2025-10-21 09:41:46
+     */
+    public void initUserNode() {
+        userNodeMapper.findAll();
+
+        long ossUser = storeConfigService.getLocalOssUser();
+        if (ossUser == -1) {
+            log.error("ossUser not exist, initUserNode failed...");
+            return;
+        }
+
+        StoreNode storeNode = storeNodeMapper.findFirstStore();
+        if (storeNode == null) {
+            log.error("StoreNode not exist, initUserNode failed...");
+            return;
+        }
+
+        int storeNodeId = storeNode.getId();
+        String domain = "ConstantId.OSS_DOMAIN";
+        String secretKey = "";
+        String referer = "";
+        UserNode userNode = new UserNode(storeNodeId, domain, secretKey, referer, ossUser);
+        userNodeMapper.save(userNode);
+    }
+
+    public void updateUserNode(NodeUpdateDto nodeUpdateDto) {
+        userNodeMapper.updateUserNode(nodeUpdateDto);
+    }
+
+    public Result delete(int id) {
+        UserNode userNode = userNodeMapper.findById(id);
+        if (userNode != null) {
+            int userNodeId = userNode.getId();
+            long ossUser = storeConfigService.getLocalOssUser();
+            List<UploadChannel> uploadChannels = uploadChannelMapper.findByCreateByAndUserNodeId(ossUser, userNodeId);
+            if (!uploadChannels.isEmpty()) {
+                return Result.fail("UploadChannel exists");
+            }
+
+            userNodeMapper.delete(userNode);
+            return Result.success();
+        }
+
+        return Result.fail("UserNode not exists");
+    }
+
+    public List<UserNode> getUserNodes(long loginUser) {
+        List<UserNode> list = userNodeMapper.findByCreateBy(loginUser);
+        list.forEach(userNode -> {
+            int storeNodeId = userNode.getStoreNodeId();
+            StoreNode storeNode = storeNodeMapper.findById(storeNodeId);
+            String nodeAddr = storeNode.getNodeAddr();
+            userNode.setStoreNodeAddr(nodeAddr);
+        });
+
+        return list;
+    }
+
+    public UserNode getUserNode(int userNodeId) {
+        return userNodeMapper.findById(userNodeId);
+    }
+
+    public UserNode getUserNodeByDomain(String domain) {
+        return userNodeMapper.findByDomain(domain);
+    }
+
+    public String getDomain(int channelCode, long loginUser) {
+        UploadChannel uploadChannel = uploadChannelMapper.findByCreateByAndChannelCode(loginUser, channelCode);
+        int userNodeId = uploadChannel.getUserNodeId();
+        UserNode userNode = userNodeMapper.findById(userNodeId);
+        return userNode.getDomain();
+    }
+
+    public StoreNode getStoreNode(int userNodeId) {
+        UserNode userNode = userNodeMapper.findById(userNodeId);
+        return storeNodeMapper.findById(userNode.getStoreNodeId());
+    }
+
+    public StoreNode getStoreNodeByChannel(int channelCode, long owner) throws Exception {
+        UploadChannel uploadChannel = uploadChannelMapper.findByCreateByAndChannelCode(owner, channelCode);
+        if (uploadChannel == null) {
+            String errMsg = String.format("channel_code %s not exist", channelCode);
+            throw new Exception(errMsg);
+        }
+
+        UserNode userNode = userNodeMapper.findById(uploadChannel.getUserNodeId());
+        return storeNodeMapper.findById(userNode.getStoreNodeId());
+    }
+
+    public StoreNode getStoreNodeByDomain(String domain) {
+        UserNode userNode = userNodeMapper.findByDomain(domain);
+        int storeNodeId = userNode.getStoreNodeId();
+        return storeNodeMapper.findById(storeNodeId);
+    }
+
+    public List<SelectOption> getNodeKeyValues(long createBy) {
+        return userNodeMapper.findByCreateBy(createBy).stream().map(userNode -> {
+            String userNodeDomain = userNode.getDomain();
+            return new SelectOption(userNodeDomain, ""+userNode.getId());
+        }).collect(Collectors.toList());
+    }
+
+    public StoreNode selectBestNode() {
+        return new StoreNode();
+    }
+}

+ 228 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/util/ServletUtil.java

@@ -0,0 +1,228 @@
+package cn.reghao.oss.mgr.util;
+
+import cn.reghao.jutil.jdk.serializer.JsonConverter;
+import cn.reghao.jutil.jdk.web.log.GatewayLog;
+import cn.reghao.jutil.jdk.http.HeaderNames;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2021-06-02 13:16:58
+ */
+public class ServletUtil {
+    public static Map<String, String> getCookies() {
+        HttpServletRequest request = getRequest();
+        Cookie[] cookies = request.getCookies();
+        Map<String, String> map = new HashMap<>();
+        if (cookies != null) {
+            for (Cookie cookie : cookies) {
+                String name = cookie.getName();
+                String value = cookie.getValue();
+                map.put(name, value);
+            }
+        }
+        return map;
+    }
+
+    public static String getCookie(String name) {
+        Map<String, String> map = getCookies();
+        return map.get(name);
+    }
+
+    public static Cookie getCookie1(String name, ServletRequest servletRequest) {
+        HttpServletRequest request = (HttpServletRequest) servletRequest;
+        Cookie[] cookies = request.getCookies();
+        if (cookies != null) {
+            for (Cookie cookie : cookies) {
+                String name1 = cookie.getName();
+                if (name1.equals(name)) {
+                    return cookie;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public static String getCookie(String name, ServletRequest servletRequest) {
+        Map<String, String> map = getCookies(servletRequest);
+        return map.get(name);
+    }
+
+    public static Map<String, String> getCookies(ServletRequest servletRequest) {
+        HttpServletRequest request = (HttpServletRequest) servletRequest;
+        Cookie[] cookies = request.getCookies();
+        Map<String, String> map = new HashMap<>();
+        if (cookies != null) {
+            for (Cookie cookie : cookies) {
+                String name = cookie.getName();
+                String value = cookie.getValue();
+                map.put(name, value);
+            }
+        }
+        return map;
+    }
+
+    public static String getHeader(String key) {
+        return getRequest().getHeader(key);
+    }
+
+    public static String getBearerToken() {
+        String auth = getRequest().getHeader("Authorization");
+        if (auth == null) {
+            return null;
+        }
+        return auth.replace("Bearer ", "");
+    }
+
+    public static String getBearerToken(ServletRequest servletRequest) {
+        HttpServletRequest request = (HttpServletRequest) servletRequest;
+        String auth = request.getHeader("Authorization");
+        if (auth == null) {
+            return null;
+        }
+        return auth.replace("Bearer ", "");
+    }
+
+    @Deprecated
+    public static String getUserId() {
+        String userId = getRequest().getHeader("x-user-id");
+        return  userId != null ? userId : "-1";
+    }
+
+    public static HttpSession getSession() {
+        return getRequest().getSession(false);
+    }
+
+    public static String getBody() throws IOException {
+        HttpServletRequest request = getRequest();
+        StringBuffer sb = new StringBuffer();
+        BufferedReader reader = request.getReader();
+        String line;
+        while ((line = reader.readLine()) != null) {
+            sb.append(line);
+        }
+        return sb.toString();
+    }
+
+    public static Object getBody(HttpServletRequest servletRequest, Class<?> clazz) throws IOException {
+        StringBuilder body = new StringBuilder();
+        BufferedReader reader = servletRequest.getReader();
+        String line;
+        while (null != (line = reader.readLine())) {
+            body.append(line);
+        }
+        reader.close();
+        return JsonConverter.jsonToObject(body.toString(), clazz);
+    }
+
+    public static String getSessionId() {
+        String sessionId = "";
+        HttpSession httpSession = getRequest().getSession();
+        if (httpSession != null) {
+            sessionId = httpSession.getId();
+        }
+
+        return sessionId;
+    }
+
+    /**
+     * 获取 query 参数值
+     *
+     * @param
+     * @return
+     * @date 2021-06-02 下午1:19
+     */
+    public static String getRequestParam(String param, String defaultValue){
+        String parameter = getRequest().getParameter(param);
+        return StringUtils.isEmpty(parameter) ? defaultValue : parameter;
+    }
+
+    public static HttpServletRequest getRequest(){
+        ServletRequestAttributes servletRequestAttributes = getServletRequest();
+        if (servletRequestAttributes != null) {
+            return servletRequestAttributes.getRequest();
+        }
+
+        return null;
+    }
+
+    public static HttpServletResponse getResponse(){
+        ServletRequestAttributes servletRequestAttributes = getServletRequest();
+        if (servletRequestAttributes != null) {
+            return servletRequestAttributes.getResponse();
+        }
+
+        return null;
+    }
+
+    private static ServletRequestAttributes getServletRequest(){
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        if (requestAttributes instanceof ServletRequestAttributes) {
+            return (ServletRequestAttributes) requestAttributes;
+        }
+
+        return null;
+    }
+
+    public static GatewayLog getGatewayLog() {
+        HttpServletRequest request = ServletUtil.getRequest();
+        return getGatewayLog(request);
+    }
+
+    public static GatewayLog getGatewayLog(HttpServletRequest request) {
+        String remoteAddr = request.getRemoteAddr();
+        int remotePort = request.getRemotePort();
+
+        Map<String, String> requestHeaders = new HashMap<>();
+        Enumeration<String> headerNames = request.getHeaderNames();
+        while (headerNames.hasMoreElements()) {
+            String headerName = headerNames.nextElement();
+            String headerValue = request.getHeader(headerName);
+            requestHeaders.put(headerName, headerValue);
+        }
+
+        String requestId = (String) request.getAttribute(HeaderNames.XRequestId);
+        long requestTime = (Long) request.getAttribute(HeaderNames.XRequestTime);
+        String targetRoute = "";
+        String targetService = "";
+        String requestUrl = request.getRequestURI();
+        String requestMethod = request.getMethod();
+        String requestBody = "";
+
+        int statusCode = getResponse().getStatus();
+        Map<String, String> responseHeaders = new HashMap<>();
+        String responseBody = "";
+        long responseTime = System.currentTimeMillis();
+        long executeTime = responseTime - requestTime;
+
+        String realIP = requestHeaders.get(HeaderNames.XRealIP);
+        if (realIP != null && !realIP.isBlank()) {
+            remoteAddr = realIP;
+        }
+
+        String realRemoteAddr = requestHeaders.get(HeaderNames.XRealRemote);
+        if (realRemoteAddr != null && !realRemoteAddr.isBlank()) {
+            remoteAddr = realRemoteAddr;
+        }
+
+        GatewayLog gatewayLog = new GatewayLog(requestId, requestTime, requestUrl, requestMethod, requestHeaders,
+                remoteAddr, remotePort, statusCode, responseHeaders, responseTime);
+        gatewayLog.setExecuteTime(executeTime);
+        return gatewayLog;
+    }
+}

+ 16 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/util/StringUtil.java

@@ -0,0 +1,16 @@
+package cn.reghao.oss.mgr.util;
+
+/**
+ * @author reghao
+ * @date 2026-02-22 00:09:08
+ */
+public class StringUtil {
+    public static String getSuffix(String filename) {
+        if (filename == null) {
+            return "";
+        }
+
+        int idx = filename.lastIndexOf(".");
+        return idx == -1 ? "" : filename.substring(idx+1);
+    }
+}

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

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

+ 5 - 0
oss-mgr/src/main/resources/application-test.yml

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

+ 59 - 0
oss-mgr/src/main/resources/application.yml

@@ -0,0 +1,59 @@
+dubbo:
+  application:
+    name: oss-mgr
+    qos-enable: false
+    metadata-type: local
+  scan:
+    base-packages: cn.reghao.oss.mgr.rpc
+  protocol:
+    name: dubbo
+    port: 18010
+  registry:
+    address: N/A
+  consumer:
+    check: false
+server:
+  port: 8010
+  tomcat:
+    max-http-form-post-size: 4MB
+  servlet:
+    session:
+      cookie:
+        secure: true
+        http-only: true
+        # org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getSessionTimeoutInMinutes 获取超时时间
+        # 两个请求间隔的最大时间, 超过此时间则会话过期
+      timeout: 10m
+spring:
+  application:
+    name: admin-service
+  profiles:
+    active: @profile.active@
+  mvc:
+    pathmatch:
+      matching-strategy: ant_path_matcher
+  servlet:
+    multipart:
+      max-request-size: 5MB
+      max-file-size: 5MB
+  session:
+    store-type: redis
+    redis:
+      namespace: tnb:auth:session
+  datasource:
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    type: com.zaxxer.hikari.HikariDataSource
+    hikari:
+      minimum-idle: 5
+      maximum-pool-size: 10
+      auto-commit: true
+      idle-timeout: 30000
+      pool-name: EvaluationHikariCP
+      max-lifetime: 1800000
+      connection-timeout: 30000
+      connection-test-query: SELECT 1
+mybatis:
+  configuration:
+    map-underscore-to-camel-case: true
+  mapper-locations: classpath*:mapper/**.xml
+  type-aliases-package: cn.reghao.oss.mgr.model.po

+ 77 - 0
oss-mgr/src/main/resources/logback-spring.xml

@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<configuration>
+    <!-- 关闭 logback 本身的状态监听器(StatusListener) 输出 -->
+    <statusListener class="ch.qos.logback.core.status.NopStatusListener" />
+
+    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
+        <layout class="ch.qos.logback.classic.PatternLayout">
+            <pattern>
+                %d{HH:mm:ss.SSS} [%thread] %-5level %c %M %L - %msg%n
+            </pattern>
+        </layout>
+    </appender>
+
+    <!-- info 日志文件 -->
+    <appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>DENY</onMatch>
+            <onMismatch>ACCEPT</onMismatch>
+        </filter>
+        <encoder>
+            <pattern>
+                %d{HH:mm:ss.SSS} %-5level %c %M %L - %msg%n
+            </pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <!-- 滚动策略 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>
+                logs/info.%d.log
+            </fileNamePattern>
+        </rollingPolicy>
+    </appender>
+
+    <!-- error 日志文件 -->
+    <appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>ERROR</level>
+        </filter>
+        <encoder>
+            <pattern>
+                %d{HH:mm:ss.SSS} %-5level %c %M %L - %msg%n
+            </pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>
+                logs/error.%d.log
+            </fileNamePattern>
+        </rollingPolicy>
+    </appender>
+
+    <springProfile name="dev">
+        <root level="info">
+            <appender-ref ref="consoleLog"></appender-ref>
+        </root>
+    </springProfile>
+    <springProfile name="test">
+        <root level="info">
+            <appender-ref ref="fileInfoLog"></appender-ref>
+            <appender-ref ref="fileErrorLog"></appender-ref>
+        </root>
+    </springProfile>
+    <springProfile name="prod">
+        <root level="info">
+            <appender-ref ref="fileInfoLog"></appender-ref>
+            <appender-ref ref="fileErrorLog"></appender-ref>
+        </root>
+    </springProfile>
+    <springProfile name="cluster">
+        <root level="info">
+            <appender-ref ref="fileInfoLog"></appender-ref>
+            <appender-ref ref="fileErrorLog"></appender-ref>
+        </root>
+    </springProfile>
+</configuration>

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

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="cn.reghao.oss.mgr.db.mapper.DataBlockMapper">
+    <insert id="save" useGeneratedKeys="true" keyProperty="id">
+        insert into data_block
+        (`sha256sum`,`host_port`,`absolute_path`,`size`)
+        values
+        (#{sha256sum}#{hostPort},#{absolutePath},#{size})
+    </insert>
+    <insert id="saveAll" useGeneratedKeys="true" keyProperty="id">
+        insert into data_block
+        (`sha256sum`,`host_port`,`absolute_path`,`size`)
+        values
+        <foreach collection="list" item="item" index="index" separator=",">
+            (#{item.sha256sum},#{item.hostPort},#{item.absolutePath},#{item.size})
+        </foreach>
+    </insert>
+
+    <update id="updatePath">
+        update data_block
+        set absolute_path=#{path}
+        where sha256sum=#{sha256sum}
+    </update>
+
+    <select id="findBySha256sum" resultType="cn.reghao.oss.mgr.model.po.DataBlock">
+        select *
+        from data_block
+        where deleted is false and sha256sum=#{sha256sum}
+    </select>
+    <select id="findByObjectId" resultType="cn.reghao.oss.mgr.model.po.DataBlock">
+        select datablock.*
+        from data_block datablock
+        inner join file_meta filemeta
+        on filemeta.sha256sum=datablock.sha256sum
+        where filemeta.`deleted` is false and filemeta.object_id=#{objectId}
+    </select>
+</mapper>

+ 70 - 0
oss-mgr/src/main/resources/mapper/FileMetaMapper.xml

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="cn.reghao.oss.mgr.db.mapper.FileMetaMapper">
+    <insert id="save" useGeneratedKeys="true" keyProperty="id">
+        insert into file_meta
+        (`object_name`,`object_id`,`pid`,`filename`,`size`,`file_type`,`content_type`,`sha256sum`,`upload_by`,`scope`)
+        values
+        (#{objectName},#{objectId},#{pid},#{filename},#{size},#{fileType},#{contentType},#{sha256sum},#{uploadBy},#{scope})
+    </insert>
+    <insert id="saveAll" useGeneratedKeys="true" keyProperty="id">
+        insert into file_meta
+        (`object_name`,`object_id`,`pid`,`filename`,`size`,`file_type`,`content_type`,`sha256sum`,`upload_by`,`scope`)
+        values
+        <foreach collection="list" item="item" index="index" separator=",">
+            (#{item.objectName},#{item.objectId},#{item.pid},#{item.filename},#{item.size},#{item.fileType},#{item.contentType},#{item.sha256sum},#{item.uploadBy},#{item.scope})
+        </foreach>
+    </insert>
+
+    <delete id="delete">
+        delete from file_meta
+        where object_id=#{objectId}
+    </delete>
+
+    <update id="updateScopeByObjectName">
+        update file_meta
+        set scope=#{scope}
+        where object_name=#{objectName}
+    </update>
+    <update id="updateScopeByObjectId">
+        update file_meta
+        set scope=#{scope}
+        where object_id=#{objectId}
+    </update>
+
+    <select id="findBySha256sum" resultType="cn.reghao.oss.mgr.model.po.FileMeta">
+        select *
+        from file_meta
+        where deleted is false and sha256sum=#{sha256sum}
+        order by create_time asc
+        limit 1
+    </select>
+    <select id="findByObjectId" resultType="cn.reghao.oss.mgr.model.po.FileMeta">
+        select *
+        from file_meta
+        where deleted is false and object_id=#{objectId}
+        limit 1
+    </select>
+    <select id="findByObjectName" resultType="cn.reghao.oss.mgr.model.po.FileMeta">
+        select *
+        from file_meta
+        where deleted is false and object_name=#{objectName} and upload_by=#{owner}
+    </select>
+    <select id="findObjectMetaByName" resultType="cn.reghao.oss.api.dto.ObjectMeta">
+        select file_meta.size,file_meta.content_type,file_meta.filename,file_meta.object_name,file_meta.object_id,file_meta.scope as scope,file_meta.upload_by,
+        data_block.absolute_path
+        from file_meta
+        inner join data_block
+        on file_meta.deleted is false and file_meta.upload_by=#{owner} and file_meta.sha256sum=data_block.sha256sum
+        and file_meta.object_name=#{objectName}
+    </select>
+    <select id="findObjectMetaById" resultType="cn.reghao.oss.api.dto.ObjectMeta">
+        select file_meta.size,file_meta.content_type,file_meta.filename,file_meta.object_name,file_meta.object_id,file_meta.scope as scope,file_meta.upload_by,
+        data_block.absolute_path
+        from file_meta
+        inner join data_block
+        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>
+</mapper>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio