小飞侠接口

This commit is contained in:
2026-07-06 14:12:06 +08:00
parent c87d79109a
commit 80d967f2cf
9 changed files with 652 additions and 6 deletions
+7 -2
View File
@@ -60,5 +60,10 @@ OSS_UPLOAD_PREFIX=uploads
OSS_UPLOAD_EXPIRE_SECONDS=900
# 单文件大小上限(字节,默认 10MB)
OSS_MAX_UPLOAD_BYTES=10485760
# 浏览器直传 OSS 时需配置 Bucket CORS;运行 pnpm oss:cors 或控制台手动添加
# OSS_CORS_ORIGINS=http://localhost:5173,http://localhost:5174,http://localhost:5175,https://your-domain.com
# 小飞侠同城配送(COURIER_PROVIDER=xiaofeixia
COURIER_PROVIDER=xiaofeixia
XIAOFEIXIA_API_URL=https://beta.51xiaoju.cn/app/api/interface.do
XIAOFEIXIA_MCH_ID=
XIAOFEIXIA_API_KEY=
XIAOFEIXIA_SIGN_TYPE=MD5
# XIAOFEIXIA_APP_ID=
@@ -29,6 +29,6 @@ import type { ICourierProvider } from './courier.types';
},
CourierService,
],
exports: [CourierService, COURIER_PROVIDER],
exports: [CourierService, CourierConfigService, COURIER_PROVIDER],
})
export class CourierModule {}
@@ -0,0 +1,57 @@
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { AdminXiaofeixiaService } from './admin-xiaofeixia.service';
import {
XiaofeixiaBatchShipmentQueryDto,
XiaofeixiaCheckCoverageDto,
XiaofeixiaCreateShipmentDto,
XiaofeixiaEstimateFreightDto,
XiaofeixiaShipmentQueryDto,
} from './dto/admin-courier.dto';
/** HQ 小飞侠接口联调(仅管理端,勿对 C 端暴露) */
@Controller('admin/courier/xiaofeixia')
@UseGuards(HqAuthGuard)
export class AdminXiaofeixiaController {
constructor(private readonly service: AdminXiaofeixiaService) {}
@Get('config')
getConfig() {
return this.service.getConfig();
}
@Post('estimate-freight')
estimateFreight(@Body() body: XiaofeixiaEstimateFreightDto) {
return this.service.estimateFreight(body);
}
@Post('check-coverage')
checkCoverage(@Body() body: XiaofeixiaCheckCoverageDto) {
return this.service.checkCoverage(body);
}
@Post('create-shipment')
createShipment(@Body() body: XiaofeixiaCreateShipmentDto) {
return this.service.createShipment(body);
}
@Post('cancel-shipment')
cancelShipment(@Body() body: XiaofeixiaShipmentQueryDto) {
return this.service.cancelShipment(body);
}
@Post('get-shipment')
getShipment(@Body() body: XiaofeixiaShipmentQueryDto) {
return this.service.getShipment(body);
}
@Post('batch-get-shipments')
batchGetShipments(@Body() body: XiaofeixiaBatchShipmentQueryDto) {
return this.service.batchGetShipments(body);
}
@Post('get-track')
getTrack(@Body() body: XiaofeixiaShipmentQueryDto) {
return this.service.getTrack(body);
}
}
@@ -0,0 +1,151 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { CourierConfigService } from '../../integrations/courier/courier.config';
import { CourierApiError } from '../../integrations/courier/courier.error';
import { CourierService } from '../../integrations/courier/courier.service';
import { CourierPayMode } from '../../integrations/courier/courier.types';
import type {
BatchShipmentQuery,
CreateShipmentInput,
ShipmentQuery,
} from '../../integrations/courier/courier.types';
import type {
XiaofeixiaBatchShipmentQueryDto,
XiaofeixiaCheckCoverageDto,
XiaofeixiaCreateShipmentDto,
XiaofeixiaEstimateFreightDto,
XiaofeixiaShipmentQueryDto,
} from './dto/admin-courier.dto';
function maskSecret(value: string, visible = 4) {
if (!value) return '';
if (value.length <= visible) return '*'.repeat(value.length);
return `${value.slice(0, visible)}${'*'.repeat(Math.min(8, value.length - visible))}`;
}
@Injectable()
export class AdminXiaofeixiaService {
constructor(
private readonly courier: CourierService,
private readonly courierConfig: CourierConfigService,
) {}
getConfig() {
const cfg = this.courierConfig.load();
const xfx = cfg.xiaofeixia;
return {
provider: cfg.provider,
activeProvider: this.courier.activeProvider,
apiUrl: xfx.apiUrl,
appId: xfx.appId ?? null,
mchId: xfx.mchId || null,
mchIdMasked: xfx.mchId ? maskSecret(xfx.mchId) : null,
hasApiKey: Boolean(xfx.apiKey),
signType: xfx.signType,
ready: Boolean(xfx.mchId && xfx.apiKey),
};
}
async estimateFreight(dto: XiaofeixiaEstimateFreightDto) {
return this.wrap(() => this.courier.estimateFreight(dto.weight));
}
async checkCoverage(dto: XiaofeixiaCheckCoverageDto) {
return this.wrap(() => this.courier.checkDeliveryCoverage(dto.toAddress));
}
async createShipment(dto: XiaofeixiaCreateShipmentDto) {
const input = this.mapCreateInput(dto);
return this.wrap(() => this.courier.createShipment(input));
}
async cancelShipment(dto: XiaofeixiaShipmentQueryDto) {
const query = this.mapShipmentQuery(dto);
return this.wrap(async () => {
await this.courier.cancelShipment(query);
return { cancelled: true };
});
}
async getShipment(dto: XiaofeixiaShipmentQueryDto) {
const query = this.mapShipmentQuery(dto);
return this.wrap(() => this.courier.getShipment(query));
}
async batchGetShipments(dto: XiaofeixiaBatchShipmentQueryDto) {
const query: BatchShipmentQuery = {
trackingNumbers: dto.trackingNumbers?.filter(Boolean),
outNumbers: dto.outNumbers?.filter(Boolean),
};
return this.wrap(() => this.courier.batchGetShipments(query));
}
async getTrack(dto: XiaofeixiaShipmentQueryDto) {
const query = this.mapShipmentQuery(dto);
return this.wrap(() => this.courier.getTrack(query));
}
private mapShipmentQuery(dto: XiaofeixiaShipmentQueryDto): ShipmentQuery {
if (!dto.trackingNumber && !dto.outNumber) {
throw new BadRequestException('运单号与商家单号至少填一个');
}
return {
trackingNumber: dto.trackingNumber,
outNumber: dto.outNumber,
};
}
private mapCreateInput(dto: XiaofeixiaCreateShipmentDto): CreateShipmentInput {
const coord = (lng?: number, lat?: number) =>
lng != null && lat != null ? { lng, lat } : undefined;
return {
outNumber: dto.outNumber,
customerId: dto.customerId,
from: {
name: dto.fromName,
mobile: dto.fromMobile,
address: dto.fromAddress,
addressDetail: dto.fromAddressDetail,
coordinate: coord(dto.fromLng, dto.fromLat),
},
to: {
name: dto.toName,
mobile: dto.toMobile,
address: dto.toAddress,
addressDetail: dto.toAddressDetail,
coordinate: coord(dto.toLng, dto.toLat),
},
goodsName: dto.goodsName,
goodsNum: dto.goodsNum,
weight: dto.weight,
insuredSumPrice: dto.insuredSumPrice,
collectionPrice: dto.collectionPrice,
payMode: dto.payMode as CourierPayMode,
remark: dto.remark,
};
}
private async wrap<T>(fn: () => Promise<T>) {
const startedAt = Date.now();
try {
const data = await fn();
return {
ok: true,
elapsedMs: Date.now() - startedAt,
data,
};
} catch (err) {
if (err instanceof CourierApiError) {
return {
ok: false,
elapsedMs: Date.now() - startedAt,
error: err.message,
code: err.code,
provider: err.providerCode,
raw: err.raw,
};
}
throw err;
}
}
}
@@ -0,0 +1,128 @@
import {
IsArray,
IsIn,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
Min,
} from 'class-validator';
export class XiaofeixiaEstimateFreightDto {
@IsNumber()
@Min(0.01)
weight: number;
}
export class XiaofeixiaCheckCoverageDto {
@IsString()
@IsNotEmpty()
toAddress: string;
}
export class XiaofeixiaShipmentQueryDto {
@IsOptional()
@IsString()
trackingNumber?: string;
@IsOptional()
@IsString()
outNumber?: string;
}
export class XiaofeixiaBatchShipmentQueryDto {
@IsOptional()
@IsArray()
@IsString({ each: true })
trackingNumbers?: string[];
@IsOptional()
@IsArray()
@IsString({ each: true })
outNumbers?: string[];
}
export class XiaofeixiaCreateShipmentDto {
@IsString()
@IsNotEmpty()
outNumber: string;
@IsOptional()
@IsString()
customerId?: string;
@IsString()
@IsNotEmpty()
fromName: string;
@IsString()
@IsNotEmpty()
fromMobile: string;
@IsString()
@IsNotEmpty()
fromAddress: string;
@IsString()
@IsNotEmpty()
fromAddressDetail: string;
@IsOptional()
@IsNumber()
fromLng?: number;
@IsOptional()
@IsNumber()
fromLat?: number;
@IsString()
@IsNotEmpty()
toName: string;
@IsString()
@IsNotEmpty()
toMobile: string;
@IsString()
@IsNotEmpty()
toAddress: string;
@IsString()
@IsNotEmpty()
toAddressDetail: string;
@IsOptional()
@IsNumber()
toLng?: number;
@IsOptional()
@IsNumber()
toLat?: number;
@IsOptional()
@IsString()
goodsName?: string;
@IsOptional()
@IsNumber()
goodsNum?: number;
@IsOptional()
@IsNumber()
weight?: number;
@IsOptional()
@IsNumber()
insuredSumPrice?: number;
@IsOptional()
@IsNumber()
collectionPrice?: number;
@IsIn(['1', '2'])
payMode: string;
@IsOptional()
@IsString()
remark?: string;
}
@@ -28,9 +28,12 @@ import { AdminTicketsService } from './admin-tickets.service';
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
import { BenefitModule } from '../benefit/benefit.module';
import { CommonModule } from '../common/common.module';
import { IntegrationsModule } from '../../integrations/integrations.module';
import { AdminXiaofeixiaController } from './admin-xiaofeixia.controller';
import { AdminXiaofeixiaService } from './admin-xiaofeixia.service';
@Module({
imports: [IamModule, TradeModule, BenefitModule, CommonModule],
imports: [IamModule, TradeModule, BenefitModule, CommonModule, IntegrationsModule],
controllers: [
AdminDashboardController,
AdminUsersController,
@@ -49,6 +52,7 @@ import { CommonModule } from '../common/common.module';
AdminProductsController,
AdminUserLogsController,
AdminTicketsController,
AdminXiaofeixiaController,
],
providers: [
AdminDashboardService,
@@ -64,6 +68,7 @@ import { CommonModule } from '../common/common.module';
AdminProductsService,
AdminUserLogsService,
AdminTicketsService,
AdminXiaofeixiaService,
SuperAdminGuard,
],
})