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
@@ -76,6 +76,51 @@ function wineryDayWindow(anchor = new Date(), lagDays = WINERY_SETTLEMENT_LAG_DA
return { start, end, billDate: start };
}
function toPartnerBillItemDto(row: {
id: bigint;
kind: string;
refId: bigint;
refNo: string;
title: string | null;
extra: string | null;
baseAmount: Prisma.Decimal | number;
rate: Prisma.Decimal | number;
commission: Prisma.Decimal | number;
occurredAt: Date;
}) {
return {
id: row.id.toString(),
kind: row.kind,
refId: row.refId.toString(),
refNo: row.refNo,
title: row.title,
extra: row.extra,
baseAmount: Number(row.baseAmount),
rate: Number(row.rate),
commission: Number(row.commission),
occurredAt: row.occurredAt.toISOString(),
};
}
function splitPartnerBillItems(
items: Array<{
id: bigint;
kind: string;
refId: bigint;
refNo: string;
title: string | null;
extra: string | null;
baseAmount: Prisma.Decimal | number;
rate: Prisma.Decimal | number;
commission: Prisma.Decimal | number;
occurredAt: Date;
}>,
) {
const orderItems = items.filter((i) => i.kind === 'ORDER').map(toPartnerBillItemDto);
const redeemItems = items.filter((i) => i.kind === 'REDEEM').map(toPartnerBillItemDto);
return { orderItems, redeemItems };
}
function round2(n: number) {
return Math.round(n * 100) / 100;
}
@@ -1526,6 +1571,7 @@ export class SettlementService implements OnModuleInit {
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
const bill = await this.prisma.partnerBill.findFirst({
where: { id: billId, partnerAccountId: primary.id },
include: { items: { orderBy: { occurredAt: 'asc' } } },
});
if (!bill) throw new NotFoundException('账单不存在');
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
@@ -1535,7 +1581,12 @@ export class SettlementService implements OnModuleInit {
refId: billId,
extraJson: { billId: billId.toString(), status: bill.status },
});
return serializeBigInt(bill);
const { items, ...header } = bill;
return serializeBigInt({
...header,
partnerId: header.partnerAccountId.toString(),
...splitPartnerBillItems(items),
});
}
async listAdminPartnerBills(query: {
@@ -1689,10 +1740,15 @@ export class SettlementService implements OnModuleInit {
async getAdminPartnerBill(id: bigint) {
const bill = await this.prisma.partnerBill.findUnique({
where: { id },
include: { partnerAccount: true },
include: { partnerAccount: true, items: { orderBy: { occurredAt: 'asc' } } },
});
if (!bill) throw new NotFoundException('账单不存在');
return serializeBigInt(bill);
const { items, ...header } = bill;
return serializeBigInt({
...header,
partnerId: header.partnerAccountId.toString(),
...splitPartnerBillItems(items),
});
}
async generatePartnerBill(
@@ -1717,30 +1773,39 @@ export class SettlementService implements OnModuleInit {
throw new BadRequestException('合伙人未绑定开城城市');
}
const orderCommissionRate = Number(primary.orderCommissionRate ?? 0);
const redeemCommissionRate = Number(primary.redeemCommissionRate ?? 0.03);
const orders = await this.prisma.order.findMany({
where: {
cityId: primary.cityId,
partnerAccountIdAtPay: primary.id,
payStatus: 'PAID',
paidAt: { gte: periodStart, lte: periodEnd },
},
orderBy: { paidAt: 'asc' },
});
const orderCommission = orders.reduce((sum, o) => {
if (o.partnerAccountIdAtPay) {
if (o.partnerAccountIdAtPay !== primary.id) return sum;
const rate = o.orderCommissionRateAtPay != null ? Number(o.orderCommissionRateAtPay) : 0;
return sum + Number(o.payAmount) * rate;
}
return sum + Number(o.payAmount) * orderCommissionRate;
}, 0);
const orderRows = orders.map((o) => {
const rate = o.orderCommissionRateAtPay != null ? Number(o.orderCommissionRateAtPay) : 0;
const baseAmount = Number(o.payAmount);
return {
kind: 'ORDER' as const,
refId: o.id,
refNo: o.orderNo,
title: o.productName,
extra: `×${o.quantity}`,
baseAmount,
rate,
commission: round2(baseAmount * rate),
occurredAt: o.paidAt ?? o.createdAt,
};
});
const orderCommission = orderRows.reduce((sum, r) => sum + r.commission, 0);
const stores = await this.prisma.store.findMany({
where: { partnerAccountId: primary.id },
select: { id: true },
select: { id: true, name: true },
});
const storeIds = stores.map((s) => s.id);
const storeNameById = new Map(stores.map((s) => [s.id.toString(), s.name]));
const redeems = !storeIds.length
? []
: await this.prisma.redeemRecord.findMany({
@@ -1748,37 +1813,71 @@ export class SettlementService implements OnModuleInit {
storeId: { in: storeIds },
createdAt: { gte: periodStart, lte: periodEnd },
},
orderBy: { createdAt: 'asc' },
});
const redeemCommission = redeems.reduce(
(sum, r) => sum + Number(r.amount) * redeemCommissionRate,
0,
);
const redeemRows = redeems.map((r) => {
const baseAmount = Number(r.amount);
return {
kind: 'REDEEM' as const,
refId: r.id,
refNo: r.redeemNo,
title: storeNameById.get(r.storeId.toString()) ?? '门店',
extra: null as string | null,
baseAmount,
rate: redeemCommissionRate,
commission: round2(baseAmount * redeemCommissionRate),
occurredAt: r.createdAt,
};
});
const redeemCommission = redeemRows.reduce((sum, r) => sum + r.commission, 0);
const totalAmount = round2(orderCommission + redeemCommission);
const itemRows = [...orderRows, ...redeemRows];
const bill = existing
? await this.prisma.partnerBill.update({
where: { id: existing.id },
data: {
orderCommission: round2(orderCommission),
redeemCommission: round2(redeemCommission),
totalAmount,
periodEnd,
status: 'PENDING_REVIEW',
},
})
: await this.prisma.partnerBill.create({
data: {
billNo: generateBillNo('PB'),
partnerAccountId: primary.id,
periodStart,
periodEnd,
orderCommission: round2(orderCommission),
redeemCommission: round2(redeemCommission),
totalAmount,
status: 'PENDING_REVIEW',
},
const bill = await this.prisma.$transaction(async (tx) => {
const header = existing
? await tx.partnerBill.update({
where: { id: existing.id },
data: {
orderCommission: round2(orderCommission),
redeemCommission: round2(redeemCommission),
totalAmount,
periodEnd,
status: 'PENDING_REVIEW',
},
})
: await tx.partnerBill.create({
data: {
billNo: generateBillNo('PB'),
partnerAccountId: primary.id,
periodStart,
periodEnd,
orderCommission: round2(orderCommission),
redeemCommission: round2(redeemCommission),
totalAmount,
status: 'PENDING_REVIEW',
},
});
await tx.partnerBillItem.deleteMany({ where: { partnerBillId: header.id } });
if (itemRows.length > 0) {
await tx.partnerBillItem.createMany({
data: itemRows.map((row) => ({
partnerBillId: header.id,
kind: row.kind,
refId: row.refId,
refNo: row.refNo,
title: row.title,
extra: row.extra,
baseAmount: row.baseAmount,
rate: new Prisma.Decimal(row.rate.toFixed(4)),
commission: row.commission,
occurredAt: row.occurredAt,
})),
});
}
return header;
});
if (opts?.notify !== false) {
let cityName = '—';
@@ -1980,6 +2079,7 @@ export class SettlementService implements OnModuleInit {
bankBranch: true,
},
},
items: { orderBy: { occurredAt: 'asc' } },
},
orderBy: { periodStart: 'desc' },
});
@@ -2024,7 +2124,25 @@ export class SettlementService implements OnModuleInit {
csvEscape(b.rejectReason ?? ''),
].join(','),
);
return { csv: `\uFEFF${[header, ...rows].join('\n')}`, count: bills.length };
const itemHeader = ['账单号', '类型', '单号', '标题', '备注', '基数', '费率', '佣金', '发生时间'].join(',');
const itemRows = bills.flatMap((b) =>
b.items.map((it) =>
[
csvEscape(b.billNo),
it.kind === 'ORDER' ? '酒订单' : '核销',
csvEscape(it.refNo),
csvEscape(it.title ?? ''),
csvEscape(it.extra ?? ''),
Number(it.baseAmount),
Number(it.rate),
Number(it.commission),
it.occurredAt.toISOString().slice(0, 19).replace('T', ' '),
].join(','),
),
);
const csv = ['账单汇总', header, ...rows, '', '酒订单/核销明细', itemHeader, ...itemRows].join('\n');
return { csv: `\uFEFF${csv}`, count: bills.length };
}
// ─── Winery bills ────────────────────────────────────