feat: multi-module iteration

This commit is contained in:
2026-08-04 21:38:49 +08:00
parent 9d96c73246
commit 71f508e02b
1366 changed files with 202004 additions and 0 deletions
@@ -0,0 +1,169 @@
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 { BenefitService } from '../benefit/benefit.service';
import { AdminRedeemService } from './admin-redeem.service';
import type { AdminBenefitCouponsQueryDto, AdminBenefitLedgersQueryDto } from './dto/admin-query.dto';
import type { AdminBenefitGrantDto } from './dto/admin-mutate.dto';
@Injectable()
export class AdminBenefitService {
constructor(
private readonly prisma: PrismaService,
private readonly benefitService: BenefitService,
private readonly adminRedeemService: AdminRedeemService,
) {}
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 } },
},
});
if (!coupon) throw new NotFoundException('权益券不存在');
const [ledgers, redeemTrace] = await Promise.all([
this.prisma.commonEvent.findMany({
where: benefitLedgerWhere(undefined, id),
orderBy: { createdAt: 'desc' },
take: 20,
}),
this.adminRedeemService.buildCouponRedeemTrace(coupon),
]);
return serializeBigInt({
...coupon,
ledgers,
redeemSummary: redeemTrace.redeemSummary,
redeemRecords: redeemTrace.redeemRecords,
});
}
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 grantCoupon(dto: AdminBenefitGrantDto) {
const phone = dto.phone.trim();
if (!/^1[3-9]\d{9}$/.test(phone)) {
throw new BadRequestException('请输入正确的用户手机号');
}
const user = await this.prisma.user.findFirst({
where: { phone, status: 1, mergedIntoUserId: null },
select: { id: true, userNo: true, phone: true, nickname: true },
});
if (!user) {
throw new NotFoundException('未找到该手机号对应的用户');
}
const coupon = await this.benefitService.grantManual({
userId: user.id,
amount: dto.amount,
remark: dto.remark,
});
return serializeBigInt({
...coupon,
user,
});
}
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,
});
}
}