From 9a12ef5e2e4c1ebbe016ebefa3b9e8364b786d02 Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Fri, 19 Dec 2025 11:43:05 +0800 Subject: [PATCH] feat: cos upload support --- .claude/settings.local.json | 3 +- plus-ui/package.json | 3 +- plus-ui/src/api/inspection/cos/index.ts | 69 +++++ plus-ui/src/api/inspection/cos/types.ts | 18 ++ plus-ui/src/components/AudioUpload/index.vue | 275 ++++++++++++++++++ plus-ui/src/views/inspection/step/index.vue | 13 +- .../src/main/resources/application-dev.yml | 10 + .../src/main/resources/application-prod.yml | 10 + ruoyi-modules/ruoyi-inspection/pom.xml | 7 + .../config/TencentCosProperties.java | 46 +++ .../controller/TencentCosController.java | 125 ++++++++ 11 files changed, 571 insertions(+), 8 deletions(-) create mode 100644 plus-ui/src/api/inspection/cos/index.ts create mode 100644 plus-ui/src/api/inspection/cos/types.ts create mode 100644 plus-ui/src/components/AudioUpload/index.vue create mode 100644 ruoyi-modules/ruoyi-inspection/src/main/java/org/dromara/inspection/config/TencentCosProperties.java create mode 100644 ruoyi-modules/ruoyi-inspection/src/main/java/org/dromara/inspection/controller/TencentCosController.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d3ed402..e6c0dab 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,7 +5,8 @@ "Bash(mvn clean package:*)", "Bash(echo:*)", "Bash(git add:*)", - "Bash(git commit:*)" + "Bash(git commit:*)", + "Bash(ls:*)" ], "deny": [], "ask": [] diff --git a/plus-ui/package.json b/plus-ui/package.json index 5910add..73e416c 100644 --- a/plus-ui/package.json +++ b/plus-ui/package.json @@ -40,7 +40,8 @@ "vue-json-pretty": "2.4.0", "vue-router": "4.5.0", "vue-types": "6.0.0", - "vxe-table": "4.13.7" + "vxe-table": "4.13.7", + "cos-js-sdk-v5": "^1.8.7" }, "devDependencies": { "@iconify/json": "^2.2.276", diff --git a/plus-ui/src/api/inspection/cos/index.ts b/plus-ui/src/api/inspection/cos/index.ts new file mode 100644 index 0000000..7737960 --- /dev/null +++ b/plus-ui/src/api/inspection/cos/index.ts @@ -0,0 +1,69 @@ +import request from '@/utils/request'; +import { AxiosPromise } from 'axios'; +import COS from 'cos-js-sdk-v5'; + +/** + * COS临时凭证响应 + */ +export interface CosCredentialResponse { + credentials: { + tmpSecretId: string; + tmpSecretKey: string; + sessionToken: string; + }; + startTime: number; + expiredTime: number; + expiration: string; +} + +/** + * COS配置响应 + */ +export interface CosConfigResponse { + bucket: string; + region: string; +} + +/** + * 获取COS临时凭证 + */ +export const getCosCredential = (): AxiosPromise => { + return request({ + url: '/inspection/cos/credential', + method: 'get' + }); +}; + +/** + * 获取COS配置信息 + */ +export const getCosConfig = (): AxiosPromise => { + return request({ + url: '/inspection/cos/config', + method: 'get' + }); +}; + +/** + * 创建COS实例(包含自动获取凭证) + */ +export const createCosInstance = (): COS => { + return new COS({ + getAuthorization: (options, callback) => { + getCosCredential() + .then((res) => { + const credentials = res.data.credentials; + callback({ + TmpSecretId: credentials.tmpSecretId, + TmpSecretKey: credentials.tmpSecretKey, + SecurityToken: credentials.sessionToken, + StartTime: res.data.startTime, + ExpiredTime: res.data.expiredTime + }); + }) + .catch((error) => { + console.error('获取COS临时凭证失败:', error); + }); + } + }); +}; diff --git a/plus-ui/src/api/inspection/cos/types.ts b/plus-ui/src/api/inspection/cos/types.ts new file mode 100644 index 0000000..d28e8a5 --- /dev/null +++ b/plus-ui/src/api/inspection/cos/types.ts @@ -0,0 +1,18 @@ +/** + * COS上传回调参数 + */ +export interface CosUploadCallbackData { + Location: string; // 完整URL(不含协议) + statusCode: number; + headers: any; +} + +/** + * COS上传进度回调 + */ +export interface CosUploadProgress { + loaded: number; // 已上传字节数 + total: number; // 总字节数 + speed: number; // 上传速度(字节/秒) + percent: number; // 上传进度(0-1) +} diff --git a/plus-ui/src/components/AudioUpload/index.vue b/plus-ui/src/components/AudioUpload/index.vue new file mode 100644 index 0000000..105e239 --- /dev/null +++ b/plus-ui/src/components/AudioUpload/index.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/plus-ui/src/views/inspection/step/index.vue b/plus-ui/src/views/inspection/step/index.vue index 9752510..b32f9db 100644 --- a/plus-ui/src/views/inspection/step/index.vue +++ b/plus-ui/src/views/inspection/step/index.vue @@ -140,8 +140,8 @@ - - + + @@ -172,8 +172,8 @@ - - + + @@ -189,8 +189,8 @@ - - + + @@ -243,6 +243,7 @@ import { listArTask } from '@/api/inspection/task'; import { ArTaskVO } from '@/api/inspection/task/types'; import { listArPoint } from '@/api/inspection/point'; import { ArPointVO } from '@/api/inspection/point/types'; +import AudioUpload from '@/components/AudioUpload/index.vue'; const { proxy } = getCurrentInstance() as ComponentInternalInstance; const route = useRoute(); diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index b97db10..c55e3fe 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -268,3 +268,13 @@ justauth: client-id: 10**********6 client-secret: 1f7d08**********5b7**********29e redirect-uri: ${justauth.address}/social-callback?source=gitea + +--- # 腾讯云COS配置 +tencent: + cos: + secret-id: AKIDBDu22pdIn8Tjx9D6nGWt68wY0JQJ0T3U + secret-key: HJ6i6MtHRP9fzDD3f3EBuPjqmUzGJ8qK + duration-seconds: 1800 + bucket: nc-1375092979 + region: ap-nanjing + app-id: 1375092979 diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index d5c00e6..478a7b7 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -270,3 +270,13 @@ justauth: client-id: 10**********6 client-secret: 1f7d08**********5b7**********29e redirect-uri: ${justauth.address}/social-callback?source=gitea + +--- # 腾讯云COS配置 +tencent: + cos: + secret-id: AKIDBDu22pdIn8Tjx9D6nGWt68wY0JQJ0T3U + secret-key: HJ6i6MtHRP9fzDD3f3EBuPjqmUzGJ8qK + duration-seconds: 1800 + bucket: nc-1375092979 + region: ap-nanjing + app-id: 1375092979 diff --git a/ruoyi-modules/ruoyi-inspection/pom.xml b/ruoyi-modules/ruoyi-inspection/pom.xml index 7ed7e08..2cf986c 100644 --- a/ruoyi-modules/ruoyi-inspection/pom.xml +++ b/ruoyi-modules/ruoyi-inspection/pom.xml @@ -78,6 +78,13 @@ ruoyi-common-oss + + + com.qcloud + cos-sts_api + 3.1.0 + + diff --git a/ruoyi-modules/ruoyi-inspection/src/main/java/org/dromara/inspection/config/TencentCosProperties.java b/ruoyi-modules/ruoyi-inspection/src/main/java/org/dromara/inspection/config/TencentCosProperties.java new file mode 100644 index 0000000..918e79d --- /dev/null +++ b/ruoyi-modules/ruoyi-inspection/src/main/java/org/dromara/inspection/config/TencentCosProperties.java @@ -0,0 +1,46 @@ +package org.dromara.inspection.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 腾讯云COS配置属性 + * + * @author LionLi + */ +@Data +@Component +@ConfigurationProperties(prefix = "tencent.cos") +public class TencentCosProperties { + + /** + * 腾讯云SecretId + */ + private String secretId; + + /** + * 腾讯云SecretKey + */ + private String secretKey; + + /** + * 临时密钥有效期(秒),默认1800秒=30分钟 + */ + private Integer durationSeconds = 1800; + + /** + * 存储桶名称 + */ + private String bucket; + + /** + * 存储桶所在地域 + */ + private String region; + + /** + * 腾讯云AppId + */ + private String appId; +} diff --git a/ruoyi-modules/ruoyi-inspection/src/main/java/org/dromara/inspection/controller/TencentCosController.java b/ruoyi-modules/ruoyi-inspection/src/main/java/org/dromara/inspection/controller/TencentCosController.java new file mode 100644 index 0000000..962d5c4 --- /dev/null +++ b/ruoyi-modules/ruoyi-inspection/src/main/java/org/dromara/inspection/controller/TencentCosController.java @@ -0,0 +1,125 @@ +package org.dromara.inspection.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.tencent.cloud.CosStsClient; +import com.tencent.cloud.Policy; +import com.tencent.cloud.Response; +import com.tencent.cloud.Statement; +import com.tencent.cloud.cos.util.Jackson; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.domain.R; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.redis.utils.RedisUtils; +import org.dromara.inspection.config.TencentCosProperties; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * 腾讯云COS临时凭证Controller + * + * @author LionLi + */ +@Slf4j +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/inspection/cos") +public class TencentCosController { + + private final TencentCosProperties cosProperties; + + private static final String CACHE_KEY = "inspection:cos:credential"; + private static final int CACHE_EXPIRE_SECONDS = 1500; // 缓存25分钟,临时密钥30分钟过期 + + /** + * 获取腾讯云COS临时上传凭证 + */ + @SaCheckPermission("inspection:step:edit") + @Log(title = "获取COS临时凭证", businessType = BusinessType.OTHER) + @GetMapping("/credential") + public R getCredential() { + // 先从缓存获取 + Response cachedCredential = RedisUtils.getCacheObject(CACHE_KEY); + if (cachedCredential != null) { + return R.ok(cachedCredential); + } + + try { + // 构建配置参数 + TreeMap config = new TreeMap<>(); + config.put("secretId", cosProperties.getSecretId()); + config.put("secretKey", cosProperties.getSecretKey()); + config.put("durationSeconds", cosProperties.getDurationSeconds()); + config.put("bucket", cosProperties.getBucket()); + config.put("region", cosProperties.getRegion()); + + // 初始化 policy + Policy policy = new Policy(); + + // 开始构建一条 statement + Statement statement = new Statement(); + // 声明设置的结果是允许操作 + statement.setEffect("allow"); + + // 添加操作权限 + statement.addActions(new String[]{ + // 简单上传 + "cos:PutObject", + "cos:PostObject", + // 分块上传 + "cos:InitiateMultipartUpload", + "cos:ListMultipartUploads", + "cos:ListParts", + "cos:UploadPart", + "cos:CompleteMultipartUpload" + }); + + // 设置允许操作的资源路径(限定只能上传到audio目录) + // 格式: qcs::cos:{region}:uid/{appid}:{bucket}/{path} + statement.addResources(new String[]{ + "qcs::cos:" + cosProperties.getRegion() + + ":uid/" + cosProperties.getAppId() + + ":" + cosProperties.getBucket() + + "/audio/*" + }); + + // 把一条 statement 添加到 policy + policy.addStatement(statement); + + // 将 Policy 实例转化成 String + config.put("policy", Jackson.toJsonPrettyString(policy)); + + // 获取临时密钥 + Response response = CosStsClient.getCredential(config); + + // 缓存凭证(25分钟,临时密钥30分钟过期) + RedisUtils.setCacheObject(CACHE_KEY, response, Duration.ofSeconds(CACHE_EXPIRE_SECONDS)); + + return R.ok(response); + } catch (Exception e) { + log.error("获取临时COS凭证失败", e); + return R.fail("获取临时凭证失败:" + e.getMessage()); + } + } + + /** + * 获取COS配置信息(供前端使用) + */ + @SaCheckPermission("inspection:step:edit") + @GetMapping("/config") + public R> getConfig() { + Map config = new HashMap<>(); + config.put("bucket", cosProperties.getBucket()); + config.put("region", cosProperties.getRegion()); + return R.ok(config); + } +}