From 890bdd9d120b64cbccfb269fe7e06e43edd653d8 Mon Sep 17 00:00:00 2001
From: huwei <3313749341@qq.com>
Date: Tue, 30 Jun 2026 11:17:58 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6=E4=B8=8A?=
=?UTF-8?q?=E4=BC=A0=E3=80=81=E7=BB=B4=E6=8A=A4=E7=AE=A1=E7=90=86=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/dev/JAVA规范.md | 2 +-
docs/gbs/OSS配置汇总.md | 169 ++++++++++++++++++
docs/gbs/Resource.java | 167 +++++++++++++++++
docs/gbs/init-filestorage.sql | 27 +++
pom.xml | 20 +++
.../adapter/web/FileStorageController.java | 118 ++++++++++++
.../adapter/web/FileUploadController.java | 44 +++++
.../web/FileUploadPublicController.java | 50 ++++++
.../app/executor/FileStorageExecutor.java | 125 +++++++++++++
.../app/executor/FileStorageHelper.java | 114 ++++++++++++
.../app/executor/FileUploadExecutor.java | 115 ++++++++++++
.../safetyeval/client/api/FileStorageApi.java | 65 +++++++
.../safetyeval/client/api/FileUploadApi.java | 50 ++++++
.../safetyeval/client/co/FileInfoCO.java | 41 +++++
.../safetyeval/domain/entity/FileStorage.java | 56 ++++++
.../domain/exception/ErrorCode.java | 10 ++
.../domain/gateway/ResourceGateway.java | 63 +++++++
.../service/FileStorageDomainService.java | 102 +++++++++++
safety-eval-infrastructure/pom.xml | 12 ++
.../config/FileStorageAutoConfiguration.java | 18 ++
.../infrastructure/dataobject/ResourceDO.java | 61 +++++++
.../gatewayimpl/ResourceGatewayImpl.java | 145 +++++++++++++++
.../infrastructure/mapper/ResourceMapper.java | 14 ++
.../src/main/resources/application-local.yml | 2 +
.../src/main/resources/application.yml | 22 ++-
25 files changed, 1610 insertions(+), 2 deletions(-)
create mode 100644 docs/gbs/OSS配置汇总.md
create mode 100644 docs/gbs/Resource.java
create mode 100644 docs/gbs/init-filestorage.sql
create mode 100644 safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/web/FileStorageController.java
create mode 100644 safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/web/FileUploadController.java
create mode 100644 safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/web/FileUploadPublicController.java
create mode 100644 safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/FileStorageExecutor.java
create mode 100644 safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/FileStorageHelper.java
create mode 100644 safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/FileUploadExecutor.java
create mode 100644 safety-eval-client/src/main/java/org/qinan/safetyeval/client/api/FileStorageApi.java
create mode 100644 safety-eval-client/src/main/java/org/qinan/safetyeval/client/api/FileUploadApi.java
create mode 100644 safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/FileInfoCO.java
create mode 100644 safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/entity/FileStorage.java
create mode 100644 safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/ResourceGateway.java
create mode 100644 safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/service/FileStorageDomainService.java
create mode 100644 safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/config/FileStorageAutoConfiguration.java
create mode 100644 safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/dataobject/ResourceDO.java
create mode 100644 safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/ResourceGatewayImpl.java
create mode 100644 safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/ResourceMapper.java
diff --git a/docs/dev/JAVA规范.md b/docs/dev/JAVA规范.md
index afd8658..7744ee7 100644
--- a/docs/dev/JAVA规范.md
+++ b/docs/dev/JAVA规范.md
@@ -143,7 +143,7 @@
7. 【强制】NPE 是程序员最常遇到的异常,必须进行预防性检查。
说明:当方法返回值为集合或对象时,先判空再操作。
-8. 【推荐】尽量使用 try-with-resources 语句关闭资源。
+8. 【推荐】尽量使用 try-with-fileStorages 语句关闭资源。
正例:
```java
try (InputStream is = new FileInputStream(path)) {
diff --git a/docs/gbs/OSS配置汇总.md b/docs/gbs/OSS配置汇总.md
new file mode 100644
index 0000000..8cf024f
--- /dev/null
+++ b/docs/gbs/OSS配置汇总.md
@@ -0,0 +1,169 @@
+# GBS 平台文件存储(OSS)配置汇总
+
+> **生成时间**:2026-06-30 10:05
+> **数据来源**:Nacos(jjb-dragon 命名空间)+ MySQL `jjb-saas-attachment` 库
+> **结论**:平台文件存储**全部使用阿里云 OSS**,无 MinIO 在用
+
+---
+
+## 一、核心结论
+
+| 项目 | 值 |
+|------|-----|
+| **存储后端** | 阿里云 OSS(Aliyun Object Storage Service) |
+| **支持的存储类型** | `ALIYUN_OSS`(普通文件 70 个渠道)、`ALIYUN_VIDEO_OSS`(视频点播 28 个渠道) |
+| **存储 Region** | 华东1(杭州)`oss-cn-hangzhou` |
+| **Bucket** | `test-dragon-yf-pub`(所有租户共用) |
+| **总容量** | 平台级 4 TB(4398046511104 字节),单租户默认 100 GB |
+| **已用容量** | 平台级 3.09 TB(3096225104031 字节 / ≈ 2.81 TiB) |
+| **Nacos 默认存储策略** | `ALIYUN_OSS` |
+| **MinIO 状态** | 容器在运行,但**仅作历史残留**,无任何业务使用 |
+
+---
+
+## 二、Nacos 默认存储策略
+
+Nacos 命名空间 `jjb-dragon` → 配置 `jjb-saas-attachment.yml`:
+
+```yaml
+attachment:
+ # 默认存储空间 支持MINIO、ALIYUN_OSS
+ storage-policies: ALIYUN_OSS # ← 当前使用阿里云 OSS
+ file-view-url: ${common.gateway.network.http.external}/fileview/onlinePreview
+ expiration: 3600
+ default-init-capacity: 102400 # 默认 100 GB
+
+minio:
+ inter:
+ replace-enum: true
+ url: http://192.168.1.117:9001 # ← 旧地址,无效(已弃用)
+```
+
+> ⚠️ 注释里仍写着"支持 MINIO、ALIYUN_OSS",但 `minio.inter.url` 指向已不存在的旧环境 `192.168.1.117:9001`。当前业务完全走 OSS,不影响上传。
+
+---
+
+## 三、阿里云 OSS 凭证(2 套 — 文件/视频)
+
+| 用途 | AccessKeyId | AccessKeySecret | Bucket | Region | 内/外网 Endpoint |
+|------|-------------|-----------------|--------|--------|------------------|
+| **ALIYUN_OSS**(文件) | `LTAI5tFerUgBCLEbXtinDD6d` | `XyGVNV2RvW8U3ceoJDiy9A9oLvCGYp` | `test-dragon-yf-pub` | oss-cn-hangzhou | `https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com` |
+| **ALIYUN_VIDEO_OSS**(视频) | `LTAI5tDAMFkin5tavp4thfaG` | `XoBYTtIgeHOiz3udiP5gm0rcdRWp25` | `test-dragon-yf-pub` | oss-cn-shanghai(regionId) | 同上 |
+
+**URL 模板**(来自 `attachment_tenant_channel.channel_config` JSON):
+
+```json
+{
+ "rootDir": "jjb",
+ "endpoint": "oss-cn-hangzhou.aliyuncs.com",
+ "bucketName": "test-dragon-yf-pub",
+ "expiration": null,
+ "accessKeyId": "LTAI5tFerUgBCLEbXtinDD6d",
+ "accessKeySecret": "XyGVNV2RvW8U3ceoJDiy9A9oLvCGYp",
+ "innerNetBaseUrl": "https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com",
+ "outerNetBaseUrl": "https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com",
+ "innerFileViewBaseUrl": "https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com",
+ "outerFileViewBaseUrl": "https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com"
+}
+```
+
+> ⚠️ 视频 OSS 凭证中 `regionId: cn-shanghai` 与 `endpoint: oss-cn-hangzhou` 不一致(**疑似配置错配**),但实际请求走 endpoint,杭州 bucket 可正常访问。如切换视频点播需注意。
+
+---
+
+## 四、数据库表结构
+
+### 4.1 `attachment_tenant_channel`(租户存储渠道绑定表)
+| 字段 | 说明 |
+|------|------|
+| `id` | 主键 |
+| `channel_owner_enum` | 渠道所属(PLATFORM / 自定义) |
+| `channel_type_enum` | 渠道类型(ALIYUN_OSS / ALIYUN_VIDEO_OSS / MinIO 等) |
+| `channel_name` | 渠道名称 |
+| `channel_config` | 渠道配置 JSON(含 endpoint/bucket/accessKey 等) |
+| `total_storage_size` | 总容量(字节) |
+| `default_enum` | 是否默认渠道 |
+| `tenant_id` / `org_id` | 租户/机构 |
+
+**数据规模**:98 条记录(70 个 ALIYUN_OSS + 28 个 ALIYUN_VIDEO_OSS)
+
+### 4.2 `oss_region_endpoint`(OSS 区域映射表)
+预置 16 个阿里云 OSS Region(杭州/上海/北京/深圳/香港/美国等),用于 endpoint 路由。
+
+| 关键 Region | 外网 endpoint | 内网 endpoint |
+|-------------|---------------|---------------|
+| 华东1(杭州) | oss-cn-hangzhou.aliyuncs.com | oss-cn-hangzhou-internal.aliyuncs.com |
+| 华东2(上海) | oss-cn-shanghai.aliyuncs.com | oss-cn-shanghai-internal.aliyuncs.com |
+| 华北2(北京) | oss-cn-beijing.aliyuncs.com | oss-cn-beijing-internal.aliyuncs.com |
+| 华南1(深圳) | oss-cn-shenzhen.aliyuncs.com | oss-cn-shenzhen-internal.aliyuncs.com |
+| 中国香港 | oss-cn-hongkong.aliyuncs.com | oss-cn-hongkong-internal.aliyuncs.com |
+
+### 4.3 `attachment_file`(文件元数据表)
+| 关键字段 | 说明 |
+|----------|------|
+| `storage_type_enum` | 存储类型(本地/阿里OSS/本地minio等) |
+| `bucket_name` | bucket 名(如 `test-dragon-yf-pub`) |
+| `url` | 上传文件 URL |
+| `preview_url` / `download_url` | 预览/下载地址 |
+| `status_enum` / `upload_status_enum` | 状态/上传状态 |
+| `file_size` / `name` / `ext` | 文件属性 |
+| `channel_id` | 渠道 id(关联 `attachment_tenant_channel`) |
+
+### 4.4 `attachment_storage`(存储用量表)
+| 字段 | 说明 |
+|------|------|
+| `tenant_id` | 租户 |
+| `total_storage_size` | 总配额(字节,默认 107374182400 = 100 GB) |
+| `occupied_storage_size` | 已用容量(字节) |
+
+**平台总和**:4 TB 配额,3.09 TB 已用(**73% 使用率**)
+
+---
+
+## 五、最近文件上传样本
+
+```
+id: 2065350515959717890
+name: 48afad885c233d3cf82fa3b779002554
+storage_type_enum: NULL
+bucket_name: test-dragon-yf-pub
+file_size: 410715
+create_time: 2026-06-12 16:28:30
+```
+
+> 注意:`storage_type_enum` 字段为 NULL(schema 里没存具体值),但 `bucket_name` = `test-dragon-yf-pub` 表明所有文件都上传到了阿里云 OSS。
+
+---
+
+## 六、MinIO 容器状态(仅作残留)
+
+```
+容器名: minio
+状态: Up 13 days
+端口: 0.0.0.0:9000-9001 (API/Console)
+数据目录: /data
+内容: .minio.sys/ jjb-attachment/ (jjb-attachment 桶内只有 test.txt)
+```
+
+**结论**:MinIO 容器仍在运行(端口 9000/9001 暴露在 master 节点),但桶内无业务数据,**仅作早期本地化部署残留**。Nacos 配置文件 `minio.inter.url` 指向的 `192.168.1.117:9001` 早已不可达(应为 192.168.20.100:9000),但因为默认策略是 ALIYUN_OSS,所以**未影响实际业务**。
+
+---
+
+## 七、问题与改进建议
+
+1. **MinIO 配置应清理**:`jjb-saas-attachment.yml` 的 `minio.inter.url` 指向旧地址 `192.168.1.117:9001`,与现状不符,应改为 `192.168.20.100:9000` 或直接移除(既然不使用)。
+2. **视频 OSS region 错配**:`ALIYUN_VIDEO_OSS` 的 `regionId: cn-shanghai` 与 `endpoint: oss-cn-hangzhou` 不一致,存在历史误配风险。
+3. **`storage_type_enum` 字段为空**:`attachment_file.storage_type_enum` 全部为 NULL,无法直接通过 SQL 区分文件走的是 OSS 还是 MinIO。判断需通过 `bucket_name` 字段。
+4. **平台总容量 73% 已用**(3.09 TB / 4 TB),建议监控告警。
+5. **凭证无主从**:OSS AK/SK 直接以明文存储在 `attachment_tenant_channel.channel_config` JSON 字段,建议改用 K8s Secret + Nacos 占位符注入。
+
+---
+
+## 八、相关服务连通性入口
+
+- **OSS 外网地址**:https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com
+- **OSS 内网地址**:https://test-dragon-yf-pub.oss-cn-hangzhou-internal.aliyuncs.com
+- **MinIO Console**(master 节点):http://192.168.20.100:9001(admin / Zcloud@zcloud2026,无业务数据)
+- **MinIO API**:http://192.168.20.100:9000
+- **attachment 服务内部访问路径**:`lb://jjb-saas-attachment/attachment/**`
+- **文件预览地址模板**:`${common.gateway.network.http.external}/fileview/onlinePreview?xxx`
diff --git a/docs/gbs/Resource.java b/docs/gbs/Resource.java
new file mode 100644
index 0000000..c893e15
--- /dev/null
+++ b/docs/gbs/Resource.java
@@ -0,0 +1,167 @@
+package com.qiyoule.resource.domain.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.io.Serializable;
+import java.util.Date;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ *
+ * @TableName filestorage
+ */
+@TableName(value ="filestorage")
+@Data
+@Accessors(chain = true)
+public class Resource implements Serializable {
+ /**
+ *
+ */
+ @TableId(type = IdType.AUTO)
+ private Integer id;
+
+ /**
+ *
+ */
+ private String url;
+
+ /**
+ *
+ */
+ private Integer size;
+
+ /**
+ *
+ */
+ private String filename;
+
+ /**
+ *
+ */
+ private String originalFilename;
+
+ /**
+ *
+ */
+ private String basePath;
+
+ /**
+ *
+ */
+ private String path;
+
+ /**
+ *
+ */
+ private String ext;
+
+ /**
+ *
+ */
+ private String contentType;
+
+ /**
+ *
+ */
+ private String platform;
+
+ /**
+ *
+ */
+ private String md5;
+
+ /**
+ *
+ */
+ private String creatorName;
+
+ /**
+ *
+ */
+ private String creatorUid;
+
+ /**
+ *
+ */
+ private Date createdTime;
+
+ @TableField(exist = false)
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+ if (that == null) {
+ return false;
+ }
+ if (getClass() != that.getClass()) {
+ return false;
+ }
+ Resource other = (Resource) that;
+ return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
+ && (this.getUrl() == null ? other.getUrl() == null : this.getUrl().equals(other.getUrl()))
+ && (this.getSize() == null ? other.getSize() == null : this.getSize().equals(other.getSize()))
+ && (this.getFilename() == null ? other.getFilename() == null : this.getFilename().equals(other.getFilename()))
+ && (this.getOriginalFilename() == null ? other.getOriginalFilename() == null : this.getOriginalFilename().equals(other.getOriginalFilename()))
+ && (this.getBasePath() == null ? other.getBasePath() == null : this.getBasePath().equals(other.getBasePath()))
+ && (this.getPath() == null ? other.getPath() == null : this.getPath().equals(other.getPath()))
+ && (this.getExt() == null ? other.getExt() == null : this.getExt().equals(other.getExt()))
+ && (this.getContentType() == null ? other.getContentType() == null : this.getContentType().equals(other.getContentType()))
+ && (this.getPlatform() == null ? other.getPlatform() == null : this.getPlatform().equals(other.getPlatform()))
+ && (this.getMd5() == null ? other.getMd5() == null : this.getMd5().equals(other.getMd5()))
+ && (this.getCreatorName() == null ? other.getCreatorName() == null : this.getCreatorName().equals(other.getCreatorName()))
+ && (this.getCreatorUid() == null ? other.getCreatorUid() == null : this.getCreatorUid().equals(other.getCreatorUid()))
+ && (this.getCreatedTime() == null ? other.getCreatedTime() == null : this.getCreatedTime().equals(other.getCreatedTime()));
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
+ result = prime * result + ((getUrl() == null) ? 0 : getUrl().hashCode());
+ result = prime * result + ((getSize() == null) ? 0 : getSize().hashCode());
+ result = prime * result + ((getFilename() == null) ? 0 : getFilename().hashCode());
+ result = prime * result + ((getOriginalFilename() == null) ? 0 : getOriginalFilename().hashCode());
+ result = prime * result + ((getBasePath() == null) ? 0 : getBasePath().hashCode());
+ result = prime * result + ((getPath() == null) ? 0 : getPath().hashCode());
+ result = prime * result + ((getExt() == null) ? 0 : getExt().hashCode());
+ result = prime * result + ((getContentType() == null) ? 0 : getContentType().hashCode());
+ result = prime * result + ((getPlatform() == null) ? 0 : getPlatform().hashCode());
+ result = prime * result + ((getMd5() == null) ? 0 : getMd5().hashCode());
+ result = prime * result + ((getCreatorName() == null) ? 0 : getCreatorName().hashCode());
+ result = prime * result + ((getCreatorUid() == null) ? 0 : getCreatorUid().hashCode());
+ result = prime * result + ((getCreatedTime() == null) ? 0 : getCreatedTime().hashCode());
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName());
+ sb.append(" [");
+ sb.append("Hash = ").append(hashCode());
+ sb.append(", id=").append(id);
+ sb.append(", url=").append(url);
+ sb.append(", size=").append(size);
+ sb.append(", filename=").append(filename);
+ sb.append(", originalFilename=").append(originalFilename);
+ sb.append(", basePath=").append(basePath);
+ sb.append(", path=").append(path);
+ sb.append(", ext=").append(ext);
+ sb.append(", contentType=").append(contentType);
+ sb.append(", platform=").append(platform);
+ sb.append(", md5=").append(md5);
+ sb.append(", creatorName=").append(creatorName);
+ sb.append(", creatorUid=").append(creatorUid);
+ sb.append(", createdTime=").append(createdTime);
+ sb.append(", serialVersionUID=").append(serialVersionUID);
+ sb.append("]");
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/docs/gbs/init-filestorage.sql b/docs/gbs/init-filestorage.sql
new file mode 100644
index 0000000..b5c4c74
--- /dev/null
+++ b/docs/gbs/init-filestorage.sql
@@ -0,0 +1,27 @@
+-- =============================================
+-- 文件存储本地管理表 filestorage
+-- 数据库: jjb_saas_safety_eval
+-- 说明: 用于记录上传到阿里云OSS的文件元信息,
+-- 在存储/删除文件时进行文件的维护管理
+-- =============================================
+
+CREATE TABLE IF NOT EXISTS `filestorage` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+ `url` VARCHAR(1024) NOT NULL COMMENT 'OSS文件完整访问URL',
+ `base_path` VARCHAR(512) DEFAULT NULL COMMENT 'OSS存储根路径 (如 jjb)',
+ `path` VARCHAR(512) DEFAULT NULL COMMENT 'OSS存储相对路径 (不含basePath)',
+ `filename` VARCHAR(512) DEFAULT NULL COMMENT 'OSS存储文件名 (uuid生成)',
+ `original_filename` VARCHAR(512) DEFAULT NULL COMMENT '文件原始名称',
+ `size` BIGINT DEFAULT 0 COMMENT '文件大小(字节)',
+ `ext` VARCHAR(64) DEFAULT NULL COMMENT '文件扩展名(不含.)',
+ `content_type` VARCHAR(128) DEFAULT NULL COMMENT '文件MIME类型',
+ `platform` VARCHAR(64) DEFAULT NULL COMMENT 'OSS平台标识 (如 aliyun-oss)',
+ `md5` VARCHAR(64) DEFAULT NULL COMMENT 'Content-MD5或文件MD5值',
+ `creator_name` VARCHAR(128) DEFAULT NULL COMMENT '上传者姓名',
+ `creator_uid` VARCHAR(128) DEFAULT NULL COMMENT '上传者UID',
+ `created_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
+ INDEX `idx_platform` (`platform`),
+ INDEX `idx_creator_uid` (`creator_uid`),
+ INDEX `idx_filename` (`filename`),
+ INDEX `idx_created_time` (`created_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文件存储本地管理表';
diff --git a/pom.xml b/pom.xml
index 2131a9d..9a37e61 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,6 +48,12 @@
+ * 提供 filestorage 表的下载、删除、获取地址等 REST 接口。 + * 文件新增由 {@link FileUploadController} 在上传时同步完成。 + *
+ * + * @author safety-eval + */ +@Api(tags = "文件存储管理") +@RestController +@RequestMapping("/file-storage") +public class FileStorageController { + + @Resource + private FileStorageApi fileStorageApi; + + // ==================== 下载 ==================== + + @ApiOperation("下载文件") + @GetMapping("/download/{fileId}") + public void download( + @ApiParam("文件ID") @PathVariable Integer fileId, + HttpServletResponse response) { + try { + SingleResponse+ * 区分公开上传(创建人默认 system)与内部上传(创建人为当前登录用户)。 + * 上传完成后自动同步元信息到 filestorage 表。 + *
+ * + * @author safety-eval + */ +@Api(tags = "文件上传") +@RestController +@RequestMapping("/file") +public class FileUploadController { + + @Resource + private FileUploadApi fileUploadApi; + @ApiOperation("内部上传单个文件(创建人为当前用户)") + @PostMapping("/upload") + public SingleResponse+ * 区分公开上传(创建人默认 system)与内部上传(创建人为当前登录用户)。 + * 上传完成后自动同步元信息到 filestorage 表。 + *
+ * + * @author safety-eval + */ +@Api(tags = "文件上传") +@RestController +@RequestMapping("/test/file") +public class FileUploadPublicController { + + @Resource + private FileUploadApi fileUploadApi; + + // ==================== 公开上传 ==================== + + @ApiOperation("公开上传单个文件(创建人默认 system)") + @PostMapping("/upload") + public SingleResponse+ * 负责 filestorage 表的查询、删除、获取签名URL等管理操作。 + * 文件新增由 {@link FileUploadExecutor} 在上传时同步完成,本执行器不提供上传能力。 + *
+ * + * @author safety-eval + */ +@Service +public class FileStorageExecutor implements FileStorageApi { + + private static final Logger log = LoggerFactory.getLogger(FileStorageExecutor.class); + + @Resource + private FileStorageService fileStorageService; + + @Resource + private FileStorageDomainService fileStorageDomainService; + + @Override + public SingleResponse+ * 提供实体转换、OSS FileInfo 构建、创建人信息填充等通用逻辑。 + *
+ * + * @author safety-eval + */ +final class FileStorageHelper { + + private static final Logger log = LoggerFactory.getLogger(FileStorageHelper.class); + + /** 签名URL默认过期时间:1小时(毫秒) */ + static final long PRESIGNED_URL_EXPIRE_MILLIS = Duration.ofHours(1).toMillis(); + + private FileStorageHelper() { + } + + /** + * 根据 OSS FileInfo 构建 FileStorage 实体 + * + * @param ossFileInfo OSS 文件信息 + * @param originalFilename 原始文件名 + * @param isInternal true-内部(取当前用户),false-公开(creator=system) + */ + static FileStorage buildResource(FileInfo ossFileInfo, String originalFilename, boolean isInternal) { + FileStorage fileStorage = new FileStorage(); + fileStorage.setUrl(ossFileInfo.getUrl()); + fileStorage.setSize(ossFileInfo.getSize()); + fileStorage.setFilename(ossFileInfo.getFilename()); + fileStorage.setOriginalFilename(originalFilename); + fileStorage.setBasePath(ossFileInfo.getBasePath()); + fileStorage.setPath(ossFileInfo.getPath()); + fileStorage.setExt(ossFileInfo.getExt()); + fileStorage.setContentType(ossFileInfo.getContentType()); + fileStorage.setPlatform(ossFileInfo.getPlatform()); + fileStorage.setCreatedTime(LocalDateTime.now()); + + if (isInternal) { + fileStorage.setCreatorName(AuthUserContextAdapter.getCurrentUserName()); + Long userId = AuthUserContextAdapter.getCurrentUserId(); + fileStorage.setCreatorUid(userId != null ? String.valueOf(userId) : "0"); + } else { + fileStorage.setCreatorName("system"); + fileStorage.setCreatorUid("0"); + } + return fileStorage; + } + + /** + * 根据 FileStorage 实体构建 OSS FileInfo(用于存在性检查、删除、生成签名URL等OOS操作) + */ + static FileInfo buildOssFileInfo(FileStorage fileStorage) { + FileInfo ossFileInfo = new FileInfo(); + ossFileInfo.setUrl(fileStorage.getUrl()); + ossFileInfo.setBasePath(fileStorage.getBasePath()); + ossFileInfo.setFilename(fileStorage.getFilename()); + ossFileInfo.setPlatform(fileStorage.getPlatform()); + return ossFileInfo; + } + + /** + * 生成签名URL + */ + static String generatePresignedUrl(FileStorageService fileStorageService, FileInfo ossFileInfo) { + Date expiration = new Date(System.currentTimeMillis() + PRESIGNED_URL_EXPIRE_MILLIS); + return fileStorageService.generatePresignedUrl(ossFileInfo, expiration).toString(); + } + + /** + * 检查文件在 OSS 是否存在 + */ + static void checkOssExists(FileStorageService fileStorageService, FileInfo ossFileInfo) { + if (!fileStorageService.exists(ossFileInfo)) { + throw new BizException(ErrorCode.FILE_NOT_FOUND); + } + } + + /** + * FileStorage 转 FileInfoCO + */ + static FileInfoCO toCO(FileStorage fileStorage, String presignedUrl) { + if (fileStorage == null) { + return null; + } + FileInfoCO co = new FileInfoCO(); + co.setId(fileStorage.getId()); + co.setUrl(fileStorage.getUrl()); + co.setPresignedUrl(presignedUrl != null ? presignedUrl : fileStorage.getUrl()); + co.setSize(fileStorage.getSize()); + co.setOriginalFilename(fileStorage.getOriginalFilename()); + co.setExt(fileStorage.getExt()); + co.setContentType(fileStorage.getContentType()); + co.setCreatorName(fileStorage.getCreatorName()); + co.setCreatedTime(fileStorage.getCreatedTime()); + return co; + } +} diff --git a/safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/FileUploadExecutor.java b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/FileUploadExecutor.java new file mode 100644 index 0000000..56f42cf --- /dev/null +++ b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/FileUploadExecutor.java @@ -0,0 +1,115 @@ +package org.qinan.safetyeval.app.executor; + +import org.dromara.x.file.storage.core.FileInfo; +import org.dromara.x.file.storage.core.FileStorageService; +import org.qinan.safetyeval.client.api.FileUploadApi; +import org.qinan.safetyeval.client.co.FileInfoCO; +import org.qinan.safetyeval.client.dto.SingleResponse; +import org.qinan.safetyeval.domain.entity.FileStorage; +import org.qinan.safetyeval.domain.exception.BizException; +import org.qinan.safetyeval.domain.exception.ErrorCode; +import org.qinan.safetyeval.domain.service.FileStorageDomainService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; + +/** + * 文件上传执行器(App层) + *+ * 负责文件上传至阿里云 OSS,并在上传完成后将元信息同步写入 filestorage 表。 + * 区分公开上传(创建人=system)与内部上传(创建人=当前登录用户)。 + *
+ * + * @author safety-eval + */ +@Service +public class FileUploadExecutor implements FileUploadApi { + + private static final Logger log = LoggerFactory.getLogger(FileUploadExecutor.class); + + @Resource + private FileStorageService fileStorageService; + + @Resource + private FileStorageDomainService fileStorageDomainService; + + // ==================== 公开上传(创建人默认 system) ==================== + + @Override + public SingleResponse+ * 提供 filestorage 表的查询、删除、获取签名URL等管理功能。 + * 文件的新增由上传接口({@link FileUploadApi})间接完成,本接口不提供直接新增能力。 + *
+ * + * @author safety-eval + */ +public interface FileStorageApi { + + /** + * 获取文件签名URL(临时访问链接) + * + * @param fileId 文件ID + * @return 文件信息(含签名URL) + */ + SingleResponse+ * 提供公开/内部两种文件上传方式。上传完成后自动同步元信息到 filestorage 表。 + *
+ * + * @author safety-eval + */ +public interface FileUploadApi { + + /** + * 公开上传单个文件(创建人默认为 system) + * + * @param file 上传的文件 + * @return 文件信息 + */ + SingleResponse+ * 协调 OSS 存储操作与本地数据库维护管理, + * 确保文件存储/删除时同步更新 filestorage 表。 + *
+ * + * @author safety-eval + */ +@Service +public class FileStorageDomainService { + + @javax.annotation.Resource + private ResourceGateway resourceGateway; + + /** + * 保存文件元信息到数据库 + */ + public FileStorage saveResource(FileStorage fileStorage) { + if (fileStorage == null) { + throw new BizException(ErrorCode.FILE_UPLOAD_EMPTY); + } + return resourceGateway.save(fileStorage); + } + + /** + * 批量保存文件元信息 + */ + public List+ * 启用 x-file-storage 的 Spring 集成, + * OSS 具体参数通过 application.yml 注入。 + *
+ * + * @author safety-eval + */ +@Configuration +@EnableFileStorage +public class FileStorageAutoConfiguration { +} diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/dataobject/ResourceDO.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/dataobject/ResourceDO.java new file mode 100644 index 0000000..54eadd5 --- /dev/null +++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/dataobject/ResourceDO.java @@ -0,0 +1,61 @@ +package org.qinan.safetyeval.infrastructure.dataobject; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 文件资源数据对象(Infrastructure层 - 对应 filestorage 表) + * + * @author safety-eval + */ +@Data +@TableName("filestorage") +public class ResourceDO { + + /** 主键ID */ + @TableId(type = IdType.AUTO) + private Integer id; + + /** OSS文件完整访问URL */ + private String url; + + /** 文件大小(字节) */ + private Long size; + + /** OSS存储文件名 */ + private String filename; + + /** 文件原始名称 */ + private String originalFilename; + + /** OSS存储根路径 */ + private String basePath; + + /** OSS存储相对路径 */ + private String path; + + /** 文件扩展名 */ + private String ext; + + /** 文件MIME类型 */ + private String contentType; + + /** OSS平台标识 */ + private String platform; + + /** Content-MD5值 */ + private String md5; + + /** 上传者姓名 */ + private String creatorName; + + /** 上传者UID */ + private String creatorUid; + + /** 上传时间 */ + private LocalDateTime createdTime; +} diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/ResourceGatewayImpl.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/ResourceGatewayImpl.java new file mode 100644 index 0000000..7a47dac --- /dev/null +++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/ResourceGatewayImpl.java @@ -0,0 +1,145 @@ +package org.qinan.safetyeval.infrastructure.gatewayimpl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.qinan.safetyeval.domain.entity.FileStorage; +import org.qinan.safetyeval.domain.gateway.ResourceGateway; +import org.qinan.safetyeval.infrastructure.dataobject.ResourceDO; +import org.qinan.safetyeval.infrastructure.mapper.ResourceMapper; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 文件资源Gateway实现(Infrastructure层) + * + * @author safety-eval + */ +@Component +public class ResourceGatewayImpl implements ResourceGateway { + + @Resource + private ResourceMapper resourceMapper; + + @Override + public FileStorage save(FileStorage fileStorage) { + ResourceDO dataObject = toDO(fileStorage); + resourceMapper.insert(dataObject); + fileStorage.setId(dataObject.getId()); + return fileStorage; + } + + @Override + public List