feat(assoc): v4.0.1 合伙人关联码、分佣账单与 H5 用户管理

订单佣金只认关联用户;合伙人备注写入独立表;H5 增加用户管理与首页统计。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 14:35:39 +08:00
parent 3b669f7e38
commit 9c8d5f2cad
125 changed files with 6355 additions and 1436 deletions
@@ -4,6 +4,7 @@ import { resolveMaxPartnerCommissionRate, validatePartnerCommissionRates } from
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import { PartnerCityService } from '../city-scope/partner-city.service';
import { PartnerAssocService } from '../store/partner-assoc.service';
import type { AdminPartnerAccountsQueryDto, AdminPartnersQueryDto } from './dto/admin-query.dto';
import type {
CreatePartnerAccountDto,
@@ -26,11 +27,24 @@ function assertPartnerCommissionRates(
if (!check.ok) throw new BadRequestException(check.message);
}
function toCommissionDecimal(rate: number): Prisma.Decimal {
return new Prisma.Decimal(Number(rate).toFixed(4));
}
/** 选填字符串:undefined 不改;null / 空白写成 null */
function trimOptionalText(value: string | null | undefined): string | null | undefined {
if (value === undefined) return undefined;
if (value == null) return null;
const t = String(value).trim();
return t.length ? t : null;
}
@Injectable()
export class AdminPartnersService {
constructor(
private readonly prisma: PrismaService,
private readonly partnerCityService: PartnerCityService,
private readonly partnerAssocService: PartnerAssocService,
) {}
async listPartners(query: AdminPartnersQueryDto) {
@@ -64,7 +78,7 @@ export class AdminPartnersService {
},
orderBy: { createdAt: 'asc' },
},
_count: { select: { stores: true, children: true } },
_count: { select: { stores: true, children: true, assocUsers: true } },
},
}),
this.prisma.partnerAccount.count({ where }),
@@ -88,6 +102,7 @@ export class AdminPartnersService {
managedWarehouseId: p.managedWarehouseId?.toString() ?? null,
managedWarehouseName: p.managedWarehouse?.name ?? null,
storeCount: p._count.stores,
assocUserCount: p._count.assocUsers,
accountCount: p._count.children + 1,
children: p.children.map((c) => ({
id: c.id.toString(),
@@ -125,7 +140,7 @@ export class AdminPartnersService {
orderBy: { createdAt: 'asc' },
},
stores: { select: { id: true, name: true, status: true }, take: 10, orderBy: { createdAt: 'desc' } },
_count: { select: { stores: true, children: true } },
_count: { select: { stores: true, children: true, assocUsers: true } },
},
});
if (!account) throw new NotFoundException('开城合伙人不存在');
@@ -140,6 +155,8 @@ export class AdminPartnersService {
bankAccountNo: account.bankAccountNo,
bankBranch: account.bankBranch,
managedWarehouseName: account.managedWarehouse?.name ?? null,
storeCount: account._count.stores,
assocUserCount: account._count.assocUsers,
accountCount: account._count.children + 1,
maxPartnerCommissionRate:
account.city?.maxPartnerCommissionRate != null
@@ -189,8 +206,8 @@ export class AdminPartnersService {
scopeType: dto.scopeType as CityPartnerScopeType,
districtCodes:
dto.scopeType === 'DISTRICT' ? (dto.districtCodes ?? []) : Prisma.JsonNull,
orderCommissionRate: orderCommissionRate,
redeemCommissionRate: redeemCommissionRate,
orderCommissionRate: toCommissionDecimal(orderCommissionRate),
redeemCommissionRate: toCommissionDecimal(redeemCommissionRate),
bindingStatus: (dto.bindingStatus ?? 'ACTIVE') as CityPartnerStatus,
companyName: dto.companyName?.trim() || null,
address: dto.address?.trim() || null,
@@ -204,7 +221,13 @@ export class AdminPartnersService {
include: { city: { select: { id: true, code: true, name: true } } },
});
return serializeBigInt(this.partnerCityService.toDto(account));
const dtoOut = serializeBigInt(this.partnerCityService.toDto(account));
try {
await this.partnerAssocService.ensureQrcode(account.id);
} catch {
/* 补码失败不挡创建 */
}
return dtoOut;
}
async updatePartner(id: bigint, dto: UpdatePartnerDto) {
@@ -221,17 +244,26 @@ export class AdminPartnersService {
? dto.districtCodes ?? this.partnerCityService.parseDistrictCodes(existing.districtCodes)
: null;
await this.partnerCityService.validatePrimaryBinding(
cityId,
{
partnerAccountId: id.toString(),
scopeType,
districtCodes: districtCodes ?? undefined,
},
id,
);
const existingDistricts = this.partnerCityService.parseDistrictCodes(existing.districtCodes);
const nextDistrictsSorted = [...(districtCodes ?? [])].sort();
const existingDistrictsSorted = [...(existingDistricts ?? [])].sort();
const scopeActuallyChanged =
(dto.scopeType !== undefined && dto.scopeType !== existing.scopeType) ||
(dto.districtCodes !== undefined &&
JSON.stringify(nextDistrictsSorted) !== JSON.stringify(existingDistrictsSorted));
if (scopeActuallyChanged) {
await this.partnerCityService.validatePrimaryBinding(
cityId,
{
partnerAccountId: id.toString(),
scopeType,
districtCodes: districtCodes ?? undefined,
},
id,
);
}
if (dto.phone !== undefined) {
if (dto.phone !== undefined && dto.phone != null) {
const phone = dto.phone.trim();
if (!/^1[3-9]\d{9}$/.test(phone)) {
throw new BadRequestException('请输入正确的登录手机号');
@@ -254,18 +286,22 @@ export class AdminPartnersService {
assertPartnerCommissionRates(city, orderCommissionRate, redeemCommissionRate);
}
const phoneChanged =
dto.phone !== undefined && dto.phone.trim() !== existing.phone;
const nextPhone = dto.phone != null ? dto.phone.trim() : undefined;
const phoneChanged = nextPhone !== undefined && nextPhone !== existing.phone;
const nextName = dto.name != null ? dto.name.trim() : undefined;
const nextCompanyName = trimOptionalText(dto.companyName);
const nextAddress = trimOptionalText(dto.address);
const nextContactPhone = trimOptionalText(dto.contactPhone);
const account = await this.prisma.partnerAccount.update({
where: { id },
data: {
...(dto.name !== undefined ? { name: dto.name.trim() } : {}),
...(dto.phone !== undefined ? { phone: dto.phone.trim() } : {}),
...(nextName !== undefined ? { name: nextName } : {}),
...(nextPhone !== undefined ? { phone: nextPhone } : {}),
...(phoneChanged ? { wxOpenId: null, wxUnionId: null } : {}),
...(dto.companyName !== undefined ? { companyName: dto.companyName.trim() } : {}),
...(dto.address !== undefined ? { address: dto.address.trim() } : {}),
...(dto.contactPhone !== undefined ? { contactPhone: dto.contactPhone.trim() } : {}),
...(nextCompanyName !== undefined ? { companyName: nextCompanyName } : {}),
...(nextAddress !== undefined ? { address: nextAddress } : {}),
...(nextContactPhone !== undefined ? { contactPhone: nextContactPhone } : {}),
...(dto.scopeType !== undefined ? { scopeType: dto.scopeType as CityPartnerScopeType } : {}),
...(dto.scopeType !== undefined || dto.districtCodes !== undefined
? {
@@ -275,8 +311,12 @@ export class AdminPartnersService {
: Prisma.JsonNull,
}
: {}),
...(dto.orderCommissionRate !== undefined ? { orderCommissionRate: dto.orderCommissionRate } : {}),
...(dto.redeemCommissionRate !== undefined ? { redeemCommissionRate: dto.redeemCommissionRate } : {}),
...(dto.orderCommissionRate !== undefined
? { orderCommissionRate: toCommissionDecimal(dto.orderCommissionRate) }
: {}),
...(dto.redeemCommissionRate !== undefined
? { redeemCommissionRate: toCommissionDecimal(dto.redeemCommissionRate) }
: {}),
...(dto.bindingStatus !== undefined ? { bindingStatus: dto.bindingStatus as CityPartnerStatus } : {}),
...(dto.contractNo !== undefined ? { contractNo: dto.contractNo } : {}),
...(dto.bankAccountName !== undefined ? { bankAccountName: dto.bankAccountName } : {}),