mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-07 00:27:44 +08:00
feat(Config): add backup and restore configuration functionality
This commit is contained in:
@@ -87,4 +87,39 @@ public class ConfigController(IConfigService configService) : BaseApiController
|
|||||||
return Error<bool>($"删除配置失败: {ex.Message}", 500);
|
return Error<bool>($"删除配置失败: {ex.Message}", 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("backup")]
|
||||||
|
public async Task<ActionResult<BaseResult<Dictionary<string, string>>>> BackupConfigs()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var backup = await configService.BackupConfigsAsync();
|
||||||
|
return Success(backup, "配置备份成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Error<Dictionary<string, string>>($"配置备份失败: {ex.Message}", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("restore")]
|
||||||
|
public async Task<ActionResult<BaseResult<bool>>> RestoreConfigs([FromBody] Dictionary<string, string> configBackup)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (configBackup == null || configBackup.Count == 0)
|
||||||
|
return Error<bool>("配置备份数据无效");
|
||||||
|
|
||||||
|
var result = await configService.RestoreConfigsAsync(configBackup);
|
||||||
|
|
||||||
|
if (result)
|
||||||
|
return Success(true, "配置恢复成功");
|
||||||
|
else
|
||||||
|
return Error<bool>("配置恢复失败", 500);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Error<bool>($"配置恢复失败: {ex.Message}", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -136,4 +136,50 @@ public class ConfigService(
|
|||||||
await using var context = await contextFactory.CreateDbContextAsync();
|
await using var context = await contextFactory.CreateDbContextAsync();
|
||||||
return await context.Configs.AnyAsync(c => c.Key == key);
|
return await context.Configs.AnyAsync(c => c.Key == key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, string>> BackupConfigsAsync()
|
||||||
|
{
|
||||||
|
var configs = await GetAllConfigsAsync();
|
||||||
|
var backup = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
foreach (var config in configs)
|
||||||
|
{
|
||||||
|
backup[config.Key] = config.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RestoreConfigsAsync(Dictionary<string, string> configBackup)
|
||||||
|
{
|
||||||
|
if (configBackup == null || configBackup.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync();
|
||||||
|
await using var transaction = await context.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var (key, value) in configBackup)
|
||||||
|
{
|
||||||
|
await SetConfigAsync(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "恢复配置时出错");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,8 @@ public interface IConfigService
|
|||||||
Task<bool> DeleteConfigAsync(string key);
|
Task<bool> DeleteConfigAsync(string key);
|
||||||
|
|
||||||
Task<bool> ExistsAsync(string key);
|
Task<bool> ExistsAsync(string key);
|
||||||
|
|
||||||
|
Task<Dictionary<string, string>> BackupConfigsAsync();
|
||||||
|
|
||||||
|
Task<bool> RestoreConfigsAsync(Dictionary<string, string> configBackup);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,3 +72,32 @@ export const hasRole = (userRole: string | undefined, requiredRole: UserRole): b
|
|||||||
// 精确匹配角色
|
// 精确匹配角色
|
||||||
return userRole === requiredRole;
|
return userRole === requiredRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 备份所有配置
|
||||||
|
export const backupConfigs = async (): Promise<BaseResult<Record<string, string>>> => {
|
||||||
|
try {
|
||||||
|
return await fetchApi<Record<string, string>>('/config/backup');
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `备份配置失败: ${error.message}`,
|
||||||
|
code: 500
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 恢复配置
|
||||||
|
export const restoreConfigs = async (configBackup: Record<string, string>): Promise<BaseResult<boolean>> => {
|
||||||
|
try {
|
||||||
|
return await fetchApi<boolean>('/config/restore', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(configBackup),
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `恢复配置失败: ${error.message}`,
|
||||||
|
code: 500
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export {
|
|||||||
getConfig,
|
getConfig,
|
||||||
setConfig,
|
setConfig,
|
||||||
deleteConfig,
|
deleteConfig,
|
||||||
hasRole
|
hasRole,
|
||||||
|
backupConfigs,
|
||||||
|
restoreConfigs
|
||||||
} from './configApi';
|
} from './configApi';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Tabs, Card, message, Spin, Select } from 'antd';
|
import { Tabs, Card, message, Spin, Select, Button, Upload, Modal, Space, Tooltip } from 'antd';
|
||||||
import { CloudOutlined, DatabaseOutlined, CloudServerOutlined, GlobalOutlined } from '@ant-design/icons';
|
import { CloudOutlined, DatabaseOutlined, CloudServerOutlined, GlobalOutlined, DownloadOutlined, UploadOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
import { getAllConfigs, setConfig } from '../../api';
|
import { getAllConfigs, setConfig, backupConfigs, restoreConfigs } from '../../api';
|
||||||
import ConfigGroup from './ConfigGroup.tsx';
|
import ConfigGroup from './ConfigGroup.tsx';
|
||||||
import useIsMobile from '../../hooks/useIsMobile';
|
import useIsMobile from '../../hooks/useIsMobile';
|
||||||
|
|
||||||
@@ -20,6 +20,10 @@ const SystemConfig: React.FC = () => {
|
|||||||
const [configs, setConfigs] = useState<ConfigStructure>({});
|
const [configs, setConfigs] = useState<ConfigStructure>({});
|
||||||
const [activeKey, setActiveKey] = useState('AI');
|
const [activeKey, setActiveKey] = useState('AI');
|
||||||
const [storageType, setStorageType] = useState('Telegram');
|
const [storageType, setStorageType] = useState('Telegram');
|
||||||
|
const [backupLoading, setBackupLoading] = useState(false);
|
||||||
|
const [restoreLoading, setRestoreLoading] = useState(false);
|
||||||
|
const [restoreModalVisible, setRestoreModalVisible] = useState(false);
|
||||||
|
const [restoreConfig, setRestoreConfig] = useState<Record<string, string> | null>(null);
|
||||||
|
|
||||||
// 获取所有配置项
|
// 获取所有配置项
|
||||||
const fetchConfigs = async () => {
|
const fetchConfigs = async () => {
|
||||||
@@ -82,6 +86,83 @@ const SystemConfig: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 备份配置
|
||||||
|
const handleBackupConfigs = async () => {
|
||||||
|
setBackupLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await backupConfigs();
|
||||||
|
if (response.success && response.data) {
|
||||||
|
const configData = JSON.stringify(response.data, null, 2);
|
||||||
|
const blob = new Blob([configData], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
a.download = `foxel-config-backup-${timestamp}.json`;
|
||||||
|
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
message.success('配置备份已下载');
|
||||||
|
} else {
|
||||||
|
message.error('备份配置失败: ' + response.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('备份配置出错');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setBackupLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传配置文件
|
||||||
|
const handleFileUpload = (file: File) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const content = e.target?.result as string;
|
||||||
|
const config = JSON.parse(content);
|
||||||
|
setRestoreConfig(config);
|
||||||
|
setRestoreModalVisible(true);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('无效的配置文件格式');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
return false; // 阻止自动上传
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认恢复配置
|
||||||
|
const handleRestoreConfigs = async () => {
|
||||||
|
if (!restoreConfig) return;
|
||||||
|
|
||||||
|
setRestoreLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await restoreConfigs(restoreConfig);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('配置恢复成功,将在3秒后刷新页面');
|
||||||
|
setRestoreModalVisible(false);
|
||||||
|
|
||||||
|
// 重新加载配置
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchConfigs();
|
||||||
|
// 可选:刷新页面以确保所有配置生效
|
||||||
|
// window.location.reload();
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
message.error('恢复配置失败: ' + response.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('恢复配置出错');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setRestoreLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 存储类型选项
|
// 存储类型选项
|
||||||
const storageOptions = [
|
const storageOptions = [
|
||||||
{ value: 'Local', label: '本地存储', icon: <DatabaseOutlined style={{ color: '#52c41a' }} /> },
|
{ value: 'Local', label: '本地存储', icon: <DatabaseOutlined style={{ color: '#52c41a' }} /> },
|
||||||
@@ -97,7 +178,38 @@ const SystemConfig: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title="系统配置"
|
title={
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<span>系统配置</span>
|
||||||
|
<Space>
|
||||||
|
<Tooltip title="下载当前所有配置的备份">
|
||||||
|
<Button
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
onClick={handleBackupConfigs}
|
||||||
|
loading={backupLoading}
|
||||||
|
size={isMobile ? "small" : "middle"}
|
||||||
|
>
|
||||||
|
{isMobile ? '' : '备份配置'}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Upload
|
||||||
|
beforeUpload={handleFileUpload}
|
||||||
|
showUploadList={false}
|
||||||
|
accept=".json"
|
||||||
|
>
|
||||||
|
<Tooltip title="从备份文件恢复配置">
|
||||||
|
<Button
|
||||||
|
icon={<UploadOutlined />}
|
||||||
|
size={isMobile ? "small" : "middle"}
|
||||||
|
>
|
||||||
|
{isMobile ? '' : '恢复配置'}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Upload>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
className="system-config-card"
|
className="system-config-card"
|
||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
padding: isMobile ? '12px 8px' : '24px'
|
padding: isMobile ? '12px 8px' : '24px'
|
||||||
@@ -427,6 +539,43 @@ const SystemConfig: React.FC = () => {
|
|||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 恢复配置确认对话框 */}
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div>
|
||||||
|
<span>确认恢复配置</span>
|
||||||
|
<Tooltip title="恢复配置将覆盖当前所有配置设置,请确认备份文件正确无误">
|
||||||
|
<QuestionCircleOutlined style={{ marginLeft: 8 }} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open={restoreModalVisible}
|
||||||
|
onCancel={() => setRestoreModalVisible(false)}
|
||||||
|
footer={[
|
||||||
|
<>
|
||||||
|
<Button key="cancel" onClick={() => setRestoreModalVisible(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
loading={restoreLoading}
|
||||||
|
onClick={handleRestoreConfigs}
|
||||||
|
>
|
||||||
|
确认恢复
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<p>您确定要从上传的备份文件恢复配置吗?</p>
|
||||||
|
<p style={{ color: '#ff4d4f' }}>警告:此操作将覆盖当前系统中的所有配置设置!</p>
|
||||||
|
{restoreConfig && (
|
||||||
|
<div>
|
||||||
|
<p>备份文件包含 {Object.keys(restoreConfig).length} 条配置项</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user