城市合伙人端的修改(后台)

This commit is contained in:
2026-07-12 10:19:39 +08:00
parent d0f0fa09af
commit 7f031cc4c2
74 changed files with 5430 additions and 975 deletions
@@ -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(),
});
}
}