feat: enhance multi-key management with pagination and statistics
This commit is contained in:
@@ -71,6 +71,13 @@ func parseStatusFilter(statusParam string) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clearChannelInfo(channel *model.Channel) {
|
||||||
|
if channel.ChannelInfo.IsMultiKey {
|
||||||
|
channel.ChannelInfo.MultiKeyDisabledReason = nil
|
||||||
|
channel.ChannelInfo.MultiKeyDisabledTime = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetAllChannels(c *gin.Context) {
|
func GetAllChannels(c *gin.Context) {
|
||||||
pageInfo := common.GetPageQuery(c)
|
pageInfo := common.GetPageQuery(c)
|
||||||
channelData := make([]*model.Channel, 0)
|
channelData := make([]*model.Channel, 0)
|
||||||
@@ -145,6 +152,10 @@ func GetAllChannels(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, datum := range channelData {
|
||||||
|
clearChannelInfo(datum)
|
||||||
|
}
|
||||||
|
|
||||||
countQuery := model.DB.Model(&model.Channel{})
|
countQuery := model.DB.Model(&model.Channel{})
|
||||||
if statusFilter == common.ChannelStatusEnabled {
|
if statusFilter == common.ChannelStatusEnabled {
|
||||||
countQuery = countQuery.Where("status = ?", common.ChannelStatusEnabled)
|
countQuery = countQuery.Where("status = ?", common.ChannelStatusEnabled)
|
||||||
@@ -371,6 +382,10 @@ func SearchChannels(c *gin.Context) {
|
|||||||
|
|
||||||
pagedData := channelData[startIdx:endIdx]
|
pagedData := channelData[startIdx:endIdx]
|
||||||
|
|
||||||
|
for _, datum := range pagedData {
|
||||||
|
clearChannelInfo(datum)
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
@@ -394,6 +409,9 @@ func GetChannel(c *gin.Context) {
|
|||||||
common.ApiError(c, err)
|
common.ApiError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if channel != nil {
|
||||||
|
clearChannelInfo(channel)
|
||||||
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
@@ -827,6 +845,7 @@ func UpdateChannel(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
model.InitChannelCache()
|
model.InitChannelCache()
|
||||||
channel.Key = ""
|
channel.Key = ""
|
||||||
|
clearChannelInfo(&channel.Channel)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
@@ -1036,11 +1055,21 @@ type MultiKeyManageRequest struct {
|
|||||||
ChannelId int `json:"channel_id"`
|
ChannelId int `json:"channel_id"`
|
||||||
Action string `json:"action"` // "disable_key", "enable_key", "delete_disabled_keys", "get_key_status"
|
Action string `json:"action"` // "disable_key", "enable_key", "delete_disabled_keys", "get_key_status"
|
||||||
KeyIndex *int `json:"key_index,omitempty"` // for disable_key and enable_key actions
|
KeyIndex *int `json:"key_index,omitempty"` // for disable_key and enable_key actions
|
||||||
|
Page int `json:"page,omitempty"` // for get_key_status pagination
|
||||||
|
PageSize int `json:"page_size,omitempty"` // for get_key_status pagination
|
||||||
}
|
}
|
||||||
|
|
||||||
// MultiKeyStatusResponse represents the response for key status query
|
// MultiKeyStatusResponse represents the response for key status query
|
||||||
type MultiKeyStatusResponse struct {
|
type MultiKeyStatusResponse struct {
|
||||||
Keys []KeyStatus `json:"keys"`
|
Keys []KeyStatus `json:"keys"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
// Statistics
|
||||||
|
EnabledCount int `json:"enabled_count"`
|
||||||
|
ManualDisabledCount int `json:"manual_disabled_count"`
|
||||||
|
AutoDisabledCount int `json:"auto_disabled_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyStatus struct {
|
type KeyStatus struct {
|
||||||
@@ -1080,8 +1109,35 @@ func ManageMultiKeys(c *gin.Context) {
|
|||||||
switch request.Action {
|
switch request.Action {
|
||||||
case "get_key_status":
|
case "get_key_status":
|
||||||
keys := channel.GetKeys()
|
keys := channel.GetKeys()
|
||||||
var keyStatusList []KeyStatus
|
total := len(keys)
|
||||||
|
|
||||||
|
// Default pagination parameters
|
||||||
|
page := request.Page
|
||||||
|
pageSize := request.PageSize
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 50 // Default page size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate pagination
|
||||||
|
totalPages := (total + pageSize - 1) / pageSize
|
||||||
|
if page > totalPages && totalPages > 0 {
|
||||||
|
page = totalPages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate range
|
||||||
|
start := (page - 1) * pageSize
|
||||||
|
end := start + pageSize
|
||||||
|
if end > total {
|
||||||
|
end = total
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistics for all keys
|
||||||
|
var enabledCount, manualDisabledCount, autoDisabledCount int
|
||||||
|
|
||||||
|
var keyStatusList []KeyStatus
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
status := 1 // default enabled
|
status := 1 // default enabled
|
||||||
var disabledTime int64
|
var disabledTime int64
|
||||||
@@ -1093,34 +1149,56 @@ func ManageMultiKeys(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if status != 1 {
|
// Count for statistics
|
||||||
if channel.ChannelInfo.MultiKeyDisabledTime != nil {
|
switch status {
|
||||||
disabledTime = channel.ChannelInfo.MultiKeyDisabledTime[i]
|
case 1:
|
||||||
}
|
enabledCount++
|
||||||
if channel.ChannelInfo.MultiKeyDisabledReason != nil {
|
case 2:
|
||||||
reason = channel.ChannelInfo.MultiKeyDisabledReason[i]
|
manualDisabledCount++
|
||||||
}
|
case 3:
|
||||||
|
autoDisabledCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create key preview (first 10 chars)
|
// Only include keys in current page
|
||||||
keyPreview := key
|
if i >= start && i < end {
|
||||||
if len(key) > 10 {
|
if status != 1 {
|
||||||
keyPreview = key[:10] + "..."
|
if channel.ChannelInfo.MultiKeyDisabledTime != nil {
|
||||||
}
|
disabledTime = channel.ChannelInfo.MultiKeyDisabledTime[i]
|
||||||
|
}
|
||||||
|
if channel.ChannelInfo.MultiKeyDisabledReason != nil {
|
||||||
|
reason = channel.ChannelInfo.MultiKeyDisabledReason[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
keyStatusList = append(keyStatusList, KeyStatus{
|
// Create key preview (first 10 chars)
|
||||||
Index: i,
|
keyPreview := key
|
||||||
Status: status,
|
if len(key) > 10 {
|
||||||
DisabledTime: disabledTime,
|
keyPreview = key[:10] + "..."
|
||||||
Reason: reason,
|
}
|
||||||
KeyPreview: keyPreview,
|
|
||||||
})
|
keyStatusList = append(keyStatusList, KeyStatus{
|
||||||
|
Index: i,
|
||||||
|
Status: status,
|
||||||
|
DisabledTime: disabledTime,
|
||||||
|
Reason: reason,
|
||||||
|
KeyPreview: keyPreview,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": MultiKeyStatusResponse{Keys: keyStatusList},
|
"data": MultiKeyStatusResponse{
|
||||||
|
Keys: keyStatusList,
|
||||||
|
Total: total,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
TotalPages: totalPages,
|
||||||
|
EnabledCount: enabledCount,
|
||||||
|
ManualDisabledCount: manualDisabledCount,
|
||||||
|
AutoDisabledCount: autoDisabledCount,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -53,12 +53,12 @@ type Channel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChannelInfo struct {
|
type ChannelInfo struct {
|
||||||
IsMultiKey bool `json:"is_multi_key"` // 是否多Key模式
|
IsMultiKey bool `json:"is_multi_key"` // 是否多Key模式
|
||||||
MultiKeySize int `json:"multi_key_size"` // 多Key模式下的Key数量
|
MultiKeySize int `json:"multi_key_size"` // 多Key模式下的Key数量
|
||||||
MultiKeyStatusList map[int]int `json:"multi_key_status_list"` // key状态列表,key index -> status
|
MultiKeyStatusList map[int]int `json:"multi_key_status_list"` // key状态列表,key index -> status
|
||||||
MultiKeyDisabledReason map[int]string `json:"multi_key_disabled_reason"` // key禁用原因列表,key index -> reason
|
MultiKeyDisabledReason map[int]string `json:"multi_key_disabled_reason,omitempty"` // key禁用原因列表,key index -> reason
|
||||||
MultiKeyDisabledTime map[int]int64 `json:"multi_key_disabled_time"` // key禁用时间列表,key index -> time
|
MultiKeyDisabledTime map[int]int64 `json:"multi_key_disabled_time,omitempty"` // key禁用时间列表,key index -> time
|
||||||
MultiKeyPollingIndex int `json:"multi_key_polling_index"` // 多Key模式下轮询的key索引
|
MultiKeyPollingIndex int `json:"multi_key_polling_index"` // 多Key模式下轮询的key索引
|
||||||
MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"`
|
MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ import {
|
|||||||
Popconfirm,
|
Popconfirm,
|
||||||
Empty,
|
Empty,
|
||||||
Spin,
|
Spin,
|
||||||
Banner
|
Banner,
|
||||||
|
Select,
|
||||||
|
Pagination
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
@@ -54,23 +56,47 @@ const MultiKeyManageModal = ({
|
|||||||
const [keyStatusList, setKeyStatusList] = useState([]);
|
const [keyStatusList, setKeyStatusList] = useState([]);
|
||||||
const [operationLoading, setOperationLoading] = useState({});
|
const [operationLoading, setOperationLoading] = useState({});
|
||||||
|
|
||||||
|
// Pagination states
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(50);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [totalPages, setTotalPages] = useState(0);
|
||||||
|
|
||||||
|
// Statistics states
|
||||||
|
const [enabledCount, setEnabledCount] = useState(0);
|
||||||
|
const [manualDisabledCount, setManualDisabledCount] = useState(0);
|
||||||
|
const [autoDisabledCount, setAutoDisabledCount] = useState(0);
|
||||||
|
|
||||||
// Load key status data
|
// Load key status data
|
||||||
const loadKeyStatus = async () => {
|
const loadKeyStatus = async (page = currentPage, size = pageSize) => {
|
||||||
if (!channel?.id) return;
|
if (!channel?.id) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await API.post('/api/channel/multi_key/manage', {
|
const res = await API.post('/api/channel/multi_key/manage', {
|
||||||
channel_id: channel.id,
|
channel_id: channel.id,
|
||||||
action: 'get_key_status'
|
action: 'get_key_status',
|
||||||
|
page: page,
|
||||||
|
page_size: size
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
setKeyStatusList(res.data.data.keys || []);
|
const data = res.data.data;
|
||||||
|
setKeyStatusList(data.keys || []);
|
||||||
|
setTotal(data.total || 0);
|
||||||
|
setCurrentPage(data.page || 1);
|
||||||
|
setPageSize(data.page_size || 50);
|
||||||
|
setTotalPages(data.total_pages || 0);
|
||||||
|
|
||||||
|
// Update statistics
|
||||||
|
setEnabledCount(data.enabled_count || 0);
|
||||||
|
setManualDisabledCount(data.manual_disabled_count || 0);
|
||||||
|
setAutoDisabledCount(data.auto_disabled_count || 0);
|
||||||
} else {
|
} else {
|
||||||
showError(res.data.message);
|
showError(res.data.message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
showError(t('获取密钥状态失败'));
|
showError(t('获取密钥状态失败'));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -91,7 +117,7 @@ const MultiKeyManageModal = ({
|
|||||||
|
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
showSuccess(t('密钥已禁用'));
|
showSuccess(t('密钥已禁用'));
|
||||||
await loadKeyStatus(); // Reload data
|
await loadKeyStatus(currentPage, pageSize); // Reload current page
|
||||||
onRefresh && onRefresh(); // Refresh parent component
|
onRefresh && onRefresh(); // Refresh parent component
|
||||||
} else {
|
} else {
|
||||||
showError(res.data.message);
|
showError(res.data.message);
|
||||||
@@ -117,7 +143,7 @@ const MultiKeyManageModal = ({
|
|||||||
|
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
showSuccess(t('密钥已启用'));
|
showSuccess(t('密钥已启用'));
|
||||||
await loadKeyStatus(); // Reload data
|
await loadKeyStatus(currentPage, pageSize); // Reload current page
|
||||||
onRefresh && onRefresh(); // Refresh parent component
|
onRefresh && onRefresh(); // Refresh parent component
|
||||||
} else {
|
} else {
|
||||||
showError(res.data.message);
|
showError(res.data.message);
|
||||||
@@ -141,7 +167,9 @@ const MultiKeyManageModal = ({
|
|||||||
|
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
showSuccess(res.data.message);
|
showSuccess(res.data.message);
|
||||||
await loadKeyStatus(); // Reload data
|
// Reset to first page after deletion as data structure might change
|
||||||
|
setCurrentPage(1);
|
||||||
|
await loadKeyStatus(1, pageSize);
|
||||||
onRefresh && onRefresh(); // Refresh parent component
|
onRefresh && onRefresh(); // Refresh parent component
|
||||||
} else {
|
} else {
|
||||||
showError(res.data.message);
|
showError(res.data.message);
|
||||||
@@ -153,13 +181,40 @@ const MultiKeyManageModal = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle page change
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
loadKeyStatus(page, pageSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle page size change
|
||||||
|
const handlePageSizeChange = (size) => {
|
||||||
|
setPageSize(size);
|
||||||
|
setCurrentPage(1); // Reset to first page
|
||||||
|
loadKeyStatus(1, size);
|
||||||
|
};
|
||||||
|
|
||||||
// Effect to load data when modal opens
|
// Effect to load data when modal opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && channel?.id) {
|
if (visible && channel?.id) {
|
||||||
loadKeyStatus();
|
setCurrentPage(1); // Reset to first page when opening
|
||||||
|
loadKeyStatus(1, pageSize);
|
||||||
}
|
}
|
||||||
}, [visible, channel?.id]);
|
}, [visible, channel?.id]);
|
||||||
|
|
||||||
|
// Reset pagination when modal closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) {
|
||||||
|
setCurrentPage(1);
|
||||||
|
setKeyStatusList([]);
|
||||||
|
setTotal(0);
|
||||||
|
setTotalPages(0);
|
||||||
|
setEnabledCount(0);
|
||||||
|
setManualDisabledCount(0);
|
||||||
|
setAutoDisabledCount(0);
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
// Get status tag component
|
// Get status tag component
|
||||||
const renderStatusTag = (status) => {
|
const renderStatusTag = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -270,12 +325,6 @@ const MultiKeyManageModal = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Calculate statistics
|
|
||||||
const enabledCount = keyStatusList.filter(key => key.status === 1).length;
|
|
||||||
const manualDisabledCount = keyStatusList.filter(key => key.status === 2).length;
|
|
||||||
const autoDisabledCount = keyStatusList.filter(key => key.status === 3).length;
|
|
||||||
const totalCount = keyStatusList.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
@@ -293,7 +342,7 @@ const MultiKeyManageModal = ({
|
|||||||
<Button onClick={onCancel}>{t('关闭')}</Button>
|
<Button onClick={onCancel}>{t('关闭')}</Button>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconRefresh />}
|
icon={<IconRefresh />}
|
||||||
onClick={loadKeyStatus}
|
onClick={() => loadKeyStatus(currentPage, pageSize)}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
{t('刷新')}
|
{t('刷新')}
|
||||||
@@ -325,7 +374,7 @@ const MultiKeyManageModal = ({
|
|||||||
<div>
|
<div>
|
||||||
<Text>
|
<Text>
|
||||||
{t('总共 {{total}} 个密钥,{{enabled}} 个已启用,{{manual}} 个手动禁用,{{auto}} 个自动禁用', {
|
{t('总共 {{total}} 个密钥,{{enabled}} 个已启用,{{manual}} 个手动禁用,{{auto}} 个自动禁用', {
|
||||||
total: totalCount,
|
total: total,
|
||||||
enabled: enabledCount,
|
enabled: enabledCount,
|
||||||
manual: manualDisabledCount,
|
manual: manualDisabledCount,
|
||||||
auto: autoDisabledCount
|
auto: autoDisabledCount
|
||||||
@@ -345,15 +394,63 @@ const MultiKeyManageModal = ({
|
|||||||
{/* Key Status Table */}
|
{/* Key Status Table */}
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
{keyStatusList.length > 0 ? (
|
{keyStatusList.length > 0 ? (
|
||||||
<Table
|
<>
|
||||||
columns={columns}
|
<Table
|
||||||
dataSource={keyStatusList}
|
columns={columns}
|
||||||
pagination={false}
|
dataSource={keyStatusList}
|
||||||
size='small'
|
pagination={false}
|
||||||
bordered
|
size='small'
|
||||||
rowKey='index'
|
bordered
|
||||||
style={{ maxHeight: '400px', overflow: 'auto' }}
|
rowKey='index'
|
||||||
/>
|
style={{ marginBottom: '16px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{total > 0 && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Text type='quaternary' style={{ fontSize: '12px' }}>
|
||||||
|
{t('显示第 {{start}}-{{end}} 条,共 {{total}} 条', {
|
||||||
|
start: (currentPage - 1) * pageSize + 1,
|
||||||
|
end: Math.min(currentPage * pageSize, total),
|
||||||
|
total: total
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
<Text type='quaternary' style={{ fontSize: '12px' }}>
|
||||||
|
{t('每页显示')}:
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
value={pageSize}
|
||||||
|
onChange={handlePageSizeChange}
|
||||||
|
size='small'
|
||||||
|
style={{ width: '80px' }}
|
||||||
|
>
|
||||||
|
<Select.Option value={50}>50</Select.Option>
|
||||||
|
<Select.Option value={100}>100</Select.Option>
|
||||||
|
<Select.Option value={200}>500</Select.Option>
|
||||||
|
<Select.Option value={1000}>1000</Select.Option>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
current={currentPage}
|
||||||
|
total={total}
|
||||||
|
pageSize={pageSize}
|
||||||
|
showSizeChanger={false}
|
||||||
|
showQuickJumper
|
||||||
|
size='small'
|
||||||
|
onChange={handlePageChange}
|
||||||
|
showTotal={(total, range) =>
|
||||||
|
t('第 {{current}} / {{total}} 页', {
|
||||||
|
current: currentPage,
|
||||||
|
total: totalPages
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
!loading && (
|
!loading && (
|
||||||
<Empty
|
<Empty
|
||||||
|
|||||||
Reference in New Issue
Block a user