此提交实现了完整的腾讯云COS存储集成方案: 1. 新增COS配置管理页面与后端接口,支持配置的增改查、连接测试和历史文件批量迁移 2. 新增COS配置实体、Mapper和VO类,完善数据库表结构初始化脚本 3. 改造媒体下载接口,优先返回COS重定向链接,失败则回退本地读取 4. 新增自动上传逻辑,新产生的媒体文件会自动同步到COS 5. 新增未上传COS的历史媒体文件查询接口,支持批量迁移本地存量文件 6. 引入腾讯云COS SDK依赖,添加侧边栏COS配置菜单入口
225 lines
7.1 KiB
TypeScript
225 lines
7.1 KiB
TypeScript
import { useState, useEffect } from 'react'
|
||
import { Card, Form, Input, Switch, Button, message, Space, Tag } from 'antd'
|
||
import { CloudUploadOutlined, SaveOutlined, ApiOutlined } from '@ant-design/icons'
|
||
import request from '../utils/request'
|
||
|
||
interface CosConfigForm {
|
||
id?: number
|
||
secretId: string
|
||
secretKey: string
|
||
bucket: string
|
||
region: string
|
||
pathPrefix: string
|
||
domain: string
|
||
enabled: boolean
|
||
}
|
||
|
||
export default function CosConfig() {
|
||
const [form] = Form.useForm()
|
||
const [loading, setLoading] = useState(false)
|
||
const [testLoading, setTestLoading] = useState(false)
|
||
const [migrateLoading, setMigrateLoading] = useState(false)
|
||
const [migratedCount, setMigratedCount] = useState<number | null>(null)
|
||
const [hasSecretKey, setHasSecretKey] = useState(false)
|
||
|
||
const loadConfig = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const res: any = await request.get('/v1/archive/cos-config')
|
||
const data = res.data
|
||
if (data) {
|
||
// 后端返回的 secretKey 是脱敏的,不能作为表单默认值,否则保存时会把掩码存回去
|
||
const { secretKey, ...rest } = data
|
||
form.setFieldsValue({
|
||
...rest,
|
||
enabled: data.enabled === 1,
|
||
})
|
||
// 如果返回的 SecretKey 是脱敏的(包含*),说明后端有值
|
||
setHasSecretKey(secretKey && secretKey.includes('*'))
|
||
} else {
|
||
form.setFieldsValue({
|
||
pathPrefix: 'archive/media/',
|
||
enabled: false,
|
||
})
|
||
}
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
loadConfig()
|
||
}, [])
|
||
|
||
const handleSave = async (values: CosConfigForm) => {
|
||
const payload = {
|
||
...values,
|
||
enabled: values.enabled ? 1 : 0,
|
||
}
|
||
// 如果 SecretKey 为空且已有配置,则保留原值由后端处理
|
||
if (!payload.secretKey && hasSecretKey) {
|
||
payload.secretKey = undefined as any
|
||
}
|
||
|
||
try {
|
||
await request.post('/v1/archive/cos-config', payload)
|
||
message.success('保存成功')
|
||
loadConfig()
|
||
} catch (e: any) {
|
||
message.error(e.response?.data?.message || '保存失败')
|
||
}
|
||
}
|
||
|
||
const handleTest = async () => {
|
||
const values = form.getFieldsValue()
|
||
if (!values.secretId || !values.bucket || !values.region) {
|
||
message.warning('请填写 SecretId、Bucket、Region')
|
||
return
|
||
}
|
||
// 如果 SecretKey 为空且已有原值,提示用户输入 SecretKey 后再测试
|
||
if (!values.secretKey && hasSecretKey) {
|
||
message.warning('已保存 SecretKey,但测试连接需要重新输入 SecretKey')
|
||
return
|
||
}
|
||
setTestLoading(true)
|
||
try {
|
||
const res: any = await request.post('/v1/archive/cos-config/test', {
|
||
secretId: values.secretId,
|
||
secretKey: values.secretKey,
|
||
bucket: values.bucket,
|
||
region: values.region,
|
||
})
|
||
if (res.data === true) {
|
||
message.success('COS 连接测试成功')
|
||
} else {
|
||
message.error('COS 连接测试失败,请检查配置')
|
||
}
|
||
} catch (e: any) {
|
||
message.error(e.response?.data?.message || '测试失败')
|
||
} finally {
|
||
setTestLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleMigrate = async () => {
|
||
setMigrateLoading(true)
|
||
setMigratedCount(null)
|
||
try {
|
||
const res: any = await request.post('/v1/archive/cos-config/migrate?batchSize=50')
|
||
setMigratedCount(res.data || 0)
|
||
message.success(`批量迁移完成,共迁移 ${res.data || 0} 个文件`)
|
||
} catch (e: any) {
|
||
message.error(e.response?.data?.message || '迁移失败')
|
||
} finally {
|
||
setMigrateLoading(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<Card title="腾讯云 COS 配置" loading={loading}>
|
||
<Form
|
||
form={form}
|
||
layout="vertical"
|
||
onFinish={handleSave}
|
||
initialValues={{ pathPrefix: 'archive/media/', enabled: false }}
|
||
>
|
||
<Form.Item
|
||
label="SecretId"
|
||
name="secretId"
|
||
rules={[{ required: true, message: '请输入 SecretId' }]}
|
||
>
|
||
<Input placeholder="腾讯云 SecretId" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
label="SecretKey"
|
||
name="secretKey"
|
||
rules={[{ required: !hasSecretKey, message: '请输入 SecretKey' }]}
|
||
extra={hasSecretKey ? '已保存 SecretKey,如需修改请重新填写' : undefined}
|
||
>
|
||
<Input.Password placeholder="腾讯云 SecretKey" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
label="Bucket"
|
||
name="bucket"
|
||
rules={[{ required: true, message: '请输入 Bucket 名称' }]}
|
||
>
|
||
<Input placeholder="存储桶名称,如 my-bucket-1250000000" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
label="Region"
|
||
name="region"
|
||
rules={[{ required: true, message: '请输入 Region' }]}
|
||
>
|
||
<Input placeholder="地域,如 ap-shanghai" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
label="路径前缀"
|
||
name="pathPrefix"
|
||
>
|
||
<Input placeholder="COS 上的路径前缀,如 archive/media/" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
label="自定义域名(可选)"
|
||
name="domain"
|
||
>
|
||
<Input placeholder="CDN 或自定义域名,为空则使用默认 COS 域名" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
label="启用 COS"
|
||
name="enabled"
|
||
valuePropName="checked"
|
||
>
|
||
<Switch checkedChildren="启用" unCheckedChildren="停用" />
|
||
</Form.Item>
|
||
|
||
<Form.Item>
|
||
<Space>
|
||
<Button type="primary" htmlType="submit" icon={<SaveOutlined />}>
|
||
保存配置
|
||
</Button>
|
||
<Button icon={<ApiOutlined />} onClick={handleTest} loading={testLoading}>
|
||
测试连接
|
||
</Button>
|
||
</Space>
|
||
</Form.Item>
|
||
</Form>
|
||
</Card>
|
||
|
||
<Card style={{ marginTop: 16 }} title="历史媒体文件迁移">
|
||
<p style={{ color: '#666', marginBottom: 16 }}>
|
||
将已下载到本地的会话存档媒体文件批量上传到腾讯云 COS。上传完成后,后续访问将直接走 COS 链接。
|
||
</p>
|
||
<Button
|
||
type="primary"
|
||
icon={<CloudUploadOutlined />}
|
||
loading={migrateLoading}
|
||
onClick={handleMigrate}
|
||
>
|
||
开始批量迁移
|
||
</Button>
|
||
{migratedCount !== null && (
|
||
<Tag color="green" style={{ marginLeft: 12 }}>
|
||
本次迁移 {migratedCount} 个文件
|
||
</Tag>
|
||
)}
|
||
</Card>
|
||
|
||
<Card style={{ marginTop: 16 }} title="说明">
|
||
<ul style={{ color: '#666', paddingLeft: 20 }}>
|
||
<li>启用 COS 后,新产生的媒体文件会在下载完成后自动上传到 COS。</li>
|
||
<li>媒体访问接口 /api/v1/archive/media/{id} 会优先返回 COS URL(302 重定向)。</li>
|
||
<li>如果 COS 未启用或上传失败,会回退到本地文件读取。</li>
|
||
<li>自定义域名需以 http:// 或 https:// 开头,保存时会自动处理。</li>
|
||
</ul>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|