diff --git a/go/conf/app.conf b/go/conf/app.conf index e31286c..29f9f07 100644 --- a/go/conf/app.conf +++ b/go/conf/app.conf @@ -1,5 +1,5 @@ appname = server -httpport = 8081 +httpport = 9000 runmode = dev # 启用请求体复制(允许多次读取请求体) diff --git a/go/controllers/api_getcard.go b/go/controllers/api_getcard.go index 5ab9220..0a71625 100644 --- a/go/controllers/api_getcard.go +++ b/go/controllers/api_getcard.go @@ -55,7 +55,7 @@ func (c *ApiGetCardController) cardOK(text string) { // - type (必填) 来源平台:xianyu / taobao / pinduoduo / jingdong / local / xubei // - module (必填) 号池模块:cursor / windsurf / krio / codex // - data_type (可选) 账号类型:account / tk / account_tk,不传则取任意未提取的 -// - id/start_id/current_id (可选) 起始 ID:从该 ID 开始向后提取,避免传 896 却取到 892 +// - id/start_id/current_id (可选) 起始 ID:仅取 id >= 起始值 的记录,按 id 从大到小提取 func (c *ApiGetCardController) GetCard() { platform := c.GetString("type") module := c.GetString("module") @@ -159,7 +159,7 @@ func (c *ApiGetCardController) extractCursor(platform, dataType string, startID if dataType != "" { qs = qs.Filter("data_type", dataType) } - if err := qs.OrderBy("id").One(&row); err != nil { + if err := qs.OrderBy("-id").One(&row); err != nil { return 0, nil, nil, "", "", nil, err } return row.ID, &row.Account, &row.Password, row.Token, row.DataType, row.IsUsed, nil @@ -178,7 +178,7 @@ func (c *ApiGetCardController) extractWindsurf(platform, dataType string, startI if dataType != "" { qs = qs.Filter("data_type", dataType) } - if err := qs.OrderBy("id").One(&row); err != nil { + if err := qs.OrderBy("-id").One(&row); err != nil { return 0, nil, nil, "", "", nil, err } return row.ID, &row.Account, &row.Password, row.Token, row.DataType, nil, nil @@ -197,7 +197,7 @@ func (c *ApiGetCardController) extractKrio(platform, dataType string, startID ui if dataType != "" { qs = qs.Filter("data_type", dataType) } - if err := qs.OrderBy("id").One(&row); err != nil { + if err := qs.OrderBy("-id").One(&row); err != nil { return 0, nil, nil, "", "", nil, err } return row.ID, &row.Account, &row.Password, row.Token, row.DataType, nil, nil @@ -216,7 +216,7 @@ func (c *ApiGetCardController) extractCodex(platform, dataType string, startID u if dataType != "" { qs = qs.Filter("data_type", dataType) } - if err := qs.OrderBy("id").One(&row); err != nil { + if err := qs.OrderBy("-id").One(&row); err != nil { return 0, nil, nil, "", "", nil, err } return row.ID, &row.Account, &row.Password, row.Token, row.DataType, nil, nil @@ -225,7 +225,7 @@ func (c *ApiGetCardController) extractCodex(platform, dataType string, startID u type poolRowFetcher func() (id uint64, account, password *string, token, rowDataType string, isUsed *int8, err error) -// extractWithProbe 按 id 顺序提取并探测 Token 可用性;不可用则标记已提取并继续下一条。 +// extractWithProbe 按 id 从大到小提取并探测 Token 可用性;不可用则标记已提取并继续下一条。 func (c *ApiGetCardController) extractWithProbe( module, platform, dataType string, now time.Time, diff --git a/go/controllers/platform_cursor_equipment.go b/go/controllers/platform_cursor_equipment.go index 88bbaac..a9569cc 100644 --- a/go/controllers/platform_cursor_equipment.go +++ b/go/controllers/platform_cursor_equipment.go @@ -520,6 +520,56 @@ func (c *PlatformCursorEquipmentController) List() { }) } +// Stats GET /platform/cursor/equipment/stats +// 全量设备统计(不受分页与列表筛选影响) +func (c *PlatformCursorEquipmentController) Stats() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + onlineSince := time.Now().Add(-5 * time.Minute) + type statsRow struct { + Total int64 + Inactive int64 + Active int64 + Expired int64 + Disabled int64 + Online int64 + } + + var row statsRow + err := models.Orm.Raw( + "SELECT COUNT(*) AS total,"+ + " SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) AS inactive,"+ + " SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS active,"+ + " SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) AS expired,"+ + " SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) AS disabled,"+ + " SUM(CASE WHEN last_heartbeat_at IS NOT NULL AND last_heartbeat_at >= ? THEN 1 ELSE 0 END) AS online"+ + " FROM yz_platform_cursor_equipment WHERE delete_time IS NULL", + onlineSince, + ).QueryRow(&row) + if err != nil { + c.jsonErr(500, 500, "获取设备统计失败: "+err.Error()) + return + } + + offline := row.Total - row.Online + if offline < 0 { + offline = 0 + } + + c.ok(map[string]interface{}{ + "total": row.Total, + "online": row.Online, + "offline": offline, + "active": row.Active, + "inactive": row.Inactive, + "expired": row.Expired, + "disabled": row.Disabled, + }) +} + // Detail GET /platform/cursor/equipment/detail/:id func (c *PlatformCursorEquipmentController) Detail() { if _, err := c.platformClaims(); err != nil { diff --git a/go/routers/platform/platform.go b/go/routers/platform/platform.go index 79953af..5864416 100644 --- a/go/routers/platform/platform.go +++ b/go/routers/platform/platform.go @@ -180,6 +180,7 @@ func Register() { // Cursor 设备管理(yz_platform_cursor_equipment) beego.Router("/platform/cursor/equipment/list", &controllers.PlatformCursorEquipmentController{}, "get:List") + beego.Router("/platform/cursor/equipment/stats", &controllers.PlatformCursorEquipmentController{}, "get:Stats") beego.Router("/platform/cursor/equipment/detail/:id", &controllers.PlatformCursorEquipmentController{}, "get:Detail") beego.Router("/platform/cursor/equipment/add", &controllers.PlatformCursorEquipmentController{}, "post:Add") beego.Router("/platform/cursor/equipment/update", &controllers.PlatformCursorEquipmentController{}, "post:Update") diff --git a/platform/src/api/cursorEquipment.js b/platform/src/api/cursorEquipment.js index ba98cf5..3f03f30 100644 --- a/platform/src/api/cursorEquipment.js +++ b/platform/src/api/cursorEquipment.js @@ -10,6 +10,13 @@ export function getCursorEquipmentList(params) { }); } +export function getCursorEquipmentStats() { + return request({ + url: `${baseUrl}/stats`, + method: 'get', + }); +} + export function getCursorEquipmentDetail(id) { return request({ url: `${baseUrl}/detail/${id}`, diff --git a/platform/src/api/cursorEquipment.ts b/platform/src/api/cursorEquipment.ts index 1017bff..f39af21 100644 --- a/platform/src/api/cursorEquipment.ts +++ b/platform/src/api/cursorEquipment.ts @@ -27,6 +27,16 @@ export interface CursorEquipmentPayload { remark?: string; } +export interface CursorEquipmentStats { + total: number; + online: number; + offline: number; + active: number; + inactive: number; + expired: number; + disabled: number; +} + export function getCursorEquipmentList(params: CursorEquipmentQuery) { return request({ url: `${baseUrl}/list`, @@ -35,6 +45,13 @@ export function getCursorEquipmentList(params: CursorEquipmentQuery) { }); } +export function getCursorEquipmentStats() { + return request({ + url: `${baseUrl}/stats`, + method: 'get', + }); +} + export function getCursorEquipmentDetail(id: number | string) { return request({ url: `${baseUrl}/detail/${id}`, diff --git a/platform/src/api/role.js b/platform/src/api/role.js index 83c0cc8..6bfedc0 100644 --- a/platform/src/api/role.js +++ b/platform/src/api/role.js @@ -36,3 +36,10 @@ export function deleteRole(id) { method: 'delete' }) } + +export function getRoleByTenantId(tenantId) { + return request({ + url: '/api/role/tenant/' + tenantId, // 根据实际 API 调整 + method: 'get' + }) +} \ No newline at end of file diff --git a/platform/src/views/cursor/equipment/index.vue b/platform/src/views/cursor/equipment/index.vue index 7bc53e3..69b10db 100644 --- a/platform/src/views/cursor/equipment/index.vue +++ b/platform/src/views/cursor/equipment/index.vue @@ -16,6 +16,7 @@ import { getCursorEquipmentExtractRecords, getCursorEquipmentIpLogs, getCursorEquipmentList, + getCursorEquipmentStats, updateCursorEquipment, } from '../../../api/cursorEquipment'; @@ -33,6 +34,15 @@ const selectedRows = ref([]); const tableData = ref([]); const total = ref(0); const isMobile = ref(false); +const summaryStats = ref({ + total: 0, + online: 0, + offline: 0, + active: 0, + inactive: 0, + expired: 0, + disabled: 0, +}); const query = reactive({ keyword: '', @@ -93,18 +103,14 @@ const statusMap: Record = { }; const summary = computed(() => { - const active = tableData.value.filter((item) => item.status === 'active').length; - const inactive = tableData.value.filter((item) => item.status === 'inactive').length; - const disabled = tableData.value.filter((item) => item.status === 'disabled').length; - const expired = tableData.value.filter((item) => item.status === 'expired').length; - const online = tableData.value.filter((item) => item.isOnline).length; - const offline = tableData.value.length - online; + const stats = summaryStats.value; return [ - { label: '当前页设备', value: tableData.value.length, type: 'primary' }, - { label: '在线 / 离线', isOnlineOffline: true, online, offline }, - { label: '已激活设备', value: active, type: 'success' }, - { label: '未激活', value: inactive, type: 'info' }, - { label: '禁用/过期', value: disabled + expired, type: 'danger' }, + { label: '设备总数', value: stats.total, type: 'primary' }, + { label: '在线 / 离线', isOnlineOffline: true, online: stats.online, offline: stats.offline }, + { label: '已激活设备', value: stats.active, type: 'success' }, + { label: '未激活', value: stats.inactive, type: 'info' }, + { label: '过期设备', value: stats.expired, type: 'warning' }, + { label: '禁用设备', value: stats.disabled, type: 'danger' }, ]; }); @@ -241,6 +247,27 @@ function handleSelectionChange(rows: EquipmentRow[]) { selectedRows.value = rows; } +async function fetchSummary() { + try { + const res = await getCursorEquipmentStats(); + if (res?.code !== 200) { + return; + } + const data = res?.data || {}; + summaryStats.value = { + total: Number(data.total || 0), + online: Number(data.online || 0), + offline: Number(data.offline || 0), + active: Number(data.active || 0), + inactive: Number(data.inactive || 0), + expired: Number(data.expired || 0), + disabled: Number(data.disabled || 0), + }; + } catch { + // 统计失败不影响列表展示 + } +} + async function fetchList() { loading.value = true; try { @@ -305,7 +332,7 @@ async function handleSave(payload: EquipmentRow) { } ElMessage.success(payload.id ? '设备已更新' : '设备已新增'); editVisible.value = false; - await fetchList(); + await Promise.all([fetchList(), fetchSummary()]); } finally { actionLoading.value = false; } @@ -322,7 +349,7 @@ async function handleDelete() { } ElMessage.success('设备已删除'); deleteVisible.value = false; - await fetchList(); + await Promise.all([fetchList(), fetchSummary()]); } finally { actionLoading.value = false; } @@ -347,7 +374,7 @@ async function handleActivate(row: EquipmentRow) { return; } ElMessage.success('设备已激活'); - await fetchList(); + await Promise.all([fetchList(), fetchSummary()]); } finally { loading.value = false; } @@ -444,10 +471,14 @@ function updateDeviceType() { isMobile.value = window.innerWidth <= 768; } +async function refreshPage() { + await Promise.all([fetchList(), fetchSummary()]); +} + onMounted(() => { updateDeviceType(); window.addEventListener('resize', updateDeviceType); - fetchList(); + refreshPage(); }); onUnmounted(() => { @@ -494,7 +525,7 @@ onUnmounted(() => { 重置
- 刷新 + 刷新
@@ -650,7 +681,7 @@ onUnmounted(() => { .summary-grid { display: grid; - grid-template-columns: repeat(5, minmax(120px, 1fr)); + grid-template-columns: repeat(6, minmax(120px, 1fr)); gap: 12px; margin-bottom: 14px; } @@ -685,6 +716,10 @@ onUnmounted(() => { color: #909399; } + &.is-warning { + color: #e6a23c; + } + &.is-danger { color: #f56c6c; }