V4.0.9版本更新合伙人H5端调整

This commit is contained in:
2026-09-02 09:46:41 +08:00
parent af3c4b6f08
commit c905b3f930
33 changed files with 893 additions and 176 deletions
@@ -18,15 +18,20 @@ import {
parseShanghaiYmd,
pickPayoutsForWithdrawAmount,
previousShanghaiMonth,
previousShanghaiWeek,
resolveSettlementRate,
shanghaiBillPeriodYmds,
shanghaiMonthLastInstant,
shanghaiMonthRange,
shanghaiPeriodYmds,
shanghaiPeriodYmdsForWeek,
shanghaiT1DayWindow,
shanghaiWeekRange,
shanghaiWineryPeriodWindow,
shanghaiYearMonth,
shanghaiYmd,
startOfShanghaiDay,
nextPartnerBillIssueAt,
sumUnbilledPayoutAmount,
validateStoreWithdraw,
wineryIssueDateFromCompletedAt,
@@ -80,7 +85,39 @@ function paymentProofUrlsInput(urls?: string[]): Prisma.InputJsonValue | typeof
}
function withShanghaiPeriod<T extends { periodStart: Date; periodEnd: Date }>(row: T) {
return { ...row, ...shanghaiPeriodYmds(row.periodStart) };
return { ...row, ...shanghaiBillPeriodYmds(row.periodStart, row.periodEnd) };
}
function resolvePartnerWeekPeriod(weekStartYmd: string): {
periodStart: Date;
periodEnd: Date;
endExclusive: Date;
} {
const ymd = String(weekStartYmd || '').trim();
if (!/^\d{4}-\d{2}-\d{2}$/.test(ymd)) {
throw new BadRequestException('请提供周起始日 weekStartYmdYYYY-MM-DD');
}
const start = parseShanghaiYmd(ymd);
const week = shanghaiWeekRange(start);
if (shanghaiYmd(week.start) !== ymd) {
throw new BadRequestException('账期起始日须为周一');
}
return {
periodStart: week.start,
periodEnd: new Date(week.endExclusive.getTime() - 1),
endExclusive: week.endExclusive,
};
}
function partnerSettlementCycleDto(anchor = new Date()) {
const { start } = shanghaiWeekRange(anchor);
const ymds = shanghaiPeriodYmdsForWeek(start);
return {
periodStart: ymds.periodStart,
periodEnd: ymds.periodEnd,
nextIssueAt: nextPartnerBillIssueAt(anchor).toISOString(),
cycleLabel: '每周一 08:00 出账',
};
}
function toPartnerBillItemDto(row: {
@@ -1619,6 +1656,7 @@ export class SettlementService implements OnModuleInit {
where: {
partnerAccountId: primary.id,
status: { in: ['AWAITING_CONFIRM', 'UNPAID', 'PAID', 'REJECTED'] },
totalAmount: { gt: 0 },
},
orderBy: { createdAt: 'desc' },
});
@@ -1630,6 +1668,58 @@ export class SettlementService implements OnModuleInit {
return serializeBigInt(bills.map(withShanghaiPeriod));
}
getPartnerSettlementCycle(anchor = new Date()) {
return partnerSettlementCycleDto(anchor);
}
async getPartnerSettlementPreview(partnerAccountId: bigint) {
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
const { start } = shanghaiWeekRange();
const now = new Date();
const periodEnd = now.getTime() < start.getTime() ? start : now;
const redeemCommissionRate = Number(primary.redeemCommissionRate ?? 0.03);
const orders = await this.prisma.order.findMany({
where: {
partnerAccountIdAtPay: primary.id,
payStatus: 'PAID',
paidAt: { gte: start, lte: periodEnd },
},
select: { payAmount: true, orderCommissionRateAtPay: true },
});
const orderCommission = round2(
orders.reduce((sum, o) => {
const rate = o.orderCommissionRateAtPay != null ? Number(o.orderCommissionRateAtPay) : 0;
return sum + Number(o.payAmount) * rate;
}, 0),
);
const stores = await this.prisma.store.findMany({
where: { partnerAccountId: primary.id },
select: { id: true },
});
const storeIds = stores.map((s) => s.id);
const redeems = !storeIds.length
? []
: await this.prisma.redeemRecord.findMany({
where: {
storeId: { in: storeIds },
createdAt: { gte: start, lte: periodEnd },
},
select: { amount: true },
});
const redeemCommission = round2(
redeems.reduce((sum, r) => sum + Number(r.amount) * redeemCommissionRate, 0),
);
return {
...partnerSettlementCycleDto(),
prepaymentAmount: round2(orderCommission + redeemCommission),
orderCommissionEstimate: orderCommission,
redeemCommissionEstimate: redeemCommission,
};
}
async getPartnerBill(partnerAccountId: bigint, billId: bigint) {
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
const bill = await this.prisma.partnerBill.findFirst({
@@ -1637,6 +1727,7 @@ export class SettlementService implements OnModuleInit {
include: { items: { orderBy: { occurredAt: 'asc' } } },
});
if (!bill) throw new NotFoundException('账单不存在');
if (Number(bill.totalAmount) <= 0) throw new NotFoundException('账单不存在');
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
partnerAccountId: primary.id,
eventName: 'partner_bill_detail_view',
@@ -1659,6 +1750,7 @@ export class SettlementService implements OnModuleInit {
partnerId?: string;
year?: number;
month?: number;
weekStartYmd?: string;
}) {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
@@ -1712,18 +1804,22 @@ export class SettlementService implements OnModuleInit {
partnerId?: string;
year?: number;
month?: number;
weekStartYmd?: string;
}): Prisma.PartnerBillWhereInput {
const where: Prisma.PartnerBillWhereInput = {};
if (query.status) where.status = query.status as Prisma.EnumPartnerBillStatusFilter['equals'];
if (query.partnerId) where.partnerAccountId = BigInt(query.partnerId);
if (query.year && query.month) {
if (query.weekStartYmd) {
const { periodStart, endExclusive } = resolvePartnerWeekPeriod(query.weekStartYmd);
where.periodStart = { gte: periodStart, lt: endExclusive };
} else if (query.year && query.month) {
const { start, endExclusive } = shanghaiMonthRange(query.year, query.month);
where.periodStart = { gte: start, lt: endExclusive };
}
return where;
}
async generateAllPartnerBills(body: { year: number; month: number }) {
async generateAllPartnerBills(body: { weekStartYmd: string }) {
const partners = await this.prisma.partnerAccount.findMany({
where: { isPrimary: 1, status: 'ACTIVE' },
select: {
@@ -1752,8 +1848,7 @@ export class SettlementService implements OnModuleInit {
const bill = await this.generatePartnerBill(
{
partnerId: p.id.toString(),
year: body.year,
month: body.month,
weekStartYmd: body.weekStartYmd,
},
{ notify: false },
);
@@ -1781,10 +1876,7 @@ export class SettlementService implements OnModuleInit {
});
}
}
this.notifyPartnerBillDigest(
`${body.year}-${String(body.month).padStart(2, '0')}`,
created,
);
this.notifyPartnerBillDigest(body.weekStartYmd, created);
return {
total: partners.length,
success: results.filter((r) => r.ok).length,
@@ -1793,10 +1885,15 @@ export class SettlementService implements OnModuleInit {
};
}
/** 每月 1 日任务:生成上一自然账单 */
/** 每周一任务:生成上一自然账单 */
async generatePreviousWeekPartnerBills(anchor = new Date()) {
const prev = previousShanghaiWeek(anchor);
return this.generateAllPartnerBills({ weekStartYmd: shanghaiYmd(prev.start) });
}
/** @deprecated 月账任务已改为周账 */
async generatePreviousMonthPartnerBills(anchor = new Date()) {
const prev = previousShanghaiMonth(anchor);
return this.generateAllPartnerBills({ year: prev.year, month: prev.month });
return this.generatePreviousWeekPartnerBills(anchor);
}
async getAdminPartnerBill(id: bigint) {
@@ -1814,13 +1911,12 @@ export class SettlementService implements OnModuleInit {
}
async generatePartnerBill(
body: { partnerId: string; year: number; month: number },
body: { partnerId: string; weekStartYmd: string },
opts?: { notify?: boolean },
) {
const partnerAccountId = BigInt(body.partnerId);
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
const { start: periodStart, endExclusive } = shanghaiMonthRange(body.year, body.month);
const periodEnd = shanghaiMonthLastInstant(body.year, body.month);
const { periodStart, periodEnd, endExclusive } = resolvePartnerWeekPeriod(body.weekStartYmd);
const existing = await this.prisma.partnerBill.findFirst({
where: {
@@ -1829,7 +1925,7 @@ export class SettlementService implements OnModuleInit {
},
});
if (existing && existing.status !== 'PENDING_REVIEW') {
throw new BadRequestException('该账单已进入审核流程,不可重复生成');
throw new BadRequestException('该账期账单已进入审核流程,不可重复生成');
}
if (!primary.cityId) {
@@ -1952,7 +2048,7 @@ export class SettlementService implements OnModuleInit {
});
cityName = city?.name?.trim() || '—';
}
this.notifyPartnerBillDigest(`${body.year}-${String(body.month).padStart(2, '0')}`, [
this.notifyPartnerBillDigest(body.weekStartYmd, [
{
cityName,
partnerLabel: formatPartnerDashLabel(primary.companyName, primary.name),
@@ -1981,6 +2077,9 @@ export class SettlementService implements OnModuleInit {
async sendPartnerBill(id: bigint) {
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
if (!bill) throw new NotFoundException('账单不存在');
if (Number(bill.totalAmount) <= 0) {
throw new BadRequestException('零元账单不同步给合伙人');
}
if (bill.status !== 'PENDING_REVIEW' && bill.status !== 'REJECTED') {
throw new BadRequestException('仅待审核或已驳回账单可发送');
}
@@ -2129,6 +2228,7 @@ export class SettlementService implements OnModuleInit {
status?: string;
year?: number;
month?: number;
weekStartYmd?: string;
}) {
const where = this.buildPartnerBillWhere(query);
const bills = await this.prisma.partnerBill.findMany({
@@ -2172,8 +2272,8 @@ export class SettlementService implements OnModuleInit {
csvEscape(b.billNo),
csvEscape(b.partnerAccount.companyName ?? ''),
csvEscape(b.partnerAccount.phone ?? ''),
shanghaiPeriodYmds(b.periodStart).periodStart,
shanghaiPeriodYmds(b.periodStart).periodEnd,
shanghaiBillPeriodYmds(b.periodStart, b.periodEnd).periodStart,
shanghaiBillPeriodYmds(b.periodStart, b.periodEnd).periodEnd,
Number(b.orderCommission),
Number(b.redeemCommission),
Number(b.totalAmount),