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; @Injectable() export class XiaofeixiaClient { constructor(private readonly courierConfig: CourierConfigService) {} async request(cmd: string, bizParams: RequestParams): Promise { 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; try { payload = rawText ? (JSON.parse(rawText) as XiaofeixiaApiResponse) : (null as unknown as XiaofeixiaApiResponse); } 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; } }