feat(store): 门店详情核销记录走马灯

新增 GET /stores/:id/recent-redeems,C 端在门店详情区上方循环展示脱敏核销动态。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 14:16:06 +08:00
parent e06d9f4711
commit 4a5b9eaffa
4 changed files with 144 additions and 1 deletions
@@ -38,6 +38,15 @@ type TokenPayload = {
allocations?: Array<{ couponId: string; amount: number }>;
};
/** C 端公示:用户138****5678 / 用户*** */
function maskRedeemUserLabel(phone?: string | null): string {
const digits = String(phone || '').replace(/\D/g, '');
if (digits.length >= 7) {
return `用户${digits.slice(0, 3)}****${digits.slice(-4)}`;
}
return '用户***';
}
type PendingSnapshot = TokenPayload & {
redeemType: 'DIRECT' | 'COUPON';
};
@@ -1131,6 +1140,22 @@ export class RedeemService {
};
}
/** C 端门店详情走马灯:脱敏用户 + 时间 + 金额 */
async listPublicStoreRecentRedeems(storeId: bigint, limit = 20) {
const take = Math.min(Math.max(limit, 1), 50);
const list = await this.prisma.redeemRecord.findMany({
where: { storeId },
orderBy: { createdAt: 'desc' },
take,
include: { user: { select: { phone: true } } },
});
return list.map((r) => ({
userLabel: maskRedeemUserLabel(r.user?.phone),
amount: Number(r.amount),
createdAt: r.createdAt.toISOString(),
}));
}
async submitRating(userId: bigint, body: { redeemRecordId: string; serviceScore: number; envScore: number }) {
const record = await this.prisma.redeemRecord.findFirst({
where: { id: BigInt(body.redeemRecordId), userId },
@@ -12,7 +12,10 @@ import { CurrentUser } from '../../common/decorators/current-user.decorator';
@Controller('stores')
export class PublicStoreController {
constructor(private readonly storeService: StoreService) {}
constructor(
private readonly storeService: StoreService,
private readonly redeemService: RedeemService,
) {}
@Get()
@UseGuards(OptionalJwtAuthGuard)
@@ -28,6 +31,24 @@ export class PublicStoreController {
return this.storeService.listOpenStores(cityCode, userLat, userLng, { phone: viewerPhone });
}
@Get(':id/recent-redeems')
@UseGuards(OptionalJwtAuthGuard)
async recentRedeems(
@CurrentUser() user: AuthUser | undefined,
@Param('id') id: string,
@Query('limit') limit?: string,
) {
const viewerPhone = await this.resolveViewerPhone(user);
// 与详情同权:白名单门店对不可见用户返回空(不泄露存在核销)
try {
await this.storeService.getStore(BigInt(id), { phone: viewerPhone });
} catch {
return [];
}
const n = limit != null && limit !== '' ? Number(limit) : 20;
return this.redeemService.listPublicStoreRecentRedeems(BigInt(id), Number.isFinite(n) ? n : 20);
}
@Get(':id')
@UseGuards(OptionalJwtAuthGuard)
async detail(@CurrentUser() user: AuthUser | undefined, @Param('id') id: string) {