2026-06-09 19:44:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 频响文件 S3 存储服务
|
|
|
|
|
|
* 本地开发环境:使用 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
|
|
|
|
|
|
* 正式环境:通过 IAM 角色访问 S3(无需配置密钥)
|
|
|
|
|
|
*/
|
2026-06-10 15:37:20 +08:00
|
|
|
|
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
|
2026-06-09 19:44:51 +08:00
|
|
|
|
const logger = require('../config/logger');
|
|
|
|
|
|
|
|
|
|
|
|
const AWS_REGION = process.env.AWS_REGION || 'eu-central-1';
|
|
|
|
|
|
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || '';
|
|
|
|
|
|
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || '';
|
|
|
|
|
|
const S3_BUCKET = process.env.AWS_S3_MEASUREMENT_BUCKET || process.env.AWS_S3_OTA_BUCKET || 'luxsin-app-bucket';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取 S3 客户端
|
|
|
|
|
|
* 有显式凭证则用凭证,否则走 IAM 角色
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getS3Client() {
|
|
|
|
|
|
const options = { region: AWS_REGION };
|
|
|
|
|
|
if (AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY) {
|
|
|
|
|
|
options.credentials = {
|
|
|
|
|
|
accessKeyId: AWS_ACCESS_KEY_ID,
|
|
|
|
|
|
secretAccessKey: AWS_SECRET_ACCESS_KEY,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return new S3Client(options);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取品牌首字符(字母则转大写)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getBrandFirstChar(brandName) {
|
|
|
|
|
|
if (!brandName || !brandName.length) return '_';
|
|
|
|
|
|
const first = brandName[0];
|
|
|
|
|
|
return /[a-zA-Z]/.test(first) ? first.toUpperCase() : first;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 构建 S3 Key
|
|
|
|
|
|
* 路径:autoeq/measurements/{source}/data/{form}/{brandFirstChar}/{brand model}.csv
|
|
|
|
|
|
* @param {object} params
|
|
|
|
|
|
* @param {string} params.source - 来源,默认 Eafonyoung
|
|
|
|
|
|
* @param {string} params.form - 佩戴方式 (in-ear / over-ear / earbud)
|
|
|
|
|
|
* @param {string} params.brandName - 品牌名称
|
|
|
|
|
|
* @param {string} params.modelName - 型号名称
|
|
|
|
|
|
* @returns {string} S3 Key
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buildMeasurementKey({ source, form, brandName, modelName }) {
|
|
|
|
|
|
const src = source || 'Eafonyoung';
|
|
|
|
|
|
const brandChar = getBrandFirstChar(brandName);
|
|
|
|
|
|
const fileName = `${brandName} ${modelName}.csv`;
|
|
|
|
|
|
return `autoeq/measurements/${src}/data/${form}/${brandChar}/${fileName}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 上传频响文件到 S3
|
|
|
|
|
|
* @param {Buffer} buffer - 文件内容(已转换为 CSV)
|
|
|
|
|
|
* @param {object} params - 路径参数
|
|
|
|
|
|
* @returns {Promise<string>} S3 Key
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function uploadMeasurementToS3(buffer, params) {
|
|
|
|
|
|
const key = buildMeasurementKey(params);
|
|
|
|
|
|
const client = getS3Client();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await client.send(
|
|
|
|
|
|
new PutObjectCommand({
|
|
|
|
|
|
Bucket: S3_BUCKET,
|
|
|
|
|
|
Key: key,
|
|
|
|
|
|
Body: buffer,
|
|
|
|
|
|
ContentType: 'text/csv',
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
logger.info(`Measurement uploaded to s3://${S3_BUCKET}/${key}`);
|
|
|
|
|
|
return key;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
logger.error(`S3 measurement upload failed: ${e.message}`);
|
|
|
|
|
|
throw new Error(`S3 上传失败:${e.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 15:37:20 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 从 S3 读取频响 CSV 文件
|
|
|
|
|
|
* @param {object} params - 路径参数(同 buildMeasurementKey)
|
|
|
|
|
|
* @returns {Promise<{ key: string, content: string }>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function getMeasurementFromS3(params) {
|
|
|
|
|
|
const key = buildMeasurementKey(params);
|
|
|
|
|
|
const client = getS3Client();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await client.send(
|
|
|
|
|
|
new GetObjectCommand({
|
|
|
|
|
|
Bucket: S3_BUCKET,
|
|
|
|
|
|
Key: key,
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
const content = await response.Body.transformToString('utf-8');
|
|
|
|
|
|
logger.info(`Measurement read from s3://${S3_BUCKET}/${key}`);
|
|
|
|
|
|
return { key, content };
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (e.name === 'NoSuchKey' || e.$metadata?.httpStatusCode === 404) {
|
|
|
|
|
|
throw new Error('S3 上未找到该型号的频响文件');
|
|
|
|
|
|
}
|
|
|
|
|
|
logger.error(`S3 measurement read failed: ${e.message}`);
|
|
|
|
|
|
throw new Error(`S3 读取失败:${e.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 19:44:51 +08:00
|
|
|
|
module.exports = {
|
|
|
|
|
|
uploadMeasurementToS3,
|
2026-06-10 15:37:20 +08:00
|
|
|
|
getMeasurementFromS3,
|
2026-06-09 19:44:51 +08:00
|
|
|
|
buildMeasurementKey,
|
|
|
|
|
|
};
|