98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
||
import { CourierApiError } from '../courier.error';
|
||
import { CourierConfigService } from '../courier.config';
|
||
import { buildXiaofeixiaSign } from './xiaofeixia.sign';
|
||
import { XIAOFEIXIA_SUCCESS_CODE } from './xiaofeixia.constants';
|
||
import type { XiaofeixiaApiResponse } from './xiaofeixia.types';
|
||
|
||
type RequestParams = Record<string, string | number | undefined>;
|
||
|
||
@Injectable()
|
||
export class XiaofeixiaClient {
|
||
constructor(private readonly courierConfig: CourierConfigService) {}
|
||
|
||
async request<T>(cmd: string, bizParams: RequestParams): Promise<T> {
|
||
const cfg = this.courierConfig.load().xiaofeixia;
|
||
|
||
if (!cfg.mchId || !cfg.apiKey) {
|
||
throw new CourierApiError(
|
||
'小飞侠商户配置不完整,请设置 XIAOFEIXIA_MCH_ID 与 XIAOFEIXIA_API_KEY',
|
||
'CONFIG_ERROR',
|
||
'XIAOFEIXIA',
|
||
);
|
||
}
|
||
|
||
const baseParams: RequestParams = {
|
||
mchId: cfg.mchId,
|
||
cmd,
|
||
signType: cfg.signType,
|
||
...bizParams,
|
||
};
|
||
|
||
if (cfg.appId) {
|
||
baseParams.appId = cfg.appId;
|
||
}
|
||
|
||
const sign = buildXiaofeixiaSign(baseParams, cfg.apiKey, cfg.signType);
|
||
const body = new URLSearchParams();
|
||
|
||
for (const [key, value] of Object.entries({ ...baseParams, sign })) {
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
body.append(key, String(value));
|
||
}
|
||
}
|
||
|
||
let response: Response;
|
||
try {
|
||
response = await fetch(cfg.apiUrl, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: body.toString(),
|
||
});
|
||
} catch (error) {
|
||
throw new CourierApiError(
|
||
'小飞侠接口网络异常',
|
||
'200000',
|
||
'XIAOFEIXIA',
|
||
error,
|
||
);
|
||
}
|
||
|
||
if (!response.ok) {
|
||
throw new CourierApiError(
|
||
`小飞侠 HTTP 请求失败: ${response.status}`,
|
||
'200000',
|
||
'XIAOFEIXIA',
|
||
);
|
||
}
|
||
|
||
const rawText = await response.text();
|
||
let payload: XiaofeixiaApiResponse<T>;
|
||
try {
|
||
payload = rawText ? (JSON.parse(rawText) as XiaofeixiaApiResponse<T>) : (null as unknown as XiaofeixiaApiResponse<T>);
|
||
} catch {
|
||
throw new CourierApiError(
|
||
`小飞侠响应非 JSON(HTTP ${response.status}): ${rawText.slice(0, 200) || '(空)'}`,
|
||
'200000',
|
||
'XIAOFEIXIA',
|
||
rawText,
|
||
);
|
||
}
|
||
|
||
if (!payload) {
|
||
throw new CourierApiError('小飞侠返回空响应', '200000', 'XIAOFEIXIA');
|
||
}
|
||
|
||
if (payload.code !== XIAOFEIXIA_SUCCESS_CODE) {
|
||
throw new CourierApiError(
|
||
payload.message || '小飞侠接口业务失败',
|
||
payload.code,
|
||
'XIAOFEIXIA',
|
||
payload,
|
||
);
|
||
}
|
||
|
||
return payload.data as T;
|
||
}
|
||
}
|