From 93d68df04b421acf0cf847b357eb05fefba180f1 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 18 Jun 2025 16:27:59 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=86=E9=A2=91=E7=82=B9=E6=92=AD=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLIENT_UPLOAD_README.md | 190 +++++++++++ package.json | 1 + src/components/AliVideoUpload/README.md | 226 +++++++++++++ src/components/AliVideoUpload/index.tsx | 306 ++++++++++++++++++ src/pages/AuditsList/detail.tsx | 2 +- .../ContentList/components/UpdateForm.tsx | 2 +- .../Dictionary/components/UpdateForm.tsx | 2 +- .../Dictionary/components/UpdateItemForm.tsx | 2 +- .../FashionTrend/components/UpdateForm.tsx | 2 +- .../components/UpdateForm.tsx | 2 +- .../MemberList/components/UpdateForm.tsx | 2 +- src/pages/MessageList/index.tsx | 2 +- .../ProductList/components/UpdateForm.tsx | 2 +- .../components/UpdateLabelForm.tsx | 2 +- .../components/UpdateLabelTypeForm.tsx | 2 +- .../components/UpdateBannerForm.tsx | 2 +- .../components/UpdateForm.tsx | 2 +- .../TrainingClasses/components/UpdateForm.tsx | 2 +- src/pages/TrainingClasses/detail.tsx | 134 ++++---- src/pages/UserList/components/UpdateForm.tsx | 2 +- src/services/pop-b2b2c/errorController.ts | 30 +- src/services/pop-b2b2c/index.ts | 2 +- .../pop-b2b2c/pbcBusinessController.ts | 15 + src/services/pop-b2b2c/pbcVodController.ts | 48 ++- src/services/pop-b2b2c/typings.d.ts | 46 ++- src/services/pop-b2b2c/wxController.ts | 15 + src/types/aliyun-upload-vod.d.ts | 40 +++ 27 files changed, 975 insertions(+), 108 deletions(-) create mode 100644 CLIENT_UPLOAD_README.md create mode 100644 src/components/AliVideoUpload/README.md create mode 100644 src/components/AliVideoUpload/index.tsx create mode 100644 src/types/aliyun-upload-vod.d.ts diff --git a/CLIENT_UPLOAD_README.md b/CLIENT_UPLOAD_README.md new file mode 100644 index 0000000..cc20974 --- /dev/null +++ b/CLIENT_UPLOAD_README.md @@ -0,0 +1,190 @@ +# 视频客户端上传修改说明 + +## 修改概述 + +本次修改将视频上传从服务端上传改为客户端上传,使用阿里云视频点播上传SDK(aliyun-upload-vod)实现直接上传到阿里云视频点播服务。 + +## 主要修改内容 + +### 1. 新增依赖包 + +- `aliyun-upload-vod`: 阿里云视频点播上传SDK,专门用于视频上传 +- 移除了 `ali-oss` 和 `@types/ali-oss` + +### 2. 新增组件 + +#### `src/components/AliVideoUpload/index.tsx` + +创建了专门的客户端视频上传组件,主要功能: + +- 使用阿里云视频点播上传SDK进行客户端上传 +- 支持上传进度显示 +- 文件类型和大小验证 +- 错误处理和用户提示 +- 上传成功后返回videoId和playAuth +- **新增**: 可配置的文件大小限制(maxSize参数) +- **新增**: 可配置的文件类型限制(accept参数) +- **新增**: 支持多文件上传(multiple参数) +- **新增**: 最大上传个数限制(maxCount参数) + +#### 组件参数 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| value | UploadFile[] | [] | 文件列表 | +| onChange | (fileList: UploadFile[]) => void | - | 文件列表变化回调 | +| onSuccess | (data: any) => void | - | 上传成功回调 | +| disabled | boolean | false | 是否禁用 | +| maxSize | number | 500 | 最大文件大小,单位MB | +| maxCount | number | 9 | 最大上传个数 | +| accept | string | 'video/*' | 接受的文件类型 | +| multiple | boolean | false | 是否支持多文件上传 | + +### 3. 修改页面 + +#### `src/pages/TrainingClasses/detail.tsx` + +- 导入新的`AliVideoUpload`组件 +- 修改ModalForm中的视频上传部分 +- 根据文件类型选择不同的上传方式: + - 视频文件:使用客户端上传(AliVideoUpload) + - 其他文件:继续使用服务端上传 +- 修改数据处理逻辑,适配客户端上传的数据格式 +- **新增**: 设置maxSize={3000},限制视频文件大小为3000MB +- **新增**: 设置maxCount={1},限制单次只能上传1个文件 + +### 4. 服务接口 + +#### `src/services/pop-b2b2c/pbcVodController.ts` + +- 使用现有的`createUploadVideoUsingPost`接口获取上传凭证 +- 该接口返回阿里云VOD的上传地址和凭证信息 + +### 5. 类型定义 + +#### `src/types/aliyun-upload-vod.d.ts` + +- 为aliyun-upload-vod包创建了TypeScript类型定义 +- 确保类型安全和开发体验 + +## 技术实现 + +### 客户端上传流程 + +1. **创建上传器**: 使用`AliyunUpload.Vod`创建上传客户端 +2. **添加文件**: 调用`addFile`方法添加文件到上传队列 +3. **获取上传凭证**: 在`onUploadstarted`回调中调用`createUploadVideoUsingPost`接口获取上传凭证 +4. **设置凭证**: 调用`setUploadAuthAndAddress`方法设置上传凭证和地址 +5. **开始上传**: 自动开始上传过程 +6. **处理结果**: 在`onUploadSucceed`回调中处理上传成功结果 + +### 关键代码 + +```typescript +// 创建VOD上传客户端 +const uploader = new AliyunUpload.Vod({ + userId: '1303984639806000', // 需要根据实际情况配置 + region: 'cn-shanghai', + partSize: 1048576, + parallel: 5, + retryCount: 3, + retryDuration: 2, + onUploadstarted: async (uploadInfo) => { + // 获取上传凭证 + const uploadAuthData = await getUploadAuth(file.name); + uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress, videoId); + }, + onUploadSucceed: (uploadInfo) => { + // 处理上传成功 + }, + onUploadProgress: (uploadInfo, totalSize, loadedPercent) => { + // 更新上传进度 + } +}); + +// 添加文件到上传队列 +uploader.addFile(file, undefined, undefined, undefined, userData); +``` + +## 使用示例 + +### 基本使用 +```tsx + { + console.log('videoId:', data.videoId); + }} +/> +``` + +### 自定义文件大小限制 +```tsx + +``` + +### 多文件上传 +```tsx + +``` + +## 优势 + +1. **减少服务器压力**:文件直接从客户端上传到阿里云,不经过业务服务器 +2. **提高上传速度**:避免了服务器中转,减少网络延迟 +3. **支持断点续传**:aliyun-upload-vod SDK内置断点续传功能 +4. **更好的用户体验**:实时显示上传进度 +5. **节省服务器带宽**:减少服务器带宽消耗 +6. **灵活配置**:支持自定义文件大小、类型和多文件上传 +7. **专门优化**:aliyun-upload-vod是专门为视频上传优化的SDK + +## 注意事项 + +1. 确保后端`createUploadVideoUsingPost`接口返回正确的上传凭证格式 +2. 上传凭证包含敏感信息,注意安全性 +3. 客户端上传需要浏览器支持aliyun-upload-vod SDK +4. 文件大小限制可通过maxSize参数配置(默认500MB) +5. 需要确保阿里云VOD服务配置正确 +6. **重要**: 需要配置正确的userId,当前使用的是示例值 + +## 测试建议 + +1. 测试小文件上传功能 +2. 测试大文件上传和断点续传 +3. 测试网络异常情况下的错误处理 +4. 验证上传后的视频播放功能 +5. 测试不同浏览器兼容性 +6. 测试不同的maxSize配置 +7. 测试多文件上传功能 +8. 测试maxCount限制功能 + +## 修改的文件列表 + +1. `src/components/AliVideoUpload/index.tsx` - 新增客户端上传组件 +2. `src/components/AliVideoUpload/README.md` - 组件使用说明文档 +3. `src/pages/TrainingClasses/detail.tsx` - 修改视频上传逻辑 +4. `src/types/aliyun-upload-vod.d.ts` - 新增类型定义文件 +5. `package.json` - 更新依赖包 +6. `CLIENT_UPLOAD_README.md` - 本说明文档 + +## 部署注意事项 + +1. 确保生产环境已安装aliyun-upload-vod依赖 +2. 检查阿里云VOD服务配置 +3. 验证上传凭证接口的可用性 +4. 测试生产环境的网络连接 +5. 根据业务需求调整maxSize等参数配置 +6. **重要**: 配置正确的userId,替换示例值 \ No newline at end of file diff --git a/package.json b/package.json index d463192..a3f3603 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@dnd-kit/utilities": "^3.2.2", "@umijs/route-utils": "^2.2.2", "aliyun-aliplayer": "^2.29.1", + "aliyun-upload-vod": "^1.0.6", "antd": "^5.2.2", "antd-img-crop": "^4.23.0", "braft-editor": "^2.3.9", diff --git a/src/components/AliVideoUpload/README.md b/src/components/AliVideoUpload/README.md new file mode 100644 index 0000000..971ab0d --- /dev/null +++ b/src/components/AliVideoUpload/README.md @@ -0,0 +1,226 @@ +# AliVideoUpload 组件使用说明 + +## 组件介绍 + +AliVideoUpload 是一个基于阿里云视频点播上传SDK的客户端视频上传组件,支持直接上传到阿里云视频点播服务。 + +## 功能特性 + +- ✅ 客户端直接上传到阿里云VOD +- ✅ 支持上传进度显示 +- ✅ 文件类型和大小验证 +- ✅ 错误处理和用户提示 +- ✅ 支持断点续传 +- ✅ 可配置的文件大小限制 +- ✅ 可配置的文件类型限制 +- ✅ 支持单文件/多文件上传 +- ✅ **新增**: 可配置的最大上传个数限制 + +## 基本用法 + +```tsx +import AliVideoUpload from '@/components/AliVideoUpload'; + +// 基本使用 + { + console.log('上传成功:', data); + }} +/> +``` + +## 参数说明 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| value | UploadFile[] | [] | 文件列表 | +| onChange | (fileList: UploadFile[]) => void | - | 文件列表变化回调 | +| onSuccess | (data: any) => void | - | 上传成功回调 | +| disabled | boolean | false | 是否禁用 | +| maxSize | number | 500 | 最大文件大小,单位MB | +| maxCount | number | 9 | 最大上传个数 | +| accept | string | 'video/*' | 接受的文件类型 | +| multiple | boolean | false | 是否支持多文件上传 | + +## 使用示例 + +### 1. 基本使用(默认配置) + +```tsx + { + console.log('videoId:', data.videoId); + console.log('playAuth:', data.playAuth); + }} +/> +``` + +### 2. 自定义文件大小限制 + +```tsx + +``` + +### 3. 限制上传个数(单文件模式) + +```tsx + +``` + +### 4. 多文件上传 + +```tsx + +``` + +### 5. 完整配置示例 + +```tsx + { + console.log('videoId:', data.videoId); + console.log('playAuth:', data.playAuth); + console.log('duration:', data.duration); + }} + maxCount={3} + maxSize={500} + multiple={true} + accept="video/*" + disabled={false} +/> +``` + +## maxCount 功能说明 + +### 功能描述 +maxCount 参数用于限制用户可以上传的最大文件个数。 + +### 工作原理 +1. 在 `beforeUpload` 回调中检查当前文件列表长度 +2. 如果已达到最大数量限制,显示错误提示并阻止上传 +3. 支持单文件和多文件模式下的数量限制 + +### 使用场景 +- **单文件上传**: 设置 `maxCount={1}` 确保只能上传一个文件 +- **多文件上传**: 设置 `maxCount={n}` 限制最多上传n个文件 +- **批量上传**: 结合 `multiple={true}` 实现批量文件上传 + +### 测试方法 +1. 设置 `maxCount={2}` +2. 尝试上传第3个文件 +3. 应该看到错误提示:"最多只能上传2个文件!" + +## 上传流程 + +1. **文件选择**: 用户选择文件 +2. **前置检查**: + - 检查文件数量限制 (maxCount) + - 检查文件大小限制 (maxSize) + - 检查文件类型 (accept) +3. **获取凭证**: 调用后端接口获取上传凭证 +4. **开始上传**: 使用阿里云VOD SDK上传文件 +5. **进度显示**: 实时显示上传进度 +6. **获取播放凭证**: 上传成功后获取播放凭证 +7. **完成回调**: 调用 onSuccess 回调 + +## 返回数据格式 + +上传成功后,`onSuccess` 回调会返回以下数据: + +```typescript +{ + videoId: string; // 视频ID + playAuth: string; // 播放凭证 + duration: string; // 视频时长 +} +``` + +## 注意事项 + +1. **userId配置**: 需要配置正确的阿里云账号ID +2. **网络环境**: 确保网络环境能够访问阿里云服务 +3. **文件格式**: 支持常见的视频格式(MP4、AVI、MOV等) +4. **文件大小**: 建议根据实际需求设置合理的文件大小限制 +5. **并发上传**: 多文件上传时注意并发数量控制 + +## 常见问题 + +### Q: maxCount设置无效怎么办? +A: 确保在 `beforeUpload` 中正确检查文件数量,并且没有其他逻辑覆盖了这个检查。 + +### Q: 如何实现单文件上传? +A: 设置 `maxCount={1}` 和 `multiple={false}`。 + +### Q: 上传失败如何处理? +A: 检查网络连接、文件格式、文件大小等,查看控制台错误信息。 + +### Q: 如何获取上传进度? +A: 组件会自动显示上传进度条,也可以通过 `onUploadProgress` 回调获取。 + +## 更新日志 + +### v1.1.0 +- ✅ 新增 maxCount 参数支持 +- ✅ 优化文件数量限制逻辑 +- ✅ 改进错误提示信息 +- ✅ 更新文档和示例 + +### v1.0.0 +- ✅ 基础视频上传功能 +- ✅ 支持文件大小和类型验证 +- ✅ 支持上传进度显示 +- ✅ 支持错误处理 + +## 错误处理 + +组件会自动处理以下错误情况: + +- 文件大小超限 +- 文件类型不支持 +- 网络连接失败 +- 上传凭证获取失败 +- OSS上传失败 + +所有错误都会通过 `message.error` 显示给用户。 + +## 样式定制 + +组件使用Ant Design的Upload和Progress组件,可以通过CSS自定义样式: + +```css +.ali-video-upload { + /* 自定义样式 */ +} + +.ali-video-upload .ant-upload { + /* 上传按钮样式 */ +} + +.ali-video-upload .ant-progress { + /* 进度条样式 */ +} +``` \ No newline at end of file diff --git a/src/components/AliVideoUpload/index.tsx b/src/components/AliVideoUpload/index.tsx new file mode 100644 index 0000000..f320b51 --- /dev/null +++ b/src/components/AliVideoUpload/index.tsx @@ -0,0 +1,306 @@ +import React, { useRef, useState } from 'react'; +import { Button, message, Modal, Progress, Upload } from 'antd'; +import { UploadOutlined } from '@ant-design/icons'; +import { createUploadVideoUsingPost, getVideoAuthByVideoIdUsingGet } from '@/services/pop-b2b2c/pbcVodController'; +import type { UploadFile, UploadProps } from 'antd/es/upload/interface'; +import AliyunUpload from 'aliyun-upload-vod'; +import AliPlayer from '../AliPlayer'; + +interface AliVideoUploadProps { + value?: UploadFile[]; + onChange?: (fileList: UploadFile[]) => void; + onSuccess?: (data: any) => void; + disabled?: boolean; + maxSize?: number; // 最大文件大小,单位MB,默认500MB + maxCount?: number; // 最大上传个数 + accept?: string; // 接受的文件类型,默认video/* + multiple?: boolean; // 是否支持多文件上传,默认false +} + +const AliVideoUpload: React.FC = ({ + value = [], + onChange, + onSuccess, + disabled = false, + maxSize = 500, + maxCount = 9, + accept = 'video/*', + multiple = false, +}) => { + const [uploading, setUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const uploadRef = useRef(null); + const vodUploadRef = useRef(null); + + // 获取上传凭证 + const getUploadAuth = async (fileName: string) => { + try { + const response = await createUploadVideoUsingPost({ + title: fileName, + fileName: fileName, + }); + + if (response.retcode && response.data) { + return response.data; + } else { + throw new Error(response.retmsg || '获取上传凭证失败'); + } + } catch (error) { + console.error('获取上传凭证失败:', error); + throw error; + } + }; + + // 客户端上传到阿里云VOD + const uploadToAliyunVOD = async (file: File) => { + return new Promise((resolve, reject) => { + try { + // 创建VOD上传客户端 + const uploader = new AliyunUpload.Vod({ + // 阿里账号ID,必须有值 + userId: '1303984639806000', // 这里需要根据实际情况配置 + // 上传到视频点播的地域,默认值为'cn-shanghai' + region: 'cn-shanghai', + // 分片大小默认1 MB,不能小于100 KB + partSize: 1048576, + // 并行上传分片个数,默认5 + parallel: 5, + // 网络原因失败时,重新上传次数,默认为3 + retryCount: 3, + // 网络原因失败时,重新上传间隔时间,默认为2秒 + retryDuration: 2, + // 添加文件成功 + addFileSuccess: () => { + if (uploader !== null) { + uploader.startUpload(); + } + }, + // 开始上传 + onUploadstarted: async (uploadInfo: any) => { + try { + // 获取上传凭证 + const uploadAuthData = await getUploadAuth(file.name); + console.log('上传凭证:', uploadAuthData); + + // 解析上传凭证数据 - 根据实际返回格式处理 + let uploadAuth: string; + let uploadAddress: string; + let videoId: string; + + if (typeof uploadAuthData === 'string') { + // 如果返回的是字符串,尝试解析JSON + try { + const parsed = JSON.parse(uploadAuthData); + uploadAuth = parsed.uploadAuth || uploadAuthData; + uploadAddress = parsed.uploadAddress || ''; + videoId = parsed.videoId || ''; + } catch { + // 如果解析失败,直接使用字符串 + uploadAuth = uploadAuthData; + uploadAddress = ''; + videoId = ''; + } + } else { + // 如果返回的是对象 + uploadAuth = (uploadAuthData as any).uploadAuth || uploadAuthData; + uploadAddress = (uploadAuthData as any).UploadAddress || ''; + videoId = (uploadAuthData as any).videoId || ''; + } + + // 设置上传凭证和地址 + uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress, videoId); + } catch (error) { + console.error('获取上传凭证失败:', error); + reject(error); + } + }, + // 文件上传成功 + onUploadSucceed: (uploadInfo: any) => { + console.log('上传成功:', uploadInfo); + getVideoAuthByVideoIdUsingGet({ id: uploadInfo.videoId }).then(res => { + if (res.retcode) { + resolve({ + videoId: uploadInfo.videoId, + playAuth: res.data?.playAuth, // 需要单独获取播放凭证 + duration: res.data?.videoMeta?.duration, + }); + } else { + message.error(res.retmsg); + reject(new Error(res.retmsg)); + } + }).catch(error => { + message.error(error); + reject(new Error(error)); + }) + }, + // 文件上传失败 + onUploadFailed: (uploadInfo: any, code: string, message: string) => { + console.error('上传失败:', uploadInfo, code, message); + reject(new Error(message || '上传失败')); + }, + // 文件上传进度,单位:字节 + onUploadProgress: (uploadInfo: any, totalSize: number, loadedPercent: number) => { + const progress = Math.ceil(loadedPercent * 100); + setUploadProgress(progress); + }, + // 上传凭证或STS token超时 + onUploadTokenExpired: () => { + message.error('文件上传token超时'); + reject(new Error('上传凭证超时')); + }, + // 全部文件上传结束 + onUploadEnd: () => { + console.log("onUploadEnd: uploaded all the files"); + } + }); + + // 保存上传器引用 + vodUploadRef.current = uploader; + + // 添加文件到上传队列 + const userData = '{"Vod":{}}'; + uploader.addFile(file, undefined, undefined, undefined, userData); + + } catch (error) { + reject(error); + } + }); + }; + + const handleUpload = async (file: File) => { + if (!file) return; + + setUploading(true); + setUploadProgress(0); + + try { + // 上传到阿里云VOD + const result = await uploadToAliyunVOD(file); + + // 更新文件列表 + const newFile: UploadFile = { + uid: (file as any).uid || Date.now().toString(), + name: file.name, + status: 'done', + url: '', + response: { + retcode: 1, + data: result, + }, + }; + + const newFileList = [...value, newFile]; + onChange?.(newFileList); + onSuccess?.(result); + + message.success('视频上传成功'); + } catch (error) { + console.error('上传失败:', error); + message.error(error instanceof Error ? error.message : '上传失败'); + + // 更新文件状态为错误 + const errorFile: UploadFile = { + uid: (file as any).uid || Date.now().toString(), + name: file.name, + status: 'error', + error: error instanceof Error ? error : new Error('上传失败'), + }; + + const newFileList = [...value, errorFile]; + onChange?.(newFileList); + } finally { + setUploading(false); + setUploadProgress(0); + } + }; + + const uploadProps: UploadProps = { + name: 'file', + accept: accept, + maxCount: maxCount, + multiple: multiple, + disabled: disabled || uploading, + beforeUpload: (file) => { + // 检查文件数量限制 + if (value.length >= maxCount) { + message.error(`最多只能上传${maxCount}个文件!`); + return Upload.LIST_IGNORE; + } + + // 检查文件大小 (限制为maxSize MB) + const isLtMaxSize = file.size / 1024 / 1024 < maxSize; + if (!isLtMaxSize) { + message.error(`视频文件大小不能超过${maxSize}MB!`); + return Upload.LIST_IGNORE; + } + + // 检查文件类型 + const isVideo = file.type.startsWith('video/'); + if (!isVideo) { + message.error('只能上传视频文件!'); + return Upload.LIST_IGNORE; + } + + // 手动处理上传 + handleUpload(file); + return Upload.LIST_IGNORE; + }, + fileList: value, + onRemove: (file) => { + const newFileList = value.filter(item => item.uid !== file.uid); + onChange?.(newFileList); + }, + onPreview: (file) => { + let videoId = "" + if (file.uid === '-1' && file.url) { + videoId = file.url + } else if (file.response?.data?.videoId) { + videoId = file.response?.data?.videoId + } + getVideoAuthByVideoIdUsingGet({ id: videoId }).then(res => { + if (res.retcode && res.data?.playAuth) { + setVideoId(videoId) + setPlayAuth(res.data?.playAuth) + handleShowVideo(true) + } + }) + }, + }; + + const [showVideo, handleShowVideo] = useState(false); + const [playAuth, setPlayAuth] = useState('') + const [videoId, setVideoId] = useState('') + + return ( +
+ + + + + {uploading && ( +
+ +
+ )} + +
+ 支持格式:MP4、AVI、MOV等视频格式,文件大小不超过{maxSize}MB +
+ handleShowVideo(false)} + width={800} + > + + +
+ ); +}; + +export default AliVideoUpload; \ No newline at end of file diff --git a/src/pages/AuditsList/detail.tsx b/src/pages/AuditsList/detail.tsx index 9dcee83..756e442 100644 --- a/src/pages/AuditsList/detail.tsx +++ b/src/pages/AuditsList/detail.tsx @@ -126,7 +126,7 @@ const Detail: React.FC = () => { title="填写驳回理由" open={isModalOpen} modalProps={{ - destroyOnClose: true, + destroyOnHidden: true, onCancel: () => setIsModalOpen(false), }} requiredMark={false} diff --git a/src/pages/ContentList/components/UpdateForm.tsx b/src/pages/ContentList/components/UpdateForm.tsx index e0a8667..563f2ec 100644 --- a/src/pages/ContentList/components/UpdateForm.tsx +++ b/src/pages/ContentList/components/UpdateForm.tsx @@ -31,7 +31,7 @@ const UpdateForm: React.FC = (props) => { return props.onSubmit({ ...props.values, ...value }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, }} initialValues={{ pbcContentTypeName: props.values.pbcContentTypeName, diff --git a/src/pages/Dictionary/components/UpdateForm.tsx b/src/pages/Dictionary/components/UpdateForm.tsx index 4e2e9d5..034c2a4 100644 --- a/src/pages/Dictionary/components/UpdateForm.tsx +++ b/src/pages/Dictionary/components/UpdateForm.tsx @@ -26,7 +26,7 @@ const UpdateForm: React.FC = (props) => { return props.onSubmit({ ...value }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, }} onOpenChange={(visible) => { formRef.current?.resetFields(); diff --git a/src/pages/Dictionary/components/UpdateItemForm.tsx b/src/pages/Dictionary/components/UpdateItemForm.tsx index ae32e94..c139b9c 100644 --- a/src/pages/Dictionary/components/UpdateItemForm.tsx +++ b/src/pages/Dictionary/components/UpdateItemForm.tsx @@ -25,7 +25,7 @@ const UpdateItemForm: React.FC = (props) => { return props.onSubmit({ ...value }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, }} onOpenChange={(visible) => { formRef.current?.resetFields(); diff --git a/src/pages/FashionTrend/components/UpdateForm.tsx b/src/pages/FashionTrend/components/UpdateForm.tsx index ad635f2..6a6746c 100644 --- a/src/pages/FashionTrend/components/UpdateForm.tsx +++ b/src/pages/FashionTrend/components/UpdateForm.tsx @@ -110,7 +110,7 @@ const UpdateForm: React.FC = (props) => { return props.onSubmit({ ...value, pbcPicAddress, pbcThumbNail, pbcContent: value.pbcType === 3 ? '预览文件' : value.pbcContent, pbcId: props.values.pbcId }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, }} initialValues={{ pbcTitle: props.values.pbcTitle, diff --git a/src/pages/InnovativeService/components/UpdateForm.tsx b/src/pages/InnovativeService/components/UpdateForm.tsx index 4705590..6e951fc 100644 --- a/src/pages/InnovativeService/components/UpdateForm.tsx +++ b/src/pages/InnovativeService/components/UpdateForm.tsx @@ -110,7 +110,7 @@ const UpdateForm: React.FC = (props) => { return props.onSubmit({ ...value, pbcPicAddress, pbcThumbNail, pbcContent: value.pbcType === 3 ? '预览文件' : value.pbcContent, pbcId: props.values.pbcId }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, }} initialValues={{ pbcTitle: props.values.pbcTitle, diff --git a/src/pages/MemberList/components/UpdateForm.tsx b/src/pages/MemberList/components/UpdateForm.tsx index d3d78a3..71235d5 100644 --- a/src/pages/MemberList/components/UpdateForm.tsx +++ b/src/pages/MemberList/components/UpdateForm.tsx @@ -30,7 +30,7 @@ const UpdateForm: React.FC = (props) => { return props.onSubmit({ ...value,pbcVipGradeDiscount: value.pbcVipGradeDiscount ? value.pbcVipGradeDiscount / 10 : 1, pbcId: props.values.pbcId }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, }} initialValues={{ pbcVipGradeName: props.values.pbcVipGradeName, diff --git a/src/pages/MessageList/index.tsx b/src/pages/MessageList/index.tsx index 6436822..28a7c22 100644 --- a/src/pages/MessageList/index.tsx +++ b/src/pages/MessageList/index.tsx @@ -173,7 +173,7 @@ const TableList: React.FC<{}> = () => { width={600} closeIcon={null} footer={null} - destroyOnClose={true} + destroyOnHidden={true} open={openDrawer} >
diff --git a/src/pages/ProductList/components/UpdateForm.tsx b/src/pages/ProductList/components/UpdateForm.tsx index d185df8..21ab103 100644 --- a/src/pages/ProductList/components/UpdateForm.tsx +++ b/src/pages/ProductList/components/UpdateForm.tsx @@ -90,7 +90,7 @@ const UpdateForm: React.FC = (props) => { return props.onSubmit({ ...props.values, ...value, pbcSpecificationList: arr, pbcCategoryImage, pbcId: props.values.pbcId }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, }} initialValues={{ pbcCategoryName: props.values.pbcCategoryName diff --git a/src/pages/ProductList/components/UpdateLabelForm.tsx b/src/pages/ProductList/components/UpdateLabelForm.tsx index 2a0aaf2..6d02bca 100644 --- a/src/pages/ProductList/components/UpdateLabelForm.tsx +++ b/src/pages/ProductList/components/UpdateLabelForm.tsx @@ -28,7 +28,7 @@ const UpdateForm: React.FC = (props) => { return props.onSubmit({ ...props.values, ...value }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, }} initialValues={{ pbcLabelName: props.values.pbcLabelName diff --git a/src/pages/ProductList/components/UpdateLabelTypeForm.tsx b/src/pages/ProductList/components/UpdateLabelTypeForm.tsx index 4f785ac..cecf95f 100644 --- a/src/pages/ProductList/components/UpdateLabelTypeForm.tsx +++ b/src/pages/ProductList/components/UpdateLabelTypeForm.tsx @@ -28,7 +28,7 @@ const UpdateForm: React.FC = (props) => { return props.onSubmit({ ...props.values, ...value }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, }} initialValues={{ pbcLabelTypeName: props.values.pbcLabelTypeName diff --git a/src/pages/ScreenAdvertisement/components/UpdateBannerForm.tsx b/src/pages/ScreenAdvertisement/components/UpdateBannerForm.tsx index 7d82740..4e54ce6 100644 --- a/src/pages/ScreenAdvertisement/components/UpdateBannerForm.tsx +++ b/src/pages/ScreenAdvertisement/components/UpdateBannerForm.tsx @@ -56,7 +56,7 @@ const UpdateBannerForm: React.FC = (props) => { return props.onSubmit({ ...value, pbcBannerImage, pbcId: props.values.pbcId, pbcLink: value.pbcLink ? linkType + value.pbcLink : '' }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, afterOpenChange: (visible) => { if (!visible) props.afterClose(); } diff --git a/src/pages/ScreenAdvertisement/components/UpdateForm.tsx b/src/pages/ScreenAdvertisement/components/UpdateForm.tsx index f69c9b7..1f22c55 100644 --- a/src/pages/ScreenAdvertisement/components/UpdateForm.tsx +++ b/src/pages/ScreenAdvertisement/components/UpdateForm.tsx @@ -42,7 +42,7 @@ const UpdateForm: React.FC = (props) => { return props.onSubmit({ ...value, pbcAdvertisement, pbcId: props.values.pbcId }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, afterOpenChange: (visible) => { if (!visible) props.afterClose(); } diff --git a/src/pages/TrainingClasses/components/UpdateForm.tsx b/src/pages/TrainingClasses/components/UpdateForm.tsx index e97560f..12aea94 100644 --- a/src/pages/TrainingClasses/components/UpdateForm.tsx +++ b/src/pages/TrainingClasses/components/UpdateForm.tsx @@ -28,7 +28,7 @@ const UpdateForm: React.FC = (props) => { return props.onSubmit({ ...props.values, ...value }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, }} initialValues={{ pbcType: props.values.pbcType diff --git a/src/pages/TrainingClasses/detail.tsx b/src/pages/TrainingClasses/detail.tsx index 6e76710..29dd417 100644 --- a/src/pages/TrainingClasses/detail.tsx +++ b/src/pages/TrainingClasses/detail.tsx @@ -22,6 +22,7 @@ import { addOrUpdateClassUsingPost, classDetailForAdminUsingGet } from '@/servic import { addOrUpdateChapterUsingPost, removeChapterUsingGet } from '@/services/pop-b2b2c/pbcTrainingClassesChapterController'; import { addOrUpdateVideoUsingPost, getVideoAuthByIdUsingGet, removeVideoUsingGet } from '@/services/pop-b2b2c/pbcTrainingClassesVideoController'; import AliPlayer from '@/components/AliPlayer'; +import AliVideoUpload from '@/components/AliVideoUpload'; /** * 删除节点 @@ -451,7 +452,7 @@ const Detail: React.FC = () => { title={stepFormValues.pbcId ? '编辑章节' : '新增章节'} open={updateModalVisible} modalProps={{ - destroyOnClose: true, + destroyOnHidden: true, onCancel: () => handleUpdateModalVisible(false), }} requiredMark={false} @@ -493,7 +494,7 @@ const Detail: React.FC = () => { title={stepFormValues1.pbcId ? '编辑文件' : '新增文件'} open={updateModalVisible1} modalProps={{ - destroyOnClose: true, + destroyOnHidden: true, onCancel: () => handleUpdateModalVisible1(false), }} requiredMark={false} @@ -522,8 +523,14 @@ const Detail: React.FC = () => { value.pbcVideoAddress[0].response && value.pbcVideoAddress[0].response.retcode ) { - pbcVideoAddress = fileType === '1' ? value.pbcVideoAddress[0].response.data.videoId : value.pbcVideoAddress[0].response.data; - pbcVideoDuration = fileType === '1' ? value.pbcVideoAddress[0].response.data.duration : ''; + if (fileType === '1') { + // 客户端上传的视频数据 + pbcVideoAddress = value.pbcVideoAddress[0].response.data.videoId; + pbcVideoDuration = value.pbcVideoAddress[0].response.data.duration || ''; + } else { + // 服务端上传的文件数据 + pbcVideoAddress = value.pbcVideoAddress[0].response.data; + } } } await addOrUpdateVideoUsingPost({ @@ -575,78 +582,71 @@ const Detail: React.FC = () => { }, ]} /> - { - switch (info.file.status) { - case 'done': - if (info.file.response.retcode === 0) { - message.error(info.file.response.retmsg); - formRef1.current?.setFieldValue('pbcVideoAddress', []) - } else { - const { data } = info.file.response - if (fileType === '1') { - setPlayAuth(data.playAuth) - setVideoId(data.videoId) - } - } - break; - default: - break; - } - }, - action: process.env.BASE_URL + (fileType === '1' ? '/b2b2c/pbcTrainingClassesVideo/uploadVideoAndGetInfo' : '/oss/imgUpload'), - onPreview: async (file) => { - if (fileType === '1') { - if (file.uid === '-1' && stepFormValues1.pbcId) { - getVideoAuthByIdUsingGet({pbcId: stepFormValues1.pbcId}).then(res => { - if (res.retcode && res.data && file.url) { - setPlayAuth(res.data) - setVideoId(file.url) - handleShowVideo(true) - } else { - message.error(res.retmsg) - } - }) + {fileType === '1' ? ( + + { + // formRef1.current?.setFieldValue('pbcVideoAddress', fileList); + // }} + onSuccess={(data) => { + console.log(data) + if (data) { + setPlayAuth(data.playAuth); + setVideoId(data.videoId); } - if (file.response && file.response.retcode) { - const { data } = file.response - setPlayAuth(data.playAuth) - setVideoId(data.videoId) - handleShowVideo(true) + }} + maxCount={1} + maxSize={3000} + /> + + ) : ( + { + switch (info.file.status) { + case 'done': + if (info.file.response.retcode === 0) { + message.error(info.file.response.retmsg); + formRef1.current?.setFieldValue('pbcVideoAddress', []) + } + break; + default: + break; } - } else { + }, + action: process.env.BASE_URL + '/oss/imgUpload', + onPreview: async (file) => { if (file.uid === '-1') { window.open(file.url); } if (file.response && file.response.retcode) { window.open(file.response.data); } - } - }, - progress: { - strokeColor: { - '0%': '#108ee9', - '100%': '#87d068', }, - strokeWidth: 3, - format: (percent) => percent && `${parseFloat(percent.toFixed(2))}%`, - }, - }} - rules={[ - { required: true, message: fileType === '1' ? '请上传视频' : '请上传文件' }, - ]} - /> - + progress: { + strokeColor: { + '0%': '#108ee9', + '100%': '#87d068', + }, + strokeWidth: 3, + format: (percent) => percent && `${parseFloat(percent.toFixed(2))}%`, + }, + }} + rules={[ + { required: true, message: '请上传文件' }, + ]} + /> + )} = (props) => { return props.onSubmit({ ...value, pbcUserRoleName: roleName, pbcUserType: 2, pbcId: props.values.pbcId }) }} drawerProps={{ - destroyOnClose: true, + destroyOnHidden: true, }} initialValues={{ pbcUserName: props.values.pbcUserName, diff --git a/src/services/pop-b2b2c/errorController.ts b/src/services/pop-b2b2c/errorController.ts index 5f0967b..f29cc7f 100644 --- a/src/services/pop-b2b2c/errorController.ts +++ b/src/services/pop-b2b2c/errorController.ts @@ -2,41 +2,41 @@ /* eslint-disable */ import request from '@/utils/request'; -/** errorHtml GET /error */ -export async function errorHtmlUsingGet(options?: { [key: string]: any }) { - return request('/error', { +/** error GET /error */ +export async function errorUsingGet(options?: { [key: string]: any }) { + return request>('/error', { method: 'GET', ...(options || {}), }); } -/** errorHtml PUT /error */ -export async function errorHtmlUsingPut(options?: { [key: string]: any }) { - return request('/error', { +/** error PUT /error */ +export async function errorUsingPut(options?: { [key: string]: any }) { + return request>('/error', { method: 'PUT', ...(options || {}), }); } -/** errorHtml POST /error */ -export async function errorHtmlUsingPost(options?: { [key: string]: any }) { - return request('/error', { +/** error POST /error */ +export async function errorUsingPost(options?: { [key: string]: any }) { + return request>('/error', { method: 'POST', ...(options || {}), }); } -/** errorHtml DELETE /error */ -export async function errorHtmlUsingDelete(options?: { [key: string]: any }) { - return request('/error', { +/** error DELETE /error */ +export async function errorUsingDelete(options?: { [key: string]: any }) { + return request>('/error', { method: 'DELETE', ...(options || {}), }); } -/** errorHtml PATCH /error */ -export async function errorHtmlUsingPatch(options?: { [key: string]: any }) { - return request('/error', { +/** error PATCH /error */ +export async function errorUsingPatch(options?: { [key: string]: any }) { + return request>('/error', { method: 'PATCH', ...(options || {}), }); diff --git a/src/services/pop-b2b2c/index.ts b/src/services/pop-b2b2c/index.ts index 8950f2a..28427b3 100644 --- a/src/services/pop-b2b2c/index.ts +++ b/src/services/pop-b2b2c/index.ts @@ -107,8 +107,8 @@ export default { pbcUserMessageController, pbcUsersController, pbcUserRecordLogController, + pbcVodController, wxController, errorController, pbcOssImgController, - pbcVodController, }; diff --git a/src/services/pop-b2b2c/pbcBusinessController.ts b/src/services/pop-b2b2c/pbcBusinessController.ts index caf58ee..48d6b91 100644 --- a/src/services/pop-b2b2c/pbcBusinessController.ts +++ b/src/services/pop-b2b2c/pbcBusinessController.ts @@ -51,6 +51,21 @@ export async function frontChangeBusinessInfoUsingPost( }); } +/** 通过店铺id得到联系人的账号 GET /b2b2c/pbcbusiness/getBusinessContactInfo */ +export async function getBusinessContactInfoUsingGet( + // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) + params: API.getBusinessContactInfoUsingGETParams, + options?: { [key: string]: any }, +) { + return request('/b2b2c/pbcbusiness/getBusinessContactInfo', { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} + /** 取得商户的图片,用以合成海报 GET /b2b2c/pbcbusiness/getBusinessImage */ export async function getBusinessImageUsingGet( // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) diff --git a/src/services/pop-b2b2c/pbcVodController.ts b/src/services/pop-b2b2c/pbcVodController.ts index 57637d8..81947a8 100644 --- a/src/services/pop-b2b2c/pbcVodController.ts +++ b/src/services/pop-b2b2c/pbcVodController.ts @@ -2,38 +2,68 @@ /* eslint-disable */ import request from '@/utils/request'; -/** 根据video id删除视频,后端自用 DELETE /vodFile/deleteAliyunVideo/${param0} */ +/** 获得音视频上传地址和凭证 POST /b2b2c/vodFile/createUploadVideo */ +export async function createUploadVideoUsingPost( + body: API.GetVideoPlayAuthDTO, + options?: { [key: string]: any }, +) { + return request('/b2b2c/vodFile/createUploadVideo', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** 根据video id删除视频,后端自用 DELETE /b2b2c/vodFile/deleteAliyunVideo/${param0} */ export async function deleteAliyunVideoUsingDelete( // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) params: API.deleteAliyunVideoUsingDELETEParams, options?: { [key: string]: any }, ) { const { id: param0, ...queryParams } = params; - return request(`/vodFile/deleteAliyunVideo/${param0}`, { + return request(`/b2b2c/vodFile/deleteAliyunVideo/${param0}`, { method: 'DELETE', params: { ...queryParams }, ...(options || {}), }); } -/** 根据video id获取播放凭证,后端自用 DELETE /vodFile/getVideoAuthByVideoId/${param0} */ -export async function getVideoAuthByVideoIdUsingDelete( +/** 根据video id获取播放凭证,后端自用 GET /b2b2c/vodFile/getVideoAuthByVideoId/${param0} */ +export async function getVideoAuthByVideoIdUsingGet( // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) - params: API.getVideoAuthByVideoIdUsingDELETEParams, + params: API.getVideoAuthByVideoIdUsingGETParams, options?: { [key: string]: any }, ) { const { id: param0, ...queryParams } = params; return request( - `/vodFile/getVideoAuthByVideoId/${param0}`, + `/b2b2c/vodFile/getVideoAuthByVideoId/${param0}`, { - method: 'DELETE', + method: 'GET', params: { ...queryParams }, ...(options || {}), }, ); } -/** 上传视频到vod,测试用 POST /vodFile/upload */ +/** 刷新视频上传凭证 GET /b2b2c/vodFile/refreshUploadVideo */ +export async function refreshUploadVideoUsingGet( + // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) + params: API.refreshUploadVideoUsingGETParams, + options?: { [key: string]: any }, +) { + return request('/b2b2c/vodFile/refreshUploadVideo', { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} + +/** 上传视频到vod,测试用 POST /b2b2c/vodFile/upload */ export async function uploadVideoUsingPost( body: {}, file?: File, @@ -61,7 +91,7 @@ export async function uploadVideoUsingPost( } }); - return request('/vodFile/upload', { + return request('/b2b2c/vodFile/upload', { method: 'POST', data: formData, requestType: 'form', diff --git a/src/services/pop-b2b2c/typings.d.ts b/src/services/pop-b2b2c/typings.d.ts index c5161fe..4c0d27e 100644 --- a/src/services/pop-b2b2c/typings.d.ts +++ b/src/services/pop-b2b2c/typings.d.ts @@ -825,6 +825,8 @@ declare namespace API { type createQrCodeUsingGETParams = { /** 类型 */ codeType: string; + /** parameterVal ue */ + 'parameterVal ue': string; /** 参数值 */ parameterValue: string; }; @@ -928,6 +930,11 @@ declare namespace API { pbcBannerType: number; }; + type getBusinessContactInfoUsingGETParams = { + /** businessId */ + businessId: number; + }; + type getBusinessImageUsingGETParams = { /** businessId */ businessId: number; @@ -1040,11 +1047,36 @@ declare namespace API { pbcId: number; }; - type getVideoAuthByVideoIdUsingDELETEParams = { + type getVideoAuthByVideoIdUsingGETParams = { /** id */ id: string; }; + type GetVideoPlayAuthDTO = { + /** 分类ID(可从点播控制台或API获取) */ + cateId?: number; + /** 自定义视频封面URL地址 */ + coverURL?: string; + /** 视频描述(上传后展示在点播控制台),UTF-8编码,最长1024字符 */ + description?: string; + /** 待上传的视频文件地址(必须带扩展名,如.mp4) */ + fileName: string; + /** 视频文件大小(单位:字节) */ + fileSize?: number; + /** 存储地址(不指定则使用默认地址) */ + storageLocation?: string; + /** 视频标签(多个标签用英文逗号分隔,最多16个,每个最长32字符) */ + tags?: string; + /** 转码模板组ID(通过控制台或API获取)。若同时指定WorkflowId,则以WorkflowId为准 */ + templateGroupId?: string; + /** 视频标题(上传后展示在点播控制台),UTF-8编码,最长128字符 */ + title: string; + /** 自定义用户数据(JSON格式),支持回调通知和上传加速。注意:回调需先在控制台配置地址 */ + userData?: string; + /** 工作流ID(通过点播控制台获取),优先级高于TemplateGroupId */ + workflowId?: string; + }; + type GetVideoPlayAuthResponse = { playAuth?: string; requestId?: string; @@ -3149,6 +3181,8 @@ declare namespace API { current?: number; /** 条数 */ pageSize?: number; + /** 商户id */ + pbcBusinessId?: number; /** 产地城市 */ pbcProductOriginalCity?: string; /** 产地城市编码 */ @@ -4543,6 +4577,16 @@ declare namespace API { expressNo: string; }; + type receiveUsingGETParams = { + /** echostr */ + echostr: string; + }; + + type refreshUploadVideoUsingGETParams = { + /** videoId */ + videoId: string; + }; + type removeAddressUsingGETParams = { /** pbcId */ pbcId: number; diff --git a/src/services/pop-b2b2c/wxController.ts b/src/services/pop-b2b2c/wxController.ts index 55c6b10..53fd181 100644 --- a/src/services/pop-b2b2c/wxController.ts +++ b/src/services/pop-b2b2c/wxController.ts @@ -39,3 +39,18 @@ export async function getWxSignUsingGet1( ...(options || {}), }); } + +/** 获取微信签名sign的参数 获取微信签名sign的参数 GET /b2b2c/wx/receive */ +export async function receiveUsingGet( + // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) + params: API.receiveUsingGETParams, + options?: { [key: string]: any }, +) { + return request('/b2b2c/wx/receive', { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} diff --git a/src/types/aliyun-upload-vod.d.ts b/src/types/aliyun-upload-vod.d.ts new file mode 100644 index 0000000..adfd53d --- /dev/null +++ b/src/types/aliyun-upload-vod.d.ts @@ -0,0 +1,40 @@ +declare module 'aliyun-upload-vod' { + interface VODUploadOptions { + userId: string; + region?: string; + partSize?: number; + parallel?: number; + retryCount?: number; + retryDuration?: number; + addFileSuccess?: () => void; + onUploadstarted?: (uploadInfo: any) => void; + onUploadSucceed?: (uploadInfo: any) => void; + onUploadFailed?: (uploadInfo: any, code: string, message: string) => void; + onUploadProgress?: (uploadInfo: any, totalSize: number, loadedPercent: number) => void; + onUploadTokenExpired?: () => void; + onUploadEnd?: () => void; + } + + interface UploadInfo { + videoId?: string; + duration?: string; + [key: string]: any; + } + + class Vod { + constructor(options: VODUploadOptions); + addFile(file: File, endpoint?: string, bucket?: string, object?: string, userData?: string): void; + startUpload(): void; + stopUpload(): void; + pauseUpload(): void; + resumeUpload(): void; + setUploadAuthAndAddress(uploadInfo: any, uploadAuth: string, uploadAddress: string, videoId: string): void; + } + + interface AliyunUpload { + Vod: typeof Vod; + } + + const AliyunUpload: AliyunUpload; + export = AliyunUpload; +} \ No newline at end of file