feat(redeem): 核销记录标记扫码/手机号并支持方式统计
落库 RedeemChannel,门店记录页增加统计按钮,HQ 可按方式筛选。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -39,6 +39,9 @@ export class AdminRedeemService {
|
||||
if (query.redeemNo) where.redeemNo = { contains: query.redeemNo };
|
||||
if (query.storeId) where.storeId = BigInt(query.storeId);
|
||||
if (query.userId) where.userId = BigInt(query.userId);
|
||||
if (query.channel === 'SCAN' || query.channel === 'PHONE') {
|
||||
where.channel = query.channel;
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.redeemRecord.findMany({
|
||||
|
||||
@@ -242,6 +242,11 @@ export class AdminRedeemRecordsQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userId?: string;
|
||||
|
||||
/** SCAN | PHONE */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
channel?: string;
|
||||
}
|
||||
|
||||
export class AdminStoreRatingsQueryDto extends PaginationQueryDto {
|
||||
|
||||
@@ -96,6 +96,16 @@ export class ShopRedeemController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get('stats')
|
||||
stats(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Query('range') range?: string,
|
||||
) {
|
||||
const normalized =
|
||||
range === '7d' || range === '30d' || range === 'today' ? range : 'today';
|
||||
return this.redeemService.getShopRedeemStats(user.actorId, user.storeId!, normalized);
|
||||
}
|
||||
|
||||
@Post('phone/send-lookup-sms')
|
||||
sendPhoneLookupSms(@CurrentUser() user: AuthUser, @Body() body: RedeemPhoneSendLookupSmsDto) {
|
||||
return this.redeemService.sendPhoneLookupSms(user.actorId, user.storeId!, body.phone);
|
||||
|
||||
@@ -178,6 +178,7 @@ export class RedeemService {
|
||||
) {
|
||||
const settlementRate = Number(account.store.settlementRate);
|
||||
const settleAmount = calcRedeemSettleAmount(amount, settlementRate);
|
||||
const redeemChannel = analyticsExtra?.channel === 'phone' ? 'PHONE' : 'SCAN';
|
||||
|
||||
let record;
|
||||
try {
|
||||
@@ -192,6 +193,7 @@ export class RedeemService {
|
||||
storeId: account.storeId,
|
||||
amount,
|
||||
settleAmount,
|
||||
channel: redeemChannel,
|
||||
allocations: {
|
||||
create: normalizedAllocations.map((item, index) => ({
|
||||
couponId: BigInt(item.couponId),
|
||||
@@ -1079,6 +1081,51 @@ export class RedeemService {
|
||||
return { list: serializeBigInt(list), total, page, pageSize };
|
||||
}
|
||||
|
||||
async getShopRedeemStats(
|
||||
storeAccountId: bigint,
|
||||
storeId: bigint,
|
||||
range: 'today' | '7d' | '30d' = 'today',
|
||||
) {
|
||||
await this.prisma.storeAccountStore.findUniqueOrThrow({
|
||||
where: { storeAccountId_storeId: { storeAccountId, storeId } },
|
||||
});
|
||||
const start = new Date();
|
||||
start.setHours(0, 0, 0, 0);
|
||||
if (range === '7d') start.setDate(start.getDate() - 6);
|
||||
if (range === '30d') start.setDate(start.getDate() - 29);
|
||||
|
||||
const records = await this.prisma.redeemRecord.findMany({
|
||||
where: { storeId, createdAt: { gte: start } },
|
||||
select: { channel: true, amount: true, settleAmount: true },
|
||||
});
|
||||
|
||||
const buckets: Record<'SCAN' | 'PHONE', { count: number; amount: number; settleAmount: number }> = {
|
||||
SCAN: { count: 0, amount: 0, settleAmount: 0 },
|
||||
PHONE: { count: 0, amount: 0, settleAmount: 0 },
|
||||
};
|
||||
for (const r of records) {
|
||||
const key = r.channel === 'PHONE' ? 'PHONE' : 'SCAN';
|
||||
buckets[key].count += 1;
|
||||
buckets[key].amount += Number(r.amount);
|
||||
buckets[key].settleAmount += Number(r.settleAmount);
|
||||
}
|
||||
|
||||
const byChannel = (['SCAN', 'PHONE'] as const).map((channel) => ({
|
||||
channel,
|
||||
count: buckets[channel].count,
|
||||
amount: Number(buckets[channel].amount.toFixed(2)),
|
||||
settleAmount: Number(buckets[channel].settleAmount.toFixed(2)),
|
||||
}));
|
||||
|
||||
return {
|
||||
range,
|
||||
totalCount: records.length,
|
||||
totalAmount: Number(byChannel.reduce((s, b) => s + b.amount, 0).toFixed(2)),
|
||||
totalSettleAmount: Number(byChannel.reduce((s, b) => s + b.settleAmount, 0).toFixed(2)),
|
||||
byChannel,
|
||||
};
|
||||
}
|
||||
|
||||
async getShopDashboard(storeAccountId: bigint, storeId: bigint) {
|
||||
const binding = await this.prisma.storeAccountStore.findUniqueOrThrow({
|
||||
where: { storeAccountId_storeId: { storeAccountId, storeId } },
|
||||
@@ -1091,6 +1138,8 @@ export class RedeemService {
|
||||
});
|
||||
const todayCount = records.length;
|
||||
const todayAmount = records.reduce((sum, r) => sum + Number(r.amount), 0);
|
||||
const todayScanCount = records.filter((r) => r.channel !== 'PHONE').length;
|
||||
const todayPhoneCount = records.filter((r) => r.channel === 'PHONE').length;
|
||||
const recent = await this.prisma.redeemRecord.findMany({
|
||||
where: { storeId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
@@ -1100,6 +1149,8 @@ export class RedeemService {
|
||||
store: binding.store,
|
||||
todayCount,
|
||||
todayAmount,
|
||||
todayScanCount,
|
||||
todayPhoneCount,
|
||||
recentRecords: recent,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user