增加登录器客户端心跳
This commit is contained in:
parent
b0629b1001
commit
b103192fac
@ -637,3 +637,84 @@ func (c *ApiCursorEquipmentController) ActivateByCode() {
|
|||||||
"expiredAt": expireTime,
|
"expiredAt": expireTime,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type cursorHeartbeatPayload struct {
|
||||||
|
MachineCode string `json:"machineCode"`
|
||||||
|
MachineCodeSnake string `json:"machine_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat POST /api/cursor/equipment/heartbeat
|
||||||
|
//
|
||||||
|
// 客户端心跳接口(无需登录),用于上报在线状态。
|
||||||
|
//
|
||||||
|
// JSON 示例:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "machineCode": "ABC-123"
|
||||||
|
// }
|
||||||
|
func (c *ApiCursorEquipmentController) Heartbeat() {
|
||||||
|
var p cursorHeartbeatPayload
|
||||||
|
body := c.Ctx.Input.RequestBody
|
||||||
|
if len(body) > 0 {
|
||||||
|
if err := json.Unmarshal(body, &p); err != nil {
|
||||||
|
c.jsonResult(400, "参数错误", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
machineCode := cursorFirstNonEmpty(p.MachineCode, p.MachineCodeSnake)
|
||||||
|
if machineCode == "" {
|
||||||
|
machineCode = c.GetString("machineCode")
|
||||||
|
}
|
||||||
|
if machineCode == "" {
|
||||||
|
machineCode = c.GetString("machine_code")
|
||||||
|
}
|
||||||
|
|
||||||
|
if machineCode == "" {
|
||||||
|
c.jsonResult(400, "缺少参数 machineCode/machine_code(机器码)", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
// 查询设备是否存在
|
||||||
|
var row models.PlatformCursorEquipment
|
||||||
|
err := models.Orm.QueryTable(new(models.PlatformCursorEquipment)).
|
||||||
|
Filter("machine_code", machineCode).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&row)
|
||||||
|
|
||||||
|
if err == orm.ErrNoRows {
|
||||||
|
// 设备不存在,可能是第一次运行心跳,也可以允许在此处静默创建,或者返回 404 让客户端先进行 report
|
||||||
|
// 为了鲁棒性,如果设备未上报过,我们可以直接创建一个基础设备记录
|
||||||
|
row = models.PlatformCursorEquipment{
|
||||||
|
MachineCode: machineCode,
|
||||||
|
Status: 0, // 未激活
|
||||||
|
LastHeartbeatAt: &now,
|
||||||
|
CreateTime: now,
|
||||||
|
}
|
||||||
|
if _, insertErr := models.Orm.Insert(&row); insertErr != nil {
|
||||||
|
c.jsonResult(500, "保存设备心跳失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
c.jsonResult(500, "设备查询失败", nil)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// 更新最后心跳时间
|
||||||
|
if _, updateErr := models.Orm.QueryTable(new(models.PlatformCursorEquipment)).
|
||||||
|
Filter("id", row.ID).
|
||||||
|
Update(map[string]interface{}{
|
||||||
|
"last_heartbeat_at": &now,
|
||||||
|
"update_time": now,
|
||||||
|
}); updateErr != nil {
|
||||||
|
c.jsonResult(500, "更新设备心跳失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.jsonResult(200, "success", map[string]interface{}{
|
||||||
|
"machineCode": machineCode,
|
||||||
|
"online": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -178,11 +178,19 @@ func (c *PlatformCursorEquipmentController) rowToMap(row *models.PlatformCursorE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在线状态计算:若最后心跳时间在 5 分钟内,则认为在线
|
||||||
|
isOnline := false
|
||||||
|
if row.LastHeartbeatAt != nil && time.Since(*row.LastHeartbeatAt) < 5*time.Minute {
|
||||||
|
isOnline = true
|
||||||
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"id": row.ID,
|
"id": row.ID,
|
||||||
"deviceInfo": row.DeviceInfo,
|
"deviceInfo": row.DeviceInfo,
|
||||||
"machineCode": row.MachineCode,
|
"machineCode": row.MachineCode,
|
||||||
"status": row.Status,
|
"status": row.Status,
|
||||||
|
"isOnline": isOnline,
|
||||||
|
"lastHeartbeatAt": row.LastHeartbeatAt,
|
||||||
"system": row.System,
|
"system": row.System,
|
||||||
"os": row.System,
|
"os": row.System,
|
||||||
"version": row.Version,
|
"version": row.Version,
|
||||||
@ -208,6 +216,7 @@ func (c *PlatformCursorEquipmentController) rowToMap(row *models.PlatformCursorE
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List GET /platform/cursor/equipment/list
|
// List GET /platform/cursor/equipment/list
|
||||||
|
// 优化:用 3 条批量 SQL 替代 N+1,列表只返回必要字段,详情由 Detail 接口按需加载完整数据。
|
||||||
func (c *PlatformCursorEquipmentController) List() {
|
func (c *PlatformCursorEquipmentController) List() {
|
||||||
if _, err := c.platformClaims(); err != nil {
|
if _, err := c.platformClaims(); err != nil {
|
||||||
c.jsonErr(401, 401, err.Error())
|
c.jsonErr(401, 401, err.Error())
|
||||||
@ -265,9 +274,242 @@ func (c *PlatformCursorEquipmentController) List() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(rows) == 0 {
|
||||||
|
c.ok(map[string]interface{}{
|
||||||
|
"list": []interface{}{},
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"pageSize": pageSize,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 收集本页所有设备的 ID 和机器码 ──────────────────────────────────
|
||||||
|
deviceIDs := make([]uint64, 0, len(rows))
|
||||||
|
machineCodes := make([]string, 0, len(rows))
|
||||||
|
codeToID := make(map[string]uint64, len(rows)) // machineCode -> equipmentID
|
||||||
|
for _, r := range rows {
|
||||||
|
deviceIDs = append(deviceIDs, r.ID)
|
||||||
|
machineCodes = append(machineCodes, r.MachineCode)
|
||||||
|
codeToID[r.MachineCode] = r.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 批量查激活码(1 条 SQL)──────────────────────────────────────────
|
||||||
|
// 只取本页设备相关的所有激活码,按 id 降序,在内存里取每个设备的 latest
|
||||||
|
type actSummary struct {
|
||||||
|
code string
|
||||||
|
codeID uint64
|
||||||
|
count int64
|
||||||
|
lastActivatedAt interface{}
|
||||||
|
expiredAt interface{}
|
||||||
|
}
|
||||||
|
actMap := make(map[uint64]*actSummary, len(rows))
|
||||||
|
|
||||||
|
var allActs []models.PlatformCursorActivationCode
|
||||||
|
if len(deviceIDs) > 0 {
|
||||||
|
// Beego ORM 的 __in 过滤
|
||||||
|
ids := make([]interface{}, len(deviceIDs))
|
||||||
|
for i, id := range deviceIDs {
|
||||||
|
ids[i] = id
|
||||||
|
}
|
||||||
|
codes := make([]interface{}, len(machineCodes))
|
||||||
|
for i, mc := range machineCodes {
|
||||||
|
codes[i] = mc
|
||||||
|
}
|
||||||
|
actCond := orm.NewCondition().
|
||||||
|
And("delete_time__isnull", true).
|
||||||
|
AndCond(orm.NewCondition().
|
||||||
|
Or("bind_device_id__in", ids).
|
||||||
|
Or("machine_code__in", codes))
|
||||||
|
models.Orm.QueryTable(new(models.PlatformCursorActivationCode)).
|
||||||
|
SetCond(actCond).
|
||||||
|
OrderBy("-activated_at", "-id").
|
||||||
|
All(&allActs)
|
||||||
|
}
|
||||||
|
for i := range allActs {
|
||||||
|
a := &allActs[i]
|
||||||
|
// 确定归属设备 ID
|
||||||
|
var devID uint64
|
||||||
|
if a.BindDeviceID != nil && *a.BindDeviceID != 0 {
|
||||||
|
devID = *a.BindDeviceID
|
||||||
|
} else if a.MachineCode != nil {
|
||||||
|
devID = codeToID[*a.MachineCode]
|
||||||
|
}
|
||||||
|
if devID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s, exists := actMap[devID]
|
||||||
|
if !exists {
|
||||||
|
s = &actSummary{}
|
||||||
|
actMap[devID] = s
|
||||||
|
}
|
||||||
|
s.count++
|
||||||
|
// 第一条就是 latest(已按 -activated_at,-id 排序)
|
||||||
|
if s.codeID == 0 {
|
||||||
|
s.code = a.Code
|
||||||
|
s.codeID = a.ID
|
||||||
|
if a.ActivatedAt != nil {
|
||||||
|
s.lastActivatedAt = a.ActivatedAt
|
||||||
|
}
|
||||||
|
if a.ExpiredAt != nil {
|
||||||
|
s.expiredAt = a.ExpiredAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 批量查提取记录(1 条 SQL)──────────────────────────────────────────
|
||||||
|
type extractSummary struct {
|
||||||
|
count int64
|
||||||
|
lastExtracted interface{}
|
||||||
|
}
|
||||||
|
extractMap := make(map[uint64]*extractSummary, len(rows))
|
||||||
|
|
||||||
|
type extractRow struct {
|
||||||
|
MachineCode string
|
||||||
|
Cnt int64
|
||||||
|
LastExtracted *string
|
||||||
|
}
|
||||||
|
var extractRows []extractRow
|
||||||
|
if len(machineCodes) > 0 {
|
||||||
|
inPlaceholders := make([]string, len(machineCodes))
|
||||||
|
inArgs := make([]interface{}, len(machineCodes))
|
||||||
|
for i, mc := range machineCodes {
|
||||||
|
inPlaceholders[i] = "?"
|
||||||
|
inArgs[i] = mc
|
||||||
|
}
|
||||||
|
sql := "SELECT machine_code, COUNT(*) AS cnt, MAX(extracted_time) AS last_extracted " +
|
||||||
|
"FROM yz_platform_account_pool_cursor " +
|
||||||
|
"WHERE is_extracted > 0 AND delete_time IS NULL " +
|
||||||
|
"AND machine_code IN (" + strings.Join(inPlaceholders, ",") + ") " +
|
||||||
|
"GROUP BY machine_code"
|
||||||
|
models.Orm.Raw(sql, inArgs...).QueryRows(&extractRows)
|
||||||
|
}
|
||||||
|
for _, er := range extractRows {
|
||||||
|
devID := codeToID[er.MachineCode]
|
||||||
|
if devID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
es := &extractSummary{count: er.Cnt}
|
||||||
|
if er.LastExtracted != nil {
|
||||||
|
es.lastExtracted = *er.LastExtracted
|
||||||
|
}
|
||||||
|
extractMap[devID] = es
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 批量查最后登录 IP(1 条 SQL)────────────────────────────────────────
|
||||||
|
// 每个设备取 id 最大的一条
|
||||||
|
type ipRow struct {
|
||||||
|
EquipmentID uint64
|
||||||
|
MachineCode string
|
||||||
|
Query string
|
||||||
|
}
|
||||||
|
ipMap := make(map[uint64]string, len(rows))
|
||||||
|
if len(deviceIDs) > 0 {
|
||||||
|
inPlaceholders := make([]string, len(deviceIDs))
|
||||||
|
inArgs := make([]interface{}, len(deviceIDs))
|
||||||
|
for i, id := range deviceIDs {
|
||||||
|
inPlaceholders[i] = "?"
|
||||||
|
inArgs[i] = id
|
||||||
|
}
|
||||||
|
codePlaceholders := make([]string, len(machineCodes))
|
||||||
|
codeArgs := make([]interface{}, len(machineCodes))
|
||||||
|
for i, mc := range machineCodes {
|
||||||
|
codePlaceholders[i] = "?"
|
||||||
|
codeArgs[i] = mc
|
||||||
|
}
|
||||||
|
allArgs := append(inArgs, codeArgs...)
|
||||||
|
ipSQL := "SELECT equipment_id, machine_code, query FROM yz_platform_cursor_equipment_ip_log " +
|
||||||
|
"WHERE id IN (" +
|
||||||
|
" SELECT MAX(id) FROM yz_platform_cursor_equipment_ip_log " +
|
||||||
|
" WHERE equipment_id IN (" + strings.Join(inPlaceholders, ",") + ")" +
|
||||||
|
" OR machine_code IN (" + strings.Join(codePlaceholders, ",") + ")" +
|
||||||
|
" GROUP BY COALESCE(NULLIF(equipment_id,0), machine_code)" +
|
||||||
|
")"
|
||||||
|
var ipRows []ipRow
|
||||||
|
models.Orm.Raw(ipSQL, allArgs...).QueryRows(&ipRows)
|
||||||
|
for _, ir := range ipRows {
|
||||||
|
devID := ir.EquipmentID
|
||||||
|
if devID == 0 {
|
||||||
|
devID = codeToID[ir.MachineCode]
|
||||||
|
}
|
||||||
|
if devID != 0 {
|
||||||
|
ipMap[devID] = ir.Query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 组装列表(纯内存操作)────────────────────────────────────────────
|
||||||
list := make([]map[string]interface{}, 0, len(rows))
|
list := make([]map[string]interface{}, 0, len(rows))
|
||||||
for i := range rows {
|
for i := range rows {
|
||||||
list = append(list, c.rowToMap(&rows[i]))
|
r := &rows[i]
|
||||||
|
|
||||||
|
// 激活码信息
|
||||||
|
var bindActivationCode interface{}
|
||||||
|
var activationCodeId interface{}
|
||||||
|
var lastActivatedAt interface{} = r.ActivationTime
|
||||||
|
var expiredAt interface{} = r.ExpireTime
|
||||||
|
var activationCount int64
|
||||||
|
if s, ok := actMap[r.ID]; ok {
|
||||||
|
activationCount = s.count
|
||||||
|
bindActivationCode = s.code
|
||||||
|
activationCodeId = s.codeID
|
||||||
|
if s.lastActivatedAt != nil {
|
||||||
|
lastActivatedAt = s.lastActivatedAt
|
||||||
|
}
|
||||||
|
if s.expiredAt != nil {
|
||||||
|
expiredAt = s.expiredAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取记录
|
||||||
|
var extractCount int64
|
||||||
|
var lastExtractedAt interface{}
|
||||||
|
if es, ok := extractMap[r.ID]; ok {
|
||||||
|
extractCount = es.count
|
||||||
|
lastExtractedAt = es.lastExtracted
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后登录 IP
|
||||||
|
var lastLoginIp interface{}
|
||||||
|
if ip, ok := ipMap[r.ID]; ok && ip != "" {
|
||||||
|
lastLoginIp = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在线状态计算:若最后心跳时间在 5 分钟内,则认为在线
|
||||||
|
isOnline := false
|
||||||
|
if r.LastHeartbeatAt != nil && time.Since(*r.LastHeartbeatAt) < 5*time.Minute {
|
||||||
|
isOnline = true
|
||||||
|
}
|
||||||
|
|
||||||
|
list = append(list, map[string]interface{}{
|
||||||
|
"id": r.ID,
|
||||||
|
"deviceInfo": r.DeviceInfo,
|
||||||
|
"machineCode": r.MachineCode,
|
||||||
|
"status": r.Status,
|
||||||
|
"isOnline": isOnline,
|
||||||
|
"lastHeartbeatAt": r.LastHeartbeatAt,
|
||||||
|
"system": r.System,
|
||||||
|
"os": r.System,
|
||||||
|
"version": r.Version,
|
||||||
|
"bindAccount": r.BindAccount,
|
||||||
|
"bindActivationCode": bindActivationCode,
|
||||||
|
"activationCode": bindActivationCode,
|
||||||
|
"activationCodeId": activationCodeId,
|
||||||
|
"ownerUserId": r.OwnerUserID,
|
||||||
|
"ownerUserName": r.OwnerUserName,
|
||||||
|
"activationTime": lastActivatedAt,
|
||||||
|
"lastActivatedAt": lastActivatedAt,
|
||||||
|
"expireTime": expiredAt,
|
||||||
|
"expiredAt": expiredAt,
|
||||||
|
"activationCount": activationCount,
|
||||||
|
"extractCount": extractCount,
|
||||||
|
"lastExtractedAt": lastExtractedAt,
|
||||||
|
"lastLoginIp": lastLoginIp,
|
||||||
|
// 列表不返回 lastLoginIpInfo,点详情时由 Detail 接口加载完整 IP 信息
|
||||||
|
"remark": r.Remark,
|
||||||
|
"createTime": r.CreateTime,
|
||||||
|
"updateTime": r.UpdateTime,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
c.ok(map[string]interface{}{
|
c.ok(map[string]interface{}{
|
||||||
|
|||||||
@ -16,6 +16,7 @@ type PlatformCursorEquipment struct {
|
|||||||
ActivationTime *time.Time `orm:"column(activation_time);type(datetime);null" json:"activationTime"`
|
ActivationTime *time.Time `orm:"column(activation_time);type(datetime);null" json:"activationTime"`
|
||||||
ExpireTime *time.Time `orm:"column(expire_time);type(datetime);null" json:"expireTime"`
|
ExpireTime *time.Time `orm:"column(expire_time);type(datetime);null" json:"expireTime"`
|
||||||
Remark *string `orm:"column(remark);size(1000);null" json:"remark"`
|
Remark *string `orm:"column(remark);size(1000);null" json:"remark"`
|
||||||
|
LastHeartbeatAt *time.Time `orm:"column(last_heartbeat_at);type(datetime);null" json:"lastHeartbeatAt"`
|
||||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"createTime"`
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"createTime"`
|
||||||
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"updateTime"`
|
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"updateTime"`
|
||||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"deleteTime"`
|
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"deleteTime"`
|
||||||
|
|||||||
@ -17,6 +17,9 @@ func Register() {
|
|||||||
// 登录器使用激活码激活/续期 Cursor 设备(无需登录)
|
// 登录器使用激活码激活/续期 Cursor 设备(无需登录)
|
||||||
beego.Router("/api/cursor/equipment/activateByCode", &controllers.ApiCursorEquipmentController{}, "post:ActivateByCode")
|
beego.Router("/api/cursor/equipment/activateByCode", &controllers.ApiCursorEquipmentController{}, "post:ActivateByCode")
|
||||||
|
|
||||||
|
// 登录器心跳接口,用于更新在线状态(无需登录)
|
||||||
|
beego.Router("/api/cursor/equipment/heartbeat", &controllers.ApiCursorEquipmentController{}, "post:Heartbeat")
|
||||||
|
|
||||||
// Cursor Token 顺序读取/检测接口(无需登录,peek 不改变号池状态)
|
// Cursor Token 顺序读取/检测接口(无需登录,peek 不改变号池状态)
|
||||||
// GET /api/cursor/token/peek?id=11&data_type=tk
|
// GET /api/cursor/token/peek?id=11&data_type=tk
|
||||||
beego.Router("/api/cursor/token/peek", &controllers.ApiCursorDetectController{}, "get:PeekToken")
|
beego.Router("/api/cursor/token/peek", &controllers.ApiCursorDetectController{}, "get:PeekToken")
|
||||||
|
|||||||
@ -112,6 +112,9 @@ function sourceLabel(source: string) {
|
|||||||
<el-descriptions-item label="创建时间">
|
<el-descriptions-item label="创建时间">
|
||||||
{{ display(row.createdAt) }}
|
{{ display(row.createdAt) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后心跳时间">
|
||||||
|
{{ display(row.lastHeartbeatAt) }}
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="备注" :span="2">
|
<el-descriptions-item label="备注" :span="2">
|
||||||
<span class="remark-text">{{ display(row.remark) }}</span>
|
<span class="remark-text">{{ display(row.remark) }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|||||||
@ -97,8 +97,11 @@ const summary = computed(() => {
|
|||||||
const inactive = tableData.value.filter((item) => item.status === 'inactive').length;
|
const inactive = tableData.value.filter((item) => item.status === 'inactive').length;
|
||||||
const disabled = tableData.value.filter((item) => item.status === 'disabled').length;
|
const disabled = tableData.value.filter((item) => item.status === 'disabled').length;
|
||||||
const expired = tableData.value.filter((item) => item.status === 'expired').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;
|
||||||
return [
|
return [
|
||||||
{ label: '当前页设备', value: tableData.value.length, type: 'primary' },
|
{ label: '当前页设备', value: tableData.value.length, type: 'primary' },
|
||||||
|
{ label: '在线 / 离线', isOnlineOffline: true, online, offline },
|
||||||
{ label: '已激活设备', value: active, type: 'success' },
|
{ label: '已激活设备', value: active, type: 'success' },
|
||||||
{ label: '未激活', value: inactive, type: 'info' },
|
{ label: '未激活', value: inactive, type: 'info' },
|
||||||
{ label: '禁用/过期', value: disabled + expired, type: 'danger' },
|
{ label: '禁用/过期', value: disabled + expired, type: 'danger' },
|
||||||
@ -186,6 +189,8 @@ function normalizeRow(raw: any): EquipmentRow {
|
|||||||
lastExtractedAt: formatTime(pick(raw, 'last_extracted_at', 'lastExtractedAt', 'extracted_at')),
|
lastExtractedAt: formatTime(pick(raw, 'last_extracted_at', 'lastExtractedAt', 'extracted_at')),
|
||||||
expiredAt: formatTime(pick(raw, 'expiredAt', 'expired_at', 'expireTime', 'expire_time')),
|
expiredAt: formatTime(pick(raw, 'expiredAt', 'expired_at', 'expireTime', 'expire_time')),
|
||||||
createdAt: formatTime(pick(raw, 'create_time', 'created_at', 'createdAt', 'CreatedAt')),
|
createdAt: formatTime(pick(raw, 'create_time', 'created_at', 'createdAt', 'CreatedAt')),
|
||||||
|
isOnline: !!pick(raw, 'isOnline', 'is_online'),
|
||||||
|
lastHeartbeatAt: formatTime(pick(raw, 'lastHeartbeatAt', 'last_heartbeat_at')),
|
||||||
lastLoginIp: pick(raw, 'lastLoginIp', 'last_login_ip') || '',
|
lastLoginIp: pick(raw, 'lastLoginIp', 'last_login_ip') || '',
|
||||||
lastLoginIpInfo: raw?.lastLoginIpInfo ?? null,
|
lastLoginIpInfo: raw?.lastLoginIpInfo ?? null,
|
||||||
remark: pick(raw, 'remark', 'Remark'),
|
remark: pick(raw, 'remark', 'Remark'),
|
||||||
@ -463,7 +468,12 @@ onUnmounted(() => {
|
|||||||
<div class="summary-grid">
|
<div class="summary-grid">
|
||||||
<div v-for="item in summary" :key="item.label" class="summary-card">
|
<div v-for="item in summary" :key="item.label" class="summary-card">
|
||||||
<div class="summary-label">{{ item.label }}</div>
|
<div class="summary-label">{{ item.label }}</div>
|
||||||
<div class="summary-value" :class="`is-${item.type}`">{{ item.value }}</div>
|
<div v-if="item.isOnlineOffline" class="summary-value combined-value">
|
||||||
|
<span class="is-online">{{ item.online }}</span>
|
||||||
|
<span class="divider">/</span>
|
||||||
|
<span class="is-offline">{{ item.offline }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="summary-value" :class="`is-${item.type}`">{{ item.value }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -500,6 +510,13 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<el-table-column type="selection" width="52" />
|
<el-table-column type="selection" width="52" />
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column label="在线" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.isOnline ? 'success' : 'info'" effect="dark">
|
||||||
|
{{ row.isOnline ? '在线' : '离线' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="设备信息" min-width="220">
|
<el-table-column label="设备信息" min-width="220">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="device-name">{{ row.name || '-' }}</div>
|
<div class="device-name">{{ row.name || '-' }}</div>
|
||||||
@ -633,7 +650,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.summary-grid {
|
.summary-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(120px, 1fr));
|
grid-template-columns: repeat(5, minmax(120px, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
@ -660,6 +677,10 @@ onUnmounted(() => {
|
|||||||
color: #67c23a;
|
color: #67c23a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-online {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
&.is-info {
|
&.is-info {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
@ -667,6 +688,26 @@ onUnmounted(() => {
|
|||||||
&.is-danger {
|
&.is-danger {
|
||||||
color: #f56c6c;
|
color: #f56c6c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.combined-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.is-online {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
color: #dcdfe6;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-offline {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
|||||||
8
sql/alter_equipment_add_heartbeat.sql
Normal file
8
sql/alter_equipment_add_heartbeat.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-- 为设备表添加心跳时间字段,用于实时判断设备是否在线
|
||||||
|
-- 在线判断逻辑:last_heartbeat_at 在 5 分钟以内 = 在线,否则 = 离线
|
||||||
|
ALTER TABLE `yz_platform_cursor_equipment`
|
||||||
|
ADD COLUMN `last_heartbeat_at` DATETIME NULL DEFAULT NULL COMMENT '最后心跳时间(客户端定期上报)' AFTER `update_time`;
|
||||||
|
|
||||||
|
-- 可选:加索引,便于管理后台统计在线设备数
|
||||||
|
ALTER TABLE `yz_platform_cursor_equipment`
|
||||||
|
ADD INDEX `idx_last_heartbeat_at` (`last_heartbeat_at`);
|
||||||
Loading…
Reference in New Issue
Block a user