添加文件上传、维护管理功能。

dev-deployment
huwei 2026-06-30 11:17:58 +08:00
parent de10f778f6
commit 890bdd9d12
25 changed files with 1610 additions and 2 deletions

View File

@ -143,7 +143,7 @@
7. 【强制】NPE 是程序员最常遇到的异常,必须进行预防性检查。
说明:当方法返回值为集合或对象时,先判空再操作。
8. 【推荐】尽量使用 try-with-resources 语句关闭资源。
8. 【推荐】尽量使用 try-with-fileStorages 语句关闭资源。
正例:
```java
try (InputStream is = new FileInputStream(path)) {

169
docs/gbs/OSS配置汇总.md Normal file
View File

@ -0,0 +1,169 @@
# GBS 平台文件存储(OSS)配置汇总
> **生成时间**2026-06-30 10:05
> **数据来源**Nacosjjb-dragon 命名空间)+ MySQL `jjb-saas-attachment`
> **结论**:平台文件存储**全部使用阿里云 OSS**,无 MinIO 在用
---
## 一、核心结论
| 项目 | 值 |
|------|-----|
| **存储后端** | 阿里云 OSSAliyun Object Storage Service |
| **支持的存储类型** | `ALIYUN_OSS`(普通文件 70 个渠道)、`ALIYUN_VIDEO_OSS`(视频点播 28 个渠道) |
| **存储 Region** | 华东1杭州`oss-cn-hangzhou` |
| **Bucket** | `test-dragon-yf-pub`(所有租户共用) |
| **总容量** | 平台级 4 TB4398046511104 字节),单租户默认 100 GB |
| **已用容量** | 平台级 3.09 TB3096225104031 字节 / ≈ 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-shanghairegionId | 同上 |
**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` 字段为 NULLschema 里没存具体值),但 `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:9001admin / Zcloud@zcloud2026无业务数据
- **MinIO API**http://192.168.20.100:9000
- **attachment 服务内部访问路径**`lb://jjb-saas-attachment/attachment/**`
- **文件预览地址模板**`${common.gateway.network.http.external}/fileview/onlinePreview?xxx`

167
docs/gbs/Resource.java Normal file
View File

@ -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();
}
}

View File

@ -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='文件存储本地管理表';

20
pom.xml
View File

