feat(ota): 新增获取各「型号+灰度」分组全局最大版本号接口
- 后端新增 /api/ota/latest-versions 接口,返回所有型号和灰度的最大版本号 - 添加数据库查询,基于全量数据分组聚合版本号,避免分页误判最新版本 - 前端新增 fetchGetOtaLatestVersions 接口调用该后端接口 - 在 OTA 页面加入全局最新版本数据加载与缓存,用于 latest 标签判断 - 修改标签判定逻辑,避免仅基于当前页数据导致的错误标记 - 调用时机调整,增删改 OTA 后刷新全局最新版本数据保持同步
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Router, type Request, type Response, type Router as RouterType } from 'express';
|
import { Router, type Request, type Response, type Router as RouterType } from 'express';
|
||||||
import { eq, gt, like, and, desc, count } from 'drizzle-orm';
|
import { eq, gt, like, and, desc, count, max } from 'drizzle-orm';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import { db } from '../config/database.js';
|
import { db } from '../config/database.js';
|
||||||
import { otas } from '../schemas/index.js';
|
import { otas } from '../schemas/index.js';
|
||||||
@@ -159,6 +159,29 @@ router.get('/api/ota/', async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/ota/latest-versions —— 各「型号+灰度」分组的全局最大版本号
|
||||||
|
// 供前端 latest / latest beta 标签判断:必须基于全量数据,
|
||||||
|
// 否则分页后「当前页内」的最大版本会被误标为 latest
|
||||||
|
router.get('/api/ota/latest-versions', async (_req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const rows = await db
|
||||||
|
.select({ model: otas.model, beta: otas.beta, maxVerCode: max(otas.verCode) })
|
||||||
|
.from(otas)
|
||||||
|
.groupBy(otas.model, otas.beta);
|
||||||
|
|
||||||
|
const items = rows.map(r => ({
|
||||||
|
model: r.model ?? '',
|
||||||
|
beta: r.beta ?? 0,
|
||||||
|
ver_code: Number(r.maxVerCode),
|
||||||
|
}));
|
||||||
|
res.json(ApiResponse.success({ items }));
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
logger.error(`Error getting OTA latest versions: ${msg}`);
|
||||||
|
res.json(ApiResponse.error('error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// GET /api/ota/:ota_id
|
// GET /api/ota/:ota_id
|
||||||
router.get('/api/ota/:ota_id', async (req: Request, res: Response) => {
|
router.get('/api/ota/:ota_id', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ export function fetchGetOta(id: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 各「型号+灰度」分组的全局最大版本号(latest 标签判断依据,不受分页/筛选影响) */
|
||||||
|
export function fetchGetOtaLatestVersions() {
|
||||||
|
return request<{ items: Api.Ota.LatestVersionItem[] }>({
|
||||||
|
url: '/ota/latest-versions',
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchCreateOta(data: Record<string, unknown>) {
|
export function fetchCreateOta(data: Record<string, unknown>) {
|
||||||
return request<Api.Ota.OtaItem>({
|
return request<Api.Ota.OtaItem>({
|
||||||
url: '/ota/',
|
url: '/ota/',
|
||||||
|
|||||||
@@ -89,6 +89,13 @@ declare namespace Api {
|
|||||||
id: number;
|
id: number;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 各「型号+灰度」分组的全局最大版本号(latest 标签判断依据) */
|
||||||
|
interface LatestVersionItem {
|
||||||
|
model: string;
|
||||||
|
beta: number;
|
||||||
|
ver_code: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Blacklist {
|
namespace Blacklist {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
fetchDeleteOta,
|
fetchDeleteOta,
|
||||||
fetchDeleteOtaTargetDevice,
|
fetchDeleteOtaTargetDevice,
|
||||||
fetchGetBlackList,
|
fetchGetBlackList,
|
||||||
|
fetchGetOtaLatestVersions,
|
||||||
fetchGetOtaList,
|
fetchGetOtaList,
|
||||||
fetchGetOtaTargetDeviceList,
|
fetchGetOtaTargetDeviceList,
|
||||||
fetchUpdateOta,
|
fetchUpdateOta,
|
||||||
@@ -141,20 +142,25 @@ const formRules: FormRules = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const latestVersionMap = computed(() => {
|
// 各「型号+灰度」分组的全局最大版本号(latest 标签判断依据)。
|
||||||
const map: Record<string, number> = {};
|
// 必须基于全量数据(后端 /ota/latest-versions):若基于当前页数据计算,
|
||||||
for (const row of tableData.value) {
|
// 翻页后「页内」最大版本也会被误标为 latest / latest beta
|
||||||
const key = `${row.model || ''}__${row.beta}`;
|
const latestVersions = ref<Record<string, number>>({});
|
||||||
if (map[key] === undefined || row.verCode > map[key]) {
|
|
||||||
map[key] = row.verCode;
|
async function loadLatestVersions() {
|
||||||
|
const { data, error } = await fetchGetOtaLatestVersions();
|
||||||
|
if (!error && data) {
|
||||||
|
const map: Record<string, number> = {};
|
||||||
|
for (const it of data.items || []) {
|
||||||
|
map[`${it.model || ''}__${it.beta}`] = it.ver_code;
|
||||||
}
|
}
|
||||||
|
latestVersions.value = map;
|
||||||
}
|
}
|
||||||
return map;
|
}
|
||||||
});
|
|
||||||
|
|
||||||
function getLatestTag(row: OtaRow) {
|
function getLatestTag(row: OtaRow) {
|
||||||
const key = `${row.model || ''}__${row.beta}`;
|
const key = `${row.model || ''}__${row.beta}`;
|
||||||
return latestVersionMap.value[key] === row.verCode;
|
return latestVersions.value[key] === row.verCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCopyUrlCell(url: string) {
|
function renderCopyUrlCell(url: string) {
|
||||||
@@ -356,6 +362,8 @@ function handlePackageRemove() {
|
|||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
// 同步刷新全局最新版本(latest 标签依据,增删改后保持一致)
|
||||||
|
loadLatestVersions();
|
||||||
const params: Record<string, unknown> = {
|
const params: Record<string, unknown> = {
|
||||||
skip: (pagination.page - 1) * pagination.pageSize,
|
skip: (pagination.page - 1) * pagination.pageSize,
|
||||||
limit: pagination.pageSize,
|
limit: pagination.pageSize,
|
||||||
|
|||||||
Reference in New Issue
Block a user