发布商品,商品图片使用oss服务器地址
This commit is contained in:
@@ -15,7 +15,15 @@ import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { RedisService } from '../../common/redis/redis.service';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { SettlementService } from '../settlement/settlement.service';
|
||||
import { buildBenefitLedgerEvent } from '../../common/event/event.helpers';
|
||||
import { BenefitService } from '../benefit/benefit.service';
|
||||
|
||||
type TokenPayload = {
|
||||
userId: string;
|
||||
couponId?: string;
|
||||
amount: number;
|
||||
storeId?: string | null;
|
||||
allocations?: Array<{ couponId: string; amount: number }>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class RedeemService {
|
||||
@@ -23,6 +31,7 @@ export class RedeemService {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly redis: RedisService,
|
||||
private readonly settlementService: SettlementService,
|
||||
private readonly benefitService: BenefitService,
|
||||
) {}
|
||||
|
||||
async createToken(userId: bigint, body: { couponId?: string; amount: number; storeId?: string }) {
|
||||
@@ -42,6 +51,7 @@ export class RedeemService {
|
||||
where: { userId, status: 'ACTIVE' },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
const totalBalance = coupons.reduce((s, c) => s + Number(c.balance), 0);
|
||||
const result = allocateBenefitCoupons(
|
||||
coupons.map((c) => ({
|
||||
id: c.id.toString(),
|
||||
@@ -51,6 +61,8 @@ export class RedeemService {
|
||||
body.amount,
|
||||
);
|
||||
if (!result.ok) throw new BadRequestException(result.message);
|
||||
const check = validateRedeemAmount(totalBalance, body.amount);
|
||||
if (!check.ok) throw new BadRequestException(check.message);
|
||||
allocations = result.allocations;
|
||||
}
|
||||
|
||||
@@ -70,7 +82,7 @@ export class RedeemService {
|
||||
REDEEM_TOKEN_TTL_SECONDS,
|
||||
);
|
||||
|
||||
return { token, expireAt, amount: body.amount };
|
||||
return { token, expireAt, amount: body.amount, boundStoreId: body.storeId ?? null };
|
||||
}
|
||||
|
||||
async getToken(token: string) {
|
||||
@@ -79,6 +91,40 @@ export class RedeemService {
|
||||
return cached;
|
||||
}
|
||||
|
||||
async previewRedeem(storeAccountId: bigint, token: string) {
|
||||
const account = await this.prisma.storeAccount.findUniqueOrThrow({
|
||||
where: { id: storeAccountId },
|
||||
include: { store: true },
|
||||
});
|
||||
if (account.store.status !== 'OPEN') {
|
||||
throw new BadRequestException('门店未营业');
|
||||
}
|
||||
|
||||
const cached = await this.redis.getJson<TokenPayload>(`redeem:token:${token}`);
|
||||
if (!cached) throw new BadRequestException('核销码无效或已过期');
|
||||
|
||||
if (cached.storeId && cached.storeId !== account.storeId.toString()) {
|
||||
throw new BadRequestException('该核销码仅限指定门店使用');
|
||||
}
|
||||
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: BigInt(cached.userId) },
|
||||
select: { id: true, userNo: true, phone: true, nickname: true },
|
||||
});
|
||||
|
||||
const ttl = await this.redis.ttl(`redeem:token:${token}`);
|
||||
|
||||
return serializeBigInt({
|
||||
token,
|
||||
amount: cached.amount,
|
||||
user,
|
||||
boundStoreId: cached.storeId,
|
||||
redeemType: cached.allocations && cached.allocations.length > 1 ? 'DIRECT' : cached.couponId ? 'COUPON' : 'DIRECT',
|
||||
expireInSeconds: ttl > 0 ? ttl : 0,
|
||||
storeMatch: !cached.storeId || cached.storeId === account.storeId.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
async confirmRedeem(storeAccountId: bigint, body: { token: string }) {
|
||||
const account = await this.prisma.storeAccount.findUniqueOrThrow({
|
||||
where: { id: storeAccountId },
|
||||
@@ -88,19 +134,16 @@ export class RedeemService {
|
||||
throw new BadRequestException('门店未营业');
|
||||
}
|
||||
|
||||
const cached = await this.redis.getJson<{
|
||||
userId: string;
|
||||
couponId?: string;
|
||||
amount: number;
|
||||
allocations?: Array<{ couponId: string; amount: number }>;
|
||||
}>(`redeem:token:${body.token}`);
|
||||
const cached = await this.redis.getJson<TokenPayload>(`redeem:token:${body.token}`);
|
||||
if (!cached) throw new BadRequestException('核销码无效或已过期');
|
||||
|
||||
if (cached.storeId && cached.storeId !== account.storeId.toString()) {
|
||||
throw new BadRequestException('该核销码仅限指定门店使用');
|
||||
}
|
||||
|
||||
const allocations =
|
||||
cached.allocations ??
|
||||
(cached.couponId
|
||||
? [{ couponId: cached.couponId, amount: cached.amount }]
|
||||
: []);
|
||||
(cached.couponId ? [{ couponId: cached.couponId, amount: cached.amount }] : []);
|
||||
if (allocations.length === 0) {
|
||||
throw new BadRequestException('核销码数据异常');
|
||||
}
|
||||
@@ -126,50 +169,30 @@ export class RedeemService {
|
||||
const settlementRate = cityRule ? Number(cityRule.storeSettlementRate) : 0.6;
|
||||
const settleAmount = calcRedeemSettleAmount(amount, settlementRate);
|
||||
|
||||
const record = await this.prisma.$transaction(async (tx) => {
|
||||
for (const alloc of allocations) {
|
||||
const coupon = await tx.benefitCoupon.findUniqueOrThrow({
|
||||
where: { id: BigInt(alloc.couponId) },
|
||||
});
|
||||
const allocAmount = alloc.amount;
|
||||
const updated = await tx.benefitCoupon.updateMany({
|
||||
where: { id: coupon.id, version: coupon.version, balance: { gte: allocAmount } },
|
||||
let record;
|
||||
try {
|
||||
record = await this.prisma.$transaction(async (tx) => {
|
||||
await this.benefitService.deductCoupons(tx, allocations, 'STORE', account.storeId);
|
||||
|
||||
const redeemRecord = await tx.redeemRecord.create({
|
||||
data: {
|
||||
usedAmount: { increment: allocAmount },
|
||||
balance: { decrement: allocAmount },
|
||||
version: { increment: 1 },
|
||||
status: Number(coupon.balance) - allocAmount <= 0 ? 'USED_UP' : 'ACTIVE',
|
||||
redeemNo: generateRedeemNo(),
|
||||
userId: BigInt(cached.userId),
|
||||
couponId: BigInt(allocations[0].couponId),
|
||||
storeId: account.storeId,
|
||||
amount,
|
||||
settleAmount,
|
||||
},
|
||||
});
|
||||
if (updated.count === 0) throw new BadRequestException('核销失败,请重试');
|
||||
|
||||
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: 'STORE',
|
||||
refId: account.storeId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const redeemRecord = await tx.redeemRecord.create({
|
||||
data: {
|
||||
redeemNo: generateRedeemNo(),
|
||||
userId: BigInt(cached.userId),
|
||||
couponId: BigInt(allocations[0].couponId),
|
||||
storeId: account.storeId,
|
||||
amount,
|
||||
settleAmount,
|
||||
},
|
||||
return redeemRecord;
|
||||
});
|
||||
|
||||
return redeemRecord;
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message === 'BENEFIT_DEDUCT_CONFLICT') {
|
||||
throw new BadRequestException('核销失败,请重试');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
await this.settlementService.createStorePayout(record.id, account.storeId, amount, settleAmount, settlementRate);
|
||||
await this.redis.del(`redeem:token:${body.token}`);
|
||||
@@ -187,6 +210,7 @@ export class RedeemService {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: { payout: true },
|
||||
}),
|
||||
this.prisma.redeemRecord.count({ where: { storeId: account.storeId } }),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user