feat(redeem): 核销记录标记扫码/手机号并支持方式统计

落库 RedeemChannel,门店记录页增加统计按钮,HQ 可按方式筛选。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 16:21:38 +08:00
parent 54fca99208
commit dc30932012
10 changed files with 531 additions and 48 deletions
@@ -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,
});
}