@@ -1,7 +1,13 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { FulfillmentProviderStatus, FulfillmentProviderType } from '@prisma/client';
|
||||
import {
|
||||
isXfxProviderCode,
|
||||
type XiaofeixiaProviderConfig,
|
||||
type XiaofeixiaProviderConfigPublic,
|
||||
} from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { XiaofeixiaConfig, XiaofeixiaSignType } from '../../integrations/courier/courier.config';
|
||||
|
||||
export type CreateFulfillmentProviderInput = {
|
||||
code: string;
|
||||
@@ -10,6 +16,7 @@ export type CreateFulfillmentProviderInput = {
|
||||
status?: FulfillmentProviderStatus;
|
||||
configJson?: string;
|
||||
capabilitiesJson?: string;
|
||||
xiaofeixiaConfig?: Partial<XiaofeixiaProviderConfig>;
|
||||
};
|
||||
|
||||
export type UpdateFulfillmentProviderInput = Partial<CreateFulfillmentProviderInput>;
|
||||
@@ -21,6 +28,8 @@ type Capabilities = {
|
||||
cancel?: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_XFX_API_URL = 'https://beta.51xiaoju.cn/app/api/interface.do';
|
||||
|
||||
@Injectable()
|
||||
export class FulfillmentProviderService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
@@ -46,6 +55,44 @@ export class FulfillmentProviderService {
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
/** 供推单使用:解析完整小飞侠凭证(含 apiKey) */
|
||||
async resolveXiaofeixiaConfig(providerId: bigint): Promise<XiaofeixiaConfig> {
|
||||
const row = await this.prisma.fulfillmentProvider.findUnique({ where: { id: providerId } });
|
||||
if (!row) throw new NotFoundException('仓配承运商不存在');
|
||||
if (!isXfxProviderCode(row.code)) {
|
||||
throw new BadRequestException('该承运商不是小飞侠');
|
||||
}
|
||||
const cfg = this.parseXiaofeixiaConfig(row.configJson);
|
||||
if (!cfg?.mchId || !cfg?.apiKey || !cfg?.apiUrl) {
|
||||
throw new BadRequestException('小飞侠仓配配置不完整,请在仓配管理中填写 API 地址、商户号与 API Key');
|
||||
}
|
||||
return {
|
||||
apiUrl: cfg.apiUrl,
|
||||
mchId: cfg.mchId,
|
||||
apiKey: cfg.apiKey,
|
||||
signType: this.resolveSignType(cfg.signType),
|
||||
appId: cfg.appId || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** 取第一个启用的小飞侠承运商配置(联调/兼容) */
|
||||
async resolveDefaultXiaofeixiaConfig(): Promise<XiaofeixiaConfig | null> {
|
||||
const row = await this.prisma.fulfillmentProvider.findFirst({
|
||||
where: {
|
||||
status: 'ACTIVE',
|
||||
type: 'API',
|
||||
code: { in: ['XFX', 'XIAOFEIXIA'] },
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
if (!row?.configJson) return null;
|
||||
try {
|
||||
return await this.resolveXiaofeixiaConfig(row.id);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async create(input: CreateFulfillmentProviderInput) {
|
||||
const code = input.code.trim().toUpperCase();
|
||||
if (!/^[A-Z0-9_]+$/.test(code)) {
|
||||
@@ -54,28 +101,54 @@ export class FulfillmentProviderService {
|
||||
const existing = await this.prisma.fulfillmentProvider.findUnique({ where: { code } });
|
||||
if (existing) throw new BadRequestException('承运商编码已存在');
|
||||
|
||||
const configJson = this.resolveConfigJsonForWrite(code, null, input);
|
||||
if (isXfxProviderCode(code) && input.type === 'API') {
|
||||
this.assertXiaofeixiaConfigComplete(configJson, true);
|
||||
}
|
||||
|
||||
const row = await this.prisma.fulfillmentProvider.create({
|
||||
data: {
|
||||
code,
|
||||
name: input.name.trim(),
|
||||
type: input.type,
|
||||
status: input.status ?? 'ACTIVE',
|
||||
configJson: input.configJson?.trim() || null,
|
||||
capabilitiesJson: input.capabilitiesJson?.trim() || null,
|
||||
configJson,
|
||||
capabilitiesJson:
|
||||
input.capabilitiesJson?.trim() ||
|
||||
(isXfxProviderCode(code)
|
||||
? JSON.stringify({
|
||||
createShipment: true,
|
||||
getTrack: true,
|
||||
callback: true,
|
||||
cancel: true,
|
||||
})
|
||||
: null),
|
||||
},
|
||||
});
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
async update(id: bigint, input: UpdateFulfillmentProviderInput) {
|
||||
await this.getById(id);
|
||||
const current = await this.prisma.fulfillmentProvider.findUnique({ where: { id } });
|
||||
if (!current) throw new NotFoundException('仓配承运商不存在');
|
||||
|
||||
const code = current.code;
|
||||
const configJson =
|
||||
input.xiaofeixiaConfig !== undefined || input.configJson !== undefined
|
||||
? this.resolveConfigJsonForWrite(code, current.configJson, input)
|
||||
: undefined;
|
||||
|
||||
if (configJson !== undefined && isXfxProviderCode(code) && (input.type ?? current.type) === 'API') {
|
||||
this.assertXiaofeixiaConfigComplete(configJson, false);
|
||||
}
|
||||
|
||||
const row = await this.prisma.fulfillmentProvider.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(input.name !== undefined ? { name: input.name.trim() } : {}),
|
||||
...(input.type !== undefined ? { type: input.type } : {}),
|
||||
...(input.status !== undefined ? { status: input.status } : {}),
|
||||
...(input.configJson !== undefined ? { configJson: input.configJson?.trim() || null } : {}),
|
||||
...(configJson !== undefined ? { configJson } : {}),
|
||||
...(input.capabilitiesJson !== undefined
|
||||
? { capabilitiesJson: input.capabilitiesJson?.trim() || null }
|
||||
: {}),
|
||||
@@ -93,6 +166,74 @@ export class FulfillmentProviderService {
|
||||
}
|
||||
}
|
||||
|
||||
parseXiaofeixiaConfig(raw: string | null): XiaofeixiaProviderConfig | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<XiaofeixiaProviderConfig>;
|
||||
if (!parsed || typeof parsed !== 'object') return null;
|
||||
return {
|
||||
apiUrl: String(parsed.apiUrl ?? '').trim(),
|
||||
mchId: String(parsed.mchId ?? '').trim(),
|
||||
apiKey: String(parsed.apiKey ?? '').trim(),
|
||||
signType: parsed.signType === 'HMAC-SHA256' ? 'HMAC-SHA256' : 'MD5',
|
||||
appId: parsed.appId ? String(parsed.appId).trim() : undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private resolveConfigJsonForWrite(
|
||||
code: string,
|
||||
existingRaw: string | null,
|
||||
input: CreateFulfillmentProviderInput | UpdateFulfillmentProviderInput,
|
||||
): string | null {
|
||||
if (isXfxProviderCode(code) && input.xiaofeixiaConfig) {
|
||||
const existing = this.parseXiaofeixiaConfig(existingRaw);
|
||||
const next: XiaofeixiaProviderConfig = {
|
||||
apiUrl: (input.xiaofeixiaConfig.apiUrl ?? existing?.apiUrl ?? DEFAULT_XFX_API_URL).trim(),
|
||||
mchId: (input.xiaofeixiaConfig.mchId ?? existing?.mchId ?? '').trim(),
|
||||
apiKey: (input.xiaofeixiaConfig.apiKey || existing?.apiKey || '').trim(),
|
||||
signType:
|
||||
input.xiaofeixiaConfig.signType === 'HMAC-SHA256'
|
||||
? 'HMAC-SHA256'
|
||||
: input.xiaofeixiaConfig.signType === 'MD5'
|
||||
? 'MD5'
|
||||
: existing?.signType ?? 'MD5',
|
||||
appId: (input.xiaofeixiaConfig.appId ?? existing?.appId)?.trim() || undefined,
|
||||
};
|
||||
return JSON.stringify(next);
|
||||
}
|
||||
if (input.configJson !== undefined) {
|
||||
return input.configJson?.trim() || null;
|
||||
}
|
||||
return existingRaw;
|
||||
}
|
||||
|
||||
private assertXiaofeixiaConfigComplete(configJson: string | null, requireApiKey: boolean) {
|
||||
const cfg = this.parseXiaofeixiaConfig(configJson);
|
||||
if (!cfg?.apiUrl) throw new BadRequestException('请填写小飞侠 API 地址');
|
||||
if (!cfg.mchId) throw new BadRequestException('请填写小飞侠商户号');
|
||||
if (requireApiKey && !cfg.apiKey) throw new BadRequestException('请填写小飞侠 API Key');
|
||||
if (!requireApiKey && !cfg.apiKey) throw new BadRequestException('小飞侠 API Key 缺失,请重新填写');
|
||||
}
|
||||
|
||||
private toPublicXiaofeixiaConfig(raw: string | null): XiaofeixiaProviderConfigPublic | null {
|
||||
const cfg = this.parseXiaofeixiaConfig(raw);
|
||||
if (!cfg) return null;
|
||||
return {
|
||||
apiUrl: cfg.apiUrl || DEFAULT_XFX_API_URL,
|
||||
mchId: cfg.mchId,
|
||||
signType: cfg.signType === 'HMAC-SHA256' ? 'HMAC-SHA256' : 'MD5',
|
||||
appId: cfg.appId,
|
||||
hasApiKey: Boolean(cfg.apiKey),
|
||||
};
|
||||
}
|
||||
|
||||
private resolveSignType(raw?: string): XiaofeixiaSignType {
|
||||
return raw?.toUpperCase() === 'HMAC-SHA256' ? 'HMAC-SHA256' : 'MD5';
|
||||
}
|
||||
|
||||
private toDto(row: {
|
||||
id: bigint;
|
||||
code: string;
|
||||
@@ -112,6 +253,9 @@ export class FulfillmentProviderService {
|
||||
status: row.status,
|
||||
capabilities: this.parseCapabilities(row.capabilitiesJson),
|
||||
hasConfig: Boolean(row.configJson),
|
||||
xiaofeixiaConfig: isXfxProviderCode(row.code)
|
||||
? this.toPublicXiaofeixiaConfig(row.configJson)
|
||||
: null,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user