fix;提交、
This commit is contained in:
@@ -478,23 +478,293 @@ export class TradeService {
|
||||
}
|
||||
|
||||
async createRefundRequest(userId: bigint, orderId: bigint, remark?: string) {
|
||||
const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } });
|
||||
if (!order) throw new NotFoundException('订单不存在');
|
||||
if (!['PENDING_SHIP', 'PENDING_RECEIVE', 'COMPLETED', 'OUT_WAREHOUSE', 'SHIPPING'].includes(order.status)) {
|
||||
throw new BadRequestException('当前订单不可申请退款');
|
||||
}
|
||||
await this.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: { status: 'REFUNDING', payStatus: 'REFUNDING' },
|
||||
});
|
||||
return this.ticketService.create({
|
||||
return this.createAfterSaleTicket(userId, orderId, {
|
||||
ticketType: 'REFUND',
|
||||
refType: 'ORDER',
|
||||
refId: orderId.toString(),
|
||||
remark: remark ?? '用户申请退款',
|
||||
});
|
||||
}
|
||||
|
||||
async createAfterSaleTicket(
|
||||
userId: bigint,
|
||||
orderId: bigint,
|
||||
body: { ticketType: string; remark?: string; evidenceUrls?: string[] },
|
||||
) {
|
||||
const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } });
|
||||
if (!order) throw new NotFoundException('订单不存在');
|
||||
if (order.status === 'PENDING_PAY' || order.status === 'CANCELLED') {
|
||||
throw new BadRequestException('当前订单不可申请售后');
|
||||
}
|
||||
if (['REFUNDING', 'REFUNDED'].includes(order.status)) {
|
||||
throw new BadRequestException('订单已在退款流程中');
|
||||
}
|
||||
|
||||
const pending = await this.prisma.commonTicket.findFirst({
|
||||
where: {
|
||||
ticketType: body.ticketType as never,
|
||||
refType: 'ORDER',
|
||||
refId: orderId,
|
||||
status: { in: ['PENDING', 'OPEN'] },
|
||||
},
|
||||
});
|
||||
if (pending) throw new BadRequestException('该类型售后工单已在处理中');
|
||||
|
||||
const evidenceUrls = (body.evidenceUrls ?? []).filter((u) => typeof u === 'string' && u.trim());
|
||||
return this.ticketService.create({
|
||||
ticketType: body.ticketType,
|
||||
refType: 'ORDER',
|
||||
refId: orderId.toString(),
|
||||
remark: body.remark ?? '',
|
||||
extraJson: evidenceUrls.length ? { evidenceUrls } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async listAfterSaleTickets(userId: bigint, page = 1, pageSize = 20) {
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: { userId },
|
||||
select: { id: true, orderNo: true },
|
||||
});
|
||||
const orderIds = orders.map((o) => o.id);
|
||||
const orderNoMap = new Map(orders.map((o) => [o.id.toString(), o.orderNo]));
|
||||
if (!orderIds.length) {
|
||||
return serializeBigInt({ items: [], total: 0, page, pageSize });
|
||||
}
|
||||
|
||||
const where = {
|
||||
refType: 'ORDER',
|
||||
refId: { in: orderIds },
|
||||
ticketType: { in: ['REFUND', 'RESHIPMENT', 'DAMAGE_RETURN', 'RETURN_REFUND'] as never[] },
|
||||
};
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.commonTicket.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.commonTicket.count({ where }),
|
||||
]);
|
||||
|
||||
return serializeBigInt({
|
||||
items: items.map((t) => ({
|
||||
...t,
|
||||
orderNo: orderNoMap.get(t.refId.toString()) ?? null,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async getAfterSaleTicket(userId: bigint, ticketId: bigint) {
|
||||
const ticket = await this.prisma.commonTicket.findUnique({ where: { id: ticketId } });
|
||||
if (!ticket || ticket.refType !== 'ORDER') throw new NotFoundException('工单不存在');
|
||||
const order = await this.prisma.order.findFirst({
|
||||
where: { id: ticket.refId, userId },
|
||||
select: { id: true, orderNo: true, status: true },
|
||||
});
|
||||
if (!order) throw new NotFoundException('工单不存在');
|
||||
return serializeBigInt({ ...ticket, orderNo: order.orderNo, orderStatus: order.status });
|
||||
}
|
||||
|
||||
private generateInvoiceNo() {
|
||||
return `INV${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
|
||||
}
|
||||
|
||||
async createInvoice(
|
||||
userId: bigint,
|
||||
orderId: bigint,
|
||||
body: {
|
||||
titleType: string;
|
||||
invoiceKind: string;
|
||||
titleName: string;
|
||||
taxNo?: string;
|
||||
addressPhone?: string;
|
||||
bankAccount?: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
remark?: string;
|
||||
},
|
||||
) {
|
||||
const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } });
|
||||
if (!order) throw new NotFoundException('订单不存在');
|
||||
if (order.status !== 'COMPLETED') {
|
||||
throw new BadRequestException('仅已完成订单可申请发票');
|
||||
}
|
||||
|
||||
const existing = await this.prisma.userInvoice.findFirst({
|
||||
where: { orderId, status: { in: ['PENDING', 'ISSUED'] } },
|
||||
});
|
||||
if (existing) throw new BadRequestException('该订单已有进行中或已开具的发票申请');
|
||||
|
||||
if (body.titleType === 'ENTERPRISE' && !body.taxNo?.trim()) {
|
||||
throw new BadRequestException('企业抬头须填写税号');
|
||||
}
|
||||
if (body.invoiceKind === 'SPECIAL') {
|
||||
if (body.titleType !== 'ENTERPRISE') {
|
||||
throw new BadRequestException('专用发票仅支持企业抬头');
|
||||
}
|
||||
if (!body.taxNo?.trim() || !body.addressPhone?.trim() || !body.bankAccount?.trim()) {
|
||||
throw new BadRequestException('专用发票须填写税号、地址电话与开户行账号');
|
||||
}
|
||||
}
|
||||
|
||||
const invoice = await this.prisma.userInvoice.create({
|
||||
data: {
|
||||
invoiceNo: this.generateInvoiceNo(),
|
||||
orderId,
|
||||
userId,
|
||||
titleType: body.titleType as never,
|
||||
invoiceKind: body.invoiceKind as never,
|
||||
titleName: body.titleName.trim(),
|
||||
taxNo: body.taxNo?.trim() || null,
|
||||
addressPhone: body.addressPhone?.trim() || null,
|
||||
bankAccount: body.bankAccount?.trim() || null,
|
||||
email: body.email.trim(),
|
||||
phone: body.phone.trim(),
|
||||
remark: body.remark?.trim() || null,
|
||||
},
|
||||
});
|
||||
return serializeBigInt({ ...invoice, orderNo: order.orderNo });
|
||||
}
|
||||
|
||||
async listInvoices(userId: bigint, page = 1, pageSize = 20) {
|
||||
const where = { userId };
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.userInvoice.findMany({
|
||||
where,
|
||||
include: { order: { select: { orderNo: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.userInvoice.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
items: items.map((inv) => ({
|
||||
...inv,
|
||||
orderNo: inv.order.orderNo,
|
||||
order: undefined,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async getInvoice(userId: bigint, invoiceId: bigint) {
|
||||
const invoice = await this.prisma.userInvoice.findFirst({
|
||||
where: { id: invoiceId, userId },
|
||||
include: { order: { select: { orderNo: true, payAmount: true } } },
|
||||
});
|
||||
if (!invoice) throw new NotFoundException('发票不存在');
|
||||
return serializeBigInt({
|
||||
...invoice,
|
||||
orderNo: invoice.order.orderNo,
|
||||
payAmount: invoice.order.payAmount,
|
||||
order: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/** 工作日差(粗略:排除周六日) */
|
||||
private businessDaysSince(from: Date, to = new Date()): number {
|
||||
let days = 0;
|
||||
const cur = new Date(from);
|
||||
cur.setHours(0, 0, 0, 0);
|
||||
const end = new Date(to);
|
||||
end.setHours(0, 0, 0, 0);
|
||||
while (cur < end) {
|
||||
cur.setDate(cur.getDate() + 1);
|
||||
const w = cur.getDay();
|
||||
if (w !== 0 && w !== 6) days += 1;
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
async adminListInvoices(query: { status?: string; page?: number; pageSize?: number }) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: { status?: never } = {};
|
||||
if (query.status) where.status = query.status as never;
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.userInvoice.findMany({
|
||||
where,
|
||||
include: { order: { select: { orderNo: true, payAmount: true } }, user: { select: { phone: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.userInvoice.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
items: items.map((inv) => ({
|
||||
...inv,
|
||||
orderNo: inv.order.orderNo,
|
||||
payAmount: inv.order.payAmount,
|
||||
userPhone: inv.user.phone,
|
||||
overdue: inv.status === 'PENDING' && this.businessDaysSince(inv.createdAt) > 2,
|
||||
order: undefined,
|
||||
user: undefined,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async adminGetInvoice(id: bigint) {
|
||||
const invoice = await this.prisma.userInvoice.findUnique({
|
||||
where: { id },
|
||||
include: { order: { select: { orderNo: true, payAmount: true, productName: true } }, user: { select: { phone: true, nickname: true } } },
|
||||
});
|
||||
if (!invoice) throw new NotFoundException('发票不存在');
|
||||
return serializeBigInt({
|
||||
...invoice,
|
||||
orderNo: invoice.order.orderNo,
|
||||
payAmount: invoice.order.payAmount,
|
||||
productName: invoice.order.productName,
|
||||
userPhone: invoice.user.phone,
|
||||
overdue: invoice.status === 'PENDING' && this.businessDaysSince(invoice.createdAt) > 2,
|
||||
});
|
||||
}
|
||||
|
||||
async adminIssueInvoice(
|
||||
id: bigint,
|
||||
operatorId: bigint,
|
||||
body: { fileUrl: string; resourceId?: string; remark?: string },
|
||||
) {
|
||||
const invoice = await this.prisma.userInvoice.findUnique({ where: { id } });
|
||||
if (!invoice) throw new NotFoundException('发票不存在');
|
||||
if (invoice.status !== 'PENDING') throw new BadRequestException('当前状态不可开票');
|
||||
const updated = await this.prisma.userInvoice.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'ISSUED',
|
||||
fileUrl: body.fileUrl,
|
||||
resourceId: body.resourceId ? BigInt(body.resourceId) : null,
|
||||
issuedAt: new Date(),
|
||||
operatorId,
|
||||
remark: body.remark ?? invoice.remark,
|
||||
},
|
||||
});
|
||||
return serializeBigInt(updated);
|
||||
}
|
||||
|
||||
async adminRejectInvoice(id: bigint, operatorId: bigint, remark?: string) {
|
||||
const invoice = await this.prisma.userInvoice.findUnique({ where: { id } });
|
||||
if (!invoice) throw new NotFoundException('发票不存在');
|
||||
if (invoice.status !== 'PENDING') throw new BadRequestException('当前状态不可驳回');
|
||||
const updated = await this.prisma.userInvoice.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'REJECTED',
|
||||
operatorId,
|
||||
remark: remark ?? '驳回',
|
||||
issuedAt: new Date(),
|
||||
},
|
||||
});
|
||||
return serializeBigInt(updated);
|
||||
}
|
||||
|
||||
async listPartnerReshipments(partnerAccountId: bigint) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const orderWhere = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
|
||||
|
||||
Reference in New Issue
Block a user