feat: 技术支持工单/企微权限/开发版本管理/消息推送等迭代

This commit is contained in:
2026-08-04 21:32:13 +08:00
parent c8ea5a3119
commit 9d96c73246
1341 changed files with 0 additions and 195605 deletions
@@ -1,192 +0,0 @@
import { Injectable } from '@nestjs/common';
import { CourierApiError } from '../courier.error';
import { CourierConfigService, type XiaofeixiaConfig } from '../courier.config';
import { PrismaService } from '../../../common/prisma/prisma.module';
import {
logCourierCall,
resolveOrderRefByOutNumber,
sanitizeXfxRequestBody,
sceneForXfxCmd,
} from '../courier-log.util';
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,
private readonly prisma: PrismaService,
) {}
async request<T>(cmd: string, bizParams: RequestParams, override?: XiaofeixiaConfig): Promise<T> {
const cfg = override ?? this.courierConfig.load().xiaofeixia;
const scene = sceneForXfxCmd(cmd);
const externalNo = this.pickExternalNo(bizParams);
if (!cfg.mchId || !cfg.apiKey || !cfg.apiUrl) {
await logCourierCall(this.prisma, {
scene,
requestUrl: cfg.apiUrl || '(未配置)',
requestBody: sanitizeXfxRequestBody({ cmd, ...bizParams, mchId: cfg.mchId }),
status: 'FAILED',
errorMessage: '小飞侠商户配置不完整',
externalNo,
});
throw new CourierApiError(
'小飞侠商户配置不完整,请在仓配管理中填写 API 地址、商户号与 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));
}
}
const logRequestBody = sanitizeXfxRequestBody({ ...baseParams, sign: '[REDACTED]' });
const orderRef = await resolveOrderRefByOutNumber(
this.prisma,
typeof bizParams.outNumber === 'string' ? bizParams.outNumber : undefined,
);
let response: Response;
try {
response = await fetch(cfg.apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
await logCourierCall(this.prisma, {
scene,
requestUrl: cfg.apiUrl,
requestBody: logRequestBody,
status: 'FAILED',
errorMessage: `网络异常: ${message}`,
externalNo,
ref: orderRef,
});
throw new CourierApiError('小飞侠接口网络异常', '200000', 'XIAOFEIXIA', error);
}
const rawText = await response.text();
if (!response.ok) {
await logCourierCall(this.prisma, {
scene,
requestUrl: cfg.apiUrl,
requestBody: logRequestBody,
responseBody: { httpStatus: response.status, body: rawText.slice(0, 500) },
status: 'FAILED',
errorMessage: `HTTP ${response.status}`,
externalNo,
ref: orderRef,
});
throw new CourierApiError(
`小飞侠 HTTP 请求失败: ${response.status}`,
'200000',
'XIAOFEIXIA',
);
}
let payload: XiaofeixiaApiResponse<T>;
try {
payload = rawText
? (JSON.parse(rawText) as XiaofeixiaApiResponse<T>)
: (null as unknown as XiaofeixiaApiResponse<T>);
} catch {
await logCourierCall(this.prisma, {
scene,
requestUrl: cfg.apiUrl,
requestBody: logRequestBody,
responseBody: { raw: rawText.slice(0, 500) },
status: 'FAILED',
errorMessage: '响应非 JSON',
externalNo,
ref: orderRef,
});
throw new CourierApiError(
`小飞侠响应非 JSONHTTP ${response.status}: ${rawText.slice(0, 200) || '(空)'}`,
'200000',
'XIAOFEIXIA',
rawText,
);
}
if (!payload) {
await logCourierCall(this.prisma, {
scene,
requestUrl: cfg.apiUrl,
requestBody: logRequestBody,
status: 'FAILED',
errorMessage: '小飞侠返回空响应',
externalNo,
ref: orderRef,
});
throw new CourierApiError('小飞侠返回空响应', '200000', 'XIAOFEIXIA');
}
if (payload.code !== XIAOFEIXIA_SUCCESS_CODE) {
await logCourierCall(this.prisma, {
scene,
requestUrl: cfg.apiUrl,
requestBody: logRequestBody,
responseBody: payload as unknown as Record<string, unknown>,
status: 'FAILED',
errorMessage: payload.message || '业务失败',
externalNo: externalNo || payload.data?.toString(),
ref: orderRef,
});
throw new CourierApiError(
payload.message || '小飞侠接口业务失败',
payload.code,
'XIAOFEIXIA',
payload,
);
}
await logCourierCall(this.prisma, {
scene,
requestUrl: cfg.apiUrl,
requestBody: logRequestBody,
responseBody: payload as unknown as Record<string, unknown>,
status: 'SUCCESS',
externalNo: externalNo || this.pickExternalNoFromData(payload.data),
ref: orderRef,
});
return payload.data as T;
}
private pickExternalNo(params: RequestParams) {
const outNumber = params.outNumber != null ? String(params.outNumber) : undefined;
const number = params.number != null ? String(params.number) : undefined;
return outNumber || number;
}
private pickExternalNoFromData(data: unknown) {
if (!data || typeof data !== 'object') return undefined;
const row = data as { number?: string; outNumber?: string; id?: string };
return row.number || row.outNumber || row.id;
}
}