优化体验,新增多项功能,美化界面
This commit is contained in:
@@ -11,7 +11,8 @@ const logger = require('../config/logger');
|
||||
const { ApiResponse } = require('../utils/response');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { fetchAndValidateCurve } = require('../services/curveClient');
|
||||
const { uploadMeasurementToS3 } = require('../services/measurementStorage');
|
||||
const { uploadMeasurementToS3, getMeasurementFromS3 } = require('../services/measurementStorage');
|
||||
const { getEqCacheKeys, getEqCacheField } = require('../services/eqCacheStorage');
|
||||
|
||||
/**
|
||||
* 将 TXT 频响文件内容转换为 CSV 格式
|
||||
@@ -77,6 +78,55 @@ const MEILISEARCH_URL = process.env.MEILISEARCH_URL || 'http://localhost:7700';
|
||||
const MEILISEARCH_API_KEY = process.env.MEILISEARCH_API_KEY || '';
|
||||
const MEILISEARCH_INDEX = process.env.MEILISEARCH_INDEX || 'models';
|
||||
|
||||
function meilisearchHeaders() {
|
||||
return {
|
||||
Authorization: `Bearer ${MEILISEARCH_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
/** 从 Meilisearch 获取型号文档(文档 id 与数据库 id 一致) */
|
||||
async function getModelFromMeilisearch(modelId) {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${MEILISEARCH_URL}/indexes/${MEILISEARCH_INDEX}/documents/${modelId}`,
|
||||
{ headers: meilisearchHeaders(), timeout: 30000 }
|
||||
);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(JSON.stringify(response.data));
|
||||
}
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
if (e.response?.status === 404) {
|
||||
return null;
|
||||
}
|
||||
const detail = e.response?.data ? JSON.stringify(e.response.data) : e.message;
|
||||
throw new Error(detail);
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 Meilisearch 删除型号文档(文档 id 与数据库 id 一致) */
|
||||
async function deleteModelFromMeilisearch(modelId) {
|
||||
try {
|
||||
const response = await axios.delete(
|
||||
`${MEILISEARCH_URL}/indexes/${MEILISEARCH_INDEX}/documents/${modelId}`,
|
||||
{ headers: meilisearchHeaders(), timeout: 30000 }
|
||||
);
|
||||
if (![200, 202].includes(response.status)) {
|
||||
throw new Error(JSON.stringify(response.data));
|
||||
}
|
||||
logger.info(`Meilisearch document deleted: id=${modelId}, taskUid=${response.data?.taskUid}`);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
if (e.response?.status === 404) {
|
||||
logger.warn(`Meilisearch document not found, skip: id=${modelId}`);
|
||||
return null;
|
||||
}
|
||||
const detail = e.response?.data ? JSON.stringify(e.response.data) : e.message;
|
||||
throw new Error(detail);
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传配置
|
||||
const ALLOWED_EXTENSIONS = ['.csv', '.txt', '.json'];
|
||||
|
||||
@@ -130,6 +180,103 @@ router.get('/api/models/', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/models/:model_id/eq-cache — 查看 Redis EQ 缓存 field 列表
|
||||
router.get('/api/models/:model_id/eq-cache', async (req, res) => {
|
||||
try {
|
||||
const modelId = parseInt(req.params.model_id, 10);
|
||||
const model = await Model.findByPk(modelId);
|
||||
if (!model) {
|
||||
return res.json(ApiResponse.noData('empty'));
|
||||
}
|
||||
|
||||
const { redis_key, field_keys } = await getEqCacheKeys(model.brand_name, model.name);
|
||||
return res.json(ApiResponse.success({
|
||||
redis_key,
|
||||
field_keys,
|
||||
}));
|
||||
} catch (e) {
|
||||
logger.error(`Error reading EQ cache for model ${req.params.model_id}: ${e.message}`);
|
||||
return res.json(ApiResponse.error(e.message || 'error'));
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/models/:model_id/eq-cache/field?key= — 查看单个 hash field 的 value
|
||||
router.get('/api/models/:model_id/eq-cache/field', async (req, res) => {
|
||||
try {
|
||||
const modelId = parseInt(req.params.model_id, 10);
|
||||
const fieldKey = (req.query.key || '').trim();
|
||||
if (!fieldKey) {
|
||||
return res.json(ApiResponse.error('缺少 key 参数'));
|
||||
}
|
||||
|
||||
const model = await Model.findByPk(modelId);
|
||||
if (!model) {
|
||||
return res.json(ApiResponse.noData('empty'));
|
||||
}
|
||||
|
||||
const data = await getEqCacheField(model.brand_name, model.name, fieldKey);
|
||||
return res.json(ApiResponse.success(data));
|
||||
} catch (e) {
|
||||
logger.error(`Error reading EQ cache field for model ${req.params.model_id}: ${e.message}`);
|
||||
return res.json(ApiResponse.error(e.message || 'error'));
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/models/:model_id/meilisearch — 查看 Meilisearch 中的推送数据
|
||||
router.get('/api/models/:model_id/meilisearch', async (req, res) => {
|
||||
try {
|
||||
const modelId = parseInt(req.params.model_id, 10);
|
||||
const model = await Model.findByPk(modelId);
|
||||
if (!model) {
|
||||
return res.json(ApiResponse.noData('empty'));
|
||||
}
|
||||
|
||||
const document = await getModelFromMeilisearch(modelId);
|
||||
if (!document) {
|
||||
return res.json(ApiResponse.success({ pushed: false, document: null }));
|
||||
}
|
||||
|
||||
return res.json(ApiResponse.success({ pushed: true, document }));
|
||||
} catch (e) {
|
||||
logger.error(`Error reading Meilisearch document for model ${req.params.model_id}: ${e.message}`);
|
||||
return res.json(ApiResponse.error(`查询 Meilisearch 失败:${e.message}`));
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/models/:model_id/measurement — 查看 S3 上的频响 CSV(仅 Eafonyoung)
|
||||
router.get('/api/models/:model_id/measurement', async (req, res) => {
|
||||
try {
|
||||
const modelId = parseInt(req.params.model_id, 10);
|
||||
const model = await Model.findByPk(modelId);
|
||||
if (!model) {
|
||||
return res.json(ApiResponse.noData('empty'));
|
||||
}
|
||||
|
||||
const source = (model.source || '').trim();
|
||||
if (source.toLowerCase() !== 'eafonyoung') {
|
||||
return res.json(ApiResponse.error('仅支持查看来源为 Eafonyoung 的频响文件'));
|
||||
}
|
||||
if (!model.form) {
|
||||
return res.json(ApiResponse.error('该型号缺少佩戴方式,无法定位 S3 文件'));
|
||||
}
|
||||
|
||||
const { key, content } = await getMeasurementFromS3({
|
||||
source: model.source,
|
||||
form: model.form,
|
||||
brandName: model.brand_name,
|
||||
modelName: model.name,
|
||||
});
|
||||
|
||||
return res.json(ApiResponse.success({
|
||||
s3_key: key,
|
||||
content,
|
||||
}));
|
||||
} catch (e) {
|
||||
logger.error(`Error reading measurement for model ${req.params.model_id}: ${e.message}`);
|
||||
return res.json(ApiResponse.error(e.message || 'error'));
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/models/:model_id
|
||||
router.get('/api/models/:model_id', async (req, res) => {
|
||||
try {
|
||||
@@ -300,6 +447,13 @@ router.delete('/api/models/:model_id', async (req, res) => {
|
||||
return res.json(ApiResponse.noData('empty'));
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteModelFromMeilisearch(modelId);
|
||||
} catch (e) {
|
||||
logger.error(`Meilisearch delete failed for model id=${modelId}: ${e.message}`);
|
||||
return res.json(ApiResponse.error(`从 Meilisearch 删除失败:${e.message}`));
|
||||
}
|
||||
|
||||
await dbModel.destroy();
|
||||
logger.info(`Model deleted successfully: id=${modelId}`);
|
||||
return res.json(ApiResponse.success(null, 'success'));
|
||||
@@ -386,16 +540,11 @@ router.post('/api/models/push-to-search', async (req, res) => {
|
||||
return doc;
|
||||
});
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${MEILISEARCH_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${MEILISEARCH_URL}/indexes/${MEILISEARCH_INDEX}/documents`,
|
||||
pushData,
|
||||
{ headers, timeout: 30000 }
|
||||
{ headers: meilisearchHeaders(), timeout: 30000 }
|
||||
);
|
||||
|
||||
if (![200, 202].includes(response.status)) {
|
||||
|
||||
Reference in New Issue
Block a user