@ -48,6 +48,12 @@
<lombok.version>1.18.30</lombok.version>
<jjb-saas-system-client.version>1.7.1.1-SNAPSHOT</jjb-saas-system-client.version>
<!-- X File Storage多OSS统一文件存储 -->
<x-file-storage.version>2.2.0</x-file-storage.version>
<!-- Aliyun OSS SDK -->
<aliyun-oss.version>3.17.4</aliyun-oss.version>
</properties>
<dependencies>
@ -201,6 +207,20 @@
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- X File Storage多OSS统一文件存储 -->
<dependency>
<groupId>org.dromara.x-file-storage</groupId>
<artifactId>x-file-storage-spring</artifactId>
<version>${x-file-storage.version}</version>
</dependency>
<!-- Aliyun OSS SDK -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun-oss.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -0,0 +1,118 @@
package org.qinan.safetyeval.adapter.web;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.qinan.safetyeval.client.api.FileStorageApi;
import org.qinan.safetyeval.client.co.FileInfoCO;
import org.qinan.safetyeval.client.dto.SingleResponse;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.List;
/**
* Controller
* <p>
* filestorage REST
* {@link FileUploadController}
* </p>
*
* @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<FileInfoCO> result = fileStorageApi.getPresignedUrl(fileId);
if (!result.isSuccess() || result.getData() == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
FileInfoCO fileInfo = result.getData();
response.setContentType(fileInfo.getContentType() != null ? fileInfo.getContentType() : "application/octet-stream");
response.setHeader("Content-Disposition",
"attachment; filename=\"" + java.net.URLEncoder.encode(fileInfo.getOriginalFilename(), "UTF-8") + "\"");
if (fileInfo.getSize() != null) {
response.setContentLengthLong(fileInfo.getSize());
}
URL url = new URL(fileInfo.getPresignedUrl());
try (java.io.InputStream in = url.openStream();
OutputStream out = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
out.flush();
}
} catch (IOException e) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
// ==================== 获取地址/信息 ====================
@ApiOperation("获取文件签名URL临时访问链接")
@GetMapping("/presigned-url/{fileId}")
public SingleResponse<FileInfoCO> getPresignedUrl(
@ApiParam("文件ID") @PathVariable Integer fileId) {
return fileStorageApi.getPresignedUrl(fileId);
}
@ApiOperation("获取文件信息")
@GetMapping("/info/{fileId}")
public SingleResponse<FileInfoCO> getInfo(
@ApiParam("文件ID") @PathVariable Integer fileId) {
return fileStorageApi.getInfo(fileId);
}
// ==================== 删除 ====================
@ApiOperation("删除文件")
@DeleteMapping("/delete/{fileId}")
public SingleResponse<Void> delete(
@ApiParam("文件ID") @PathVariable Integer fileId) {
return fileStorageApi.delete(fileId);
}
@ApiOperation("批量删除文件")
@DeleteMapping("/delete-batch")
public SingleResponse<Void> deleteBatch(
@ApiParam("文件ID列表") @RequestBody List<Integer> fileIds) {
return fileStorageApi.deleteBatch(fileIds);
}
// ==================== 列表查询 ====================
@ApiOperation("获取所有文件列表")
@GetMapping("/list-all")
public SingleResponse<List<FileInfoCO>> listAll() {
return fileStorageApi.listAll();
}
@ApiOperation("根据上传者获取文件列表")
@GetMapping("/list-by-creator")
public SingleResponse<List<FileInfoCO>> listByCreator(
@ApiParam("上传者UID") @RequestParam("creatorUid") String creatorUid) {
return fileStorageApi.listByCreator(creatorUid);
}
}

View File

@ -0,0 +1,44 @@
package org.qinan.safetyeval.adapter.web;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.qinan.safetyeval.client.api.FileUploadApi;
import org.qinan.safetyeval.client.co.FileInfoCO;
import org.qinan.safetyeval.client.dto.SingleResponse;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.List;
/**
* Controller
* <p>
* system
* filestorage
* </p>
*
* @author safety-eval
*/
@Api(tags = "文件上传")
@RestController
@RequestMapping("/file")
public class FileUploadController {
@Resource
private FileUploadApi fileUploadApi;
@ApiOperation("内部上传单个文件(创建人为当前用户)")
@PostMapping("/upload")
public SingleResponse<FileInfoCO> uploadInternal(
@ApiParam("上传文件") @RequestParam("file") MultipartFile file) {
return fileUploadApi.uploadInternal(file);
}
@ApiOperation("内部批量上传文件(创建人为当前用户)")
@PostMapping("/upload/batch")
public SingleResponse<List<FileInfoCO>> uploadInternalBatch(
@ApiParam("上传文件列表") @RequestParam("files") List<MultipartFile> files) {
return fileUploadApi.uploadInternalBatch(files);
}
}

View File

@ -0,0 +1,50 @@
package org.qinan.safetyeval.adapter.web;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.qinan.safetyeval.client.api.FileUploadApi;
import org.qinan.safetyeval.client.co.FileInfoCO;
import org.qinan.safetyeval.client.dto.SingleResponse;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.List;
/**
* Controller
* <p>
* system
* filestorage
* </p>
*
* @author safety-eval
*/
@Api(tags = "文件上传")
@RestController
@RequestMapping("/test/file")
public class FileUploadPublicController {
@Resource
private FileUploadApi fileUploadApi;
// ==================== 公开上传 ====================
@ApiOperation("公开上传单个文件(创建人默认 system")
@PostMapping("/upload")
public SingleResponse<FileInfoCO> uploadPublic(
@ApiParam("上传文件") @RequestParam("file") MultipartFile file) {
return fileUploadApi.uploadPublic(file);
}
@ApiOperation("公开批量上传文件(创建人默认 system")
@PostMapping("/upload/batch")
public SingleResponse<List<FileInfoCO>> uploadPublicBatch(
@ApiParam("上传文件列表") @RequestParam("files") List<MultipartFile> files) {
return fileUploadApi.uploadPublicBatch(files);
}
}

