feat(trade): 仓配可调配履约与三分支推单
CI / verify (pull_request) Has been cancelled

同城有仓按仓库绑定承运商自动推单或自管填单,无仓/跨城走总部快递;新增仓配注册表、FulfillmentService 及三端运单追踪。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-16 17:51:20 +08:00
parent f1dc23c54c
commit f6d97b4ee8
30 changed files with 1615 additions and 57 deletions
@@ -5,7 +5,22 @@ import { HqOperationAction } from '../../common/hq-operation/hq-operation.consta
import { CityWarehouseService } from '../city-scope/city-warehouse.service';
import { CreateCityWarehouseDto, UpdateCityWarehouseDto } from './dto/admin-mutate.dto';
import { AdminCityWarehousesQueryDto } from './dto/admin-query.dto';
import type { WarehouseManagerType, WarehouseStatus } from '@prisma/client';
import type {
WarehouseFulfillmentMode,
WarehouseManagerType,
WarehouseStatus,
} from '@prisma/client';
function mapWarehouseFulfillment(dto: CreateCityWarehouseDto | UpdateCityWarehouseDto) {
return {
fulfillmentMode: dto.fulfillmentMode as WarehouseFulfillmentMode | undefined,
fulfillmentProviderId: dto.fulfillmentProviderId ? BigInt(dto.fulfillmentProviderId) : undefined,
manualCarrierLabel: dto.manualCarrierLabel ?? undefined,
manualQueryUrlTemplate: dto.manualQueryUrlTemplate ?? undefined,
lng: dto.lng ?? undefined,
lat: dto.lat ?? undefined,
};
}
@Controller('admin/cities/:cityId/warehouses')
@UseGuards(HqAuthGuard)
@@ -33,6 +48,7 @@ export class AdminCityWarehousesController {
managerType: dto.managerType as WarehouseManagerType,
partnerAccountId: dto.partnerAccountId ? BigInt(dto.partnerAccountId) : undefined,
status: dto.status as WarehouseStatus | undefined,
...mapWarehouseFulfillment(dto),
});
}
}
@@ -63,11 +79,22 @@ export class AdminCityWarehouseMutationsController {
managerType: dto.managerType as WarehouseManagerType | undefined,
partnerAccountId:
dto.partnerAccountId === null
? undefined
? null
: dto.partnerAccountId
? BigInt(dto.partnerAccountId)
: undefined,
status: dto.status as WarehouseStatus | undefined,
fulfillmentMode: dto.fulfillmentMode as WarehouseFulfillmentMode | undefined,
fulfillmentProviderId:
dto.fulfillmentProviderId === null
? null
: dto.fulfillmentProviderId
? BigInt(dto.fulfillmentProviderId)
: undefined,
manualCarrierLabel: dto.manualCarrierLabel,
manualQueryUrlTemplate: dto.manualQueryUrlTemplate,
lng: dto.lng,
lat: dto.lat,
});
}
@@ -0,0 +1,61 @@
import { Body, Controller, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
import {
CreateFulfillmentProviderDto,
UpdateFulfillmentProviderDto,
} from './dto/admin-mutate.dto';
import type { FulfillmentProviderStatus, FulfillmentProviderType } from '@prisma/client';
@Controller('admin/fulfillment-providers')
@UseGuards(HqAuthGuard)
export class AdminFulfillmentProvidersController {
constructor(private readonly service: FulfillmentProviderService) {}
@Get()
list() {
return this.service.listAll();
}
@Get('active-api')
listActiveApi() {
return this.service.listActiveApiProviders();
}
@Post()
@HqOperation({
action: HqOperationAction.WAREHOUSE_UPDATE,
refType: 'FULFILLMENT_PROVIDER',
refIdField: 'id',
includeBody: true,
})
create(@Body() dto: CreateFulfillmentProviderDto) {
return this.service.create({
code: dto.code,
name: dto.name,
type: dto.type as FulfillmentProviderType,
status: dto.status as FulfillmentProviderStatus | undefined,
configJson: dto.configJson,
capabilitiesJson: dto.capabilitiesJson,
});
}
@Put(':id')
@HqOperation({
action: HqOperationAction.WAREHOUSE_UPDATE,
refType: 'FULFILLMENT_PROVIDER',
refIdParam: 'id',
includeBody: true,
})
update(@Param('id') id: string, @Body() dto: UpdateFulfillmentProviderDto) {
return this.service.update(BigInt(id), {
name: dto.name,
type: dto.type as FulfillmentProviderType | undefined,
status: dto.status as FulfillmentProviderStatus | undefined,
configJson: dto.configJson,
capabilitiesJson: dto.capabilitiesJson,
});
}
}
@@ -4,7 +4,7 @@ import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
import { AdminOrdersService } from './admin-orders.service';
import { AdminShipOrderDto, BatchDeleteOrdersDto, UpdateOrderStatusDto } from './dto/admin-mutate.dto';
import { AdminShipOrderDto, BatchDeleteOrdersDto, HqLogisticsShipDto, UpdateOrderStatusDto } from './dto/admin-mutate.dto';
import { AdminOrdersQueryDto } from './dto/admin-query.dto';
@Controller('admin/orders')
@@ -51,6 +51,17 @@ export class AdminOrdersController {
return this.ordersService.shipOrder(BigInt(id), dto);
}
@Post(':id/logistics-ship')
@HqOperation({
action: HqOperationAction.ORDER_SHIP,
refType: 'ORDER',
refIdParam: 'id',
includeBody: true,
})
shipLogistics(@Param('id') id: string, @Body() dto: HqLogisticsShipDto) {
return this.ordersService.shipLogistics(BigInt(id), dto);
}
/** preV1 调试:直接改订单状态,不走业务校验 */
@Put(':id/status')
@HqOperation({
@@ -8,8 +8,9 @@ import { mapOrderCompat, mapStatusLogCompat } from '../../common/compat/v31-comp
import { TradeService } from '../trade/trade.service';
import { AdminXiaofeixiaService } from './admin-xiaofeixia.service';
import type { AdminOrdersQueryDto } from './dto/admin-query.dto';
import type { AdminShipOrderDto } from './dto/admin-mutate.dto';
import type { AdminShipOrderDto, HqLogisticsShipDto } from './dto/admin-mutate.dto';
import type { XiaofeixiaCreateShipmentDto } from './dto/admin-courier.dto';
import { FulfillmentService } from '../fulfillment/fulfillment.service';
@Injectable()
export class AdminOrdersService {
@@ -17,6 +18,7 @@ export class AdminOrdersService {
private readonly prisma: PrismaService,
private readonly tradeService: TradeService,
private readonly xiaofeixiaService: AdminXiaofeixiaService,
private readonly fulfillmentService: FulfillmentService,
private readonly config: ConfigService,
) {}
@@ -183,6 +185,12 @@ export class AdminOrdersService {
return this.detail(id);
}
/** 总部传统快递填单(同城无仓 / 跨城) */
async shipLogistics(id: bigint, dto: HqLogisticsShipDto) {
await this.fulfillmentService.shipHqLogistics(id, dto);
return this.detail(id);
}
async batchDeleteOrders(ids: bigint[]) {
const uniqueIds = [...new Set(ids)];
if (!uniqueIds.length) {
@@ -474,6 +474,30 @@ export class CreateCityWarehouseDto {
@IsOptional()
@IsIn(['ACTIVE', 'PAUSED'])
status?: 'ACTIVE' | 'PAUSED';
@IsOptional()
@IsIn(['API_AUTO', 'MANUAL'])
fulfillmentMode?: 'API_AUTO' | 'MANUAL';
@IsOptional()
@IsString()
fulfillmentProviderId?: string;
@IsOptional()
@IsString()
manualCarrierLabel?: string;
@IsOptional()
@IsString()
manualQueryUrlTemplate?: string;
@IsOptional()
@IsNumber()
lng?: number;
@IsOptional()
@IsNumber()
lat?: number;
}
export class UpdateCityWarehouseDto {
@@ -505,8 +529,100 @@ export class UpdateCityWarehouseDto {
@IsOptional()
@IsIn(['ACTIVE', 'PAUSED'])
status?: 'ACTIVE' | 'PAUSED';
@IsOptional()
@IsIn(['API_AUTO', 'MANUAL'])
fulfillmentMode?: 'API_AUTO' | 'MANUAL';
@IsOptional()
@ValidateIf((_, v) => v !== null)
@IsString()
fulfillmentProviderId?: string | null;
@IsOptional()
@ValidateIf((_, v) => v !== null)
@IsString()
manualCarrierLabel?: string | null;
@IsOptional()
@ValidateIf((_, v) => v !== null)
@IsString()
manualQueryUrlTemplate?: string | null;
@IsOptional()
@ValidateIf((_, v) => v !== null)
@IsNumber()
lng?: number | null;
@IsOptional()
@ValidateIf((_, v) => v !== null)
@IsNumber()
lat?: number | null;
}
export class CreateFulfillmentProviderDto {
@IsString()
@IsNotEmpty()
code: string;
@IsString()
@IsNotEmpty()
name: string;
@IsIn(['API', 'MANUAL'])
type: 'API' | 'MANUAL';
@IsOptional()
@IsIn(['ACTIVE', 'DISABLED'])
status?: 'ACTIVE' | 'DISABLED';
@IsOptional()
@IsString()
configJson?: string;
@IsOptional()
@IsString()
capabilitiesJson?: string;
}
export class UpdateFulfillmentProviderDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
@IsIn(['API', 'MANUAL'])
type?: 'API' | 'MANUAL';
@IsOptional()
@IsIn(['ACTIVE', 'DISABLED'])
status?: 'ACTIVE' | 'DISABLED';
@IsOptional()
@IsString()
configJson?: string;
@IsOptional()
@IsString()
capabilitiesJson?: string;
}
export class ManualShipOrderDto {
@IsString()
@IsNotEmpty()
logisticsCompany: string;
@IsString()
@IsNotEmpty()
trackingNo: string;
@IsOptional()
@IsString()
manualQueryUrl?: string;
}
export class HqLogisticsShipDto extends ManualShipOrderDto {}
export class CreateStoreMediaDto {
@IsString()
@IsNotEmpty()
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { CityScopeModule } from '../city-scope/city-scope.module';
import { IamModule } from '../iam/iam.module';
import { TradeModule } from '../trade/trade.module';
import { FulfillmentModule } from '../fulfillment/fulfillment.module';
import { AdminDashboardController } from './admin-dashboard.controller';
import { AdminDashboardService } from './admin-dashboard.service';
import { AdminUsersController } from './admin-users.controller';
@@ -54,9 +55,10 @@ import { AdminHqPermissionsService } from './admin-hq-permissions.service';
import { AdminDeployController } from './admin-deploy.controller';
import { AdminDeployService } from './admin-deploy.service';
import { AdminSystemConfigController } from './admin-system-config.controller';
import { AdminFulfillmentProvidersController } from './admin-fulfillment-providers.controller';
@Module({
imports: [CityScopeModule, IamModule, TradeModule, BenefitModule, CommonModule, IntegrationsModule, RedeemModule],
imports: [CityScopeModule, IamModule, TradeModule, FulfillmentModule, BenefitModule, CommonModule, IntegrationsModule, RedeemModule],
controllers: [
AdminDashboardController,
AdminDeployController,
@@ -89,6 +91,7 @@ import { AdminSystemConfigController } from './admin-system-config.controller';
AdminWechatBindingsController,
AdminHqPermissionsController,
AdminSystemConfigController,
AdminFulfillmentProvidersController,
],
providers: [
AdminDashboardService,