feat: multi-module iteration
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { BenefitService } from './benefit.service';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
@Controller('benefit')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class BenefitController {
|
||||
constructor(private readonly benefitService: BenefitService) {}
|
||||
|
||||
@Get('coupons')
|
||||
coupons(@CurrentUser() user: AuthUser) {
|
||||
return this.benefitService.listCoupons(user.actorId);
|
||||
}
|
||||
|
||||
@Get('summary')
|
||||
summary(@CurrentUser() user: AuthUser) {
|
||||
return this.benefitService.getSummary(user.actorId);
|
||||
}
|
||||
|
||||
@Get('coupons/:id')
|
||||
coupon(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.benefitService.getCoupon(user.actorId, BigInt(id));
|
||||
}
|
||||
|
||||
@Get('ledger')
|
||||
ledger(@CurrentUser() user: AuthUser, @Query('couponId') couponId?: string) {
|
||||
return this.benefitService.getLedger(
|
||||
user.actorId,
|
||||
couponId ? BigInt(couponId) : undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { IamModule } from '../iam/iam.module';
|
||||
import { BenefitController } from './benefit.controller';
|
||||
import { BenefitService } from './benefit.service';
|
||||
|
||||
@Module({
|
||||
imports: [IamModule],
|
||||
controllers: [BenefitController],
|
||||
providers: [BenefitService],
|
||||
exports: [BenefitService],
|
||||
})
|
||||
export class BenefitModule {}
|
||||
@@ -0,0 +1,230 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { calcBenefitAmount, calcBenefitSummary, generateCouponNo } from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { buildBenefitLedgerEvent, benefitLedgerWhere } from '../../common/event/event.helpers';
|
||||
|
||||
export type CouponAllocation = { couponId: string; amount: number };
|
||||
|
||||
@Injectable()
|
||||
export class BenefitService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async grantOnOrderPaid(orderId: bigint) {
|
||||
const order = await this.prisma.order.findUniqueOrThrow({
|
||||
where: { id: orderId },
|
||||
});
|
||||
|
||||
const product = await this.prisma.commonProductItem.findUnique({ where: { id: order.productId } });
|
||||
const unitBenefit = calcBenefitAmount({
|
||||
price: Number(order.listUnitPrice),
|
||||
benefitAmount: product?.benefitAmount ? Number(product.benefitAmount) : null,
|
||||
});
|
||||
const totalBenefit = unitBenefit * order.quantity;
|
||||
|
||||
const coupon = await this.prisma.benefitCoupon.create({
|
||||
data: {
|
||||
couponNo: generateCouponNo(),
|
||||
userId: order.userId,
|
||||
orderId: order.id,
|
||||
totalAmount: totalBenefit,
|
||||
balance: totalBenefit,
|
||||
sourceProduct: order.productName,
|
||||
},
|
||||
});
|
||||
|
||||
await this.prisma.commonEvent.create({
|
||||
data: buildBenefitLedgerEvent({
|
||||
userId: order.userId,
|
||||
couponId: coupon.id,
|
||||
type: 'GRANT',
|
||||
amount: totalBenefit,
|
||||
balanceAfter: totalBenefit,
|
||||
refType: 'ORDER',
|
||||
refId: order.id,
|
||||
remark: '购酒赠券',
|
||||
}),
|
||||
});
|
||||
|
||||
return serializeBigInt(coupon);
|
||||
}
|
||||
|
||||
/** HQ 手动发放权益(无关联订单) */
|
||||
async grantManual(params: {
|
||||
userId: bigint;
|
||||
amount: number;
|
||||
remark?: string;
|
||||
sourceProduct?: string;
|
||||
}) {
|
||||
const amount = Number(params.amount);
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
throw new BadRequestException('权益金额须大于 0');
|
||||
}
|
||||
if (amount > 999_999.99) {
|
||||
throw new BadRequestException('权益金额超出上限');
|
||||
}
|
||||
|
||||
const sourceProduct = params.sourceProduct?.trim() || '总部手动发放';
|
||||
const remark = params.remark?.trim() || '总部手动发放';
|
||||
|
||||
const coupon = await this.prisma.$transaction(async (tx) => {
|
||||
const created = await tx.benefitCoupon.create({
|
||||
data: {
|
||||
couponNo: generateCouponNo(),
|
||||
userId: params.userId,
|
||||
totalAmount: amount,
|
||||
balance: amount,
|
||||
sourceProduct,
|
||||
},
|
||||
});
|
||||
await tx.commonEvent.create({
|
||||
data: buildBenefitLedgerEvent({
|
||||
userId: params.userId,
|
||||
couponId: created.id,
|
||||
type: 'GRANT',
|
||||
amount,
|
||||
balanceAfter: amount,
|
||||
refType: 'ADMIN_GRANT',
|
||||
remark,
|
||||
}),
|
||||
});
|
||||
return created;
|
||||
});
|
||||
|
||||
return serializeBigInt(coupon);
|
||||
}
|
||||
|
||||
async listCoupons(userId: bigint) {
|
||||
const list = await this.prisma.benefitCoupon.findMany({
|
||||
where: { userId, status: { in: ['ACTIVE', 'USED_UP'] } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return serializeBigInt(list);
|
||||
}
|
||||
|
||||
async getSummary(userId: bigint) {
|
||||
const coupons = await this.prisma.benefitCoupon.findMany({
|
||||
where: { userId, status: 'ACTIVE' },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
const summary = calcBenefitSummary(
|
||||
coupons.map((c) => Number(c.balance)),
|
||||
);
|
||||
return serializeBigInt(summary);
|
||||
}
|
||||
|
||||
async getLedger(userId: bigint, couponId?: bigint) {
|
||||
const list = await this.prisma.commonEvent.findMany({
|
||||
where: benefitLedgerWhere(userId, couponId),
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return serializeBigInt(
|
||||
list.map((e) => {
|
||||
const type = e.param1 || '';
|
||||
const amount = Number(e.amount1 ?? 0);
|
||||
const title =
|
||||
e.remark ||
|
||||
(type === 'REDEEM'
|
||||
? '门店核销'
|
||||
: type === 'GRANT'
|
||||
? '购酒入账'
|
||||
: type === 'REFUND_VOID'
|
||||
? '退款作废'
|
||||
: '权益变动');
|
||||
return {
|
||||
id: e.id,
|
||||
type,
|
||||
amount,
|
||||
title,
|
||||
createdAt: e.createdAt,
|
||||
balanceAfter: e.amount2 != null ? Number(e.amount2) : null,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getCoupon(userId: bigint, couponId: bigint) {
|
||||
const coupon = await this.prisma.benefitCoupon.findFirst({
|
||||
where: { id: couponId, userId },
|
||||
});
|
||||
if (!coupon) return null;
|
||||
const ledgers = await this.prisma.commonEvent.findMany({
|
||||
where: benefitLedgerWhere(undefined, couponId),
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return serializeBigInt({ coupon, ledgers });
|
||||
}
|
||||
|
||||
/** 核销扣减券余额(乐观锁),由 redeem 模块调用 */
|
||||
async deductCoupons(
|
||||
tx: Prisma.TransactionClient,
|
||||
allocations: CouponAllocation[],
|
||||
refType: 'STORE',
|
||||
refId: bigint,
|
||||
) {
|
||||
for (const alloc of allocations) {
|
||||
const coupon = await tx.benefitCoupon.findUniqueOrThrow({
|
||||
where: { id: BigInt(alloc.couponId) },
|
||||
});
|
||||
const allocAmount = Number(alloc.amount);
|
||||
if (!Number.isFinite(allocAmount) || allocAmount <= 0) {
|
||||
throw new Error('BENEFIT_ALLOC_INVALID');
|
||||
}
|
||||
const updated = await tx.benefitCoupon.updateMany({
|
||||
where: { id: coupon.id, version: coupon.version, balance: { gte: allocAmount } },
|
||||
data: {
|
||||
usedAmount: { increment: allocAmount },
|
||||
balance: { decrement: allocAmount },
|
||||
version: { increment: 1 },
|
||||
status: Number(coupon.balance) - allocAmount <= 0 ? 'USED_UP' : 'ACTIVE',
|
||||
},
|
||||
});
|
||||
if (updated.count === 0) throw new Error('BENEFIT_DEDUCT_CONFLICT');
|
||||
|
||||
const newBalance = Number(coupon.balance) - allocAmount;
|
||||
await tx.commonEvent.create({
|
||||
data: buildBenefitLedgerEvent({
|
||||
userId: coupon.userId,
|
||||
couponId: coupon.id,
|
||||
type: 'REDEEM',
|
||||
amount: -allocAmount,
|
||||
balanceAfter: newBalance,
|
||||
refType,
|
||||
refId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 退款作废权益 */
|
||||
async voidCouponsOnRefund(orderId: bigint) {
|
||||
const coupons = await this.prisma.benefitCoupon.findMany({
|
||||
where: { orderId, status: { in: ['ACTIVE', 'USED_UP'] } },
|
||||
});
|
||||
for (const coupon of coupons) {
|
||||
const balance = Number(coupon.balance);
|
||||
if (balance <= 0 && coupon.status === 'USED_UP') continue;
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.benefitCoupon.update({
|
||||
where: { id: coupon.id },
|
||||
data: { status: 'VOID', balance: 0 },
|
||||
});
|
||||
if (balance > 0) {
|
||||
await tx.commonEvent.create({
|
||||
data: buildBenefitLedgerEvent({
|
||||
userId: coupon.userId,
|
||||
couponId: coupon.id,
|
||||
type: 'REFUND_VOID',
|
||||
amount: -balance,
|
||||
balanceAfter: 0,
|
||||
refType: 'ORDER',
|
||||
refId: orderId,
|
||||
remark: '退款作废权益',
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user