perf: 数据看板支持选择时间粒度

This commit is contained in:
CaIon 2024-01-13 00:33:52 +08:00
parent d30b9321b2
commit 00306aa142
7 changed files with 132 additions and 65 deletions

View File

@ -26,7 +26,8 @@ var DisplayInCurrencyEnabled = true
var DisplayTokenStatEnabled = true var DisplayTokenStatEnabled = true
var DrawingEnabled = true var DrawingEnabled = true
var DataExportEnabled = true var DataExportEnabled = true
var DataExportInterval = 5 // unit: minute var DataExportInterval = 5 // unit: minute
var DataExportDefaultTime = "hour" // unit: minute
// Any options with "Secret", "Token" in its key won't be return by GetOptions // Any options with "Secret", "Token" in its key won't be return by GetOptions

View File

@ -16,26 +16,27 @@ func GetStatus(c *gin.Context) {
"success": true, "success": true,
"message": "", "message": "",
"data": gin.H{ "data": gin.H{
"start_time": common.StartTime, "start_time": common.StartTime,
"email_verification": common.EmailVerificationEnabled, "email_verification": common.EmailVerificationEnabled,
"github_oauth": common.GitHubOAuthEnabled, "github_oauth": common.GitHubOAuthEnabled,
"github_client_id": common.GitHubClientId, "github_client_id": common.GitHubClientId,
"system_name": common.SystemName, "system_name": common.SystemName,
"logo": common.Logo, "logo": common.Logo,
"footer_html": common.Footer, "footer_html": common.Footer,
"wechat_qrcode": common.WeChatAccountQRCodeImageURL, "wechat_qrcode": common.WeChatAccountQRCodeImageURL,
"wechat_login": common.WeChatAuthEnabled, "wechat_login": common.WeChatAuthEnabled,
"server_address": common.ServerAddress, "server_address": common.ServerAddress,
"price": common.Price, "price": common.Price,
"turnstile_check": common.TurnstileCheckEnabled, "turnstile_check": common.TurnstileCheckEnabled,
"turnstile_site_key": common.TurnstileSiteKey, "turnstile_site_key": common.TurnstileSiteKey,
"top_up_link": common.TopUpLink, "top_up_link": common.TopUpLink,
"chat_link": common.ChatLink, "chat_link": common.ChatLink,
"quota_per_unit": common.QuotaPerUnit, "quota_per_unit": common.QuotaPerUnit,
"display_in_currency": common.DisplayInCurrencyEnabled, "display_in_currency": common.DisplayInCurrencyEnabled,
"enable_batch_update": common.BatchUpdateEnabled, "enable_batch_update": common.BatchUpdateEnabled,
"enable_drawing": common.DrawingEnabled, "enable_drawing": common.DrawingEnabled,
"enable_data_export": common.DataExportEnabled, "enable_data_export": common.DataExportEnabled,
"data_export_default_time": common.DataExportDefaultTime,
}, },
}) })
return return

View File

