improve(admin):优化管理功能搜索条件
This commit is contained in:
1 parent
a73cf411a5
commit
c7d15d82a4
18 files changed
+438
-119
No files matched your search
@@ -63,6 +63,7 @@ type IAdminV1 interface {
|
||||
ChannelHealthTrend(ctx context.Context, req *v1.ChannelHealthTrendReq) (res *v1.ChannelHealthTrendRes, err error)
|
||||
ChannelExport(ctx context.Context, req *v1.ChannelExportReq) (res *v1.ChannelExportRes, err error)
|
||||
ChannelClone(ctx context.Context, req *v1.ChannelCloneReq) (res *v1.ChannelCloneRes, err error)
|
||||
ChannelResetHealth(ctx context.Context, req *v1.ChannelResetHealthReq) (res *v1.ChannelResetHealthRes, err error)
|
||||
ChannelErrorEventList(ctx context.Context, req *v1.ChannelErrorEventListReq) (res *v1.ChannelErrorEventListRes, err error)
|
||||
ChannelErrorStats(ctx context.Context, req *v1.ChannelErrorStatsReq) (res *v1.ChannelErrorStatsRes, err error)
|
||||
ChannelErrorTrend(ctx context.Context, req *v1.ChannelErrorTrendReq) (res *v1.ChannelErrorTrendRes, err error)
|
||||
|
||||
@@ -62,7 +62,9 @@ type RequestAuditLogListReq struct {
|
||||
g.Meta `path:"/audit/request-logs" method:"get" mime:"json" tags:"管理后台-审计" summary:"请求审计日志"`
|
||||
Page int `json:"page" d:"1"`
|
||||
PageSize int `json:"page_size" d:"20"`
|
||||
ID int64 `json:"id" dc:"日志ID"`
|
||||
TenantID int `json:"tenant_id" dc:"租户ID"`
|
||||
UserID int64 `json:"user_id" dc:"用户ID"`
|
||||
ApiKeyID int `json:"api_key_id" dc:"API Key ID"`
|
||||
Username string `json:"username" dc:"用户名(模糊匹配)"`
|
||||
RequestId string `json:"request_id" dc:"Request ID(精确匹配)"`
|
||||
@@ -70,8 +72,8 @@ type RequestAuditLogListReq struct {
|
||||
Method string `json:"method" dc:"HTTP 方法"`
|
||||
Path string `json:"path" dc:"请求路径"`
|
||||
StatusCode int `json:"status_code" dc:"状态码"`
|
||||
StartDate string `json:"start_date" dc:"开始日期"`
|
||||
EndDate string `json:"end_date" dc:"结束日期"`
|
||||
StartDate string `json:"start_date" dc:"开始时间(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)"`
|
||||
EndDate string `json:"end_date" dc:"结束时间(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)"`
|
||||
}
|
||||
|
||||
type RequestAuditLogListRes struct {
|
||||
|
||||
+18
-7
@@ -12,6 +12,7 @@ type ChannelListReq struct {
|
||||
Search string `json:"search" dc:"搜索关键词(渠道名称/备注)"`
|
||||
ID int64 `json:"id" dc:"按渠道 ID 精确筛选"`
|
||||
Model string `json:"model" dc:"按支持的模型名筛选(模糊匹配平台模型名/上游模型名)"`
|
||||
BaseURL string `json:"base_url" dc:"按 API 地址筛选(模糊匹配)"`
|
||||
}
|
||||
|
||||
// ChannelListRes 渠道列表响应
|
||||
@@ -248,13 +249,14 @@ type HealthTrendPoint struct {
|
||||
|
||||
// ChannelExportReq 导出渠道列表请求
|
||||
type ChannelExportReq struct {
|
||||
g.Meta `path:"/channels/export" method:"get" mime:"json" tags:"管理后台-渠道" summary:"导出渠道列表"`
|
||||
Format string `json:"format" in:"query" d:"csv" v:"in:csv,xlsx" dc:"导出格式:csv / xlsx"`
|
||||
Type int `json:"type" in:"query" dc:"供应商类型筛选"`
|
||||
Status string `json:"status" in:"query" dc:"状态筛选:active/disabled/testing"`
|
||||
Search string `json:"search" in:"query" dc:"搜索关键词(渠道名称/备注)"`
|
||||
ID int64 `json:"id" in:"query" dc:"按渠道 ID 精确筛选"`
|
||||
Model string `json:"model" in:"query" dc:"按支持的模型名筛选(模糊匹配平台模型名/上游模型名)"`
|
||||
g.Meta `path:"/channels/export" method:"get" mime:"json" tags:"管理后台-渠道" summary:"导出渠道列表"`
|
||||
Format string `json:"format" in:"query" d:"csv" v:"in:csv,xlsx" dc:"导出格式:csv / xlsx"`
|
||||
Type int `json:"type" in:"query" dc:"供应商类型筛选"`
|
||||
Status string `json:"status" in:"query" dc:"状态筛选:active/disabled/testing"`
|
||||
Search string `json:"search" in:"query" dc:"搜索关键词(渠道名称/备注)"`
|
||||
ID int64 `json:"id" in:"query" dc:"按渠道 ID 精确筛选"`
|
||||
Model string `json:"model" in:"query" dc:"按支持的模型名筛选(模糊匹配平台模型名/上游模型名)"`
|
||||
BaseURL string `json:"base_url" in:"query" dc:"按 API 地址筛选(模糊匹配)"`
|
||||
}
|
||||
|
||||
type ChannelExportRes struct{}
|
||||
@@ -271,3 +273,12 @@ type ChannelCloneReq struct {
|
||||
type ChannelCloneRes struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
// ChannelResetHealthReq 重置渠道健康度请求
|
||||
type ChannelResetHealthReq struct {
|
||||
g.Meta `path:"/channels/{id}/reset-health" method:"post" mime:"json" tags:"管理后台-渠道" summary:"重置渠道健康度"`
|
||||
ID int64 `json:"id" in:"path" v:"required" dc:"渠道ID"`
|
||||
}
|
||||
|
||||
// ChannelResetHealthRes 重置渠道健康度响应
|
||||
type ChannelResetHealthRes struct{}
|
||||
@@ -122,13 +122,17 @@ type AdminUsageLogListReq struct {
|
||||
g.Meta `path:"/usage-logs" method:"get" mime:"json" tags:"管理后台-用量" summary:"用量日志列表"`
|
||||
Page int `json:"page" d:"1" dc:"页码"`
|
||||
PageSize int `json:"page_size" d:"20" v:"between:1,100" dc:"每页数量"`
|
||||
ID int64 `json:"id" dc:"用量记录ID"`
|
||||
TenantID int64 `json:"tenant_id" dc:"租户ID"`
|
||||
UserID int64 `json:"user_id" dc:"用户ID"`
|
||||
Username string `json:"username" dc:"用户名(模糊匹配)"`
|
||||
ApiKeyID int64 `json:"api_key_id" dc:"API Key ID"`
|
||||
ChannelID int64 `json:"channel_id" dc:"渠道ID"`
|
||||
Model string `json:"model" dc:"模型名称"`
|
||||
Status string `json:"status" dc:"状态"`
|
||||
RequestType int `json:"request_type" dc:"请求类型: 1=同步, 2=流式, 3=异步, 4=WebSocket"`
|
||||
StartDate string `json:"start_date" dc:"开始日期"`
|
||||
EndDate string `json:"end_date" dc:"结束日期"`
|
||||
StartDate string `json:"start_date" dc:"开始时间(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)"`
|
||||
EndDate string `json:"end_date" dc:"结束时间(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)"`
|
||||
}
|
||||
|
||||
type AdminUsageLogItem struct {
|
||||
@@ -373,13 +377,17 @@ type AdminWalletSetWarningThresholdRes struct{}
|
||||
type AdminUsageLogExportReq struct {
|
||||
g.Meta `path:"/usage-logs/export" method:"get" mime:"json" tags:"管理后台-用量" summary:"导出用量日志"`
|
||||
Format string `json:"format" in:"query" d:"csv" v:"in:csv,xlsx" dc:"导出格式:csv / xlsx"`
|
||||
ID int64 `json:"id" in:"query" dc:"用量记录ID"`
|
||||
TenantID int64 `json:"tenant_id" in:"query" dc:"租户ID"`
|
||||
UserID int64 `json:"user_id" in:"query" dc:"用户ID"`
|
||||
Username string `json:"username" in:"query" dc:"用户名(模糊匹配)"`
|
||||
ApiKeyID int64 `json:"api_key_id" in:"query" dc:"API Key ID"`
|
||||
ChannelID int64 `json:"channel_id" in:"query" dc:"渠道ID"`
|
||||
Model string `json:"model" in:"query" dc:"模型名称"`
|
||||
Status string `json:"status" in:"query" dc:"状态"`
|
||||
RequestType int `json:"request_type" in:"query" dc:"请求类型: 1=同步, 2=流式, 3=异步, 4=WebSocket"`
|
||||
StartDate string `json:"start_date" in:"query" dc:"开始日期"`
|
||||
EndDate string `json:"end_date" in:"query" dc:"结束日期"`
|
||||
StartDate string `json:"start_date" in:"query" dc:"开始时间(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)"`
|
||||
EndDate string `json:"end_date" in:"query" dc:"结束时间(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)"`
|
||||
}
|
||||
|
||||
type AdminUsageLogExportRes struct{}
|
||||
|
||||
@@ -11,6 +11,11 @@ type TaskListReq struct {
|
||||
Status string `json:"status" in:"query" dc:"筛选状态"`
|
||||
Platform string `json:"platform" in:"query" dc:"筛选平台(sora/kling/midjourney/suno)"`
|
||||
PublicTaskID string `json:"public_task_id" in:"query" dc:"任务ID(精确匹配)"`
|
||||
ModelName string `json:"model_name" in:"query" dc:"筛选模型名(精确匹配)"`
|
||||
TenantID int64 `json:"tenant_id" in:"query" dc:"筛选租户ID"`
|
||||
UserID int64 `json:"user_id" in:"query" dc:"筛选用户ID"`
|
||||
StartDate string `json:"start_date" in:"query" dc:"开始时间(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)"`
|
||||
EndDate string `json:"end_date" in:"query" dc:"结束时间(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)"`
|
||||
}
|
||||
|
||||
type ModelTaskItem struct {
|
||||
|
||||
@@ -49,3 +49,6 @@ func (c *ControllerV1) ChannelExport(ctx context.Context, req *v1.ChannelExportR
|
||||
func (c *ControllerV1) ChannelClone(ctx context.Context, req *v1.ChannelCloneReq) (res *v1.ChannelCloneRes, err error) {
|
||||
return service.Admin().CloneChannel(ctx, req)
|
||||
}
|
||||
func (c *ControllerV1) ChannelResetHealth(ctx context.Context, req *v1.ChannelResetHealthReq) (res *v1.ChannelResetHealthRes, err error) {
|
||||
return service.Admin().ResetChannelHealth(ctx, req)
|
||||
}
|
||||
@@ -326,6 +326,44 @@ func (s *RedisState) MarkChannelRecovered(ctx context.Context, channelID int64)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 健康度重置
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// resetHealthSuccEwma 重置健康度时写入的成功率 EWMA:对应健康分≈80(0.9²×100=81)。
|
||||
const resetHealthSuccEwma = 0.9
|
||||
|
||||
// luaHealthReset 健康 EWMA 重置(管理后台"重置健康度"):成功率恢复为健康值、延迟清零。
|
||||
// ARGV: succ, nowMs, keyTtlMs
|
||||
const luaHealthReset = `
|
||||
local succ = tonumber(ARGV[1])
|
||||
if succ <= 0 then succ = 1 end
|
||||
redis.call('HSET', KEYS[1], 'succ_ewma', succ, 'lat_ewma', 0, 'updated_ms', ARGV[2])
|
||||
redis.call('PEXPIRE', KEYS[1], ARGV[3])
|
||||
return 1`
|
||||
|
||||
// ResetHealth 重置渠道健康(管理后台"重置健康度"):渠道级 + 各模型级熔断复位为 CLOSED
|
||||
// 并开启爬坡窗口(recovered_ms),各模型成功率 EWMA 恢复为健康值,渠道立即恢复被调度选择的能力。
|
||||
// 失败仅记 Debug 日志(管理后台手动操作,重试成本低)。
|
||||
func (s *RedisState) ResetHealth(ctx context.Context, channelID int64, models []string) {
|
||||
chStr := strconv.FormatInt(channelID, 10)
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
s.resetBreakerState(ctx, keyBreaker+chStr, now)
|
||||
for _, model := range models {
|
||||
s.resetBreakerState(ctx, keyBreaker+chStr+":"+model, now)
|
||||
if _, err := g.Redis().Do(ctx, "EVAL", luaHealthReset, 1,
|
||||
keyHealth+chStr+":"+model, resetHealthSuccEwma, now, stateKeyTTLMs); err != nil {
|
||||
g.Log().Debugf(ctx, "[Dispatch] 健康重置失败: channel=%d model=%s err=%v", channelID, model, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resetBreakerState 熔断复位为 CLOSED 并记录 recovered_ms(开启爬坡窗口)。
|
||||
func (s *RedisState) resetBreakerState(ctx context.Context, key string, nowMs int64) {
|
||||
_, _ = g.Redis().Do(ctx, "EVAL", luaBreakerManualReset, 1, key, nowMs, stateKeyTTLMs)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 熔断探测令牌
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -91,6 +91,19 @@ func MarkChannelRecovered(ctx context.Context, channelID int64) {
|
||||
}
|
||||
}
|
||||
|
||||
// ResetChannelHealth 重置渠道健康度(管理后台"重置健康度"按钮):熔断复位 + 成功率恢复,
|
||||
// 渠道立即恢复被调度选择的能力。models 从目录快照取(该渠道服务的模型列表)。
|
||||
func ResetChannelHealth(ctx context.Context, channelID int64) {
|
||||
if redisState == nil {
|
||||
return
|
||||
}
|
||||
var models []string
|
||||
if catalog != nil {
|
||||
models = catalog.ChannelModels()[channelID]
|
||||
}
|
||||
redisState.ResetHealth(ctx, channelID, models)
|
||||
}
|
||||
|
||||
// InvalidateChannel 渠道禁用/删除时的联动清理:清绑定 + 跨实例目录失效。
|
||||
// 管理后台渠道写操作(更新/删除/Key 变更/能力变更)后调用。
|
||||
func InvalidateChannel(ctx context.Context, channelID int64) {
|
||||
|
||||
@@ -286,10 +286,10 @@ func MaskSensitiveData(data string) string {
|
||||
// ListRequestAuditLogs 分页查询请求审计日志(不返回 request_body/response_body 以优化性能)
|
||||
// 审计数据从审计库查询,关联信息(用户名、租户名等)从主库批量查询后在应用层合并。
|
||||
func (s *sAdmin) ListRequestAuditLogs(ctx context.Context, req *v1.RequestAuditLogListReq) (*v1.RequestAuditLogListRes, error) {
|
||||
if err := common.ValidateDateParam(req.StartDate, "开始日期"); err != nil {
|
||||
if err := common.ValidateDateTimeParam(req.StartDate, "开始时间"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := common.ValidateDateParam(req.EndDate, "结束日期"); err != nil {
|
||||
if err := common.ValidateDateTimeParam(req.EndDate, "结束时间"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -298,10 +298,18 @@ func (s *sAdmin) ListRequestAuditLogs(ctx context.Context, req *v1.RequestAuditL
|
||||
var conditions []string
|
||||
var args []any
|
||||
|
||||
if req.ID > 0 {
|
||||
conditions = append(conditions, "id = ?")
|
||||
args = append(args, req.ID)
|
||||
}
|
||||
if req.TenantID > 0 {
|
||||
conditions = append(conditions, "tenant_id = ?")
|
||||
args = append(args, int64(req.TenantID))
|
||||
}
|
||||
if req.UserID > 0 {
|
||||
conditions = append(conditions, "user_id = ?")
|
||||
args = append(args, req.UserID)
|
||||
}
|
||||
if req.ApiKeyID > 0 {
|
||||
conditions = append(conditions, "api_key_id = ?")
|
||||
args = append(args, int64(req.ApiKeyID))
|
||||
@@ -354,11 +362,11 @@ func (s *sAdmin) ListRequestAuditLogs(ctx context.Context, req *v1.RequestAuditL
|
||||
}
|
||||
if req.StartDate != "" {
|
||||
conditions = append(conditions, "created_at >= ?")
|
||||
args = append(args, req.StartDate+" 00:00:00")
|
||||
args = append(args, common.StartOfRange(req.StartDate))
|
||||
}
|
||||
if req.EndDate != "" {
|
||||
conditions = append(conditions, "created_at <= ?")
|
||||
args = append(args, req.EndDate+" 23:59:59")
|
||||
args = append(args, common.EndOfRange(req.EndDate))
|
||||
}
|
||||
|
||||
where := strings.Join(conditions, " AND ")
|
||||
|
||||
@@ -61,6 +61,9 @@ func (s *sAdmin) ListChannels(ctx context.Context, req *v1.ChannelListReq) (*v1.
|
||||
if req.ID > 0 {
|
||||
query = query.Where("chn_channels.id", req.ID)
|
||||
}
|
||||
if req.BaseURL != "" {
|
||||
query = query.Where("chn_channels.base_url LIKE ?", "%"+req.BaseURL+"%")
|
||||
}
|
||||
if req.Model != "" {
|
||||
// 按支持的模型名筛选:渠道需具备匹配的模型能力(chn_abilities)。
|
||||
// 用 EXISTS 相关子查询而非 JOIN,避免一个渠道命中多个模型时产生重复行、破坏分页与总数。
|
||||
@@ -706,6 +709,55 @@ func (s *sAdmin) GetChannelHealthTrend(ctx context.Context, req *v1.ChannelHealt
|
||||
return &v1.ChannelHealthTrendRes{Points: points}, nil
|
||||
}
|
||||
|
||||
// ResetChannelHealth 重置渠道健康度(渠道可能已修复、重新可用):落库健康分 80(展示层),
|
||||
// 并复位熔断 + 成功率 EWMA(调度层),使渠道立即恢复被调度选择的能力。
|
||||
func (s *sAdmin) ResetChannelHealth(ctx context.Context, req *v1.ChannelResetHealthReq) (*v1.ChannelResetHealthRes, error) {
|
||||
// 渠道必须存在
|
||||
count, err := dao.ChnChannels.Ctx(ctx).Where("id", req.ID).Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count == 0 {
|
||||
return nil, common.NewBusinessError(404, "渠道不存在")
|
||||
}
|
||||
|
||||
// 展示层:健康分落库为 80(调度不读此表,仅供仪表盘/审计展示;下次维护快照会按策略重算)
|
||||
affected, err := dao.ChnHealthScores.Ctx(ctx).
|
||||
Where("channel_id", req.ID).
|
||||
Data(do.ChnHealthScores{
|
||||
SuccessRate: 90.00,
|
||||
LatencyMs: 0,
|
||||
StabilityScore: 100.00,
|
||||
ConsecutiveFailures: 0,
|
||||
HealthScore: 80.00,
|
||||
CalculatedAt: gtime.Now(),
|
||||
}).
|
||||
UpdateAndGetAffected()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 健康分记录缺失时兜底插入(正常情况下创建渠道时已由 InitHealthScore 初始化)
|
||||
if affected == 0 {
|
||||
_, err = dao.ChnHealthScores.Ctx(ctx).Insert(do.ChnHealthScores{
|
||||
ChannelId: req.ID,
|
||||
SuccessRate: 90.00,
|
||||
LatencyMs: 0,
|
||||
StabilityScore: 100.00,
|
||||
ConsecutiveFailures: 0,
|
||||
HealthScore: 80.00,
|
||||
CalculatedAt: gtime.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 调度层:复位渠道级/模型级熔断 + 成功率恢复,渠道立即恢复被选择能力
|
||||
dispatchadapter.ResetChannelHealth(ctx, req.ID)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ExportChannels exports channel list to CSV or Excel.
|
||||
func (s *sAdmin) ExportChannels(ctx context.Context, req *v1.ChannelExportReq) (*v1.ChannelExportRes, error) {
|
||||
channelFields := "chn_channels.id, chn_channels.name, chn_channels.type, chn_channels.status, chn_channels.priority, chn_channels.weight, chn_channels.created_at, h.health_score"
|
||||
@@ -742,6 +794,9 @@ func (s *sAdmin) ExportChannels(ctx context.Context, req *v1.ChannelExportReq) (
|
||||
if req.ID > 0 {
|
||||
query = query.Where("chn_channels.id", req.ID)
|
||||
}
|
||||
if req.BaseURL != "" {
|
||||
query = query.Where("chn_channels.base_url LIKE ?", "%"+req.BaseURL+"%")
|
||||
}
|
||||
if req.Model != "" {
|
||||
// 与 ListChannels 保持一致:EXISTS 相关子查询避免 JOIN 重复行
|
||||
query = query.Where("EXISTS (SELECT 1 FROM chn_abilities WHERE chn_abilities.channel_id = chn_channels.id AND (chn_abilities.model_name LIKE ? OR chn_abilities.upstream_model LIKE ?))", "%"+req.Model+"%", "%"+req.Model+"%")
|
||||
|
||||
@@ -351,10 +351,10 @@ func (s *sAdmin) GetModelHourlyCost(ctx context.Context, req *v1.AdminDashboardM
|
||||
|
||||
// GetAllUsageLogs 获取所有租户的用量日志(管理后台)
|
||||
func (s *sAdmin) GetAllUsageLogs(ctx context.Context, req *v1.AdminUsageLogListReq) (*v1.AdminUsageLogListRes, error) {
|
||||
if err := common.ValidateDateParam(req.StartDate, "开始日期"); err != nil {
|
||||
if err := common.ValidateDateTimeParam(req.StartDate, "开始时间"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := common.ValidateDateParam(req.EndDate, "结束日期"); err != nil {
|
||||
if err := common.ValidateDateTimeParam(req.EndDate, "结束时间"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -363,14 +363,30 @@ func (s *sAdmin) GetAllUsageLogs(ctx context.Context, req *v1.AdminUsageLogListR
|
||||
var conditions []string
|
||||
var args []any
|
||||
|
||||
if req.ID > 0 {
|
||||
conditions = append(conditions, "u.id = ?")
|
||||
args = append(args, req.ID)
|
||||
}
|
||||
if req.TenantID > 0 {
|
||||
conditions = append(conditions, "u.tenant_id = ?")
|
||||
args = append(args, req.TenantID)
|
||||
}
|
||||
if req.UserID > 0 {
|
||||
conditions = append(conditions, "u.user_id = ?")
|
||||
args = append(args, req.UserID)
|
||||
}
|
||||
if req.Username != "" {
|
||||
conditions = append(conditions, "t.username LIKE ?")
|
||||
args = append(args, "%"+req.Username+"%")
|
||||
}
|
||||
if req.ApiKeyID > 0 {
|
||||
conditions = append(conditions, "u.api_key_id = ?")
|
||||
args = append(args, req.ApiKeyID)
|
||||
}
|
||||
if req.ChannelID > 0 {
|
||||
conditions = append(conditions, "u.channel_id = ?")
|
||||
args = append(args, req.ChannelID)
|
||||
}
|
||||
if req.Model != "" {
|
||||
conditions = append(conditions, "u.model_name = ?")
|
||||
args = append(args, req.Model)
|
||||
@@ -385,11 +401,11 @@ func (s *sAdmin) GetAllUsageLogs(ctx context.Context, req *v1.AdminUsageLogListR
|
||||
}
|
||||
if req.StartDate != "" {
|
||||
conditions = append(conditions, "u.created_at >= ?")
|
||||
args = append(args, req.StartDate+" 00:00:00")
|
||||
args = append(args, common.StartOfRange(req.StartDate))
|
||||
}
|
||||
if req.EndDate != "" {
|
||||
conditions = append(conditions, "u.created_at <= ?")
|
||||
args = append(args, req.EndDate+" 23:59:59")
|
||||
args = append(args, common.EndOfRange(req.EndDate))
|
||||
}
|
||||
|
||||
where := ""
|
||||
@@ -1120,10 +1136,10 @@ func (s *sAdmin) GetDashboardRecentAlerts(ctx context.Context, req *v1.AdminDash
|
||||
|
||||
// ExportUsageLogs exports usage logs to CSV or Excel.
|
||||
func (s *sAdmin) ExportUsageLogs(ctx context.Context, req *v1.AdminUsageLogExportReq) (*v1.AdminUsageLogExportRes, error) {
|
||||
if err := common.ValidateDateParam(req.StartDate, "开始日期"); err != nil {
|
||||
if err := common.ValidateDateTimeParam(req.StartDate, "开始时间"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := common.ValidateDateParam(req.EndDate, "结束日期"); err != nil {
|
||||
if err := common.ValidateDateTimeParam(req.EndDate, "结束时间"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1149,14 +1165,30 @@ func (s *sAdmin) ExportUsageLogs(ctx context.Context, req *v1.AdminUsageLogExpor
|
||||
buildUsageWhere := func() (string, []any) {
|
||||
var conditions []string
|
||||
var args []any
|
||||
if req.ID > 0 {
|
||||
conditions = append(conditions, "u.id = ?")
|
||||
args = append(args, req.ID)
|
||||
}
|
||||
if req.TenantID > 0 {
|
||||
conditions = append(conditions, "u.tenant_id = ?")
|
||||
args = append(args, req.TenantID)
|
||||
}
|
||||
if req.UserID > 0 {
|
||||
conditions = append(conditions, "u.user_id = ?")
|
||||
args = append(args, req.UserID)
|
||||
}
|
||||
if req.Username != "" {
|
||||
conditions = append(conditions, "t.username LIKE ?")
|
||||
args = append(args, "%"+req.Username+"%")
|
||||
}
|
||||
if req.ApiKeyID > 0 {
|
||||
conditions = append(conditions, "u.api_key_id = ?")
|
||||
args = append(args, req.ApiKeyID)
|
||||
}
|
||||
if req.ChannelID > 0 {
|
||||
conditions = append(conditions, "u.channel_id = ?")
|
||||
args = append(args, req.ChannelID)
|
||||
}
|
||||
if req.Model != "" {
|
||||
conditions = append(conditions, "u.model_name = ?")
|
||||
args = append(args, req.Model)
|
||||
@@ -1171,11 +1203,11 @@ func (s *sAdmin) ExportUsageLogs(ctx context.Context, req *v1.AdminUsageLogExpor
|
||||
}
|
||||
if req.StartDate != "" {
|
||||
conditions = append(conditions, "u.created_at >= ?")
|
||||
args = append(args, req.StartDate+" 00:00:00")
|
||||
args = append(args, common.StartOfRange(req.StartDate))
|
||||
}
|
||||
if req.EndDate != "" {
|
||||
conditions = append(conditions, "u.created_at <= ?")
|
||||
args = append(args, req.EndDate+" 23:59:59")
|
||||
args = append(args, common.EndOfRange(req.EndDate))
|
||||
}
|
||||
where := ""
|
||||
if len(conditions) > 0 {
|
||||
|
||||
@@ -17,6 +17,13 @@ import (
|
||||
|
||||
// TaskList 大模型异步任务列表
|
||||
func (s *sAdmin) TaskList(ctx context.Context, req *v1.TaskListReq) (*v1.TaskListRes, error) {
|
||||
if err := common.ValidateDateTimeParam(req.StartDate, "开始时间"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := common.ValidateDateTimeParam(req.EndDate, "结束时间"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := dao.TskModelTasks.Ctx(ctx).OrderDesc("id")
|
||||
if req.Status != "" {
|
||||
m = m.Where("status", req.Status)
|
||||
@@ -27,6 +34,21 @@ func (s *sAdmin) TaskList(ctx context.Context, req *v1.TaskListReq) (*v1.TaskLis
|
||||
if req.PublicTaskID != "" {
|
||||
m = m.Where("public_task_id", req.PublicTaskID)
|
||||
}
|
||||
if req.ModelName != "" {
|
||||
m = m.Where("model_name", req.ModelName)
|
||||
}
|
||||
if req.TenantID > 0 {
|
||||
m = m.Where("tenant_id", req.TenantID)
|
||||
}
|
||||
if req.UserID > 0 {
|
||||
m = m.Where("user_id", req.UserID)
|
||||
}
|
||||
if req.StartDate != "" {
|
||||
m = m.Where("created_at >= ?", common.StartOfRange(req.StartDate))
|
||||
}
|
||||
if req.EndDate != "" {
|
||||
m = m.Where("created_at <= ?", common.EndOfRange(req.EndDate))
|
||||
}
|
||||
|
||||
var tasks []struct {
|
||||
Id int64 `json:"id"`
|
||||
|
||||
@@ -81,6 +81,36 @@ func ValidateDateParam(date string, fieldName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateDateTimeParam 校验时间筛选参数,兼容 YYYY-MM-DD 与 YYYY-MM-DD HH:mm:ss
|
||||
// (前端时间范围选择器精确到分钟时传完整时间戳,仅选日期时传日期)。
|
||||
func ValidateDateTimeParam(value string, fieldName string) error {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
for _, layout := range []string{"2006-01-02", "2006-01-02 15:04:05"} {
|
||||
if _, err := time.Parse(layout, value); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return gerror.NewCode(gcode.New(consts.CodeBadRequest, "", nil), "%s格式无效,应为 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss", fieldName)
|
||||
}
|
||||
|
||||
// StartOfRange 返回时间范围过滤起点:仅日期(YYYY-MM-DD)时补齐当天 00:00:00,带时间则原样使用
|
||||
func StartOfRange(value string) string {
|
||||
if len(value) == 10 {
|
||||
return value + " 00:00:00"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// EndOfRange 返回时间范围过滤终点:仅日期(YYYY-MM-DD)时补齐当天 23:59:59,带时间则原样使用
|
||||
func EndOfRange(value string) string {
|
||||
if len(value) == 10 {
|
||||
return value + " 23:59:59"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// ValidateForbiddenWords 校验名称是否包含系统禁用词
|
||||
// 从系统配置 register_forbidden_words 读取逗号分隔的禁用词列表,对 value 做大小写不敏感的包含检查
|
||||
func ValidateForbiddenWords(ctx context.Context, value, fieldName string) error {
|
||||
|
||||
@@ -119,6 +119,9 @@ type (
|
||||
GetProviderDefaultURLs(ctx context.Context, _ *v1.ProviderDefaultURLReq) (*v1.ProviderDefaultURLRes, error)
|
||||
// GetChannelHealthTrend 获取渠道健康趋势数据
|
||||
GetChannelHealthTrend(ctx context.Context, req *v1.ChannelHealthTrendReq) (*v1.ChannelHealthTrendRes, error)
|
||||
// ResetChannelHealth 重置渠道健康度(渠道可能已修复、重新可用):落库健康分 80(展示层),
|
||||
// 并复位熔断 + 成功率 EWMA(调度层),使渠道立即恢复被调度选择的能力。
|
||||
ResetChannelHealth(ctx context.Context, req *v1.ChannelResetHealthReq) (*v1.ChannelResetHealthRes, error)
|
||||
// ExportChannels exports channel list to CSV or Excel.
|
||||
ExportChannels(ctx context.Context, req *v1.ChannelExportReq) (*v1.ChannelExportRes, error)
|
||||
// ChannelErrorEventList 渠道错误事件列表
|
||||
|
||||
@@ -28,6 +28,7 @@ const filterStatus = ref<string | undefined>(undefined)
|
||||
const filterSearch = ref('')
|
||||
const filterID = ref<number | undefined>(undefined)
|
||||
const filterModel = ref('')
|
||||
const filterBaseURL = ref('')
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '全部状态', value: '' },
|
||||
@@ -122,7 +123,7 @@ const columns: TableColumnData[] = [
|
||||
},
|
||||
{ title: '创建时间', dataIndex: 'created_at', width: 170 },
|
||||
{
|
||||
title: '操作', dataIndex: 'actions', width: 210, fixed: 'right',
|
||||
title: '操作', dataIndex: 'actions', width: 300, fixed: 'right',
|
||||
render({ record }) {
|
||||
const isActive = record.status === 'active'
|
||||
const action = isActive ? '禁用' : '启用'
|
||||
@@ -137,6 +138,15 @@ const columns: TableColumnData[] = [
|
||||
status: isActive ? 'normal' : 'success',
|
||||
}, () => action))
|
||||
: null,
|
||||
hasPermission('channel:edit')
|
||||
? h(Popconfirm, {
|
||||
content: '确定重置该渠道健康度?熔断将复位、成功率恢复,渠道立即恢复被调度选择。',
|
||||
onOk: () => resetHealth(record),
|
||||
}, () => h(Button, {
|
||||
size: 'small',
|
||||
status: 'warning',
|
||||
}, () => '重置健康度'))
|
||||
: null,
|
||||
h(Button, { size: 'small', type: 'primary', onClick: () => openDetail(record) }, () => '详情'),
|
||||
hasPermission('channel:delete')
|
||||
? h(Popconfirm, { content: '确定删除该渠道?', onOk: () => deleteChannel(record) }, () => h(Button, { size: 'small', status: 'danger' }, () => '删除'))
|
||||
@@ -156,6 +166,7 @@ async function fetchData() {
|
||||
if (filterSearch.value) params.search = filterSearch.value
|
||||
if (filterID.value) params.id = filterID.value
|
||||
if (filterModel.value) params.model = filterModel.value
|
||||
if (filterBaseURL.value) params.base_url = filterBaseURL.value
|
||||
const res: any = await request.get('/admin/channels', { params })
|
||||
data.value = res.data?.data?.list || res.data?.list || []
|
||||
// 记录本次加载时已持久化的权重/层级,供行内编辑失败回滚
|
||||
@@ -234,6 +245,15 @@ async function deleteChannel(row: any) {
|
||||
catch { /* interceptor handles error toast */ }
|
||||
}
|
||||
|
||||
// 重置渠道健康度:熔断复位 + 成功率恢复,渠道立即恢复被调度选择的能力。
|
||||
async function resetHealth(row: any) {
|
||||
try {
|
||||
await request.post(`/admin/channels/${row.id}/reset-health`)
|
||||
message.success('已重置健康度')
|
||||
fetchData()
|
||||
} catch { /* interceptor handles error toast */ }
|
||||
}
|
||||
|
||||
// 快捷启用/禁用渠道:复用 PUT /admin/channels/{id}。
|
||||
// 后端 UpdateChannel 会无条件写入 priority/weight,故必须带上当前值,否则会被重置为 0。
|
||||
// status 为 active → 禁用;其它(disabled/testing)→ 启用为 active。
|
||||
@@ -385,6 +405,7 @@ const { exporting, exportFile } = useExport({
|
||||
search: filterSearch.value,
|
||||
id: filterID.value,
|
||||
model: filterModel.value,
|
||||
base_url: filterBaseURL.value,
|
||||
}),
|
||||
})
|
||||
</script>
|
||||
@@ -446,11 +467,12 @@ const { exporting, exportFile } = useExport({
|
||||
<!-- Filters -->
|
||||
<ACard :bordered="false" class="mb-4">
|
||||
<ASpace>
|
||||
<ASelect v-model="filterType" :options="[{ label: '全部类型', value: '' }, ...providerTypeOptions]" placeholder="供应商类型" allow-clear allow-search style="width: 180px" @change="handleFilter" />
|
||||
<ASelect v-model="filterStatus" :options="statusOptions" placeholder="状态" allow-clear style="width: 120px" @change="handleFilter" />
|
||||
<AInput v-model="filterSearch" placeholder="搜索渠道名..." allow-clear style="width: 200px" @keydown.enter="handleFilter" @clear="handleFilter" />
|
||||
<AInputNumber v-model="filterID" placeholder="渠道 ID" :min="1" allow-clear style="width: 120px" @change="handleFilter" @clear="handleFilter" />
|
||||
<ASelect v-model="filterType" :options="[{ label: '全部类型', value: '' }, ...providerTypeOptions]" placeholder="供应商类型" allow-clear allow-search style="width: 180px" @change="handleFilter" />
|
||||
<AInput v-model="filterModel" placeholder="搜索模型名..." allow-clear style="width: 160px" @keydown.enter="handleFilter" @clear="handleFilter" />
|
||||
<ASelect v-model="filterStatus" :options="statusOptions" placeholder="状态" allow-clear style="width: 120px" @change="handleFilter" />
|
||||
<AInput v-model="filterBaseURL" placeholder="搜索地址..." allow-clear style="width: 220px" @keydown.enter="handleFilter" @clear="handleFilter" />
|
||||
<AInput v-model="filterSearch" placeholder="搜索渠道名..." allow-clear style="width: 200px" @keydown.enter="handleFilter" @clear="handleFilter" />
|
||||
<AButton @click="handleFilter">搜索</AButton>
|
||||
</ASpace>
|
||||
</ACard>
|
||||
|
||||
@@ -22,6 +22,10 @@ const pagination = reactive({
|
||||
const filterStatus = ref<string | null>(null)
|
||||
const filterPlatform = ref<string | null>(null)
|
||||
const filterTaskId = ref('')
|
||||
const filterDateRange = ref<string[]>([])
|
||||
const filterModel = ref('')
|
||||
const filterTenantId = ref<number | undefined>(undefined)
|
||||
const filterUserId = ref<number | undefined>(undefined)
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '全部状态', value: '' },
|
||||
@@ -156,6 +160,13 @@ async function fetchData() {
|
||||
if (filterStatus.value) params.status = filterStatus.value
|
||||
if (filterPlatform.value) params.platform = filterPlatform.value
|
||||
if (filterTaskId.value) params.public_task_id = filterTaskId.value
|
||||
if (filterDateRange.value && filterDateRange.value.length === 2) {
|
||||
params.start_date = filterDateRange.value[0]
|
||||
params.end_date = filterDateRange.value[1]
|
||||
}
|
||||
if (filterModel.value) params.model_name = filterModel.value
|
||||
if (filterTenantId.value) params.tenant_id = filterTenantId.value
|
||||
if (filterUserId.value) params.user_id = filterUserId.value
|
||||
const res: any = await request.get('/admin/tasks', { params })
|
||||
const raw = res.data?.data
|
||||
data.value = (raw?.list || []).filter(Boolean)
|
||||
@@ -177,6 +188,10 @@ function resetFilter() {
|
||||
filterStatus.value = null
|
||||
filterPlatform.value = null
|
||||
filterTaskId.value = ''
|
||||
filterDateRange.value = []
|
||||
filterModel.value = ''
|
||||
filterTenantId.value = undefined
|
||||
filterUserId.value = undefined
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
@@ -229,6 +244,12 @@ onMounted(() => {
|
||||
<!-- Filters -->
|
||||
<ACard :bordered="false" class="mb-4">
|
||||
<ASpace wrap>
|
||||
<ARangePicker
|
||||
v-model="filterDateRange"
|
||||
show-time
|
||||
style="width: 340px"
|
||||
@change="handleFilter"
|
||||
/>
|
||||
<AInput
|
||||
v-model="filterTaskId"
|
||||
placeholder="任务ID"
|
||||
@@ -236,6 +257,14 @@ onMounted(() => {
|
||||
style="width: 200px"
|
||||
@keydown.enter="handleFilter"
|
||||
/>
|
||||
<ASelect
|
||||
v-model="filterPlatform"
|
||||
:options="platformOptions"
|
||||
placeholder="平台"
|
||||
allow-clear
|
||||
style="width: 130px"
|
||||
@change="handleFilter"
|
||||
/>
|
||||
<ASelect
|
||||
v-model="filterStatus"
|
||||
:options="statusOptions"
|
||||
@@ -244,13 +273,30 @@ onMounted(() => {
|
||||
style="width: 130px"
|
||||
@change="handleFilter"
|
||||
/>
|
||||
<ASelect
|
||||
v-model="filterPlatform"
|
||||
:options="platformOptions"
|
||||
placeholder="平台"
|
||||
<AInput
|
||||
v-model="filterModel"
|
||||
placeholder="模型"
|
||||
allow-clear
|
||||
style="width: 130px"
|
||||
style="width: 160px"
|
||||
@keydown.enter="handleFilter"
|
||||
/>
|
||||
<AInputNumber
|
||||
v-model="filterTenantId"
|
||||
placeholder="租户ID"
|
||||
:min="1"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
@clear="handleFilter"
|
||||
/>
|
||||
<AInputNumber
|
||||
v-model="filterUserId"
|
||||
placeholder="用户ID"
|
||||
:min="1"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
@clear="handleFilter"
|
||||
/>
|
||||
<AButton type="primary" @click="handleFilter">搜索</AButton>
|
||||
<AButton @click="resetFilter">重置</AButton>
|
||||
|
||||
@@ -22,13 +22,11 @@ const pagination = reactive({
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
})
|
||||
const filter = reactive({
|
||||
tenant_id: null as number | null,
|
||||
api_key_id: '',
|
||||
username: '',
|
||||
id: null as number | null,
|
||||
request_id: '',
|
||||
task_id: '',
|
||||
method: '',
|
||||
status_code: '',
|
||||
tenant_id: null as number | null,
|
||||
user_id: null as number | null,
|
||||
api_key_id: '',
|
||||
date_range: [] as string[],
|
||||
})
|
||||
|
||||
@@ -197,13 +195,11 @@ async function fetchData() {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
}
|
||||
if (filter.tenant_id) params.tenant_id = filter.tenant_id
|
||||
if (filter.api_key_id) params.api_key_id = parseInt(filter.api_key_id)
|
||||
if (filter.username) params.username = filter.username
|
||||
if (filter.method) params.method = filter.method
|
||||
if (filter.id) params.id = filter.id
|
||||
if (filter.request_id) params.request_id = filter.request_id
|
||||
if (filter.task_id) params.task_id = filter.task_id
|
||||
if (filter.status_code) params.status_code = parseInt(filter.status_code)
|
||||
if (filter.tenant_id) params.tenant_id = filter.tenant_id
|
||||
if (filter.user_id) params.user_id = filter.user_id
|
||||
if (filter.api_key_id) params.api_key_id = parseInt(filter.api_key_id)
|
||||
if (filter.date_range.length === 2) {
|
||||
params.start_date = filter.date_range[0]
|
||||
params.end_date = filter.date_range[1]
|
||||
@@ -246,13 +242,11 @@ function handleFilter() {
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
filter.tenant_id = null
|
||||
filter.api_key_id = ''
|
||||
filter.username = ''
|
||||
filter.id = null
|
||||
filter.request_id = ''
|
||||
filter.task_id = ''
|
||||
filter.method = ''
|
||||
filter.status_code = ''
|
||||
filter.tenant_id = null
|
||||
filter.user_id = null
|
||||
filter.api_key_id = ''
|
||||
filter.date_range = []
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
@@ -264,9 +258,6 @@ onMounted(() => {
|
||||
if (route.query.request_id) {
|
||||
filter.request_id = String(route.query.request_id)
|
||||
}
|
||||
if (route.query.task_id) {
|
||||
filter.task_id = String(route.query.task_id)
|
||||
}
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
@@ -277,6 +268,28 @@ onMounted(() => {
|
||||
|
||||
<a-card :bordered="false" class="mb-4">
|
||||
<a-space wrap>
|
||||
<a-range-picker
|
||||
v-model="filter.date_range"
|
||||
show-time
|
||||
style="width: 340px"
|
||||
@change="handleFilter"
|
||||
/>
|
||||
<a-input-number
|
||||
v-model="filter.id"
|
||||
placeholder="日志ID"
|
||||
:min="1"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
@clear="handleFilter"
|
||||
/>
|
||||
<a-input
|
||||
v-model="filter.request_id"
|
||||
placeholder="Request ID"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
@keydown.enter="handleFilter"
|
||||
/>
|
||||
<a-select
|
||||
v-model="filter.tenant_id"
|
||||
:options="tenantOptions"
|
||||
@@ -289,6 +302,15 @@ onMounted(() => {
|
||||
@change="handleFilter"
|
||||
@clear="handleFilter"
|
||||
/>
|
||||
<a-input-number
|
||||
v-model="filter.user_id"
|
||||
placeholder="用户ID"
|
||||
:min="1"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
@clear="handleFilter"
|
||||
/>
|
||||
<a-input
|
||||
v-model="filter.api_key_id"
|
||||
placeholder="Key ID"
|
||||
@@ -296,50 +318,6 @@ onMounted(() => {
|
||||
style="width: 100px"
|
||||
@keydown.enter="handleFilter"
|
||||
/>
|
||||
<a-input
|
||||
v-model="filter.username"
|
||||
placeholder="用户名"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
@keydown.enter="handleFilter"
|
||||
/>
|
||||
<a-input
|
||||
v-model="filter.request_id"
|
||||
placeholder="Request ID"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
@keydown.enter="handleFilter"
|
||||
/>
|
||||
<a-input
|
||||
v-model="filter.task_id"
|
||||
placeholder="Task ID"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
@keydown.enter="handleFilter"
|
||||
/>
|
||||
<a-select
|
||||
v-model="filter.method"
|
||||
placeholder="方法"
|
||||
allow-clear
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-option value="GET">GET</a-option>
|
||||
<a-option value="POST">POST</a-option>
|
||||
<a-option value="PUT">PUT</a-option>
|
||||
<a-option value="DELETE">DELETE</a-option>
|
||||
</a-select>
|
||||
<a-input
|
||||
v-model="filter.status_code"
|
||||
placeholder="状态码"
|
||||
allow-clear
|
||||
style="width: 100px"
|
||||
@keydown.enter="handleFilter"
|
||||
/>
|
||||
<a-range-picker
|
||||
v-model="filter.date_range"
|
||||
style="width: 260px"
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
<a-button type="primary" @click="handleFilter">搜索</a-button>
|
||||
<a-button @click="handleReset">重置</a-button>
|
||||
</a-space>
|
||||
|
||||
@@ -21,11 +21,14 @@ const pagination = reactive({
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
})
|
||||
|
||||
const filterId = ref<number | undefined>(undefined)
|
||||
const filterTenantId = ref<number | undefined>(undefined)
|
||||
const filterUsername = ref('')
|
||||
const filterUserId = ref<number | undefined>(undefined)
|
||||
const filterModel = ref<string | undefined>(undefined)
|
||||
const filterStatus = ref<string | undefined>(undefined)
|
||||
const filterApiKeyId = ref<number | undefined>(undefined)
|
||||
const filterRequestType = ref<string | undefined>(undefined)
|
||||
const filterChannelId = ref<number | undefined>(undefined)
|
||||
const filterStatus = ref<string | undefined>(undefined)
|
||||
const filterDateRange = ref<string[]>([])
|
||||
|
||||
const tenantOptions = ref<{ label: string; value: number }[]>([])
|
||||
@@ -389,11 +392,14 @@ async function fetchData() {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
}
|
||||
if (filterId.value) params.id = filterId.value
|
||||
if (filterTenantId.value) params.tenant_id = filterTenantId.value
|
||||
if (filterUsername.value) params.username = filterUsername.value
|
||||
if (filterUserId.value) params.user_id = filterUserId.value
|
||||
if (filterModel.value) params.model = filterModel.value
|
||||
if (filterStatus.value) params.status = filterStatus.value
|
||||
if (filterApiKeyId.value) params.api_key_id = filterApiKeyId.value
|
||||
if (filterRequestType.value) params.request_type = filterRequestType.value
|
||||
if (filterChannelId.value) params.channel_id = filterChannelId.value
|
||||
if (filterStatus.value) params.status = filterStatus.value
|
||||
if (filterDateRange.value && filterDateRange.value.length === 2) {
|
||||
params.start_date = filterDateRange.value[0]
|
||||
params.end_date = filterDateRange.value[1]
|
||||
@@ -417,11 +423,14 @@ function handleFilter() {
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
filterId.value = undefined
|
||||
filterTenantId.value = undefined
|
||||
filterUsername.value = ''
|
||||
filterUserId.value = undefined
|
||||
filterModel.value = undefined
|
||||
filterStatus.value = undefined
|
||||
filterApiKeyId.value = undefined
|
||||
filterRequestType.value = undefined
|
||||
filterChannelId.value = undefined
|
||||
filterStatus.value = undefined
|
||||
filterDateRange.value = []
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
@@ -436,11 +445,14 @@ onMounted(() => {
|
||||
const { exporting, exportFile } = useExport({
|
||||
url: '/admin/usage-logs/export',
|
||||
getFilters: () => ({
|
||||
id: filterId.value,
|
||||
tenant_id: filterTenantId.value,
|
||||
username: filterUsername.value,
|
||||
user_id: filterUserId.value,
|
||||
model: filterModel.value,
|
||||
status: filterStatus.value,
|
||||
api_key_id: filterApiKeyId.value,
|
||||
request_type: filterRequestType.value,
|
||||
channel_id: filterChannelId.value,
|
||||
status: filterStatus.value,
|
||||
start_date: filterDateRange.value?.[0],
|
||||
end_date: filterDateRange.value?.[1],
|
||||
}),
|
||||
@@ -464,10 +476,25 @@ const { exporting, exportFile } = useExport({
|
||||
|
||||
<a-card :bordered="false" class="mb-4">
|
||||
<a-space wrap>
|
||||
<a-range-picker
|
||||
v-model="filterDateRange"
|
||||
show-time
|
||||
style="width: 340px"
|
||||
@change="handleFilter"
|
||||
/>
|
||||
<a-input-number
|
||||
v-model="filterId"
|
||||
placeholder="记录ID"
|
||||
:min="1"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
@clear="handleFilter"
|
||||
/>
|
||||
<a-select
|
||||
v-model="filterTenantId"
|
||||
:options="tenantOptions"
|
||||
placeholder="搜索租户"
|
||||
placeholder="租户ID"
|
||||
allow-search
|
||||
allow-clear
|
||||
:filter-option="false"
|
||||
@@ -476,12 +503,14 @@ const { exporting, exportFile } = useExport({
|
||||
@change="handleFilter"
|
||||
@clear="handleFilter"
|
||||
/>
|
||||
<a-input
|
||||
v-model="filterUsername"
|
||||
placeholder="用户名"
|
||||
<a-input-number
|
||||
v-model="filterUserId"
|
||||
placeholder="用户ID"
|
||||
:min="1"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
@keydown.enter="handleFilter"
|
||||
@change="handleFilter"
|
||||
@clear="handleFilter"
|
||||
/>
|
||||
<a-select
|
||||
v-model="filterModel"
|
||||
@@ -495,13 +524,14 @@ const { exporting, exportFile } = useExport({
|
||||
@change="handleFilter"
|
||||
@clear="handleFilter"
|
||||
/>
|
||||
<a-select
|
||||
v-model="filterStatus"
|
||||
:options="statusOptions"
|
||||
placeholder="状态"
|
||||
<a-input-number
|
||||
v-model="filterApiKeyId"
|
||||
placeholder="API Key ID"
|
||||
:min="1"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
@clear="handleFilter"
|
||||
/>
|
||||
<a-select
|
||||
v-model="filterRequestType"
|
||||
@@ -511,9 +541,21 @@ const { exporting, exportFile } = useExport({
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
/>
|
||||
<a-range-picker
|
||||
v-model="filterDateRange"
|
||||
style="width: 280px"
|
||||
<a-input-number
|
||||
v-model="filterChannelId"
|
||||
placeholder="渠道ID"
|
||||
:min="1"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
@clear="handleFilter"
|
||||
/>
|
||||
<a-select
|
||||
v-model="filterStatus"
|
||||
:options="statusOptions"
|
||||
placeholder="状态"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
/>
|
||||
<a-button type="primary" @click="handleFilter">搜索</a-button>
|
||||
|
||||
Reference in new issue
Block a user