cangku/backend/src/services/alibaba1688ProductCodeClient.js

140 lines
4.4 KiB
JavaScript

'use strict';
const axios = require('axios');
const DEFAULT_BASE_URL = 'https://yyd.jiansuntuzhi.cn';
const DEFAULT_TIMEOUT_MS = 30000;
const MAX_TIMEOUT_MS = 300000;
function clientError(message, statusCode = 502, code = 'PRODUCT_CODE_UPSTREAM_ERROR') {
const error = new Error(message);
error.statusCode = statusCode;
error.code = code;
return error;
}
function normalizeBaseUrl(value) {
const raw = String(value || DEFAULT_BASE_URL).trim().replace(/\/+$/, '');
let parsed;
try {
parsed = new URL(raw);
} catch {
throw clientError('商品编码接口地址配置不正确', 503, 'PRODUCT_CODE_API_CONFIG_INVALID');
}
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw clientError('商品编码接口地址仅支持 HTTP 或 HTTPS', 503, 'PRODUCT_CODE_API_CONFIG_INVALID');
}
if (process.env.NODE_ENV === 'production' && parsed.protocol !== 'https:') {
throw clientError('生产环境商品编码接口必须使用 HTTPS', 503, 'PRODUCT_CODE_API_HTTPS_REQUIRED');
}
return raw;
}
function getProductCodeApiConfig() {
const apiKey = String(process.env.PRODUCT_CODE_API_KEY || '').trim();
if (!apiKey) {
throw clientError('商品编码接口尚未配置 API Key', 503, 'PRODUCT_CODE_API_KEY_MISSING');
}
const configuredTimeout = Number.parseInt(process.env.PRODUCT_CODE_API_TIMEOUT_MS, 10);
const timeout = Number.isFinite(configuredTimeout) && configuredTimeout > 0
? Math.min(configuredTimeout, MAX_TIMEOUT_MS)
: DEFAULT_TIMEOUT_MS;
return {
baseUrl: normalizeBaseUrl(process.env.PRODUCT_CODE_API_BASE_URL),
apiKey,
timeout,
};
}
function normalizeUpstreamError(error, action) {
if (error?.code === 'ECONNABORTED' || /timeout/i.test(String(error?.message || ''))) {
return clientError(
`商品编码接口${action}超时;请使用同一 1688 商品 ID 重试以确认已有编码`,
504,
'PRODUCT_CODE_API_TIMEOUT'
);
}
const upstreamStatus = Number(error?.response?.status) || 0;
const upstreamMessage = String(
error?.response?.data?.error
|| error?.response?.data?.message
|| ''
).trim();
if (upstreamStatus === 400) {
return clientError(upstreamMessage || '商品编码参数不正确', 400, 'PRODUCT_CODE_API_INVALID_REQUEST');
}
if (upstreamStatus === 401 || upstreamStatus === 403) {
return clientError('商品编码服务鉴权失败,请联系管理员检查 API Key', 502, 'PRODUCT_CODE_API_AUTH_FAILED');
}
if (upstreamStatus === 409) {
return clientError(
upstreamMessage || '商品编码服务正在处理相同商品,请稍后使用同一商品 ID 重试',
409,
'PRODUCT_CODE_API_CONFLICT'
);
}
if (upstreamStatus === 503) {
return clientError('商品编码服务暂不可用,请联系管理员检查服务端配置', 502, 'PRODUCT_CODE_API_UNAVAILABLE');
}
return clientError(upstreamMessage || `商品编码接口${action}失败`, 502, 'PRODUCT_CODE_UPSTREAM_ERROR');
}
async function requestProductCodeApi({ method, path, data, action, httpClient = axios }) {
const config = getProductCodeApiConfig();
try {
const response = await httpClient({
method,
url: `${config.baseUrl}${path}`,
data,
timeout: config.timeout,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-API-Key': config.apiKey,
},
});
const payload = response?.data;
if (!payload?.success || !payload.data) {
throw clientError(payload?.error || payload?.message || `商品编码接口${action}返回无效数据`);
}
return {
statusCode: Number(response.status) || 200,
message: payload.message || '',
data: payload.data,
};
} catch (error) {
if (error?.code?.startsWith('PRODUCT_CODE_')) throw error;
throw normalizeUpstreamError(error, action);
}
}
async function getProductCodeMeta(options = {}) {
return requestProductCodeApi({
method: 'GET',
path: '/api/public/product-code-board/meta',
action: '读取字段枚举',
httpClient: options.httpClient,
});
}
async function createProductCodes(payload, options = {}) {
return requestProductCodeApi({
method: 'POST',
path: '/api/public/product-code-board/products',
data: payload,
action: '提交',
httpClient: options.httpClient,
});
}
module.exports = {
DEFAULT_BASE_URL,
DEFAULT_TIMEOUT_MS,
getProductCodeApiConfig,
normalizeUpstreamError,
getProductCodeMeta,
createProductCodes,
};