🚀 perf: optimize model management APIs, unify pricing types as array, and remove redundancies

Backend
- Add GetBoundChannelsByModelsMap to batch-fetch bound channels via a single JOIN (Distinct), compatible with SQLite/MySQL/PostgreSQL
- Replace per-record enrichment with a single-pass enrichModels to avoid N+1 queries; compute unions for prefix/suffix/contains matches in memory
- Change Model.QuotaType to QuotaTypes []int and expose quota_types in responses
- Add GetModelQuotaTypes for cached O(1) lookups; exact models return a single-element array
- Sort quota_types for stable output order
- Remove unused code: GetModelByName, GetBoundChannels, GetBoundChannelsForModels, FindModelByNameWithRule, buildPrefixes, buildSuffixes
- Clean up redundant comments, keeping concise and readable code

Frontend
- Models table: switch to quota_types, render multiple billing modes ([0], [1], [0,1], future values supported)
- Pricing table: switch to quota_types; ratio display now checks quota_types.includes(0); array rendering for billing tags

Compatibility
- SQL uses standard JOIN/IN/Distinct; works across SQLite/MySQL/PostgreSQL
- Lint passes; no DB schema changes (quota_types is a JSON response field only)

Breaking Change
- API field renamed: quota_type -> quota_types (array). Update clients accordingly.
This commit is contained in:
t0ng7u
2025-08-11 14:40:01 +08:00
parent e64b13c925
commit 4ad8eefaec
5 changed files with 239 additions and 254 deletions

View File

@@ -23,23 +23,31 @@ import { IconHelpCircle } from '@douyinfe/semi-icons';
import { renderModelTag, stringToColor, calculateModelPrice, getLobeHubIcon } from '../../../../../helpers';
import { renderLimitedItems, renderDescription } from '../../../../common/ui/RenderUtils';
function renderQuotaType(type, t) {
switch (type) {
case 1:
return (
<Tag color='teal' shape='circle'>
{t('按次计费')}
</Tag>
);
case 0:
return (
<Tag color='violet' shape='circle'>
{t('按量计费')}
</Tag>
);
default:
return t('未知');
}
function renderQuotaTypes(types, t) {
if (!Array.isArray(types) || types.length === 0) return '-';
const renderOne = (type, idx) => {
switch (type) {
case 1:
return (
<Tag key={`qt-${type}-${idx}`} color='teal' shape='circle'>
{t('按次计费')}
</Tag>
);
case 0:
return (
<Tag key={`qt-${type}-${idx}`} color='violet' shape='circle'>
{t('按量计费')}
</Tag>
);
default:
return (
<Tag key={`qt-${type}-${idx}`} color='white' shape='circle'>
{type}
</Tag>
);
}
};
return <Space wrap>{types.map((t0, idx) => renderOne(t0, idx))}</Space>;
}
// Render vendor name
@@ -122,11 +130,8 @@ export const getPricingTableColumns = ({
const quotaColumn = {
title: t('计费类型'),
dataIndex: 'quota_type',
render: (text, record, index) => {
return renderQuotaType(parseInt(text), t);
},
sorter: (a, b) => a.quota_type - b.quota_type,
dataIndex: 'quota_types',
render: (text, record, index) => renderQuotaTypes(text, t),
};
const descriptionColumn = {
@@ -170,11 +175,11 @@ export const getPricingTableColumns = ({
const content = (
<div className="space-y-1">
<div className="text-gray-700">
{t('模型倍率')}{record.quota_type === 0 ? text : t('无')}
{t('模型倍率')}{Array.isArray(record.quota_types) && record.quota_types.includes(0) ? text : t('无')}
</div>
<div className="text-gray-700">
{t('补全倍率')}
{record.quota_type === 0 ? completionRatio : t('无')}
{Array.isArray(record.quota_types) && record.quota_types.includes(0) ? completionRatio : t('无')}
</div>
<div className="text-gray-700">
{t('分组倍率')}{groupRatio[selectedGroup]}

View File

@@ -121,24 +121,36 @@ const renderEndpoints = (value) => {
}
};
// Render quota type
const renderQuotaType = (qt, t) => {
if (qt === 1) {
// Render quota types (array)
const renderQuotaTypes = (arr, t) => {
if (!Array.isArray(arr) || arr.length === 0) return '-';
const renderOne = (qt, idx) => {
if (qt === 1) {
return (
<Tag key={`${qt}-${idx}`} color='teal' size='small' shape='circle'>
{t('按次计费')}
</Tag>
);
}
if (qt === 0) {
return (
<Tag key={`${qt}-${idx}`} color='violet' size='small' shape='circle'>
{t('按量计费')}
</Tag>
);
}
// 未来新增模式的兜底展示
return (
<Tag color='teal' size='small' shape='circle'>
{t('按次计费')}
<Tag key={`${qt}-${idx}`} color='white' size='small' shape='circle'>
{qt}
</Tag>
);
}
if (qt === 0) {
return (
<Tag color='violet' size='small' shape='circle'>
{t('按量计费')}
</Tag>
);
}
// 未知
return '-';
};
return (
<Space wrap>
{arr.map((qt, idx) => renderOne(qt, idx))}
</Space>
);
};
// Render bound channels
@@ -303,8 +315,8 @@ export const getModelsColumns = ({
},
{
title: t('计费类型'),
dataIndex: 'quota_type',
render: (qt) => renderQuotaType(qt, t),
dataIndex: 'quota_types',
render: (qts) => renderQuotaTypes(qts, t),
},
{
title: t('创建时间'),