城市合伙人端的修改(后台)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PartnerCityService } from './partner-city.service';
|
||||
import { CityWarehouseService } from './city-warehouse.service';
|
||||
|
||||
@Module({
|
||||
providers: [PartnerCityService, CityWarehouseService],
|
||||
exports: [PartnerCityService, CityWarehouseService],
|
||||
})
|
||||
export class CityScopeModule {}
|
||||
@@ -0,0 +1,207 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma, 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';
|
||||
import type { AdminCityWarehousesQueryDto } from '../ops/dto/admin-query.dto';
|
||||
|
||||
export type CreateCityWarehouseInput = {
|
||||
name: string;
|
||||
address: string;
|
||||
contactName: string;
|
||||
contactPhone: string;
|
||||
managerType: WarehouseManagerType;
|
||||
partnerAccountId?: bigint;
|
||||
status?: WarehouseStatus;
|
||||
};
|
||||
|
||||
export type UpdateCityWarehouseInput = Partial<CreateCityWarehouseInput>;
|
||||
|
||||
@Injectable()
|
||||
export class CityWarehouseService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
async listByCity(cityId: bigint) {
|
||||
const rows = await this.prisma.cityWarehouse.findMany({
|
||||
where: { cityId },
|
||||
include: {
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return rows.map((row) => this.toDto(row));
|
||||
}
|
||||
|
||||
async listAll(query: AdminCityWarehousesQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.CityWarehouseWhereInput = {};
|
||||
if (query.cityId) where.cityId = BigInt(query.cityId);
|
||||
if (query.name) where.name = { contains: query.name };
|
||||
if (query.managerType) where.managerType = query.managerType as Prisma.EnumWarehouseManagerTypeFilter['equals'];
|
||||
if (query.status) where.status = query.status as Prisma.EnumWarehouseStatusFilter['equals'];
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.cityWarehouse.findMany({
|
||||
where,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
city: { select: { id: true, name: true, code: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
this.prisma.cityWarehouse.count({ where }),
|
||||
]);
|
||||
|
||||
return serializeBigInt({
|
||||
items: rows.map((row) => ({
|
||||
...this.toDto(row),
|
||||
cityName: row.city.name,
|
||||
cityCode: row.city.code,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async create(cityId: bigint, input: CreateCityWarehouseInput) {
|
||||
await this.assertCityExists(cityId);
|
||||
await this.validateManager(input.managerType, input.partnerAccountId, cityId);
|
||||
|
||||
const row = await this.prisma.cityWarehouse.create({
|
||||
data: {
|
||||
cityId,
|
||||
name: input.name.trim(),
|
||||
address: input.address.trim(),
|
||||
contactName: input.contactName.trim(),
|
||||
contactPhone: input.contactPhone.trim(),
|
||||
managerType: input.managerType,
|
||||
partnerAccountId: input.managerType === 'PARTNER' ? input.partnerAccountId : null,
|
||||
status: input.status ?? 'ACTIVE',
|
||||
},
|
||||
include: {
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
},
|
||||
});
|
||||
await this.syncManagedWarehouse(row.id, row.managerType as WarehouseManagerType, row.partnerAccountId);
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
async update(id: bigint, input: UpdateCityWarehouseInput) {
|
||||
const current = await this.prisma.cityWarehouse.findUnique({ where: { id } });
|
||||
if (!current) throw new NotFoundException('仓库不存在');
|
||||
|
||||
const managerType = input.managerType ?? (current.managerType as WarehouseManagerType);
|
||||
const partnerAccountId =
|
||||
managerType === 'PARTNER'
|
||||
? input.partnerAccountId ?? current.partnerAccountId ?? undefined
|
||||
: null;
|
||||
|
||||
await this.validateManager(managerType, partnerAccountId ?? undefined, current.cityId);
|
||||
|
||||
const row = await this.prisma.cityWarehouse.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(input.name !== undefined ? { name: input.name.trim() } : {}),
|
||||
...(input.address !== undefined ? { address: input.address.trim() } : {}),
|
||||
...(input.contactName !== undefined ? { contactName: input.contactName.trim() } : {}),
|
||||
...(input.contactPhone !== undefined ? { contactPhone: input.contactPhone.trim() } : {}),
|
||||
...(input.managerType !== undefined ? { managerType: input.managerType } : {}),
|
||||
...(input.managerType !== undefined || input.partnerAccountId !== undefined
|
||||
? { partnerAccountId: managerType === 'PARTNER' ? partnerAccountId : null }
|
||||
: {}),
|
||||
...(input.status !== undefined ? { status: input.status } : {}),
|
||||
},
|
||||
include: {
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
},
|
||||
});
|
||||
await this.syncManagedWarehouse(row.id, row.managerType as WarehouseManagerType, row.partnerAccountId);
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
async remove(id: bigint) {
|
||||
const current = await this.prisma.cityWarehouse.findUnique({ where: { id } });
|
||||
if (!current) throw new NotFoundException('仓库不存在');
|
||||
await this.prisma.partnerAccount.updateMany({
|
||||
where: { managedWarehouseId: id },
|
||||
data: { managedWarehouseId: null },
|
||||
});
|
||||
await this.prisma.cityWarehouse.delete({ where: { id } });
|
||||
return { ok: true, id: id.toString() };
|
||||
}
|
||||
|
||||
private async syncManagedWarehouse(
|
||||
warehouseId: bigint,
|
||||
managerType: WarehouseManagerType,
|
||||
partnerAccountId: bigint | null,
|
||||
) {
|
||||
await this.prisma.partnerAccount.updateMany({
|
||||
where: { managedWarehouseId: warehouseId },
|
||||
data: { managedWarehouseId: null },
|
||||
});
|
||||
|
||||
if (managerType === 'PARTNER' && partnerAccountId) {
|
||||
await this.prisma.partnerAccount.updateMany({
|
||||
where: { id: partnerAccountId, managedWarehouseId: { not: warehouseId } },
|
||||
data: { managedWarehouseId: null },
|
||||
});
|
||||
await this.prisma.partnerAccount.update({
|
||||
where: { id: partnerAccountId },
|
||||
data: { managedWarehouseId: warehouseId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private validateManager(
|
||||
managerType: WarehouseManagerType,
|
||||
partnerAccountId: bigint | undefined,
|
||||
cityId: bigint,
|
||||
) {
|
||||
if (managerType === 'PARTNER') {
|
||||
if (!partnerAccountId) throw new BadRequestException('合伙人管仓须指定合伙人');
|
||||
return this.partnerCityService.assertPartnerAccountBoundToCity(partnerAccountId, cityId);
|
||||
}
|
||||
}
|
||||
|
||||
private async assertCityExists(cityId: bigint) {
|
||||
const city = await this.prisma.commonCity.findUnique({ where: { id: cityId } });
|
||||
if (!city) throw new NotFoundException('开城城市不存在');
|
||||
}
|
||||
|
||||
private toDto(row: {
|
||||
id: bigint;
|
||||
cityId: bigint;
|
||||
name: string;
|
||||
address: string;
|
||||
contactName: string;
|
||||
contactPhone: string;
|
||||
managerType: string;
|
||||
partnerAccountId: bigint | null;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
partnerAccount?: { id: bigint; companyName: string | null } | null;
|
||||
}) {
|
||||
return serializeBigInt({
|
||||
id: row.id.toString(),
|
||||
cityId: row.cityId.toString(),
|
||||
name: row.name,
|
||||
address: row.address,
|
||||
contactName: row.contactName,
|
||||
contactPhone: row.contactPhone,
|
||||
managerType: row.managerType,
|
||||
partnerAccountId: row.partnerAccountId?.toString() ?? null,
|
||||
partnerCompanyName: row.partnerAccount?.companyName ?? null,
|
||||
status: row.status,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma, CityPartnerScopeType, CityPartnerStatus } from '@prisma/client';
|
||||
import { resolveOrderCityPartner, validatePartnerCityBinding } from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
const PRIMARY_WHERE = { isPrimary: 1 } as const;
|
||||
|
||||
@Injectable()
|
||||
export class PartnerCityService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async listByCity(cityId: bigint) {
|
||||
const rows = await this.prisma.partnerAccount.findMany({
|
||||
where: { ...PRIMARY_WHERE, cityId },
|
||||
include: { city: { select: { id: true, code: true, name: true } } },
|
||||
orderBy: [{ scopeType: 'asc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
return rows.map((row) => this.toDto(row));
|
||||
}
|
||||
|
||||
async listCityIdsForPartnerAccount(partnerAccountId: bigint): Promise<bigint[]> {
|
||||
const primary = await this.resolvePrimaryAccount(partnerAccountId);
|
||||
if (!primary.cityId) return [];
|
||||
return [primary.cityId];
|
||||
}
|
||||
|
||||
async assertPartnerAccountBoundToCity(partnerAccountId: bigint, cityId: bigint) {
|
||||
const row = await this.prisma.partnerAccount.findFirst({
|
||||
where: {
|
||||
id: partnerAccountId,
|
||||
...PRIMARY_WHERE,
|
||||
cityId,
|
||||
bindingStatus: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
if (!row) {
|
||||
throw new BadRequestException('合伙人未绑定该开城城市');
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
async assertPartnerBoundToCity(partnerAccountId: bigint, cityId: bigint) {
|
||||
return this.assertPartnerAccountBoundToCity(partnerAccountId, cityId);
|
||||
}
|
||||
|
||||
async resolveForOrder(cityId: bigint, receiverDistrict?: string | null) {
|
||||
const bindings = await this.prisma.partnerAccount.findMany({
|
||||
where: { ...PRIMARY_WHERE, cityId, bindingStatus: 'ACTIVE' },
|
||||
});
|
||||
const ref = resolveOrderCityPartner(
|
||||
bindings.map((b) => ({
|
||||
id: b.id.toString(),
|
||||
partnerAccountId: b.id.toString(),
|
||||
scopeType: b.scopeType as CityPartnerScopeType,
|
||||
districtCodes: this.parseDistrictCodes(b.districtCodes),
|
||||
orderCommissionRate: Number(b.orderCommissionRate ?? 0),
|
||||
redeemCommissionRate: Number(b.redeemCommissionRate ?? 0.03),
|
||||
bindingStatus: b.bindingStatus as CityPartnerStatus,
|
||||
})),
|
||||
receiverDistrict,
|
||||
);
|
||||
if (!ref) return null;
|
||||
return {
|
||||
partnerAccountId: BigInt(ref.partnerAccountId),
|
||||
orderCommissionRate: ref.orderCommissionRate,
|
||||
redeemCommissionRate: ref.redeemCommissionRate,
|
||||
};
|
||||
}
|
||||
|
||||
async validatePrimaryBinding(
|
||||
cityId: bigint,
|
||||
input: {
|
||||
partnerAccountId?: string;
|
||||
scopeType: CityPartnerScopeType;
|
||||
districtCodes?: string[];
|
||||
},
|
||||
excludeId?: bigint,
|
||||
) {
|
||||
const existing = await this.prisma.partnerAccount.findMany({
|
||||
where: { ...PRIMARY_WHERE, cityId, ...(excludeId ? { NOT: { id: excludeId } } : {}) },
|
||||
});
|
||||
const validation = validatePartnerCityBinding(
|
||||
existing.map((r) => ({
|
||||
id: r.id.toString(),
|
||||
partnerAccountId: r.id.toString(),
|
||||
scopeType: r.scopeType as CityPartnerScopeType,
|
||||
districtCodes: this.parseDistrictCodes(r.districtCodes),
|
||||
})),
|
||||
{
|
||||
partnerAccountId: input.partnerAccountId ?? 'new',
|
||||
scopeType: input.scopeType,
|
||||
districtCodes: input.districtCodes,
|
||||
},
|
||||
excludeId?.toString(),
|
||||
);
|
||||
if (!validation.ok) throw new BadRequestException(validation.message);
|
||||
}
|
||||
|
||||
async buildPartnerOrderWhere(partnerAccountId: bigint): Promise<Prisma.OrderWhereInput> {
|
||||
const primary = await this.resolvePrimaryAccount(partnerAccountId);
|
||||
if (!primary.cityId) return { id: -1n };
|
||||
return { cityId: primary.cityId };
|
||||
}
|
||||
|
||||
async buildPartnerCityWhere(partnerAccountId: bigint): Promise<Prisma.CommonCityWhereInput> {
|
||||
const primary = await this.resolvePrimaryAccount(partnerAccountId);
|
||||
if (!primary.cityId) return { id: -1n };
|
||||
return { id: primary.cityId };
|
||||
}
|
||||
|
||||
async resolvePrimaryAccount(accountId: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findUnique({ where: { id: accountId } });
|
||||
if (!account) throw new NotFoundException('合伙人账号不存在');
|
||||
if (account.isPrimary === 1) return account;
|
||||
if (!account.parentAccountId) {
|
||||
throw new BadRequestException('子账号缺少主账号');
|
||||
}
|
||||
return this.prisma.partnerAccount.findUniqueOrThrow({ where: { id: account.parentAccountId } });
|
||||
}
|
||||
|
||||
parseDistrictCodes(value: Prisma.JsonValue | null): string[] | null {
|
||||
if (!value || !Array.isArray(value)) return null;
|
||||
return value.map((v) => String(v));
|
||||
}
|
||||
|
||||
toDto(row: {
|
||||
id: bigint;
|
||||
cityId: bigint | null;
|
||||
companyName: string | null;
|
||||
phone: string;
|
||||
name: string;
|
||||
scopeType: string | null;
|
||||
districtCodes: Prisma.JsonValue | null;
|
||||
orderCommissionRate: Prisma.Decimal | null;
|
||||
redeemCommissionRate: Prisma.Decimal | null;
|
||||
bindingStatus: string | null;
|
||||
managedWarehouseId: bigint | null;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
city?: { id: bigint; code: string; name: string } | null;
|
||||
}) {
|
||||
return serializeBigInt({
|
||||
id: row.id.toString(),
|
||||
cityId: row.cityId?.toString() ?? null,
|
||||
cityName: row.city?.name ?? null,
|
||||
cityCode: row.city?.code ?? null,
|
||||
companyName: row.companyName,
|
||||
phone: row.phone,
|
||||
name: row.name,
|
||||
scopeType: row.scopeType,
|
||||
districtCodes: this.parseDistrictCodes(row.districtCodes),
|
||||
orderCommissionRate: Number(row.orderCommissionRate ?? 0),
|
||||
redeemCommissionRate: Number(row.redeemCommissionRate ?? 0.03),
|
||||
bindingStatus: row.bindingStatus,
|
||||
managedWarehouseId: row.managedWarehouseId?.toString() ?? null,
|
||||
status: row.status,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user