View File

@ -0,0 +1,125 @@
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.FileStorageApi;
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 javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
/**
* App
* <p>
* filestorage URL
* {@link FileUploadExecutor}
* </p>
*
* @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<FileInfoCO> getPresignedUrl(Integer fileId) {
try {
FileStorage fileStorage = fileStorageDomainService.getResource(fileId);
FileInfo ossFileInfo = FileStorageHelper.buildOssFileInfo(fileStorage);
FileStorageHelper.checkOssExists(fileStorageService, ossFileInfo);
String presignedUrl = FileStorageHelper.generatePresignedUrl(fileStorageService, ossFileInfo);
return SingleResponse.success(FileStorageHelper.toCO(fileStorage, presignedUrl));
} catch (BizException e) {
throw e;
} catch (Exception e) {
log.error("Generate presigned URL failed: fileId={}", fileId, e);
throw new BizException(ErrorCode.FILE_URL_GENERATE_FAILED, e);
}
}
@Override
public SingleResponse<FileInfoCO> getInfo(Integer fileId) {
FileStorage fileStorage = fileStorageDomainService.getResource(fileId);
String presignedUrl = null;
try {
FileInfo ossFileInfo = FileStorageHelper.buildOssFileInfo(fileStorage);
if (fileStorageService.exists(ossFileInfo)) {
presignedUrl = FileStorageHelper.generatePresignedUrl(fileStorageService, ossFileInfo);
}
} catch (Exception e) {
log.warn("Failed to generate presigned URL for info query: fileId={}", fileId, e);
}
return SingleResponse.success(FileStorageHelper.toCO(fileStorage, presignedUrl));
}
@Override
public SingleResponse<Void> delete(Integer fileId) {
try {
FileStorage fileStorage = fileStorageDomainService.getResource(fileId);
FileInfo ossFileInfo = FileStorageHelper.buildOssFileInfo(fileStorage);
if (!fileStorageService.delete(ossFileInfo)) {
log.warn("OSS delete returned false for fileId={}, url={}", fileId, fileStorage.getUrl());
}
fileStorageDomainService.deleteResource(fileId);
log.info("File deleted successfully: fileId={}, filename={}", fileId, fileStorage.getFilename());
return SingleResponse.success();
} catch (BizException e) {
throw e;
} catch (Exception e) {
log.error("File delete failed: fileId={}", fileId, e);
throw new BizException(ErrorCode.FILE_DELETE_FAILED, e);
}
}
@Override
public SingleResponse<Void> deleteBatch(List<Integer> fileIds) {
if (fileIds == null || fileIds.isEmpty()) {
return SingleResponse.success();
}
for (Integer fileId : fileIds) {
try {
delete(fileId);
} catch (Exception e) {
log.warn("Batch delete: failed for fileId={}, continue with next", fileId, e);
}
}
return SingleResponse.success();
}
@Override
public SingleResponse<List<FileInfoCO>> listAll() {
List<FileStorage> fileStorages = fileStorageDomainService.listAllResources();
List<FileInfoCO> result = fileStorages.stream()
.map(r -> FileStorageHelper.toCO(r, null))
.collect(Collectors.toList());
return SingleResponse.success(result);
}
@Override
public SingleResponse<List<FileInfoCO>> listByCreator(String creatorUid) {
List<FileStorage> fileStorages = fileStorageDomainService.listResourcesByCreator(creatorUid);
List<FileInfoCO> result = fileStorages.stream()
.map(r -> FileStorageHelper.toCO(r, null))
.collect(Collectors.toList());
return SingleResponse.success(result);
}
}

View File

@ -0,0 +1,114 @@
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.co.FileInfoCO;
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.infrastructure.adapter.auth.AuthUserContextAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Date;
/**
* App
* <p>
* OSS FileInfo
* </p>
*
* @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 FileInfoURLOOS
*/
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;
}
}

