发布商品,商品图片使用oss服务器地址

This commit is contained in:
2026-07-06 08:53:03 +08:00
parent a0466f023e
commit 5ba69eb935
60 changed files with 2776 additions and 157 deletions
@@ -1,7 +1,8 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
import { SettlementService } from './settlement.service';
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { PrismaService } from '../../common/prisma/prisma.module';
@Controller('partner/settlement')
@@ -15,6 +16,101 @@ export class SettlementController {
}
}
@Controller('shop/payouts')
@UseGuards(JwtAuthGuard)
export class ShopPayoutController {
constructor(private readonly settlementService: SettlementService) {}
@Get()
list(
@CurrentUser() user: AuthUser,
@Query('page') page = '1',
@Query('pageSize') pageSize = '20',
) {
return this.settlementService.listShopPayouts(user.actorId, Number(page), Number(pageSize));
}
}
@Controller('admin/store-payouts')
@UseGuards(HqAuthGuard)
export class AdminStorePayoutController {
constructor(private readonly settlementService: SettlementService) {}
@Get()
list(@Query() query: Record<string, string>) {
return this.settlementService.listAdminStorePayouts({
page: query.page ? Number(query.page) : 1,
pageSize: query.pageSize ? Number(query.pageSize) : 20,
status: query.status,
storeId: query.storeId,
});
}
@Get(':id')
detail(@Param('id') id: string) {
return this.settlementService.getAdminStorePayout(BigInt(id));
}
@Post(':id/confirm')
confirm(@Param('id') id: string, @Body() body: { paymentRef?: string; batchNo?: string; remark?: string }) {
return this.settlementService.confirmStorePayout(BigInt(id), body);
}
@Post('batch-confirm')
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')
@UseGuards(HqAuthGuard)
export class AdminPartnerBillController {
constructor(private readonly settlementService: SettlementService) {}
@Post('generate')
generate(@Body() body: { partnerId: string; year: number; month: number }) {
return this.settlementService.generatePartnerBill(body);
}
@Get()
list(@Query() query: Record<string, string>) {
return this.settlementService.listAdminPartnerBills({
page: query.page ? Number(query.page) : 1,
pageSize: query.pageSize ? Number(query.pageSize) : 20,
status: query.status,
partnerId: query.partnerId,
});
}
@Get('export')
export(@Query() query: Record<string, string>) {
return this.settlementService.exportPartnerBills({
partnerId: query.partnerId,
status: query.status,
});
}
@Get(':id')
detail(@Param('id') id: string) {
return this.settlementService.getAdminPartnerBill(BigInt(id));
}
@Post(':id/confirm')
confirm(@Param('id') id: string) {
return this.settlementService.confirmPartnerBill(BigInt(id));
}
@Post(':id/mark-paid')
markPaid(@Param('id') id: string, @Body() body: { paymentRef?: string }) {
return this.settlementService.markPartnerBillPaid(BigInt(id), body);
}
}
@Controller('partner/me')
@UseGuards(JwtAuthGuard)
export class PartnerMeController {
@@ -1,11 +1,23 @@
import { Module } from '@nestjs/common';
import { IamModule } from '../iam/iam.module';
import { SettlementService } from './settlement.service';
import { PartnerMeController, SettlementController } from './settlement.controller';
import {
AdminPartnerBillController,
AdminStorePayoutController,
PartnerMeController,
SettlementController,
ShopPayoutController,
} from './settlement.controller';
@Module({
imports: [IamModule],
controllers: [SettlementController, PartnerMeController],
controllers: [
SettlementController,
PartnerMeController,
ShopPayoutController,
AdminStorePayoutController,
AdminPartnerBillController,
],
providers: [SettlementService],
exports: [SettlementService],
})
@@ -1,7 +1,12 @@
import { Injectable } from '@nestjs/common';
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
function generateBillNo() {
return `PB${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
}
@Injectable()
export class SettlementService {
constructor(private readonly prisma: PrismaService) {}
@@ -29,6 +34,106 @@ export class SettlementService {
return serializeBigInt(payout);
}
async listShopPayouts(storeAccountId: bigint, page = 1, pageSize = 20) {
const account = await this.prisma.storeAccount.findUniqueOrThrow({
where: { id: storeAccountId },
});
const where = { storeId: account.storeId };
const [items, total] = await Promise.all([
this.prisma.storePayout.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
include: { redeemRecord: { select: { redeemNo: true, amount: true } } },
}),
this.prisma.storePayout.count({ where }),
]);
return serializeBigInt({ items, total, page, pageSize });
}
async listAdminStorePayouts(query: {
page?: number;
pageSize?: number;
status?: string;
storeId?: 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 [items, total] = await Promise.all([
this.prisma.storePayout.findMany({
where,
orderBy: { expectedPayAt: 'asc' },
skip: (page - 1) * pageSize,
take: pageSize,
include: {
store: { select: { id: true, name: true, cityName: true } },
redeemRecord: { select: { redeemNo: true, amount: true, userId: true } },
},
}),
this.prisma.storePayout.count({ where }),
]);
return serializeBigInt({ items, total, page, pageSize });
}
async getAdminStorePayout(id: bigint) {
const payout = await this.prisma.storePayout.findUnique({
where: { id },
include: {
store: true,
redeemRecord: { include: { user: { select: { userNo: true, phone: true } } } },
},
});
if (!payout) throw new NotFoundException('打款记录不存在');
return serializeBigInt(payout);
}
async confirmStorePayout(id: bigint, dto: { paymentRef?: string; batchNo?: string; remark?: string }) {
const payout = await this.prisma.storePayout.findUnique({ where: { id } });
if (!payout) throw new NotFoundException('打款记录不存在');
if (payout.status === 'PAID') throw new BadRequestException('已打款');
const updated = await this.prisma.storePayout.update({
where: { id },
data: {
status: 'PAID',
paidAt: new Date(),
batchNo: dto.batchNo ?? payout.batchNo,
},
});
await this.prisma.commonEvent.create({
data: {
eventType: 'HQ_OPERATION',
refType: 'STORE_PAYOUT',
refId: id,
actorType: 'HQ',
status: 'PAID',
param1: dto.paymentRef ?? '',
remark: dto.remark ?? '门店 T+1 打款确认',
},
});
return serializeBigInt(updated);
}
async batchConfirmStorePayouts(ids: string[], dto: { batchNo?: string }) {
const results: Array<{ id: string; ok: boolean; message?: string }> = [];
for (const id of ids) {
try {
await this.confirmStorePayout(BigInt(id), { batchNo: dto.batchNo });
results.push({ id, ok: true });
} catch (e) {
results.push({ id, ok: false, message: e instanceof Error ? e.message : '失败' });
}
}
return results;
}
async listPartnerBills(partnerAccountId: bigint) {
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
where: { id: partnerAccountId },
@@ -39,4 +144,203 @@ export class SettlementService {
});
return serializeBigInt(bills);
}
async listAdminPartnerBills(query: {
page?: number;
pageSize?: number;
status?: string;
partnerId?: string;
}) {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const where: Prisma.PartnerBillWhereInput = {};
if (query.status) where.status = query.status as Prisma.EnumPartnerBillStatusFilter['equals'];
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
const [items, total] = await Promise.all([
this.prisma.partnerBill.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
include: { partner: { select: { companyName: true } } },
}),
this.prisma.partnerBill.count({ where }),
]);
return serializeBigInt({ items, total, page, pageSize });
}
async getAdminPartnerBill(id: bigint) {
const bill = await this.prisma.partnerBill.findUnique({
where: { id },
include: { partner: true },
});
if (!bill) throw new NotFoundException('账单不存在');
return serializeBigInt(bill);
}
async generatePartnerBill(body: { partnerId: string; year: number; month: number }) {
const partnerId = BigInt(body.partnerId);
const periodStart = new Date(body.year, body.month - 1, 1);
const periodEnd = new Date(body.year, body.month, 0, 23, 59, 59, 999);
const existing = await this.prisma.partnerBill.findFirst({
where: {
partnerId,
periodStart,
status: { not: 'DRAFT' },
},
});
if (existing && existing.status !== 'DRAFT') {
throw new BadRequestException('该月账单已确认,不可重复生成');
}
const cities = await this.prisma.commonCity.findMany({
where: { partnerId },
include: { commissionRule: true },
});
const cityIds = cities.map((c) => c.id);
const defaultOrderRate = cities[0]?.commissionRule?.orderCommissionRate
? Number(cities[0].commissionRule.orderCommissionRate)
: 0.05;
const defaultRedeemRate = cities[0]?.commissionRule?.redeemCommissionRate
? Number(cities[0].commissionRule.redeemCommissionRate)
: 0.03;
const orders = await this.prisma.order.findMany({
where: {
cityId: { in: cityIds },
payStatus: 'PAID',
paidAt: { gte: periodStart, lte: periodEnd },
},
});
const orderCommission = orders.reduce(
(sum, o) => sum + Number(o.payAmount) * defaultOrderRate,
0,
);
const stores = await this.prisma.store.findMany({ where: { partnerId }, select: { id: true } });
const storeIds = stores.map((s) => s.id);
const redeems = await this.prisma.redeemRecord.findMany({
where: {
storeId: { in: storeIds },
createdAt: { gte: periodStart, lte: periodEnd },
},
});
const redeemCommission = redeems.reduce(
(sum, r) => sum + Number(r.amount) * defaultRedeemRate,
0,
);
const totalAmount = Math.round((orderCommission + redeemCommission) * 100) / 100;
const draft = await this.prisma.partnerBill.findFirst({
where: { partnerId, periodStart, status: 'DRAFT' },
});
const bill = draft
? await this.prisma.partnerBill.update({
where: { id: draft.id },
data: { orderCommission, redeemCommission, totalAmount, periodEnd },
})
: await this.prisma.partnerBill.create({
data: {
billNo: generateBillNo(),
partnerId,
periodStart,
periodEnd,
orderCommission,
redeemCommission,
totalAmount,
status: 'DRAFT',
},
});
return serializeBigInt(bill);
}
async confirmPartnerBill(id: bigint) {
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
if (!bill) throw new NotFoundException('账单不存在');
if (bill.status !== 'DRAFT') throw new BadRequestException('仅草稿可确认');
const updated = await this.prisma.partnerBill.update({
where: { id },
data: { status: 'CONFIRMED', confirmedAt: new Date() },
});
await this.prisma.commonEvent.create({
data: {
eventType: 'HQ_OPERATION',
refType: 'PARTNER_BILL',
refId: id,
actorType: 'HQ',
status: 'CONFIRMED',
amount1: Number(updated.totalAmount),
},
});
return serializeBigInt(updated);
}
async markPartnerBillPaid(id: bigint, dto: { paymentRef?: string }) {
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
if (!bill) throw new NotFoundException('账单不存在');
if (bill.status !== 'CONFIRMED') throw new BadRequestException('仅已确认账单可标记打款');
const updated = await this.prisma.partnerBill.update({
where: { id },
data: { status: 'PAID', paidAt: new Date() },
});
await this.prisma.commonEvent.create({
data: {
eventType: 'HQ_OPERATION',
refType: 'PARTNER_BILL',
refId: id,
actorType: 'HQ',
status: 'PAID',
param1: dto.paymentRef ?? '',
amount1: Number(updated.totalAmount),
},
});
return serializeBigInt(updated);
}
async exportPartnerBills(query: { partnerId?: string; status?: string }) {
const where: Prisma.PartnerBillWhereInput = {};
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
if (query.status) where.status = query.status as Prisma.EnumPartnerBillStatusFilter['equals'];
const bills = await this.prisma.partnerBill.findMany({
where,
include: { partner: { select: { companyName: true } } },
orderBy: { createdAt: 'desc' },
});
const header = 'billNo,partner,periodStart,periodEnd,orderCommission,redeemCommission,totalAmount,status';
const rows = bills.map((b) =>
[
b.billNo,
b.partner.companyName,
b.periodStart.toISOString().slice(0, 10),
b.periodEnd.toISOString().slice(0, 10),
Number(b.orderCommission),
Number(b.redeemCommission),
Number(b.totalAmount),
b.status,
].join(','),
);
return { csv: [header, ...rows].join('\n'), count: bills.length };
}
async scanDueStorePayouts() {
const now = new Date();
const due = await this.prisma.storePayout.findMany({
where: { status: 'PENDING', expectedPayAt: { lte: now } },
take: 100,
});
return serializeBigInt({ dueCount: due.length, items: due });
}
}