城市合伙人端的修改(后台)
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { resolveMaxPartnerCommissionRate, validatePartnerCommissionRates } from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import type { AdminCitiesQueryDto } from './dto/admin-query.dto';
|
||||
import type { CreateCityDto, UpdateCityDto } from './dto/admin-mutate.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminCitiesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
async list(query: AdminCitiesQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
@@ -16,7 +21,9 @@ export class AdminCitiesService {
|
||||
if (query.name) where.name = { contains: query.name };
|
||||
if (query.code) where.code = { contains: query.code };
|
||||
if (query.status) where.status = query.status as Prisma.EnumCityStatusFilter['equals'];
|
||||
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
||||
if (query.partnerId) {
|
||||
where.partnerAccounts = { some: { id: BigInt(query.partnerId), isPrimary: 1 } };
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.commonCity.findMany({
|
||||
@@ -25,8 +32,12 @@ export class AdminCitiesService {
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
partner: { select: { id: true, companyName: true } },
|
||||
_count: { select: { stores: true, orders: true } },
|
||||
partnerAccounts: {
|
||||
where: { isPrimary: 1 },
|
||||
select: { id: true, companyName: true, scopeType: true, bindingStatus: true },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
},
|
||||
_count: { select: { stores: true, orders: true, partnerAccounts: true, warehouses: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.commonCity.count({ where }),
|
||||
@@ -34,8 +45,18 @@ export class AdminCitiesService {
|
||||
return serializeBigInt({
|
||||
items: items.map((c) => ({
|
||||
...c,
|
||||
partnerBindings: c.partnerAccounts.map((bp) => ({
|
||||
id: bp.id.toString(),
|
||||
partnerAccountId: bp.id.toString(),
|
||||
partnerCompanyName: bp.companyName,
|
||||
scopeType: bp.scopeType,
|
||||
status: bp.bindingStatus,
|
||||
})),
|
||||
partnerAccounts: undefined,
|
||||
storeCount: c._count.stores,
|
||||
orderCount: c._count.orders,
|
||||
partnerBindingCount: c.partnerAccounts.length,
|
||||
warehouseCount: c._count.warehouses,
|
||||
_count: undefined,
|
||||
})),
|
||||
total,
|
||||
@@ -48,13 +69,22 @@ export class AdminCitiesService {
|
||||
const city = await this.prisma.commonCity.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
partner: true,
|
||||
commissionRule: true,
|
||||
warehouses: {
|
||||
include: { partnerAccount: { select: { id: true, companyName: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
},
|
||||
_count: { select: { stores: true, orders: true } },
|
||||
},
|
||||
});
|
||||
if (!city) throw new NotFoundException('开城城市不存在');
|
||||
return serializeBigInt(city);
|
||||
const cityPartners = await this.partnerCityService.listByCity(id);
|
||||
return serializeBigInt({
|
||||
...city,
|
||||
cityPartners,
|
||||
storeCount: city._count.stores,
|
||||
orderCount: city._count.orders,
|
||||
_count: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async create(dto: CreateCityDto) {
|
||||
@@ -65,31 +95,48 @@ export class AdminCitiesService {
|
||||
code: dto.code,
|
||||
name: dto.name,
|
||||
province: dto.province,
|
||||
partnerId: dto.partnerId ? BigInt(dto.partnerId) : null,
|
||||
status: (dto.status ?? 'PENDING') as 'PENDING' | 'ACTIVE' | 'PAUSED',
|
||||
commissionRule: {
|
||||
create: {
|
||||
orderCommissionRate: 0,
|
||||
redeemCommissionRate: 0.03,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return serializeBigInt(city);
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdateCityDto) {
|
||||
if (dto.maxPartnerCommissionRate !== undefined) {
|
||||
const maxRate = resolveMaxPartnerCommissionRate(dto.maxPartnerCommissionRate);
|
||||
const partners = await this.prisma.partnerAccount.findMany({
|
||||
where: { cityId: id, isPrimary: 1 },
|
||||
select: {
|
||||
companyName: true,
|
||||
orderCommissionRate: true,
|
||||
redeemCommissionRate: true,
|
||||
},
|
||||
});
|
||||
for (const partner of partners) {
|
||||
const check = validatePartnerCommissionRates(
|
||||
Number(partner.orderCommissionRate ?? 0),
|
||||
Number(partner.redeemCommissionRate ?? 0.03),
|
||||
maxRate,
|
||||
);
|
||||
if (!check.ok) {
|
||||
throw new BadRequestException(
|
||||
`无法保存:合伙人「${partner.companyName ?? '—'}」${check.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const city = await this.prisma.commonCity.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
||||
...(dto.province !== undefined ? { province: dto.province } : {}),
|
||||
...(dto.partnerId !== undefined
|
||||
? { partnerId: dto.partnerId ? BigInt(dto.partnerId) : null }
|
||||
: {}),
|
||||
...(dto.status !== undefined ? { status: dto.status as 'PENDING' | 'ACTIVE' | 'PAUSED' } : {}),
|
||||
...(dto.localMinQty !== undefined ? { localMinQty: dto.localMinQty } : {}),
|
||||
...(dto.crossMinQty !== undefined ? { crossMinQty: dto.crossMinQty } : {}),
|
||||
...(dto.maxPartnerCommissionRate !== undefined
|
||||
? { maxPartnerCommissionRate: dto.maxPartnerCommissionRate }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
return serializeBigInt(city);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, Query, 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 { 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';
|
||||
|
||||
@Controller('admin/cities/:cityId/warehouses')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminCityWarehousesController {
|
||||
constructor(private readonly service: CityWarehouseService) {}
|
||||
|
||||
@Get()
|
||||
list(@Param('cityId') cityId: string) {
|
||||
return this.service.listByCity(BigInt(cityId));
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HqOperation({
|
||||
action: HqOperationAction.WAREHOUSE_CREATE,
|
||||
refType: 'WAREHOUSE',
|
||||
refIdField: 'id',
|
||||
includeBody: true,
|
||||
})
|
||||
create(@Param('cityId') cityId: string, @Body() dto: CreateCityWarehouseDto) {
|
||||
return this.service.create(BigInt(cityId), {
|
||||
name: dto.name,
|
||||
address: dto.address,
|
||||
contactName: dto.contactName,
|
||||
contactPhone: dto.contactPhone,
|
||||
managerType: dto.managerType as WarehouseManagerType,
|
||||
partnerAccountId: dto.partnerAccountId ? BigInt(dto.partnerAccountId) : undefined,
|
||||
status: dto.status as WarehouseStatus | undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('admin/city-warehouses')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminCityWarehouseMutationsController {
|
||||
constructor(private readonly service: CityWarehouseService) {}
|
||||
|
||||
@Get()
|
||||
listAll(@Query() query: AdminCityWarehousesQueryDto) {
|
||||
return this.service.listAll(query);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.WAREHOUSE_UPDATE,
|
||||
refType: 'WAREHOUSE',
|
||||
refIdParam: 'id',
|
||||
includeBody: true,
|
||||
})
|
||||
update(@Param('id') id: string, @Body() dto: UpdateCityWarehouseDto) {
|
||||
return this.service.update(BigInt(id), {
|
||||
name: dto.name,
|
||||
address: dto.address,
|
||||
contactName: dto.contactName,
|
||||
contactPhone: dto.contactPhone,
|
||||
managerType: dto.managerType as WarehouseManagerType | undefined,
|
||||
partnerAccountId:
|
||||
dto.partnerAccountId === null
|
||||
? undefined
|
||||
: dto.partnerAccountId
|
||||
? BigInt(dto.partnerAccountId)
|
||||
: undefined,
|
||||
status: dto.status as WarehouseStatus | undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.WAREHOUSE_DELETE,
|
||||
refType: 'WAREHOUSE',
|
||||
refIdParam: 'id',
|
||||
})
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.remove(BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export class AdminDashboardService {
|
||||
_count: { status: true },
|
||||
}),
|
||||
this.prisma.store.count(),
|
||||
this.prisma.partner.count(),
|
||||
this.prisma.partnerAccount.count({ where: { isPrimary: 1 } }),
|
||||
this.prisma.redeemRecord.count({ where: { createdAt: { gte: todayStart } } }),
|
||||
this.prisma.orderDelivery.count(),
|
||||
this.prisma.storePayout.count({ where: { status: 'PENDING' } }),
|
||||
|
||||
@@ -15,12 +15,12 @@ export class AdminPartnerLogsService {
|
||||
async list(query: AdminPartnerLogsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const partnerIds = await this.resolvePartnerIds(query);
|
||||
if (partnerIds && partnerIds.length === 0) {
|
||||
const partnerAccountIds = await this.resolvePartnerAccountIds(query);
|
||||
if (partnerAccountIds && partnerAccountIds.length === 0) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
|
||||
const where = this.buildWhere(query, partnerIds);
|
||||
const where = this.buildWhere(query, partnerAccountIds);
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.logPartnerAnalytics.findMany({
|
||||
where,
|
||||
@@ -44,10 +44,10 @@ export class AdminPartnerLogsService {
|
||||
|
||||
private buildWhere(
|
||||
query: AdminPartnerLogsQueryDto,
|
||||
partnerIds?: bigint[],
|
||||
partnerAccountIds?: bigint[],
|
||||
): Prisma.LogPartnerAnalyticsWhereInput {
|
||||
const where: Prisma.LogPartnerAnalyticsWhereInput = {};
|
||||
if (partnerIds) where.partnerId = { in: partnerIds };
|
||||
if (partnerAccountIds) where.partnerAccountId = { in: partnerAccountIds };
|
||||
if (query.partnerAccountId) where.partnerAccountId = BigInt(query.partnerAccountId);
|
||||
const categoryEvents = query.eventName
|
||||
? [query.eventName]
|
||||
@@ -64,40 +64,23 @@ export class AdminPartnerLogsService {
|
||||
return where;
|
||||
}
|
||||
|
||||
private async resolvePartnerIds(query: AdminPartnerLogsQueryDto): Promise<bigint[] | undefined> {
|
||||
private async resolvePartnerAccountIds(
|
||||
query: AdminPartnerLogsQueryDto,
|
||||
): Promise<bigint[] | undefined> {
|
||||
if (query.partnerAccountId) return [BigInt(query.partnerAccountId)];
|
||||
if (query.partnerId) return [BigInt(query.partnerId)];
|
||||
|
||||
const partnerWhere: Prisma.PartnerWhereInput = {};
|
||||
if (query.companyName) partnerWhere.companyName = { contains: query.companyName };
|
||||
const accountWhere: Prisma.PartnerAccountWhereInput = { isPrimary: 1 };
|
||||
if (query.companyName) accountWhere.companyName = { contains: query.companyName };
|
||||
if (query.phone) accountWhere.phone = { contains: query.phone };
|
||||
|
||||
if (query.partnerAccountId || query.phone) {
|
||||
const accountWhere: Prisma.PartnerAccountWhereInput = {};
|
||||
if (query.partnerAccountId) accountWhere.id = BigInt(query.partnerAccountId);
|
||||
if (query.phone) accountWhere.phone = { contains: query.phone };
|
||||
if (query.companyName || query.phone) {
|
||||
const accounts = await this.prisma.partnerAccount.findMany({
|
||||
where: accountWhere,
|
||||
select: { partnerId: true },
|
||||
take: 100,
|
||||
});
|
||||
if (accounts.length === 0) return [];
|
||||
const ids = [...new Set(accounts.map((a) => a.partnerId))];
|
||||
if (query.companyName) {
|
||||
const partners = await this.prisma.partner.findMany({
|
||||
where: { id: { in: ids }, ...partnerWhere },
|
||||
select: { id: true },
|
||||
});
|
||||
return partners.map((p) => p.id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
if (query.companyName) {
|
||||
const partners = await this.prisma.partner.findMany({
|
||||
where: partnerWhere,
|
||||
select: { id: true },
|
||||
take: 100,
|
||||
});
|
||||
return partners.map((p) => p.id);
|
||||
return accounts.map((a) => a.id);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -106,8 +89,7 @@ export class AdminPartnerLogsService {
|
||||
private async enrichRows(
|
||||
rows: Array<{
|
||||
id: bigint;
|
||||
partnerAccountId: bigint | null;
|
||||
partnerId: bigint;
|
||||
partnerAccountId: bigint;
|
||||
eventName: string;
|
||||
clientApp: string | null;
|
||||
refType: string | null;
|
||||
@@ -116,37 +98,25 @@ export class AdminPartnerLogsService {
|
||||
createdAt: Date;
|
||||
}>,
|
||||
) {
|
||||
const partnerIds = [...new Set(rows.map((r) => r.partnerId))];
|
||||
const accountIds = [...new Set(rows.map((r) => r.partnerAccountId).filter((id): id is bigint => id != null))];
|
||||
const accountIds = [...new Set(rows.map((r) => r.partnerAccountId))];
|
||||
const accounts = accountIds.length
|
||||
? await this.prisma.partnerAccount.findMany({
|
||||
where: { id: { in: accountIds } },
|
||||
select: { id: true, name: true, phone: true, companyName: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const [partners, accounts] = await Promise.all([
|
||||
partnerIds.length
|
||||
? this.prisma.partner.findMany({
|
||||
where: { id: { in: partnerIds } },
|
||||
select: { id: true, companyName: true },
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
accountIds.length
|
||||
? this.prisma.partnerAccount.findMany({
|
||||
where: { id: { in: accountIds } },
|
||||
select: { id: true, name: true, phone: true },
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const partnerMap = new Map(partners.map((p) => [p.id.toString(), p] as const));
|
||||
const accountMap = new Map(accounts.map((a) => [a.id.toString(), a] as const));
|
||||
|
||||
return rows.map((row) => {
|
||||
const partner = partnerMap.get(row.partnerId.toString());
|
||||
const account = row.partnerAccountId ? accountMap.get(row.partnerAccountId.toString()) : undefined;
|
||||
const account = accountMap.get(row.partnerAccountId.toString());
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
partnerId: row.partnerId.toString(),
|
||||
partnerAccountId: row.partnerAccountId?.toString() ?? null,
|
||||
partnerId: row.partnerAccountId.toString(),
|
||||
partnerAccountId: row.partnerAccountId.toString(),
|
||||
accountName: account?.name ?? null,
|
||||
accountPhone: account?.phone ?? null,
|
||||
companyName: partner?.companyName ?? null,
|
||||
companyName: account?.companyName ?? null,
|
||||
category: resolvePartnerLogCategory(row.eventName),
|
||||
eventName: row.eventName,
|
||||
clientApp: row.clientApp,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { Prisma, CityPartnerScopeType, CityPartnerStatus } from '@prisma/client';
|
||||
import { resolveMaxPartnerCommissionRate, validatePartnerCommissionRates } from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import type { AdminPartnerAccountsQueryDto, AdminPartnersQueryDto } from './dto/admin-query.dto';
|
||||
import type {
|
||||
CreatePartnerAccountDto,
|
||||
@@ -10,77 +12,36 @@ import type {
|
||||
UpdatePartnerDto,
|
||||
} from './dto/admin-mutate.dto';
|
||||
|
||||
const PRIMARY_WHERE = { isPrimary: 1 } as const;
|
||||
|
||||
function assertPartnerCommissionRates(
|
||||
city: { maxPartnerCommissionRate: Prisma.Decimal | number | null },
|
||||
orderCommissionRate: number,
|
||||
redeemCommissionRate: number,
|
||||
) {
|
||||
const maxRate = resolveMaxPartnerCommissionRate(
|
||||
city.maxPartnerCommissionRate != null ? Number(city.maxPartnerCommissionRate) : null,
|
||||
);
|
||||
const check = validatePartnerCommissionRates(orderCommissionRate, redeemCommissionRate, maxRate);
|
||||
if (!check.ok) throw new BadRequestException(check.message);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminPartnersService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
async listPartners(query: AdminPartnersQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.PartnerWhereInput = {};
|
||||
const where: Prisma.PartnerAccountWhereInput = { ...PRIMARY_WHERE };
|
||||
if (query.companyName) where.companyName = { contains: query.companyName };
|
||||
if (query.contactPhone) where.contactPhone = { contains: query.contactPhone };
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.partner.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
_count: { select: { stores: true, accounts: true, cities: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.partner.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
items: items.map((p) => ({
|
||||
...p,
|
||||
storeCount: p._count.stores,
|
||||
accountCount: p._count.accounts,
|
||||
cityCount: p._count.cities,
|
||||
_count: undefined,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detailPartner(id: bigint) {
|
||||
const partner = await this.prisma.partner.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
cities: { select: { id: true, code: true, name: true, status: true } },
|
||||
accounts: { select: { id: true, phone: true, name: true, isPrimary: true, status: true } },
|
||||
stores: { select: { id: true, name: true, status: true }, take: 10, orderBy: { createdAt: 'desc' } },
|
||||
_count: { select: { stores: true, accounts: true } },
|
||||
},
|
||||
});
|
||||
if (!partner) throw new NotFoundException('开城合伙人不存在');
|
||||
return serializeBigInt(partner);
|
||||
}
|
||||
|
||||
async createPartner(dto: CreatePartnerDto) {
|
||||
const partner = await this.prisma.partner.create({ data: dto });
|
||||
return serializeBigInt(partner);
|
||||
}
|
||||
|
||||
async updatePartner(id: bigint, dto: UpdatePartnerDto) {
|
||||
const partner = await this.prisma.partner.update({ where: { id }, data: dto });
|
||||
return serializeBigInt(partner);
|
||||
}
|
||||
|
||||
async listPartnerAccounts(query: AdminPartnerAccountsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.PartnerAccountWhereInput = {};
|
||||
if (query.phone) where.phone = { contains: query.phone };
|
||||
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
||||
if (query.status) where.status = query.status as Prisma.EnumAccountStatusFilter['equals'];
|
||||
if (query.isPrimary === '0' || query.isPrimary === '1') {
|
||||
where.isPrimary = Number(query.isPrimary);
|
||||
}
|
||||
if (query.cityId) where.cityId = BigInt(query.cityId);
|
||||
if (query.partnerId) where.id = BigInt(query.partnerId);
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.partnerAccount.findMany({
|
||||
@@ -89,8 +50,257 @@ export class AdminPartnersService {
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
partner: { select: { id: true, companyName: true } },
|
||||
parent: { select: { id: true, name: true, phone: true } },
|
||||
city: { select: { id: true, code: true, name: true, status: true, maxPartnerCommissionRate: true } },
|
||||
managedWarehouse: { select: { id: true, name: true } },
|
||||
children: {
|
||||
select: {
|
||||
id: true,
|
||||
phone: true,
|
||||
name: true,
|
||||
staffRole: true,
|
||||
permissions: true,
|
||||
status: true,
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
},
|
||||
_count: { select: { stores: true, children: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.partnerAccount.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
items: items.map((p) => ({
|
||||
id: p.id.toString(),
|
||||
companyName: p.companyName,
|
||||
contactPhone: p.contactPhone,
|
||||
phone: p.phone,
|
||||
name: p.name,
|
||||
cityId: p.cityId?.toString() ?? null,
|
||||
cityName: p.city?.name ?? null,
|
||||
maxPartnerCommissionRate:
|
||||
p.city?.maxPartnerCommissionRate != null ? Number(p.city.maxPartnerCommissionRate) : null,
|
||||
scopeType: p.scopeType,
|
||||
orderCommissionRate: Number(p.orderCommissionRate ?? 0),
|
||||
redeemCommissionRate: Number(p.redeemCommissionRate ?? 0.03),
|
||||
bindingStatus: p.bindingStatus,
|
||||
managedWarehouseId: p.managedWarehouseId?.toString() ?? null,
|
||||
managedWarehouseName: p.managedWarehouse?.name ?? null,
|
||||
storeCount: p._count.stores,
|
||||
accountCount: p._count.children + 1,
|
||||
children: p.children.map((c) => ({
|
||||
id: c.id.toString(),
|
||||
phone: c.phone,
|
||||
name: c.name,
|
||||
staffRole: c.staffRole,
|
||||
permissions: c.permissions,
|
||||
status: c.status,
|
||||
})),
|
||||
createdAt: p.createdAt,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detailPartner(id: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findFirst({
|
||||
where: { id, ...PRIMARY_WHERE },
|
||||
include: {
|
||||
city: { select: { id: true, code: true, name: true, status: true, maxPartnerCommissionRate: true } },
|
||||
managedWarehouse: { select: { id: true, name: true } },
|
||||
children: {
|
||||
where: { isPrimary: 0 },
|
||||
select: {
|
||||
id: true,
|
||||
phone: true,
|
||||
name: true,
|
||||
staffRole: true,
|
||||
permissions: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
},
|
||||
stores: { select: { id: true, name: true, status: true }, take: 10, orderBy: { createdAt: 'desc' } },
|
||||
_count: { select: { stores: true, children: true } },
|
||||
},
|
||||
});
|
||||
if (!account) throw new NotFoundException('开城合伙人不存在');
|
||||
return serializeBigInt({
|
||||
...this.partnerCityService.toDto({
|
||||
...account,
|
||||
city: account.city,
|
||||
}),
|
||||
contactPhone: account.contactPhone,
|
||||
address: account.address,
|
||||
bankAccountName: account.bankAccountName,
|
||||
bankAccountNo: account.bankAccountNo,
|
||||
bankBranch: account.bankBranch,
|
||||
managedWarehouseName: account.managedWarehouse?.name ?? null,
|
||||
accountCount: account._count.children + 1,
|
||||
maxPartnerCommissionRate:
|
||||
account.city?.maxPartnerCommissionRate != null
|
||||
? Number(account.city.maxPartnerCommissionRate)
|
||||
: null,
|
||||
children: account.children.map((c) => ({
|
||||
id: c.id.toString(),
|
||||
phone: c.phone,
|
||||
name: c.name,
|
||||
staffRole: c.staffRole,
|
||||
permissions: c.permissions,
|
||||
status: c.status,
|
||||
createdAt: c.createdAt,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async createPartner(dto: CreatePartnerDto) {
|
||||
const phone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
throw new BadRequestException('请输入正确的登录手机号');
|
||||
}
|
||||
const phoneTaken = await this.prisma.partnerAccount.findUnique({ where: { phone } });
|
||||
if (phoneTaken) throw new BadRequestException('该手机号已被使用');
|
||||
|
||||
const cityId = BigInt(dto.cityId);
|
||||
const city = await this.prisma.commonCity.findUnique({ where: { id: cityId } });
|
||||
if (!city) throw new NotFoundException('开城城市不存在');
|
||||
|
||||
await this.partnerCityService.validatePrimaryBinding(cityId, {
|
||||
scopeType: dto.scopeType as CityPartnerScopeType,
|
||||
districtCodes: dto.districtCodes,
|
||||
});
|
||||
|
||||
const orderCommissionRate = dto.orderCommissionRate ?? 0;
|
||||
const redeemCommissionRate = dto.redeemCommissionRate ?? 0.03;
|
||||
assertPartnerCommissionRates(city, orderCommissionRate, redeemCommissionRate);
|
||||
|
||||
const account = await this.prisma.partnerAccount.create({
|
||||
data: {
|
||||
phone,
|
||||
name: dto.name.trim(),
|
||||
isPrimary: 1,
|
||||
staffRole: 'PARTNER',
|
||||
status: 'ACTIVE',
|
||||
cityId,
|
||||
scopeType: dto.scopeType as CityPartnerScopeType,
|
||||
districtCodes:
|
||||
dto.scopeType === 'DISTRICT' ? (dto.districtCodes ?? []) : Prisma.JsonNull,
|
||||
orderCommissionRate: orderCommissionRate,
|
||||
redeemCommissionRate: redeemCommissionRate,
|
||||
bindingStatus: (dto.bindingStatus ?? 'ACTIVE') as CityPartnerStatus,
|
||||
companyName: dto.companyName.trim(),
|
||||
address: dto.address.trim(),
|
||||
contactPhone: dto.contactPhone?.trim() ?? phone,
|
||||
contractNo: dto.contractNo,
|
||||
bankAccountName: dto.bankAccountName,
|
||||
bankAccountNo: dto.bankAccountNo,
|
||||
bankBranch: dto.bankBranch,
|
||||
weeklyStoreTarget: dto.weeklyStoreTarget ?? 20,
|
||||
},
|
||||
include: { city: { select: { id: true, code: true, name: true } } },
|
||||
});
|
||||
|
||||
return serializeBigInt(this.partnerCityService.toDto(account));
|
||||
}
|
||||
|
||||
async updatePartner(id: bigint, dto: UpdatePartnerDto) {
|
||||
const existing = await this.prisma.partnerAccount.findFirst({
|
||||
where: { id, ...PRIMARY_WHERE },
|
||||
});
|
||||
if (!existing) throw new NotFoundException('开城合伙人不存在');
|
||||
|
||||
const city = await this.prisma.commonCity.findUniqueOrThrow({ where: { id: existing.cityId! } });
|
||||
const cityId = existing.cityId!;
|
||||
const scopeType = (dto.scopeType ?? existing.scopeType) as CityPartnerScopeType;
|
||||
const districtCodes =
|
||||
scopeType === 'DISTRICT'
|
||||
? dto.districtCodes ?? this.partnerCityService.parseDistrictCodes(existing.districtCodes)
|
||||
: null;
|
||||
|
||||
await this.partnerCityService.validatePrimaryBinding(
|
||||
cityId,
|
||||
{
|
||||
partnerAccountId: id.toString(),
|
||||
scopeType,
|
||||
districtCodes: districtCodes ?? undefined,
|
||||
},
|
||||
id,
|
||||
);
|
||||
|
||||
if (dto.phone !== undefined) {
|
||||
const phone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
throw new BadRequestException('请输入正确的登录手机号');
|
||||
}
|
||||
const phoneTaken = await this.prisma.partnerAccount.findUnique({ where: { phone } });
|
||||
if (phoneTaken && phoneTaken.id !== id) {
|
||||
throw new BadRequestException('该手机号已被使用');
|
||||
}
|
||||
}
|
||||
|
||||
const orderCommissionRate =
|
||||
dto.orderCommissionRate !== undefined
|
||||
? dto.orderCommissionRate
|
||||
: Number(existing.orderCommissionRate ?? 0);
|
||||
const redeemCommissionRate =
|
||||
dto.redeemCommissionRate !== undefined
|
||||
? dto.redeemCommissionRate
|
||||
: Number(existing.redeemCommissionRate ?? 0.03);
|
||||
if (dto.orderCommissionRate !== undefined || dto.redeemCommissionRate !== undefined) {
|
||||
assertPartnerCommissionRates(city, orderCommissionRate, redeemCommissionRate);
|
||||
}
|
||||
|
||||
const account = await this.prisma.partnerAccount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name.trim() } : {}),
|
||||
...(dto.phone !== undefined ? { phone: dto.phone.trim() } : {}),
|
||||
...(dto.companyName !== undefined ? { companyName: dto.companyName.trim() } : {}),
|
||||
...(dto.address !== undefined ? { address: dto.address.trim() } : {}),
|
||||
...(dto.contactPhone !== undefined ? { contactPhone: dto.contactPhone.trim() } : {}),
|
||||
...(dto.scopeType !== undefined ? { scopeType: dto.scopeType as CityPartnerScopeType } : {}),
|
||||
...(dto.scopeType !== undefined || dto.districtCodes !== undefined
|
||||
? {
|
||||
districtCodes:
|
||||
scopeType === 'DISTRICT'
|
||||
? ((districtCodes ?? []) as Prisma.InputJsonValue)
|
||||
: Prisma.JsonNull,
|
||||
}
|
||||
: {}),
|
||||
...(dto.orderCommissionRate !== undefined ? { orderCommissionRate: dto.orderCommissionRate } : {}),
|
||||
...(dto.redeemCommissionRate !== undefined ? { redeemCommissionRate: dto.redeemCommissionRate } : {}),
|
||||
...(dto.bindingStatus !== undefined ? { bindingStatus: dto.bindingStatus as CityPartnerStatus } : {}),
|
||||
...(dto.contractNo !== undefined ? { contractNo: dto.contractNo } : {}),
|
||||
...(dto.bankAccountName !== undefined ? { bankAccountName: dto.bankAccountName } : {}),
|
||||
...(dto.bankAccountNo !== undefined ? { bankAccountNo: dto.bankAccountNo } : {}),
|
||||
...(dto.bankBranch !== undefined ? { bankBranch: dto.bankBranch } : {}),
|
||||
...(dto.weeklyStoreTarget !== undefined ? { weeklyStoreTarget: dto.weeklyStoreTarget } : {}),
|
||||
...(dto.status !== undefined ? { status: dto.status as 'ACTIVE' | 'DISABLED' } : {}),
|
||||
},
|
||||
include: { city: { select: { id: true, code: true, name: true } } },
|
||||
});
|
||||
|
||||
return serializeBigInt(this.partnerCityService.toDto(account));
|
||||
}
|
||||
|
||||
async listPartnerAccounts(query: AdminPartnerAccountsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.PartnerAccountWhereInput = { isPrimary: 0 };
|
||||
if (query.phone) where.phone = { contains: query.phone };
|
||||
if (query.partnerId) where.parentAccountId = BigInt(query.partnerId);
|
||||
if (query.status) where.status = query.status as Prisma.EnumAccountStatusFilter['equals'];
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.partnerAccount.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
parent: { select: { id: true, name: true, phone: true, companyName: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.partnerAccount.count({ where }),
|
||||
@@ -98,14 +308,14 @@ export class AdminPartnersService {
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
}
|
||||
|
||||
async listPartnerAccountTree(partnerId?: bigint) {
|
||||
const where: Prisma.PartnerAccountWhereInput = {};
|
||||
if (partnerId) where.partnerId = partnerId;
|
||||
async listPartnerAccountTree(primaryAccountId?: bigint) {
|
||||
const where: Prisma.PartnerAccountWhereInput = primaryAccountId
|
||||
? { OR: [{ id: primaryAccountId }, { parentAccountId: primaryAccountId }] }
|
||||
: { isPrimary: 1 };
|
||||
|
||||
const accounts = await this.prisma.partnerAccount.findMany({
|
||||
where,
|
||||
orderBy: [{ isPrimary: 'desc' }, { createdAt: 'asc' }],
|
||||
include: { partner: { select: { id: true, companyName: true } } },
|
||||
});
|
||||
|
||||
type TreeNode = (typeof accounts)[number] & { children: TreeNode[] };
|
||||
@@ -122,7 +332,7 @@ export class AdminPartnersService {
|
||||
const parent = nodeMap.get(account.parentAccountId.toString());
|
||||
if (parent) parent.children.push(node);
|
||||
else roots.push(node);
|
||||
} else {
|
||||
} else if (account.isPrimary === 1) {
|
||||
roots.push(node);
|
||||
}
|
||||
}
|
||||
@@ -134,8 +344,9 @@ export class AdminPartnersService {
|
||||
status: node.status,
|
||||
isPrimary: node.isPrimary,
|
||||
staffRole: node.staffRole,
|
||||
permissions: node.permissions,
|
||||
parentAccountId: node.parentAccountId,
|
||||
partner: node.partner,
|
||||
companyName: node.companyName,
|
||||
createdAt: node.createdAt,
|
||||
lastLoginAt: node.lastLoginAt,
|
||||
children: node.children.length ? node.children.map(mapNode) : undefined,
|
||||
@@ -148,27 +359,21 @@ export class AdminPartnersService {
|
||||
const account = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
partner: {
|
||||
select: {
|
||||
id: true,
|
||||
companyName: true,
|
||||
contactPhone: true,
|
||||
address: true,
|
||||
},
|
||||
},
|
||||
parent: { select: { id: true, name: true, phone: true } },
|
||||
parent: { select: { id: true, name: true, phone: true, companyName: true } },
|
||||
},
|
||||
});
|
||||
if (!account) throw new NotFoundException('开城合伙人账号不存在');
|
||||
if (!account) throw new NotFoundException('合伙人账号不存在');
|
||||
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(id);
|
||||
const orderWhere = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
|
||||
const [bills, orders] = await Promise.all([
|
||||
this.prisma.partnerBill.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
where: { partnerAccountId: primary.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
}),
|
||||
this.prisma.order.findMany({
|
||||
where: { city: { partnerId: account.partnerId } },
|
||||
where: orderWhere,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
select: {
|
||||
@@ -182,10 +387,14 @@ export class AdminPartnersService {
|
||||
}),
|
||||
]);
|
||||
|
||||
return serializeBigInt({ ...account, bills, orders });
|
||||
return serializeBigInt({ ...account, primaryAccountId: primary.id, bills, orders });
|
||||
}
|
||||
|
||||
async createPartnerAccount(dto: CreatePartnerAccountDto) {
|
||||
if (!dto.parentAccountId) {
|
||||
throw new BadRequestException('请指定主账号 parentAccountId 创建子账号');
|
||||
}
|
||||
|
||||
const phone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
throw new BadRequestException('请输入正确的手机号码');
|
||||
@@ -193,53 +402,40 @@ export class AdminPartnersService {
|
||||
const phoneTaken = await this.prisma.partnerAccount.findUnique({ where: { phone } });
|
||||
if (phoneTaken) throw new BadRequestException('该手机号已被使用');
|
||||
|
||||
if (dto.parentAccountId) {
|
||||
const parent = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: BigInt(dto.parentAccountId) },
|
||||
});
|
||||
if (!parent) throw new BadRequestException('主账号不存在');
|
||||
if (parent.isPrimary !== 1) throw new BadRequestException('仅可向主账号添加子账号');
|
||||
if (dto.partnerId && dto.partnerId !== parent.partnerId.toString()) {
|
||||
throw new BadRequestException('开城合伙人与主账号不匹配');
|
||||
}
|
||||
|
||||
const account = await this.prisma.partnerAccount.create({
|
||||
data: {
|
||||
partnerId: parent.partnerId,
|
||||
phone,
|
||||
name: dto.name.trim(),
|
||||
staffRole: (dto.staffRole ?? 'INTERNAL') as 'PARTNER' | 'INTERNAL' | 'PROMOTER',
|
||||
isPrimary: 0,
|
||||
parentAccountId: parent.id,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
include: { partner: { select: { id: true, companyName: true } } },
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
const parent = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: BigInt(dto.parentAccountId) },
|
||||
});
|
||||
if (!parent) throw new BadRequestException('主账号不存在');
|
||||
if (parent.isPrimary !== 1) {
|
||||
throw new BadRequestException('仅可向主账号添加子账号,不支持多级子账号');
|
||||
}
|
||||
|
||||
const partner = await this.prisma.partner.findUnique({ where: { id: BigInt(dto.partnerId!) } });
|
||||
if (!partner) throw new BadRequestException('开城合伙人不存在');
|
||||
const account = await this.prisma.partnerAccount.create({
|
||||
data: {
|
||||
partnerId: partner.id,
|
||||
phone,
|
||||
name: dto.name.trim(),
|
||||
staffRole: dto.staffRole ? (dto.staffRole as 'PARTNER' | 'INTERNAL' | 'PROMOTER') : undefined,
|
||||
staffRole: (dto.staffRole ?? 'INTERNAL') as 'PARTNER' | 'INTERNAL' | 'PROMOTER',
|
||||
permissions: dto.permissions ?? undefined,
|
||||
isPrimary: 0,
|
||||
parentAccountId: parent.id,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
include: { partner: { select: { id: true, companyName: true } } },
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
}
|
||||
|
||||
async updatePartnerAccount(id: bigint, dto: UpdatePartnerAccountDto) {
|
||||
const existing = await this.prisma.partnerAccount.findUnique({ where: { id } });
|
||||
if (!existing) throw new NotFoundException('开城合伙人账号不存在');
|
||||
if (!existing) throw new NotFoundException('合伙人账号不存在');
|
||||
if (existing.isPrimary === 1) {
|
||||
throw new BadRequestException('请通过开城合伙人接口编辑主账号');
|
||||
}
|
||||
|
||||
const data: Prisma.PartnerAccountUpdateInput = {};
|
||||
if (dto.name !== undefined) data.name = dto.name;
|
||||
if (dto.status !== undefined) data.status = dto.status as 'ACTIVE' | 'DISABLED';
|
||||
if (dto.staffRole !== undefined) data.staffRole = dto.staffRole as 'PARTNER' | 'INTERNAL' | 'PROMOTER';
|
||||
if (dto.permissions !== undefined) data.permissions = dto.permissions;
|
||||
if (dto.phone !== undefined) {
|
||||
const phone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
@@ -258,7 +454,7 @@ export class AdminPartnersService {
|
||||
|
||||
async deletePartnerSubAccount(id: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findUnique({ where: { id } });
|
||||
if (!account) throw new NotFoundException('开城合伙人账号不存在');
|
||||
if (!account) throw new NotFoundException('合伙人账号不存在');
|
||||
if (!account.parentAccountId) {
|
||||
throw new BadRequestException('仅可删除子账号');
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export class AdminRedeemService {
|
||||
where: { id },
|
||||
include: {
|
||||
user: true,
|
||||
store: { include: { partner: { select: { id: true, companyName: true } } } },
|
||||
store: { include: { partnerAccount: { select: { id: true, companyName: true } } } },
|
||||
coupon: true,
|
||||
payout: true,
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { mapStoreCompat } from '../../common/compat/v31-compat';
|
||||
import type { AdminStoreAccountsQueryDto, AdminStoreMediaQueryDto, AdminStoresQueryDto } from './dto/admin-query.dto';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import type {
|
||||
CreateStoreAccountDto,
|
||||
CreateStoreDto,
|
||||
@@ -16,7 +17,10 @@ import type {
|
||||
|
||||
@Injectable()
|
||||
export class AdminStoresService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
async listStores(query: AdminStoresQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
@@ -25,7 +29,7 @@ export class AdminStoresService {
|
||||
if (query.name) where.name = { contains: query.name };
|
||||
if (query.status) where.status = query.status as Prisma.EnumStoreStatusFilter['equals'];
|
||||
if (query.cityId) where.cityId = BigInt(query.cityId);
|
||||
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
||||
if (query.partnerId) where.partnerAccountId = BigInt(query.partnerId);
|
||||
if (query.phone) where.phone = { contains: query.phone };
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
@@ -36,7 +40,7 @@ export class AdminStoresService {
|
||||
take: pageSize,
|
||||
include: {
|
||||
cityRef: { select: { id: true, name: true, code: true } },
|
||||
partner: { select: { id: true, companyName: true } },
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
account: { select: { id: true, phone: true, name: true, status: true } },
|
||||
coverResource: { select: { id: true, url: true } },
|
||||
},
|
||||
@@ -56,7 +60,7 @@ export class AdminStoresService {
|
||||
where: { id },
|
||||
include: {
|
||||
cityRef: true,
|
||||
partner: true,
|
||||
partnerAccount: true,
|
||||
category: true,
|
||||
account: true,
|
||||
coverResource: true,
|
||||
@@ -123,6 +127,7 @@ export class AdminStoresService {
|
||||
...(dto.intro !== undefined ? { intro: dto.intro } : {}),
|
||||
...(dto.address !== undefined ? { address: dto.address } : {}),
|
||||
...(dto.district !== undefined ? { district: dto.district } : {}),
|
||||
...(dto.settlementRate !== undefined ? { settlementRate: dto.settlementRate } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -162,18 +167,22 @@ export class AdminStoresService {
|
||||
});
|
||||
if (existingAccount) throw new BadRequestException('该手机号已绑定门店');
|
||||
|
||||
const partner = await this.prisma.partner.findUnique({ where: { id: BigInt(dto.partnerId) } });
|
||||
if (!partner) throw new BadRequestException('开城合伙人不存在');
|
||||
const partnerAccountId = BigInt(dto.partnerAccountId);
|
||||
const partnerAccount = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: partnerAccountId },
|
||||
});
|
||||
if (!partnerAccount || partnerAccount.isPrimary !== 1) {
|
||||
throw new BadRequestException('开城合伙人不存在');
|
||||
}
|
||||
const city = await this.prisma.commonCity.findUnique({ where: { id: BigInt(dto.cityId) } });
|
||||
if (!city) throw new BadRequestException('开城城市不存在');
|
||||
if (city.partnerId && city.partnerId !== partner.id) {
|
||||
throw new BadRequestException('开城城市与合伙人不匹配');
|
||||
}
|
||||
await this.partnerCityService.assertPartnerAccountBoundToCity(partnerAccountId, city.id);
|
||||
|
||||
const store = await this.prisma.store.create({
|
||||
data: {
|
||||
cityId: city.id,
|
||||
partnerId: partner.id,
|
||||
partnerAccountId,
|
||||
settlementRate: dto.settlementRate ?? 0.6,
|
||||
categoryId: dto.categoryId ? BigInt(dto.categoryId) : null,
|
||||
name: dto.name,
|
||||
phone: normalizedPhone,
|
||||
@@ -359,7 +368,7 @@ export class AdminStoresService {
|
||||
async detailStoreAccount(id: bigint) {
|
||||
const account = await this.prisma.storeAccount.findUnique({
|
||||
where: { id },
|
||||
include: { store: { include: { cityRef: true, partner: true } } },
|
||||
include: { store: { include: { cityRef: true, partnerAccount: true } } },
|
||||
});
|
||||
if (!account) throw new NotFoundException('门店账号不存在');
|
||||
return serializeBigInt(account);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BenefitService } from '../benefit/benefit.service';
|
||||
import { TradeService } from '../trade/trade.service';
|
||||
import { TicketService } from '../common/ticket.service';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import type { TicketListQueryDto } from '../common/dto/common-query.dto';
|
||||
|
||||
@Injectable()
|
||||
@@ -13,6 +14,7 @@ export class AdminTicketsService {
|
||||
private readonly ticketService: TicketService,
|
||||
private readonly tradeService: TradeService,
|
||||
private readonly benefitService: BenefitService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
list(query: TicketListQueryDto) {
|
||||
@@ -82,15 +84,9 @@ export class AdminTicketsService {
|
||||
}
|
||||
|
||||
async listPartnerReshipments(partnerAccountId: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: partnerAccountId },
|
||||
});
|
||||
const cities = await this.prisma.commonCity.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
select: { id: true },
|
||||
});
|
||||
const orderWhere = await this.partnerCityService.buildPartnerOrderWhere(partnerAccountId);
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: { cityId: { in: cities.map((c) => c.id) } },
|
||||
where: orderWhere,
|
||||
select: { id: true },
|
||||
});
|
||||
const orderIds = orders.map((o) => o.id);
|
||||
|
||||
@@ -213,11 +213,23 @@ export class AdminWechatBindingsService {
|
||||
|
||||
const accounts = await this.prisma.partnerAccount.findMany({
|
||||
where,
|
||||
include: { partner: { select: { id: true, companyName: true } } },
|
||||
select: {
|
||||
id: true,
|
||||
phone: true,
|
||||
name: true,
|
||||
wxOpenId: true,
|
||||
wxUnionId: true,
|
||||
lastLoginAt: true,
|
||||
status: true,
|
||||
isPrimary: true,
|
||||
parentAccountId: true,
|
||||
companyName: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const a of accounts) {
|
||||
if (!a.wxOpenId) continue;
|
||||
const refId = a.isPrimary === 1 ? a.id : (a.parentAccountId ?? a.id);
|
||||
rows.push({
|
||||
actorType: 'PARTNER',
|
||||
actorId: a.id,
|
||||
@@ -225,8 +237,8 @@ export class AdminWechatBindingsService {
|
||||
name: a.name,
|
||||
wxOpenId: a.wxOpenId,
|
||||
wxUnionId: a.wxUnionId,
|
||||
refId: a.partnerId,
|
||||
refLabel: a.partner.companyName,
|
||||
refId,
|
||||
refLabel: a.companyName,
|
||||
lastLoginAt: a.lastLoginAt,
|
||||
status: a.status,
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
Max,
|
||||
MinLength,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
@@ -22,7 +23,7 @@ export class UpdateStoreStatusDto {
|
||||
export class CreateStoreDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
partnerId: string;
|
||||
partnerAccountId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@@ -88,6 +89,11 @@ export class CreateStoreDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contractUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
settlementRate?: number;
|
||||
}
|
||||
|
||||
export class UpdateStoreDto {
|
||||
@@ -114,6 +120,11 @@ export class UpdateStoreDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
district?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
settlementRate?: number;
|
||||
}
|
||||
|
||||
export class CreateStoreAccountDto {
|
||||
@@ -145,6 +156,18 @@ export class UpdateStoreAccountDto {
|
||||
}
|
||||
|
||||
export class CreatePartnerDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
cityId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
companyName: string;
|
||||
@@ -153,9 +176,40 @@ export class CreatePartnerDto {
|
||||
@IsNotEmpty()
|
||||
address: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
contactPhone: string;
|
||||
contactPhone?: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['CITY_WIDE', 'DISTRICT'])
|
||||
scopeType: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
districtCodes?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
orderCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
redeemCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'PAUSED'])
|
||||
bindingStatus?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
managedWarehouseId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contractNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@@ -168,9 +222,21 @@ export class CreatePartnerDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankBranch?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
weeklyStoreTarget?: number;
|
||||
}
|
||||
|
||||
export class UpdatePartnerDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
companyName?: string;
|
||||
@@ -183,6 +249,38 @@ export class UpdatePartnerDto {
|
||||
@IsString()
|
||||
contactPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['CITY_WIDE', 'DISTRICT'])
|
||||
scopeType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
districtCodes?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
orderCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
redeemCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'PAUSED'])
|
||||
bindingStatus?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((_, v) => v !== null)
|
||||
@IsString()
|
||||
managedWarehouseId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contractNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankAccountName?: string;
|
||||
@@ -194,6 +292,14 @@ export class UpdatePartnerDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankBranch?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
weeklyStoreTarget?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class UpdatePartnerAccountDto {
|
||||
@@ -208,13 +314,21 @@ export class UpdatePartnerAccountDto {
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['PARTNER', 'INTERNAL', 'PROMOTER'])
|
||||
staffRole?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export class CreatePartnerAccountDto {
|
||||
@ValidateIf((o: CreatePartnerAccountDto) => !o.parentAccountId)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
partnerId?: string;
|
||||
parentAccountId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@@ -228,10 +342,10 @@ export class CreatePartnerAccountDto {
|
||||
@IsIn(['PARTNER', 'INTERNAL', 'PROMOTER'])
|
||||
staffRole?: string;
|
||||
|
||||
/** 主账号 ID;传入则创建子账号 */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentAccountId?: string;
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export class CreateCityDto {
|
||||
@@ -247,10 +361,6 @@ export class CreateCityDto {
|
||||
@IsNotEmpty()
|
||||
province: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['PENDING', 'ACTIVE', 'PAUSED'])
|
||||
status?: string;
|
||||
@@ -265,10 +375,6 @@ export class UpdateCityDto {
|
||||
@IsString()
|
||||
province?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['PENDING', 'ACTIVE', 'PAUSED'])
|
||||
status?: string;
|
||||
@@ -278,6 +384,127 @@ export class UpdateCityDto {
|
||||
|
||||
@IsOptional()
|
||||
crossMinQty?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
maxPartnerCommissionRate?: number;
|
||||
}
|
||||
|
||||
export class BindCityPartnerDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
partnerId: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['CITY_WIDE', 'DISTRICT'])
|
||||
scopeType: 'CITY_WIDE' | 'DISTRICT';
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
districtCodes?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
orderCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
redeemCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'PAUSED'])
|
||||
status?: 'ACTIVE' | 'PAUSED';
|
||||
}
|
||||
|
||||
export class UpdateCityPartnerDto {
|
||||
@IsOptional()
|
||||
@IsIn(['CITY_WIDE', 'DISTRICT'])
|
||||
scopeType?: 'CITY_WIDE' | 'DISTRICT';
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
districtCodes?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
orderCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
redeemCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'PAUSED'])
|
||||
status?: 'ACTIVE' | 'PAUSED';
|
||||
}
|
||||
|
||||
export class CreateCityWarehouseDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
address: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
contactName: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
contactPhone: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['HQ', 'PARTNER'])
|
||||
managerType: 'HQ' | 'PARTNER';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerAccountId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'PAUSED'])
|
||||
status?: 'ACTIVE' | 'PAUSED';
|
||||
}
|
||||
|
||||
export class UpdateCityWarehouseDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['HQ', 'PARTNER'])
|
||||
managerType?: 'HQ' | 'PARTNER';
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((_, v) => v !== null)
|
||||
@IsString()
|
||||
partnerAccountId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'PAUSED'])
|
||||
status?: 'ACTIVE' | 'PAUSED';
|
||||
}
|
||||
|
||||
export class CreateStoreMediaDto {
|
||||
|
||||
@@ -109,6 +109,36 @@ export class AdminPartnersQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cityId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerId?: string;
|
||||
}
|
||||
|
||||
export class AdminCityWarehousesQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cityId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
managerType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class AdminPartnerAccountsQueryDto extends PaginationQueryDto {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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 { AdminDashboardController } from './admin-dashboard.controller';
|
||||
@@ -12,6 +13,7 @@ import { AdminStoresService } from './admin-stores.service';
|
||||
import { AdminPartnersController, AdminPartnerAccountsController } from './admin-partners.controller';
|
||||
import { AdminPartnersService } from './admin-partners.service';
|
||||
import { AdminCitiesController } from './admin-cities.controller';
|
||||
import { AdminCityWarehousesController, AdminCityWarehouseMutationsController } from './admin-city-warehouses.controller';
|
||||
import { AdminCitiesService } from './admin-cities.service';
|
||||
import { AdminBenefitCouponsController, AdminBenefitLedgersController } from './admin-benefit.controller';
|
||||
import { AdminBenefitService } from './admin-benefit.service';
|
||||
@@ -50,7 +52,7 @@ import { AdminHqPermissionsController } from './admin-hq-permissions.controller'
|
||||
import { AdminHqPermissionsService } from './admin-hq-permissions.service';
|
||||
|
||||
@Module({
|
||||
imports: [IamModule, TradeModule, BenefitModule, CommonModule, IntegrationsModule, RedeemModule],
|
||||
imports: [CityScopeModule, IamModule, TradeModule, BenefitModule, CommonModule, IntegrationsModule, RedeemModule],
|
||||
controllers: [
|
||||
AdminDashboardController,
|
||||
AdminUsersController,
|
||||
@@ -61,6 +63,8 @@ import { AdminHqPermissionsService } from './admin-hq-permissions.service';
|
||||
AdminPartnersController,
|
||||
AdminPartnerAccountsController,
|
||||
AdminCitiesController,
|
||||
AdminCityWarehousesController,
|
||||
AdminCityWarehouseMutationsController,
|
||||
AdminBenefitCouponsController,
|
||||
AdminBenefitLedgersController,
|
||||
AdminRedeemRecordsController,
|
||||
@@ -104,5 +108,6 @@ import { AdminHqPermissionsService } from './admin-hq-permissions.service';
|
||||
AdminHqPermissionsService,
|
||||
SuperAdminGuard,
|
||||
],
|
||||
exports: [CityScopeModule],
|
||||
})
|
||||
export class OpsModule {}
|
||||
|
||||
Reference in New Issue
Block a user