View File

@ -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
* <p>
* OSS filestorage
* =system=
* </p>
*
* @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<FileInfoCO> uploadPublic(MultipartFile file) {
return doUpload(file, false);
}
@Override
public SingleResponse<List<FileInfoCO>> uploadPublicBatch(List<MultipartFile> files) {
return doUploadBatch(files, false);
}
// ==================== 内部上传(创建人为当前用户) ====================
@Override
public SingleResponse<FileInfoCO> uploadInternal(MultipartFile file) {
return doUpload(file, true);
}
@Override
public SingleResponse<List<FileInfoCO>> uploadInternalBatch(List<MultipartFile> files) {
return doUploadBatch(files, true);
}
// ==================== 私有方法 ====================
/**
* filestorage
*
* @param file
* @param isInternal true-false-creator=system
*/
private SingleResponse<FileInfoCO> doUpload(MultipartFile file, boolean isInternal) {
if (file == null || file.isEmpty()) {
throw new BizException(ErrorCode.FILE_UPLOAD_EMPTY);
}
try {
// 1. 上传到 OSS
FileInfo ossFileInfo = fileStorageService.of(file).upload();
log.info("OSS upload success: url={}, size={}, isInternal={}",
ossFileInfo.getUrl(), ossFileInfo.getSize(), isInternal);
// 2. 同步元信息到本地 filestorage 表
FileStorage fileStorage = FileStorageHelper.buildResource(ossFileInfo, file.getOriginalFilename(), isInternal);
FileStorage saved = fileStorageDomainService.saveResource(fileStorage);
return SingleResponse.success(FileStorageHelper.toCO(saved, null));
} catch (BizException e) {
throw e;
} catch (Exception e) {
log.error("File upload failed: {}, isInternal={}", file.getOriginalFilename(), isInternal, e);
throw new BizException(ErrorCode.FILE_UPLOAD_FAILED, e);
}
}
/**
*
*/
private SingleResponse<List<FileInfoCO>> doUploadBatch(List<MultipartFile> files, boolean isInternal) {
if (files == null || files.isEmpty()) {
throw new BizException(ErrorCode.FILE_UPLOAD_EMPTY);
}
List<FileInfoCO> results = new ArrayList<>();
for (MultipartFile file : files) {
if (file == null || file.isEmpty()) {
continue;
}
SingleResponse<FileInfoCO> result = doUpload(file, isInternal);
if (result.isSuccess() && result.getData() != null) {
results.add(result.getData());
}
}
return SingleResponse.success(results);
}
}

View File

@ -0,0 +1,65 @@
package org.qinan.safetyeval.client.api;
import org.qinan.safetyeval.client.co.FileInfoCO;
import org.qinan.safetyeval.client.dto.SingleResponse;
import java.util.List;
/**
* Client
* <p>
* filestorage URL
* {@link FileUploadApi}
* </p>
*
* @author safety-eval
*/
public interface FileStorageApi {
/**
* URL访
*
* @param fileId ID
* @return URL
*/
SingleResponse<FileInfoCO> getPresignedUrl(Integer fileId);
/**
*
*
* @param fileId ID
* @return
*/
SingleResponse<FileInfoCO> getInfo(Integer fileId);
/**
* OSS + DB
*
* @param fileId ID
* @return
*/
SingleResponse<Void> delete(Integer fileId);
/**
* OSS + DB
*
* @param fileIds ID
* @return
*/
SingleResponse<Void> deleteBatch(List<Integer> fileIds);
/**
*
*
* @return
*/
SingleResponse<List<FileInfoCO>> listAll();
/**
*
*
* @param creatorUid UID
* @return
*/
SingleResponse<List<FileInfoCO>> listByCreator(String creatorUid);
}

View File

