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'; import { buildBenefitLedgerEvent, benefitLedgerWhere } from '../../common/event/event.helpers'; import { mapBenefitLedgerCompat } from '../../common/compat/v31-compat'; import type { AdminBenefitCouponsQueryDto, AdminBenefitLedgersQueryDto } from './dto/admin-query.dto'; @Injectable() export class AdminBenefitService { constructor(private readonly prisma: PrismaService) {} async listCoupons(query: AdminBenefitCouponsQueryDto) { const page = query.page ?? 1; const pageSize = query.pageSize ?? 20; const where: Prisma.BenefitCouponWhereInput = {}; if (query.couponNo) where.couponNo = { contains: query.couponNo }; if (query.userId) where.userId = BigInt(query.userId); if (query.status) where.status = query.status as Prisma.EnumBenefitCouponStatusFilter['equals']; const [items, total] = await Promise.all([ this.prisma.benefitCoupon.findMany({ where, orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize, include: { user: { select: { id: true, userNo: true, phone: true, nickname: true } }, order: { select: { id: true, orderNo: true, status: true } }, }, }), this.prisma.benefitCoupon.count({ where }), ]); return serializeBigInt({ items, total, page, pageSize }); } async detailCoupon(id: bigint) { const coupon = await this.prisma.benefitCoupon.findUnique({ where: { id }, include: { user: { select: { id: true, userNo: true, phone: true, nickname: true } }, order: { select: { id: true, orderNo: true, status: true, payAmount: true } }, redeemRecords: { orderBy: { createdAt: 'desc' }, take: 10, include: { store: { select: { id: true, name: true } } } }, }, }); if (!coupon) throw new NotFoundException('权益券不存在'); const ledgers = await this.prisma.commonEvent.findMany({ where: benefitLedgerWhere(undefined, id), orderBy: { createdAt: 'desc' }, take: 20, }); return serializeBigInt({ ...coupon, ledgers }); } async voidCoupon(id: bigint) { const coupon = await this.prisma.benefitCoupon.findUnique({ where: { id } }); if (!coupon) throw new NotFoundException('权益券不存在'); if (coupon.status === 'VOID') throw new BadRequestException('权益券已作废'); const updated = await this.prisma.$transaction(async (tx) => { const row = await tx.benefitCoupon.update({ where: { id }, data: { status: 'VOID', balance: 0 }, }); if (Number(coupon.balance) > 0) { await tx.commonEvent.create({ data: buildBenefitLedgerEvent({ userId: coupon.userId, couponId: coupon.id, type: 'ADJUST', amount: -Number(coupon.balance), balanceAfter: 0, refType: 'ADMIN_VOID', remark: 'HQ 手动作废', }), }); } return row; }); return serializeBigInt(updated); } async listLedgers(query: AdminBenefitLedgersQueryDto) { const page = query.page ?? 1; const pageSize = query.pageSize ?? 20; const where: Prisma.CommonEventWhereInput = { eventType: 'BENEFIT_LEDGER', ...(query.userId ? { actorType: 'USER', actorId: BigInt(query.userId) } : {}), ...(query.couponId ? { param2: BigInt(query.couponId).toString() } : {}), ...(query.type ? { param1: query.type } : {}), }; const [items, total] = await Promise.all([ this.prisma.commonEvent.findMany({ where, orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize, }), this.prisma.commonEvent.count({ where }), ]); const userIds = [...new Set(items.map((i) => i.actorId).filter(Boolean))] as bigint[]; const couponIds = [...new Set(items.map((i) => i.param2).filter(Boolean))].map((id) => BigInt(id!)); const [users, coupons] = await Promise.all([ userIds.length ? this.prisma.user.findMany({ where: { id: { in: userIds } }, select: { id: true, userNo: true } }) : Promise.resolve([] as { id: bigint; userNo: string | null }[]), couponIds.length ? this.prisma.benefitCoupon.findMany({ where: { id: { in: couponIds } }, select: { id: true, couponNo: true } }) : Promise.resolve([] as { id: bigint; couponNo: string }[]), ]); const userMap = new Map(users.map((u) => [u.id.toString(), u] as const)); const couponMap = new Map(coupons.map((c) => [c.id.toString(), c] as const)); return serializeBigInt({ items: items.map((e) => mapBenefitLedgerCompat( e, e.actorId ? userMap.get(e.actorId.toString()) : null, e.param2 ? couponMap.get(e.param2) : null, ), ), total, page, pageSize, }); } }