webadmin增加财务模块
This commit is contained in:
@@ -59,9 +59,49 @@ export class AdminStorePayoutController {
|
||||
pageSize: query.pageSize ? Number(query.pageSize) : 20,
|
||||
status: query.status,
|
||||
storeId: query.storeId,
|
||||
dateFrom: query.dateFrom,
|
||||
dateTo: query.dateTo,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('daily')
|
||||
daily(@Query() query: Record<string, string>) {
|
||||
return this.settlementService.listAdminStoreDailyBills({
|
||||
page: query.page ? Number(query.page) : 1,
|
||||
pageSize: query.pageSize ? Number(query.pageSize) : 50,
|
||||
status: query.status,
|
||||
storeId: query.storeId,
|
||||
dateFrom: query.dateFrom,
|
||||
dateTo: query.dateTo,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('export')
|
||||
export(@Query() query: Record<string, string>) {
|
||||
return this.settlementService.exportAdminStorePayouts({
|
||||
status: query.status,
|
||||
storeId: query.storeId,
|
||||
dateFrom: query.dateFrom,
|
||||
dateTo: query.dateTo,
|
||||
});
|
||||
}
|
||||
|
||||
@Post('batch-confirm')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.STORE_PAYOUT_BATCH_CONFIRM,
|
||||
refType: 'STORE_PAYOUT',
|
||||
batch: true,
|
||||
includeBody: true,
|
||||
})
|
||||
batchConfirm(@Body() body: { ids: string[]; batchNo?: string }) {
|
||||
return this.settlementService.batchConfirmStorePayouts(body.ids ?? [], body);
|
||||
}
|
||||
|
||||
@Post('scan-due')
|
||||
scanDue() {
|
||||
return this.settlementService.scanDueStorePayouts();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.settlementService.getAdminStorePayout(BigInt(id));
|
||||
@@ -77,22 +117,6 @@ export class AdminStorePayoutController {
|
||||
confirm(@Param('id') id: string, @Body() body: { paymentRef?: string; batchNo?: string; remark?: string }) {
|
||||
return this.settlementService.confirmStorePayout(BigInt(id), body);
|
||||
}
|
||||
|
||||
@Post('batch-confirm')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.STORE_PAYOUT_BATCH_CONFIRM,
|
||||
refType: 'STORE_PAYOUT',
|
||||
batch: true,
|
||||
includeBody: true,
|
||||
})
|
||||
batchConfirm(@Body() body: { ids: string[]; batchNo?: string }) {
|
||||
return this.settlementService.batchConfirmStorePayouts(body.ids ?? [], body);
|
||||
}
|
||||
|
||||
@Post('scan-due')
|
||||
scanDue() {
|
||||
return this.settlementService.scanDueStorePayouts();
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('admin/partner-bills')
|
||||
@@ -111,6 +135,17 @@ export class AdminPartnerBillController {
|
||||
return this.settlementService.generatePartnerBill(body);
|
||||
}
|
||||
|
||||
@Post('generate-all')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.PARTNER_BILL_GENERATE,
|
||||
refType: 'PARTNER_BILL',
|
||||
batch: true,
|
||||
includeBody: true,
|
||||
})
|
||||
generateAll(@Body() body: { year: number; month: number }) {
|
||||
return this.settlementService.generateAllPartnerBills(body);
|
||||
}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: Record<string, string>) {
|
||||
return this.settlementService.listAdminPartnerBills({
|
||||
@@ -118,6 +153,8 @@ export class AdminPartnerBillController {
|
||||
pageSize: query.pageSize ? Number(query.pageSize) : 20,
|
||||
status: query.status,
|
||||
partnerId: query.partnerId,
|
||||
year: query.year ? Number(query.year) : undefined,
|
||||
month: query.month ? Number(query.month) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,6 +163,8 @@ export class AdminPartnerBillController {
|
||||
return this.settlementService.exportPartnerBills({
|
||||
partnerId: query.partnerId,
|
||||
status: query.status,
|
||||
year: query.year ? Number(query.year) : undefined,
|
||||
month: query.month ? Number(query.month) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,6 +195,32 @@ export class AdminPartnerBillController {
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('admin/winery-bills')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminWineryBillController {
|
||||
constructor(private readonly settlementService: SettlementService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: Record<string, string>) {
|
||||
return this.settlementService.listAdminWineryBills({
|
||||
page: query.page ? Number(query.page) : 1,
|
||||
pageSize: query.pageSize ? Number(query.pageSize) : 20,
|
||||
year: query.year ? Number(query.year) : undefined,
|
||||
month: query.month ? Number(query.month) : undefined,
|
||||
deliveryType: query.deliveryType,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('export')
|
||||
export(@Query() query: Record<string, string>) {
|
||||
return this.settlementService.exportAdminWineryBills({
|
||||
year: query.year ? Number(query.year) : undefined,
|
||||
month: query.month ? Number(query.month) : undefined,
|
||||
deliveryType: query.deliveryType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('partner/me')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class PartnerMeController {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SettlementService } from './settlement.service';
|
||||
import {
|
||||
AdminPartnerBillController,
|
||||
AdminStorePayoutController,
|
||||
AdminWineryBillController,
|
||||
PartnerMeController,
|
||||
SettlementController,
|
||||
ShopPayoutController,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
ShopPayoutController,
|
||||
AdminStorePayoutController,
|
||||
AdminPartnerBillController,
|
||||
AdminWineryBillController,
|
||||
],
|
||||
providers: [SettlementService],
|
||||
exports: [SettlementService],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { WINERY_SETTLEMENT_RATE } from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { AnalyticsService } from '../analytics/analytics.service';
|
||||
@@ -9,6 +10,11 @@ function generateBillNo() {
|
||||
return `PB${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
|
||||
}
|
||||
|
||||
function csvEscape(value: string) {
|
||||
if (/[",\n\r]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
|
||||
return value;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SettlementService {
|
||||
constructor(
|
||||
@@ -77,27 +83,192 @@ export class SettlementService {
|
||||
pageSize?: number;
|
||||
status?: string;
|
||||
storeId?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.StorePayoutWhereInput = {};
|
||||
if (query.status) where.status = query.status as Prisma.EnumStorePayoutStatusFilter['equals'];
|
||||
if (query.storeId) where.storeId = BigInt(query.storeId);
|
||||
const where = this.buildStorePayoutWhere(query);
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
const [items, total, aggregates] = await Promise.all([
|
||||
this.prisma.storePayout.findMany({
|
||||
where,
|
||||
orderBy: { expectedPayAt: 'asc' },
|
||||
orderBy: [{ createdAt: 'desc' }, { expectedPayAt: 'asc' }],
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
store: { select: { id: true, name: true, cityName: true, phone: true } },
|
||||
redeemRecord: { select: { redeemNo: true, amount: true, userId: true } },
|
||||
redeemRecord: { select: { redeemNo: true, amount: true, userId: true, createdAt: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.storePayout.count({ where }),
|
||||
this.prisma.storePayout.aggregate({
|
||||
where,
|
||||
_sum: { redeemAmount: true, payoutAmount: true },
|
||||
_count: true,
|
||||
}),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
|
||||
const summary = {
|
||||
count: aggregates._count,
|
||||
redeemAmount: Number(aggregates._sum.redeemAmount ?? 0),
|
||||
payoutAmount: Number(aggregates._sum.payoutAmount ?? 0),
|
||||
totalAmount: Number(aggregates._sum.payoutAmount ?? 0),
|
||||
};
|
||||
|
||||
return serializeBigInt({ items, total, page, pageSize, summary });
|
||||
}
|
||||
|
||||
/** 门店账单按日汇总(T+1:按核销日归集) */
|
||||
async listAdminStoreDailyBills(query: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
status?: string;
|
||||
storeId?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 50;
|
||||
const where = this.buildStorePayoutWhere(query);
|
||||
|
||||
const payouts = await this.prisma.storePayout.findMany({
|
||||
where,
|
||||
include: {
|
||||
store: { select: { id: true, name: true, cityName: true, phone: true } },
|
||||
redeemRecord: { select: { createdAt: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
type DailyAgg = {
|
||||
billDate: string;
|
||||
storeId: string;
|
||||
storeName: string;
|
||||
storePhone?: string;
|
||||
cityName?: string;
|
||||
redeemCount: number;
|
||||
redeemAmount: number;
|
||||
settlementRate: number;
|
||||
payoutAmount: number;
|
||||
pendingCount: number;
|
||||
paidCount: number;
|
||||
};
|
||||
|
||||
const map = new Map<string, DailyAgg>();
|
||||
for (const p of payouts) {
|
||||
const billDate = (p.redeemRecord?.createdAt ?? p.createdAt).toISOString().slice(0, 10);
|
||||
const storeId = p.storeId.toString();
|
||||
const key = `${billDate}:${storeId}`;
|
||||
const existing = map.get(key);
|
||||
if (!existing) {
|
||||
map.set(key, {
|
||||
billDate,
|
||||
storeId,
|
||||
storeName: p.store?.name ?? '—',
|
||||
storePhone: p.store?.phone ?? undefined,
|
||||
cityName: p.store?.cityName ?? undefined,
|
||||
redeemCount: 1,
|
||||
redeemAmount: Number(p.redeemAmount),
|
||||
settlementRate: Number(p.settlementRate),
|
||||
payoutAmount: Number(p.payoutAmount),
|
||||
pendingCount: p.status === 'PENDING' ? 1 : 0,
|
||||
paidCount: p.status === 'PAID' ? 1 : 0,
|
||||
});
|
||||
} else {
|
||||
existing.redeemCount += 1;
|
||||
existing.redeemAmount += Number(p.redeemAmount);
|
||||
existing.payoutAmount += Number(p.payoutAmount);
|
||||
if (p.status === 'PENDING') existing.pendingCount += 1;
|
||||
if (p.status === 'PAID') existing.paidCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const all = Array.from(map.values()).sort((a, b) => {
|
||||
if (a.billDate === b.billDate) return a.storeName.localeCompare(b.storeName, 'zh-CN');
|
||||
return b.billDate.localeCompare(a.billDate);
|
||||
});
|
||||
|
||||
const summary = {
|
||||
count: all.length,
|
||||
redeemAmount: Math.round(all.reduce((s, r) => s + r.redeemAmount, 0) * 100) / 100,
|
||||
payoutAmount: Math.round(all.reduce((s, r) => s + r.payoutAmount, 0) * 100) / 100,
|
||||
totalAmount: Math.round(all.reduce((s, r) => s + r.payoutAmount, 0) * 100) / 100,
|
||||
};
|
||||
|
||||
const start = (page - 1) * pageSize;
|
||||
const items = all.slice(start, start + pageSize).map((row) => ({
|
||||
...row,
|
||||
redeemAmount: Math.round(row.redeemAmount * 100) / 100,
|
||||
payoutAmount: Math.round(row.payoutAmount * 100) / 100,
|
||||
}));
|
||||
|
||||
return { items, total: all.length, page, pageSize, summary };
|
||||
}
|
||||
|
||||
async exportAdminStorePayouts(query: {
|
||||
status?: string;
|
||||
storeId?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}) {
|
||||
const where = this.buildStorePayoutWhere(query);
|
||||
const payouts = await this.prisma.storePayout.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
store: { select: { name: true, phone: true, cityName: true } },
|
||||
redeemRecord: { select: { redeemNo: true, createdAt: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const header = [
|
||||
'核销日期',
|
||||
'门店',
|
||||
'登录手机',
|
||||
'城市',
|
||||
'核销单号',
|
||||
'核销面额',
|
||||
'结算比例',
|
||||
'应付金额',
|
||||
'状态',
|
||||
'预计打款',
|
||||
'实际打款',
|
||||
].join(',');
|
||||
const rows = payouts.map((p) =>
|
||||
[
|
||||
(p.redeemRecord?.createdAt ?? p.createdAt).toISOString().slice(0, 10),
|
||||
csvEscape(p.store?.name ?? ''),
|
||||
csvEscape(p.store?.phone ?? ''),
|
||||
csvEscape(p.store?.cityName ?? ''),
|
||||
csvEscape(p.redeemRecord?.redeemNo ?? ''),
|
||||
Number(p.redeemAmount),
|
||||
`${Math.round(Number(p.settlementRate) * 100)}%`,
|
||||
Number(p.payoutAmount),
|
||||
p.status,
|
||||
p.expectedPayAt.toISOString().slice(0, 10),
|
||||
p.paidAt ? p.paidAt.toISOString().slice(0, 10) : '',
|
||||
].join(','),
|
||||
);
|
||||
return { csv: `\uFEFF${[header, ...rows].join('\n')}`, count: payouts.length };
|
||||
}
|
||||
|
||||
private buildStorePayoutWhere(query: {
|
||||
status?: string;
|
||||
storeId?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}): Prisma.StorePayoutWhereInput {
|
||||
const where: Prisma.StorePayoutWhereInput = {};
|
||||
if (query.status) where.status = query.status as Prisma.EnumStorePayoutStatusFilter['equals'];
|
||||
if (query.storeId) where.storeId = BigInt(query.storeId);
|
||||
if (query.dateFrom || query.dateTo) {
|
||||
const createdAt: Prisma.DateTimeFilter = {};
|
||||
if (query.dateFrom) createdAt.gte = new Date(`${query.dateFrom}T00:00:00.000`);
|
||||
if (query.dateTo) createdAt.lte = new Date(`${query.dateTo}T23:59:59.999`);
|
||||
where.createdAt = createdAt;
|
||||
}
|
||||
return where;
|
||||
}
|
||||
|
||||
async getAdminStorePayout(id: bigint) {
|
||||
@@ -172,24 +343,95 @@ export class SettlementService {
|
||||
pageSize?: number;
|
||||
status?: string;
|
||||
partnerId?: string;
|
||||
year?: number;
|
||||
month?: number;
|
||||
}) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where = this.buildPartnerBillWhere(query);
|
||||
|
||||
const [rawItems, total, aggregates] = await Promise.all([
|
||||
this.prisma.partnerBill.findMany({
|
||||
where,
|
||||
orderBy: { periodStart: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
partnerAccount: {
|
||||
select: { id: true, companyName: true, name: true, phone: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
this.prisma.partnerBill.count({ where }),
|
||||
this.prisma.partnerBill.aggregate({
|
||||
where,
|
||||
_sum: { orderCommission: true, redeemCommission: true, totalAmount: true },
|
||||
_count: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const items = rawItems.map((b) => ({
|
||||
...b,
|
||||
partner: b.partnerAccount,
|
||||
}));
|
||||
|
||||
const summary = {
|
||||
count: aggregates._count,
|
||||
orderCommission: Number(aggregates._sum.orderCommission ?? 0),
|
||||
redeemCommission: Number(aggregates._sum.redeemCommission ?? 0),
|
||||
totalAmount: Number(aggregates._sum.totalAmount ?? 0),
|
||||
};
|
||||
|
||||
return serializeBigInt({ items, total, page, pageSize, summary });
|
||||
}
|
||||
|
||||
private buildPartnerBillWhere(query: {
|
||||
status?: string;
|
||||
partnerId?: string;
|
||||
year?: number;
|
||||
month?: number;
|
||||
}): 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) {
|
||||
const periodStart = new Date(query.year, query.month - 1, 1);
|
||||
const periodEnd = new Date(query.year, query.month, 0, 23, 59, 59, 999);
|
||||
where.periodStart = { gte: periodStart, lte: periodEnd };
|
||||
}
|
||||
return where;
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.partnerBill.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: { partnerAccount: { select: { companyName: true } } },
|
||||
}),
|
||||
this.prisma.partnerBill.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
/** 为所有主合伙人生成指定自然月账单 */
|
||||
async generateAllPartnerBills(body: { year: number; month: number }) {
|
||||
const partners = await this.prisma.partnerAccount.findMany({
|
||||
where: { isPrimary: 1, status: 'ACTIVE' },
|
||||
select: { id: true, companyName: true },
|
||||
});
|
||||
const results: Array<{ partnerId: string; companyName: string; ok: boolean; message?: string }> = [];
|
||||
for (const p of partners) {
|
||||
try {
|
||||
await this.generatePartnerBill({
|
||||
partnerId: p.id.toString(),
|
||||
year: body.year,
|
||||
month: body.month,
|
||||
});
|
||||
results.push({ partnerId: p.id.toString(), companyName: p.companyName ?? '', ok: true });
|
||||
} catch (e) {
|
||||
results.push({
|
||||
partnerId: p.id.toString(),
|
||||
companyName: p.companyName ?? '',
|
||||
ok: false,
|
||||
message: e instanceof Error ? e.message : '失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
total: partners.length,
|
||||
success: results.filter((r) => r.ok).length,
|
||||
failed: results.filter((r) => !r.ok).length,
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
async getAdminPartnerBill(id: bigint) {
|
||||
@@ -310,31 +552,170 @@ export class SettlementService {
|
||||
return serializeBigInt(updated);
|
||||
}
|
||||
|
||||
async exportPartnerBills(query: { partnerId?: string; status?: string }) {
|
||||
const where: Prisma.PartnerBillWhereInput = {};
|
||||
if (query.partnerId) where.partnerAccountId = BigInt(query.partnerId);
|
||||
if (query.status) where.status = query.status as Prisma.EnumPartnerBillStatusFilter['equals'];
|
||||
|
||||
async exportPartnerBills(query: {
|
||||
partnerId?: string;
|
||||
status?: string;
|
||||
year?: number;
|
||||
month?: number;
|
||||
}) {
|
||||
const where = this.buildPartnerBillWhere(query);
|
||||
const bills = await this.prisma.partnerBill.findMany({
|
||||
where,
|
||||
include: { partnerAccount: { select: { companyName: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: { partnerAccount: { select: { companyName: true, phone: true } } },
|
||||
orderBy: { periodStart: 'desc' },
|
||||
});
|
||||
|
||||
const header = 'billNo,partner,periodStart,periodEnd,orderCommission,redeemCommission,totalAmount,status';
|
||||
const header = [
|
||||
'账单号',
|
||||
'合伙人',
|
||||
'登录手机',
|
||||
'账期起',
|
||||
'账期止',
|
||||
'酒单佣金',
|
||||
'核销佣金',
|
||||
'合计应付',
|
||||
'状态',
|
||||
'确认时间',
|
||||
'打款时间',
|
||||
].join(',');
|
||||
const rows = bills.map((b) =>
|
||||
[
|
||||
b.billNo,
|
||||
b.partnerAccount.companyName,
|
||||
csvEscape(b.billNo),
|
||||
csvEscape(b.partnerAccount.companyName ?? ''),
|
||||
csvEscape(b.partnerAccount.phone ?? ''),
|
||||
b.periodStart.toISOString().slice(0, 10),
|
||||
b.periodEnd.toISOString().slice(0, 10),
|
||||
Number(b.orderCommission),
|
||||
Number(b.redeemCommission),
|
||||
Number(b.totalAmount),
|
||||
b.status,
|
||||
b.confirmedAt ? b.confirmedAt.toISOString().slice(0, 10) : '',
|
||||
b.paidAt ? b.paidAt.toISOString().slice(0, 10) : '',
|
||||
].join(','),
|
||||
);
|
||||
return { csv: [header, ...rows].join('\n'), count: bills.length };
|
||||
return { csv: `\uFEFF${[header, ...rows].join('\n')}`, count: bills.length };
|
||||
}
|
||||
|
||||
/** 酒厂账单:T+30,按自然月列出已付酒单,应付 = 实付 × WINERY_SETTLEMENT_RATE */
|
||||
async listAdminWineryBills(query: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
year?: number;
|
||||
month?: number;
|
||||
deliveryType?: string;
|
||||
}) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where = this.buildWineryOrderWhere(query);
|
||||
|
||||
const [orders, total, aggregates] = await Promise.all([
|
||||
this.prisma.order.findMany({
|
||||
where,
|
||||
orderBy: { paidAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: { city: { select: { name: true } } },
|
||||
}),
|
||||
this.prisma.order.count({ where }),
|
||||
this.prisma.order.aggregate({
|
||||
where,
|
||||
_sum: { payAmount: true },
|
||||
_count: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const rate = WINERY_SETTLEMENT_RATE;
|
||||
const items = orders.map((o) => {
|
||||
const payAmount = Number(o.payAmount);
|
||||
const wineryAmount = Math.round(payAmount * rate * 100) / 100;
|
||||
return {
|
||||
orderId: o.id.toString(),
|
||||
orderNo: o.orderNo,
|
||||
deliveryType: o.deliveryType,
|
||||
cityName: o.city?.name,
|
||||
payAmount,
|
||||
wineryRate: rate,
|
||||
wineryAmount,
|
||||
paidAt: o.paidAt?.toISOString() ?? '',
|
||||
receiverCity: o.receiverCity,
|
||||
productName: o.productName,
|
||||
quantity: o.quantity,
|
||||
};
|
||||
});
|
||||
|
||||
const orderAmount = Number(aggregates._sum.payAmount ?? 0);
|
||||
const summary = {
|
||||
count: aggregates._count,
|
||||
orderAmount,
|
||||
wineryAmount: Math.round(orderAmount * rate * 100) / 100,
|
||||
totalAmount: Math.round(orderAmount * rate * 100) / 100,
|
||||
};
|
||||
|
||||
return { items, total, page, pageSize, summary };
|
||||
}
|
||||
|
||||
async exportAdminWineryBills(query: {
|
||||
year?: number;
|
||||
month?: number;
|
||||
deliveryType?: string;
|
||||
}) {
|
||||
const where = this.buildWineryOrderWhere(query);
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where,
|
||||
orderBy: { paidAt: 'desc' },
|
||||
include: { city: { select: { name: true } } },
|
||||
});
|
||||
const rate = WINERY_SETTLEMENT_RATE;
|
||||
const header = [
|
||||
'订单号',
|
||||
'配送类型',
|
||||
'开城城市',
|
||||
'收货城市',
|
||||
'商品',
|
||||
'数量',
|
||||
'实付金额',
|
||||
'酒厂比例',
|
||||
'酒厂应付',
|
||||
'支付时间',
|
||||
].join(',');
|
||||
const rows = orders.map((o) => {
|
||||
const payAmount = Number(o.payAmount);
|
||||
return [
|
||||
csvEscape(o.orderNo),
|
||||
o.deliveryType === 'LOCAL' ? '同城' : o.deliveryType === 'CROSS_CITY' ? '跨城' : o.deliveryType,
|
||||
csvEscape(o.city?.name ?? ''),
|
||||
csvEscape(o.receiverCity ?? ''),
|
||||
csvEscape(o.productName ?? ''),
|
||||
o.quantity,
|
||||
payAmount,
|
||||
`${Math.round(rate * 100)}%`,
|
||||
Math.round(payAmount * rate * 100) / 100,
|
||||
o.paidAt ? o.paidAt.toISOString().slice(0, 19).replace('T', ' ') : '',
|
||||
].join(',');
|
||||
});
|
||||
return { csv: `\uFEFF${[header, ...rows].join('\n')}`, count: orders.length };
|
||||
}
|
||||
|
||||
private buildWineryOrderWhere(query: {
|
||||
year?: number;
|
||||
month?: number;
|
||||
deliveryType?: string;
|
||||
}): Prisma.OrderWhereInput {
|
||||
const where: Prisma.OrderWhereInput = {
|
||||
payStatus: 'PAID',
|
||||
deliveryType: { in: ['LOCAL', 'CROSS_CITY'] },
|
||||
};
|
||||
if (query.deliveryType === 'LOCAL' || query.deliveryType === 'CROSS_CITY') {
|
||||
where.deliveryType = query.deliveryType;
|
||||
}
|
||||
if (query.year && query.month) {
|
||||
const start = new Date(query.year, query.month - 1, 1);
|
||||
const end = new Date(query.year, query.month, 0, 23, 59, 59, 999);
|
||||
where.paidAt = { gte: start, lte: end };
|
||||
} else {
|
||||
where.paidAt = { not: null };
|
||||
}
|
||||
return where;
|
||||
}
|
||||
|
||||
async scanDueStorePayouts() {
|
||||
|
||||
Reference in New Issue
Block a user