@ -0,0 +1,50 @@
package org.qinan.safetyeval.client.api;
import org.qinan.safetyeval.client.co.FileInfoCO;
import org.qinan.safetyeval.client.dto.SingleResponse;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* Client
* <p>
* / filestorage
* </p>
*
* @author safety-eval
*/
public interface FileUploadApi {
/**
* system
*
* @param file
* @return
*/
SingleResponse<FileInfoCO> uploadPublic(MultipartFile file);
/**
* system
*
* @param files
* @return
*/
SingleResponse<List<FileInfoCO>> uploadPublicBatch(List<MultipartFile> files);
/**
*
*
* @param file
* @return
*/
SingleResponse<FileInfoCO> uploadInternal(MultipartFile file);
/**
*
*
* @param files
* @return
*/
SingleResponse<List<FileInfoCO>> uploadInternalBatch(List<MultipartFile> files);
}

View File

@ -0,0 +1,41 @@
package org.qinan.safetyeval.client.co;
import lombok.Data;
import java.time.LocalDateTime;
/**
* COClient Object -
*
* @author safety-eval
*/
@Data
public class FileInfoCO {
/** 文件ID */
private Integer id;
/** OSS文件访问URL */
private String url;
/** 文件签名URL带过期时间的临时访问链接 */
private String presignedUrl;
/** 文件大小(字节) */
private Long size;
/** 文件原始名称 */
private String originalFilename;
/** 文件扩展名 */
private String ext;
/** 文件MIME类型 */
private String contentType;
/** 上传者姓名 */
private String creatorName;
/** 上传时间 */
private LocalDateTime createdTime;
}

View File

@ -0,0 +1,56 @@
package org.qinan.safetyeval.domain.entity;
import lombok.Data;
import java.time.LocalDateTime;
/**
*
*
* @author safety-eval
*/
@Data
public class FileStorage {
/** 主键ID */
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;
}

View File

@ -62,6 +62,16 @@ public enum ErrorCode {
// ---- 备案申请人员证书 ----
QUAL_FILING_PERSONNEL_CERT_NOT_FOUND("02-08-001", "备案申请人员证书不存在"),
// ---- 文件存储 ----
FILE_UPLOAD_FAILED("03-01-001", "文件上传失败"),
FILE_UPLOAD_EMPTY("03-01-002", "上传文件为空"),
FILE_NOT_FOUND("03-01-003", "文件不存在"),
FILE_DELETE_FAILED("03-01-004", "文件删除失败"),
FILE_DOWNLOAD_FAILED("03-01-005", "文件下载失败"),
FILE_OSS_OPERATION_FAILED("03-01-006", "OSS操作异常"),
FILE_URL_GENERATE_FAILED("03-01-007", "文件地址生成失败"),
FILE_SIZE_EXCEEDED("03-01-008", "文件大小超出限制"),
/** 未知错误 */
UNKNOWN_ERROR("99-99-999", "系统异常,请稍后重试");

View File

@ -0,0 +1,63 @@
package org.qinan.safetyeval.domain.gateway;
import org.qinan.safetyeval.domain.entity.FileStorage;
import java.util.List;
/**
* GatewayDomain
*
* @author safety-eval
*/
public interface ResourceGateway {
/**
*
*/
FileStorage save(FileStorage fileStorage);
/**
*
*/
List<FileStorage> saveBatch(List<FileStorage> fileStorages);
/**
* ID
*/
FileStorage get(Integer id);
/**
*
*/
FileStorage getByFilename(String filename);
/**
* URL
*/
FileStorage getByUrl(String url);
/**
* ID
*/
void delete(Integer id);
/**
* ID
*/
void deleteBatch(List<Integer> ids);
/**
*
*/
FileStorage update(FileStorage fileStorage);
/**
*
*/
List<FileStorage> listAll();
/**
*
*/
List<FileStorage> listByCreator(String creatorUid);
}

View File

@ -0,0 +1,102 @@
package org.qinan.safetyeval.domain.service;
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.gateway.ResourceGateway;
import org.springframework.stereotype.Service;
import java.util.List;
/**
*
* <p>
* OSS
* / filestorage
* </p>
*
* @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<FileStorage> saveResourceBatch(List<FileStorage> fileStorages) {
if (fileStorages == null || fileStorages.isEmpty()) {
throw new BizException(ErrorCode.FILE_UPLOAD_EMPTY);
}
return resourceGateway.saveBatch(fileStorages);
}
/**
* ID
*/
public FileStorage getResource(Integer id) {
FileStorage fileStorage = resourceGateway.get(id);
if (fileStorage == null) {
throw new BizException(ErrorCode.FILE_NOT_FOUND);
}
return fileStorage;
}
/**
*
*/
public FileStorage getResourceByFilename(String filename) {
FileStorage fileStorage = resourceGateway.getByFilename(filename);
if (fileStorage == null) {
throw new BizException(ErrorCode.FILE_NOT_FOUND);
}
return fileStorage;
}
/**
*
*/
public void deleteResource(Integer id) {
FileStorage fileStorage = resourceGateway.get(id);
if (fileStorage == null) {
throw new BizException(ErrorCode.FILE_NOT_FOUND);
}
resourceGateway.delete(id);
}
/**
*
*/
public void deleteResourceBatch(List<Integer> ids) {
if (ids == null || ids.isEmpty()) {
return;
}
resourceGateway.deleteBatch(ids);
}
/**
*
*/
public List<FileStorage> listAllResources() {
return resourceGateway.listAll();
}
/**
*
*/
public List<FileStorage> listResourcesByCreator(String creatorUid) {
return resourceGateway.listByCreator(creatorUid);
}
}

