同城有仓按仓库绑定承运商自动推单或自管填单,无仓/跨城走总部快递;新增仓配注册表、FulfillmentService 及三端运单追踪。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma, WarehouseManagerType, WarehouseStatus } from '@prisma/client';
|
||||
import {
|
||||
Prisma,
|
||||
WarehouseFulfillmentMode,
|
||||
WarehouseManagerType,
|
||||
WarehouseStatus,
|
||||
} from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { PartnerCityService } from './partner-city.service';
|
||||
@@ -13,9 +18,27 @@ export type CreateCityWarehouseInput = {
|
||||
managerType: WarehouseManagerType;
|
||||
partnerAccountId?: bigint;
|
||||
status?: WarehouseStatus;
|
||||
fulfillmentMode?: WarehouseFulfillmentMode;
|
||||
fulfillmentProviderId?: bigint;
|
||||
manualCarrierLabel?: string;
|
||||
manualQueryUrlTemplate?: string;
|
||||
lng?: number;
|
||||
lat?: number;
|
||||
};
|
||||
|
||||
export type UpdateCityWarehouseInput = Partial<CreateCityWarehouseInput>;
|
||||
export type UpdateCityWarehouseInput = Partial<
|
||||
Omit<
|
||||
CreateCityWarehouseInput,
|
||||
'partnerAccountId' | 'fulfillmentProviderId' | 'manualCarrierLabel' | 'manualQueryUrlTemplate' | 'lng' | 'lat'
|
||||
>
|
||||
> & {
|
||||
partnerAccountId?: bigint | null;
|
||||
fulfillmentProviderId?: bigint | null;
|
||||
manualCarrierLabel?: string | null;
|
||||
manualQueryUrlTemplate?: string | null;
|
||||
lng?: number | null;
|
||||
lat?: number | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CityWarehouseService {
|
||||
@@ -29,6 +52,7 @@ export class CityWarehouseService {
|
||||
where: { cityId },
|
||||
include: {
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
fulfillmentProvider: { select: { id: true, code: true, name: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
@@ -51,6 +75,7 @@ export class CityWarehouseService {
|
||||
take: pageSize,
|
||||
include: {
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
fulfillmentProvider: { select: { id: true, code: true, name: true } },
|
||||
city: { select: { id: true, name: true, code: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
@@ -73,6 +98,7 @@ export class CityWarehouseService {
|
||||
async create(cityId: bigint, input: CreateCityWarehouseInput) {
|
||||
await this.assertCityExists(cityId);
|
||||
await this.validateManager(input.managerType, input.partnerAccountId, cityId);
|
||||
await this.validateFulfillment(input.fulfillmentMode, input.fulfillmentProviderId);
|
||||
|
||||
const row = await this.prisma.cityWarehouse.create({
|
||||
data: {
|
||||
@@ -84,9 +110,17 @@ export class CityWarehouseService {
|
||||
managerType: input.managerType,
|
||||
partnerAccountId: input.managerType === 'PARTNER' ? input.partnerAccountId : null,
|
||||
status: input.status ?? 'ACTIVE',
|
||||
fulfillmentMode: input.fulfillmentMode ?? 'MANUAL',
|
||||
fulfillmentProviderId:
|
||||
input.fulfillmentMode === 'API_AUTO' ? input.fulfillmentProviderId : null,
|
||||
manualCarrierLabel: input.manualCarrierLabel?.trim() || null,
|
||||
manualQueryUrlTemplate: input.manualQueryUrlTemplate?.trim() || null,
|
||||
lng: input.lng != null ? input.lng : null,
|
||||
lat: input.lat != null ? input.lat : null,
|
||||
},
|
||||
include: {
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
fulfillmentProvider: { select: { id: true, code: true, name: true } },
|
||||
},
|
||||
});
|
||||
await this.syncManagedWarehouse(row.id, row.managerType as WarehouseManagerType, row.partnerAccountId);
|
||||
@@ -102,8 +136,16 @@ export class CityWarehouseService {
|
||||
managerType === 'PARTNER'
|
||||
? input.partnerAccountId ?? current.partnerAccountId ?? undefined
|
||||
: null;
|
||||
const fulfillmentMode = input.fulfillmentMode ?? current.fulfillmentMode;
|
||||
const fulfillmentProviderId =
|
||||
fulfillmentMode === 'API_AUTO'
|
||||
? input.fulfillmentProviderId !== undefined
|
||||
? input.fulfillmentProviderId
|
||||
: current.fulfillmentProviderId
|
||||
: null;
|
||||
|
||||
await this.validateManager(managerType, partnerAccountId ?? undefined, current.cityId);
|
||||
await this.validateFulfillment(fulfillmentMode, fulfillmentProviderId ?? undefined);
|
||||
|
||||
const row = await this.prisma.cityWarehouse.update({
|
||||
where: { id },
|
||||
@@ -117,9 +159,22 @@ export class CityWarehouseService {
|
||||
? { partnerAccountId: managerType === 'PARTNER' ? partnerAccountId : null }
|
||||
: {}),
|
||||
...(input.status !== undefined ? { status: input.status } : {}),
|
||||
...(input.fulfillmentMode !== undefined ? { fulfillmentMode } : {}),
|
||||
...(input.fulfillmentMode !== undefined || input.fulfillmentProviderId !== undefined
|
||||
? { fulfillmentProviderId }
|
||||
: {}),
|
||||
...(input.manualCarrierLabel !== undefined
|
||||
? { manualCarrierLabel: input.manualCarrierLabel?.trim() || null }
|
||||
: {}),
|
||||
...(input.manualQueryUrlTemplate !== undefined
|
||||
? { manualQueryUrlTemplate: input.manualQueryUrlTemplate?.trim() || null }
|
||||
: {}),
|
||||
...(input.lng !== undefined ? { lng: input.lng } : {}),
|
||||
...(input.lat !== undefined ? { lat: input.lat } : {}),
|
||||
},
|
||||
include: {
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
fulfillmentProvider: { select: { id: true, code: true, name: true } },
|
||||
},
|
||||
});
|
||||
await this.syncManagedWarehouse(row.id, row.managerType as WarehouseManagerType, row.partnerAccountId);
|
||||
@@ -170,6 +225,19 @@ export class CityWarehouseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async validateFulfillment(
|
||||
mode?: WarehouseFulfillmentMode,
|
||||
providerId?: bigint,
|
||||
) {
|
||||
if (mode === 'API_AUTO') {
|
||||
if (!providerId) throw new BadRequestException('API 自动推单须选择仓配承运商');
|
||||
const provider = await this.prisma.fulfillmentProvider.findUnique({ where: { id: providerId } });
|
||||
if (!provider || provider.status !== 'ACTIVE' || provider.type !== 'API') {
|
||||
throw new BadRequestException('所选仓配承运商不可用');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async assertCityExists(cityId: bigint) {
|
||||
const city = await this.prisma.commonCity.findUnique({ where: { id: cityId } });
|
||||
if (!city) throw new NotFoundException('开城城市不存在');
|
||||
@@ -185,9 +253,16 @@ export class CityWarehouseService {
|
||||
managerType: string;
|
||||
partnerAccountId: bigint | null;
|
||||
status: string;
|
||||
fulfillmentMode: string;
|
||||
fulfillmentProviderId: bigint | null;
|
||||
manualCarrierLabel: string | null;
|
||||
manualQueryUrlTemplate: string | null;
|
||||
lng: Prisma.Decimal | null;
|
||||
lat: Prisma.Decimal | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
partnerAccount?: { id: bigint; companyName: string | null } | null;
|
||||
fulfillmentProvider?: { id: bigint; code: string; name: string } | null;
|
||||
}) {
|
||||
return serializeBigInt({
|
||||
id: row.id.toString(),
|
||||
@@ -200,6 +275,14 @@ export class CityWarehouseService {
|
||||
partnerAccountId: row.partnerAccountId?.toString() ?? null,
|
||||
partnerCompanyName: row.partnerAccount?.companyName ?? null,
|
||||
status: row.status,
|
||||
fulfillmentMode: row.fulfillmentMode,
|
||||
fulfillmentProviderId: row.fulfillmentProviderId?.toString() ?? null,
|
||||
fulfillmentProviderName: row.fulfillmentProvider?.name ?? null,
|
||||
fulfillmentProviderCode: row.fulfillmentProvider?.code ?? null,
|
||||
manualCarrierLabel: row.manualCarrierLabel,
|
||||
manualQueryUrlTemplate: row.manualQueryUrlTemplate,
|
||||
lng: row.lng != null ? Number(row.lng) : null,
|
||||
lat: row.lat != null ? Number(row.lat) : null,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
@@ -101,9 +101,38 @@ export class PartnerCityService {
|
||||
async buildPartnerOrderWhere(partnerAccountId: bigint): Promise<Prisma.OrderWhereInput> {
|
||||
const primary = await this.resolvePrimaryAccount(partnerAccountId);
|
||||
if (!primary.cityId) return { id: -1n };
|
||||
|
||||
const warehouseIds = await this.resolveManagedWarehouseIds(primary.id);
|
||||
if (warehouseIds.length > 0) {
|
||||
return { fulfillmentWarehouseId: { in: warehouseIds } };
|
||||
}
|
||||
return { cityId: primary.cityId };
|
||||
}
|
||||
|
||||
/** 合伙人可管仓库:主账号 managedWarehouseId + 绑定为管仓合伙人的仓 */
|
||||
async resolveManagedWarehouseIds(partnerAccountId: bigint): Promise<bigint[]> {
|
||||
const primary = await this.resolvePrimaryAccount(partnerAccountId);
|
||||
const ids = new Set<bigint>();
|
||||
|
||||
if (primary.managedWarehouseId) {
|
||||
ids.add(primary.managedWarehouseId);
|
||||
}
|
||||
|
||||
const managed = await this.prisma.cityWarehouse.findMany({
|
||||
where: {
|
||||
status: 'ACTIVE',
|
||||
OR: [
|
||||
{ partnerAccountId: primary.id },
|
||||
...(primary.managedWarehouseId ? [{ id: primary.managedWarehouseId }] : []),
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
for (const row of managed) ids.add(row.id);
|
||||
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
async buildPartnerCityWhere(partnerAccountId: bigint): Promise<Prisma.CommonCityWhereInput> {
|
||||
const primary = await this.resolvePrimaryAccount(partnerAccountId);
|
||||
if (!primary.cityId) return { id: -1n };
|
||||
|
||||
Reference in New Issue
Block a user