@ -79,6 +79,7 @@ func InitOptionMap() {
common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64) common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64)
common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes) common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes)
common.OptionMap["DataExportInterval"] = strconv.Itoa(common.DataExportInterval) common.OptionMap["DataExportInterval"] = strconv.Itoa(common.DataExportInterval)
common.OptionMap["DataExportDefaultTime"] = common.DataExportDefaultTime
common.OptionMapRWMutex.Unlock() common.OptionMapRWMutex.Unlock()
loadOptionsFromDatabase() loadOptionsFromDatabase()
@ -228,6 +229,8 @@ func updateOptionMap(key string, value string) (err error) {
common.RetryTimes, _ = strconv.Atoi(value) common.RetryTimes, _ = strconv.Atoi(value)
case "DataExportInterval": case "DataExportInterval":
common.DataExportInterval, _ = strconv.Atoi(value) common.DataExportInterval, _ = strconv.Atoi(value)
case "DataExportDefaultTime":
common.DataExportDefaultTime = value
case "ModelRatio": case "ModelRatio":
err = common.UpdateModelRatioByJSONString(value) err = common.UpdateModelRatioByJSONString(value)
case "GroupRatio": case "GroupRatio":

View File

@ -51,6 +51,7 @@ function App() {
localStorage.setItem('display_in_currency', data.display_in_currency); localStorage.setItem('display_in_currency', data.display_in_currency);
localStorage.setItem('enable_drawing', data.enable_drawing); localStorage.setItem('enable_drawing', data.enable_drawing);
localStorage.setItem('enable_data_export', data.enable_data_export); localStorage.setItem('enable_data_export', data.enable_data_export);
localStorage.setItem('data_export_default_time', data.data_export_default_time);
if (data.chat_link) { if (data.chat_link) {
localStorage.setItem('chat_link', data.chat_link); localStorage.setItem('chat_link', data.chat_link);
} else { } else {

View File

@ -23,13 +23,19 @@ const OperationSetting = () => {
DisplayTokenStatEnabled: '', DisplayTokenStatEnabled: '',
DrawingEnabled: '', DrawingEnabled: '',
DataExportEnabled: '', DataExportEnabled: '',
DataExportDefaultTime: 'hour',
DataExportInterval: 5, DataExportInterval: 5,
RetryTimes: 0 RetryTimes: 0
}); });
const [originInputs, setOriginInputs] = useState({}); const [originInputs, setOriginInputs] = useState({});
let [loading, setLoading] = useState(false); let [loading, setLoading] = useState(false);
let [historyTimestamp, setHistoryTimestamp] = useState(timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600)); // a month ago let [historyTimestamp, setHistoryTimestamp] = useState(timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600)); // a month ago
// 精确时间选项(小时,天,周)
const timeOptions = [
{key: 'hour', text: '小时', value: 'hour'},
{key: 'day', text: '天', value: 'day'},
{key: 'week', text: '周', value: 'week'}
];
const getOptions = async () => { const getOptions = async () => {
const res = await API.get('/api/option/'); const res = await API.get('/api/option/');
const {success, message, data} = res.data; const {success, message, data} = res.data;
@ -71,7 +77,10 @@ const OperationSetting = () => {
}; };
const handleInputChange = async (e, {name, value}) => { const handleInputChange = async (e, {name, value}) => {
if (name.endsWith('Enabled') || name === 'DataExportInterval') { if (name.endsWith('Enabled') || name === 'DataExportInterval' || name === 'DataExportDefaultTime') {
if (name === 'DataExportDefaultTime') {
localStorage.setItem('data_export_default_time', value);
}
await updateOption(name, value); await updateOption(name, value);
} else { } else {
setInputs((inputs) => ({...inputs, [name]: value})); setInputs((inputs) => ({...inputs, [name]: value}));
@ -234,15 +243,28 @@ const OperationSetting = () => {
name='LogConsumeEnabled' name='LogConsumeEnabled'
onChange={handleInputChange} onChange={handleInputChange}
/> />
</Form.Group> </Form.Group>
<Form.Group inline> <Form.Group widths={4}>
<Form.Checkbox <Form.Input label='目标时间' value={historyTimestamp} type='datetime-local'
checked={inputs.DataExportEnabled === 'true'} name='history_timestamp'
label='启用数据看板(实验性)' onChange={(e, {name, value}) => {
name='DataExportEnabled' setHistoryTimestamp(value);
onChange={handleInputChange} }}/>
/> </Form.Group>
<Form.Button onClick={() => {
deleteHistoryLogs().then();
}}>清理历史日志</Form.Button>
<Divider/>
<Header as='h3'>
数据看板
</Header>
<Form.Checkbox
checked={inputs.DataExportEnabled === 'true'}
label='启用数据看板(实验性)'
name='DataExportEnabled'
onChange={handleInputChange}
/>
<Form.Group>
<Form.Input <Form.Input
label='数据看板更新间隔(分钟,设置过短会影响数据库性能)' label='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
name='DataExportInterval' name='DataExportInterval'
@ -254,19 +276,17 @@ const OperationSetting = () => {
value={inputs.DataExportInterval} value={inputs.DataExportInterval}
placeholder='数据看板更新间隔(分钟,设置过短会影响数据库性能)' placeholder='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
/> />
<Form.Select
label='数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)'
options={timeOptions}
name='DataExportDefaultTime'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.DataExportDefaultTime}
placeholder='数据看板默认时间粒度'
/>
</Form.Group> </Form.Group>
<Divider/> <Divider/>
<Form.Group widths={4}>
<Form.Input label='目标时间' value={historyTimestamp} type='datetime-local'
name='history_timestamp'
onChange={(e, {name, value}) => {
setHistoryTimestamp(value);
}}/>
</Form.Group>
<Form.Button onClick={() => {
deleteHistoryLogs().then();
}}>清理历史日志</Form.Button>
<Divider/>
<Header as='h3'> <Header as='h3'>
监控设置 监控设置
</Header> </Header>

View File

@ -171,7 +171,7 @@ export function timestamp2string(timestamp) {
); );
} }
export function timestamp2string1(timestamp) { export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
let date = new Date(timestamp * 1000); let date = new Date(timestamp * 1000);
// let year = date.getFullYear().toString(); // let year = date.getFullYear().toString();
let month = (date.getMonth() + 1).toString(); let month = (date.getMonth() + 1).toString();
@ -186,15 +186,22 @@ export function timestamp2string1(timestamp) {
if (hour.length === 1) { if (hour.length === 1) {
hour = '0' + hour; hour = '0' + hour;
} }
return ( let str = month + '-' + day
// year + if (dataExportDefaultTime === 'hour') {
// '-' + str += ' ' + hour + ":00"
month + } else if (dataExportDefaultTime === 'week') {
'-' + let nextWeek = new Date(timestamp * 1000 + 6 * 24 * 60 * 60 * 1000);
day + let nextMonth = (nextWeek.getMonth() + 1).toString();
' ' + let nextDay = nextWeek.getDate().toString();
hour + ":00" if (nextMonth.length === 1) {
); nextMonth = '0' + nextMonth;
}
if (nextDay.length === 1) {
nextDay = '0' + nextDay;
}
str += ' - ' + nextMonth + '-' + nextDay
}
return str;
} }
export function downloadTextAsFile(text, filename) { export function downloadTextAsFile(text, filename) {

View File

@ -12,17 +12,18 @@ import {
} from "../../helpers/render"; } from "../../helpers/render";
const Detail = (props) => { const Detail = (props) => {
const formRef = useRef();
let now = new Date(); let now = new Date();
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
username: '', username: '',
token_name: '', token_name: '',
model_name: '', model_name: '',
start_timestamp: timestamp2string(now.getTime() / 1000 - 86400), start_timestamp: localStorage.getItem('data_export_default_time') === 'hour' ? timestamp2string(now.getTime() / 1000 - 86400) : (localStorage.getItem('data_export_default_time') === 'week' ? timestamp2string(now.getTime() / 1000 - 86400 * 30) : timestamp2string(now.getTime() / 1000 - 86400 * 7)),
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600), end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
channel: '' channel: '',
data_export_default_time: ''
}); });
const {username, token_name, model_name, start_timestamp, end_timestamp, channel} = inputs; const {username, model_name, start_timestamp, end_timestamp, channel} = inputs;
const isAdminUser = isAdmin(); const isAdminUser = isAdmin();
const initialized = useRef(false) const initialized = useRef(false)
const [modelDataChart, setModelDataChart] = useState(null); const [modelDataChart, setModelDataChart] = useState(null);
@ -31,8 +32,13 @@ const Detail = (props) => {
const [quotaData, setQuotaData] = useState([]); const [quotaData, setQuotaData] = useState([]);
const [consumeQuota, setConsumeQuota] = useState(0); const [consumeQuota, setConsumeQuota] = useState(0);
const [times, setTimes] = useState(0); const [times, setTimes] = useState(0);
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(localStorage.getItem('data_export_default_time') || 'hour');
const handleInputChange = (value, name) => { const handleInputChange = (value, name) => {
if (name === 'data_export_default_time') {
setDataExportDefaultTime(value);
return
}
setInputs((inputs) => ({...inputs, [name]: value})); setInputs((inputs) => ({...inputs, [name]: value}));
}; };
@ -41,8 +47,7 @@ const Detail = (props) => {
data: [ data: [
{ {
id: 'barData', id: 'barData',
values: [ values: []
]
} }
], ],
xField: 'Time', xField: 'Time',
@ -54,7 +59,7 @@ const Detail = (props) => {
}, },
title: { title: {
visible: true, visible: true,
text: '模型消耗分布(小时)', text: '模型消耗分布',
subtext: '0' subtext: '0'
}, },
bar: { bar: {
@ -104,7 +109,7 @@ const Detail = (props) => {
{ {
id: 'id0', id: 'id0',
values: [ values: [
{ type: 'null', value: '0' }, {type: 'null', value: '0'},
] ]
} }
], ],
@ -163,9 +168,9 @@ const Detail = (props) => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000; let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) { if (isAdminUser) {
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
} else { } else {
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
} }
const res = await API.get(url); const res = await API.get(url);
const {success, message, data} = res.data; const {success, message, data} = res.data;
@ -179,6 +184,16 @@ const Detail = (props) => {
'created_at': now.getTime() / 1000 'created_at': now.getTime() / 1000
}) })
} }
// 根据dataExportDefaultTime重制时间粒度
let timeGranularity = 3600;
if (dataExportDefaultTime === 'day') {
timeGranularity = 86400;
} else if (dataExportDefaultTime === 'week') {
timeGranularity = 604800;
}
data.forEach(item => {
item['created_at'] = Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
});
updateChart(lineChart, pieChart, data); updateChart(lineChart, pieChart, data);
} else { } else {
showError(message); showError(message);
@ -190,7 +205,7 @@ const Detail = (props) => {
await loadQuotaData(modelDataChart, modelDataPieChart); await loadQuotaData(modelDataChart, modelDataPieChart);
}; };
const initChart = async () => { const initChart = async () => {
let lineChart = modelDataChart let lineChart = modelDataChart
if (!modelDataChart) { if (!modelDataChart) {
lineChart = new VChart(spec_line, {dom: 'model_data'}); lineChart = new VChart(spec_line, {dom: 'model_data'});
@ -231,7 +246,7 @@ const Detail = (props) => {
} }
// 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳 // 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
// 转换日期格式 // 转换日期格式
let createTime = timestamp2string1(item.created_at); let createTime = timestamp2string1(item.created_at, dataExportDefaultTime);
let lineItem = lineData.find(it => it.Time === createTime && it.Model === item.model_name); let lineItem = lineData.find(it => it.Time === createTime && it.Model === item.model_name);
if (lineItem) { if (lineItem) {
lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota)); lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
@ -263,6 +278,13 @@ const Detail = (props) => {
} }
useEffect(() => { useEffect(() => {
// setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
// if (dataExportDefaultTime === 'day') {
// // 设置开始时间为7天前
// let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
// inputs.start_timestamp = st;
// formRef.current.formApi.setValue('start_timestamp', st);
// }
if (!initialized.current) { if (!initialized.current) {
initialized.current = true; initialized.current = true;
initChart(); initChart();
@ -276,7 +298,7 @@ const Detail = (props) => {
<h3>数据看板</h3> <h3>数据看板</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<Form layout='horizontal' style={{marginTop: 10}}> <Form ref={formRef} layout='horizontal' style={{marginTop: 10}}>
<> <>
<Form.DatePicker field="start_timestamp" label='起始时间' style={{width: 272}} <Form.DatePicker field="start_timestamp" label='起始时间' style={{width: 272}}
initValue={start_timestamp} initValue={start_timestamp}
@ -288,6 +310,18 @@ const Detail = (props) => {
value={end_timestamp} type='dateTime' value={end_timestamp} type='dateTime'
name='end_timestamp' name='end_timestamp'
onChange={value => handleInputChange(value, 'end_timestamp')}/> onChange={value => handleInputChange(value, 'end_timestamp')}/>
<Form.Select field="data_export_default_time" label='时间粒度' style={{width: 176}}
initValue={dataExportDefaultTime}
placeholder={'时间粒度'} name='data_export_default_time'
optionList={
[
{label: '小时', value: 'hour'},
{label: '天', value: 'day'},
{label: '周', value: 'week'}
]
}
onChange={value => handleInputChange(value, 'data_export_default_time')}>
</Form.Select>
{ {
isAdminUser && <> isAdminUser && <>
<Form.Input field="username" label='用户名称' style={{width: 176}} value={username} <Form.Input field="username" label='用户名称' style={{width: 176}} value={username}