View File

@ -79,5 +79,17 @@
<artifactId>zcloud_gbscommon</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<!-- X File Storage多OSS统一文件存储 -->
<dependency>
<groupId>org.dromara.x-file-storage</groupId>
<artifactId>x-file-storage-spring</artifactId>
</dependency>
<!-- Aliyun OSS SDK -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,18 @@
package org.qinan.safetyeval.infrastructure.config;
import org.dromara.x.file.storage.spring.EnableFileStorage;
import org.springframework.context.annotation.Configuration;
/**
*
* <p>
* x-file-storage Spring
* OSS application.yml
* </p>
*
* @author safety-eval
*/
@Configuration
@EnableFileStorage
public class FileStorageAutoConfiguration {
}

View File

@ -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;
}

View File

@ -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;
/**
* GatewayInfrastructure
*
* @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<FileStorage> saveBatch(List<FileStorage> fileStorages) {
List<ResourceDO> dataObjects = fileStorages.stream()
.map(this::toDO)
.collect(Collectors.toList());
for (ResourceDO dataObject : dataObjects) {
resourceMapper.insert(dataObject);
}
for (int i = 0; i < fileStorages.size(); i++) {
fileStorages.get(i).setId(dataObjects.get(i).getId());
}
return fileStorages;
}
@Override
public FileStorage get(Integer id) {
ResourceDO dataObject = resourceMapper.selectById(id);
return toEntity(dataObject);
}
@Override
public FileStorage getByFilename(String filename) {
LambdaQueryWrapper<ResourceDO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ResourceDO::getFilename, filename);
ResourceDO dataObject = resourceMapper.selectOne(wrapper);
return toEntity(dataObject);
}
@Override
public FileStorage getByUrl(String url) {
LambdaQueryWrapper<ResourceDO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ResourceDO::getUrl, url);
ResourceDO dataObject = resourceMapper.selectOne(wrapper);
return toEntity(dataObject);
}
@Override
public void delete(Integer id) {
resourceMapper.deleteById(id);
}
@Override
public void deleteBatch(List<Integer> ids) {
resourceMapper.deleteBatchIds(ids);
}
@Override
public FileStorage update(FileStorage fileStorage) {
ResourceDO dataObject = toDO(fileStorage);
dataObject.setId(fileStorage.getId());
resourceMapper.updateById(dataObject);
return get(fileStorage.getId());
}
@Override
public List<FileStorage> listAll() {
List<ResourceDO> dataObjects = resourceMapper.selectList(null);
return dataObjects.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
@Override
public List<FileStorage> listByCreator(String creatorUid) {
LambdaQueryWrapper<ResourceDO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ResourceDO::getCreatorUid, creatorUid);
List<ResourceDO> dataObjects = resourceMapper.selectList(wrapper);
return dataObjects.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
// ---------- 实体转换 ----------
private ResourceDO toDO(FileStorage entity) {
if (entity == null) return null;
ResourceDO dataObject = new ResourceDO();
dataObject.setUrl(entity.getUrl());
dataObject.setSize(entity.getSize());
dataObject.setFilename(entity.getFilename());
dataObject.setOriginalFilename(entity.getOriginalFilename());
dataObject.setBasePath(entity.getBasePath());
dataObject.setPath(entity.getPath());
dataObject.setExt(entity.getExt());
dataObject.setContentType(entity.getContentType());
dataObject.setPlatform(entity.getPlatform());
dataObject.setMd5(entity.getMd5());
dataObject.setCreatorName(entity.getCreatorName());
dataObject.setCreatorUid(entity.getCreatorUid());
dataObject.setCreatedTime(entity.getCreatedTime());
return dataObject;
}
private FileStorage toEntity(ResourceDO dataObject) {
if (dataObject == null) return null;
FileStorage entity = new FileStorage();
entity.setId(dataObject.getId());
entity.setUrl(dataObject.getUrl());
entity.setSize(dataObject.getSize());
entity.setFilename(dataObject.getFilename());
entity.setOriginalFilename(dataObject.getOriginalFilename());
entity.setBasePath(dataObject.getBasePath());
entity.setPath(dataObject.getPath());
entity.setExt(dataObject.getExt());
entity.setContentType(dataObject.getContentType());
entity.setPlatform(dataObject.getPlatform());
entity.setMd5(dataObject.getMd5());
entity.setCreatorName(dataObject.getCreatorName());
entity.setCreatorUid(dataObject.getCreatorUid());
entity.setCreatedTime(dataObject.getCreatedTime());
return entity;
}
}

View File

@ -0,0 +1,14 @@
package org.qinan.safetyeval.infrastructure.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.qinan.safetyeval.infrastructure.dataobject.ResourceDO;
/**
* Mapper
*
* @author safety-eval
*/
@Mapper
public interface ResourceMapper extends BaseMapper<ResourceDO> {
}

