发布商品,商品图片使用oss服务器地址

This commit is contained in:
2026-07-06 08:53:03 +08:00
parent a0466f023e
commit 5ba69eb935
60 changed files with 2776 additions and 157 deletions
@@ -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 } }),
]);