View File

@ -114,6 +114,8 @@ safety-eval:
- /org-personnel-change
- /org-resign-apply
- /org-equipment
- /file-storage
- /file
# 本地联调默认用户上下文(与 init-v2.sql 测试数据 @org_id=1 @tenant_id=1001 一致)
public-api:
system-user-id: 1

View File

@ -24,6 +24,11 @@ spring:
config:
import:
- classpath:sdk-prod2.yml
servlet:
multipart:
enabled: true
max-file-size: 100MB
max-request-size: 500MB
# ---------- MyBatis Plus ----------
@ -38,6 +43,21 @@ mybatis-plus:
configuration:
map-underscore-to-camel-case: true
# ---------- 文件存储(阿里云 OSS ----------
dromara:
x-file-storage:
default-platform: aliyun-oss-1
thumbnail-suffix: "" # 不使用缩略图
aliyun-oss:
- platform: aliyun-oss-1
enable-storage: true
access-key: LTAI5tFerUgBCLEbXtinDD6d
secret-key: XyGVNV2RvW8U3ceoJDiy9A9oLvCGYp
end-point: oss-cn-hangzhou.aliyuncs.com
bucket-name: test-dragon-yf-pub
domain: https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com/
base-path: jjb/
# ---------- Dubbo环境无关的公共部分各profile覆盖地址/端口) ----------
dubbo:
application:
@ -71,7 +91,7 @@ safety-eval:
- /swagger-ui
- /swagger-resources
- /webjars
- /public/org-personnel
- /public
- /test
- /tmp
- /mock