门店账户多账号
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { RedeemService } from './redeem.service';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { ShopStoreGuard } from '../../common/guards/shop-store.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import {
|
||||
RedeemPhoneBalanceDto,
|
||||
@@ -37,18 +38,18 @@ export class UserRedeemController {
|
||||
}
|
||||
|
||||
@Controller('shop/redeem')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, ShopStoreGuard)
|
||||
export class ShopRedeemController {
|
||||
constructor(private readonly redeemService: RedeemService) {}
|
||||
|
||||
@Post('preview')
|
||||
preview(@CurrentUser() user: AuthUser, @Body() body: { token: string }) {
|
||||
return this.redeemService.previewRedeem(user.actorId, body.token);
|
||||
return this.redeemService.previewRedeem(user.actorId, user.storeId!, body.token);
|
||||
}
|
||||
|
||||
@Post('confirm')
|
||||
confirm(@CurrentUser() user: AuthUser, @Body() body: { token: string }) {
|
||||
return this.redeemService.confirmRedeem(user.actorId, body);
|
||||
return this.redeemService.confirmRedeem(user.actorId, user.storeId!, body);
|
||||
}
|
||||
|
||||
@Post('failures')
|
||||
@@ -56,7 +57,7 @@ export class ShopRedeemController {
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Body() body: RedeemFailureReportDto,
|
||||
) {
|
||||
return this.redeemService.reportNetworkFailure(user.actorId, body);
|
||||
return this.redeemService.reportNetworkFailure(user.actorId, user.storeId!, body);
|
||||
}
|
||||
|
||||
@Post('pending')
|
||||
@@ -64,7 +65,7 @@ export class ShopRedeemController {
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Body() body: RedeemPendingSubmitDto,
|
||||
) {
|
||||
return this.redeemService.submitPendingRedeem(user.actorId, body);
|
||||
return this.redeemService.submitPendingRedeem(user.actorId, user.storeId!, body);
|
||||
}
|
||||
|
||||
@Get('records')
|
||||
@@ -73,26 +74,46 @@ export class ShopRedeemController {
|
||||
@Query('page') page = '1',
|
||||
@Query('pageSize') pageSize = '20',
|
||||
) {
|
||||
return this.redeemService.listShopRecords(user.actorId, Number(page), Number(pageSize));
|
||||
return this.redeemService.listShopRecords(
|
||||
user.actorId,
|
||||
user.storeId!,
|
||||
Number(page),
|
||||
Number(pageSize),
|
||||
);
|
||||
}
|
||||
|
||||
@Post('phone/send-lookup-sms')
|
||||
sendPhoneLookupSms(@CurrentUser() user: AuthUser, @Body() body: RedeemPhoneSendLookupSmsDto) {
|
||||
return this.redeemService.sendPhoneLookupSms(user.actorId, body.phone);
|
||||
return this.redeemService.sendPhoneLookupSms(user.actorId, user.storeId!, body.phone);
|
||||
}
|
||||
|
||||
@Post('phone/balance')
|
||||
phoneBalance(@CurrentUser() user: AuthUser, @Body() body: RedeemPhoneBalanceDto) {
|
||||
return this.redeemService.verifyPhoneAndGetBalance(user.actorId, body.phone, body.code);
|
||||
return this.redeemService.verifyPhoneAndGetBalance(
|
||||
user.actorId,
|
||||
user.storeId!,
|
||||
body.phone,
|
||||
body.code,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('phone/prepare')
|
||||
phonePrepare(@CurrentUser() user: AuthUser, @Body() body: RedeemPhonePrepareDto) {
|
||||
return this.redeemService.preparePhoneRedeem(user.actorId, body.sessionId, body.amount);
|
||||
return this.redeemService.preparePhoneRedeem(
|
||||
user.actorId,
|
||||
user.storeId!,
|
||||
body.sessionId,
|
||||
body.amount,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('phone/confirm')
|
||||
phoneConfirm(@CurrentUser() user: AuthUser, @Body() body: RedeemPhoneConfirmDto) {
|
||||
return this.redeemService.confirmPhoneRedeem(user.actorId, body.sessionId, body.code);
|
||||
return this.redeemService.confirmPhoneRedeem(
|
||||
user.actorId,
|
||||
user.storeId!,
|
||||
body.sessionId,
|
||||
body.code,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,15 +89,28 @@ export class RedeemService {
|
||||
return `redeem:phone-session:${sessionId}`;
|
||||
}
|
||||
|
||||
private async loadOpenStoreAccount(storeAccountId: bigint) {
|
||||
const account = await this.prisma.storeAccount.findUniqueOrThrow({
|
||||
where: { id: storeAccountId },
|
||||
include: { store: true },
|
||||
private async loadOpenStoreAccount(storeAccountId: bigint, storeId: bigint) {
|
||||
const binding = await this.prisma.storeAccountStore.findUnique({
|
||||
where: {
|
||||
storeAccountId_storeId: { storeAccountId, storeId },
|
||||
},
|
||||
include: {
|
||||
storeAccount: true,
|
||||
store: true,
|
||||
},
|
||||
});
|
||||
if (account.store.status !== 'OPEN') {
|
||||
if (!binding) throw new BadRequestException('无权访问该门店');
|
||||
if (binding.storeAccount.status !== 'ACTIVE') {
|
||||
throw new BadRequestException('门店账号已停用');
|
||||
}
|
||||
if (binding.store.status !== 'OPEN') {
|
||||
throw new BadRequestException('门店未营业');
|
||||
}
|
||||
return account;
|
||||
return {
|
||||
...binding.storeAccount,
|
||||
storeId: binding.store.id,
|
||||
store: binding.store,
|
||||
};
|
||||
}
|
||||
|
||||
private async resolveUserByPhone(phone: string) {
|
||||
@@ -228,8 +241,8 @@ export class RedeemService {
|
||||
return record;
|
||||
}
|
||||
|
||||
async sendPhoneLookupSms(storeAccountId: bigint, phone: string) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId);
|
||||
async sendPhoneLookupSms(storeAccountId: bigint, storeId: bigint, phone: string) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId, storeId);
|
||||
const normalizedPhone = this.normalizeMobilePhone(phone);
|
||||
await this.resolveUserByPhone(normalizedPhone);
|
||||
await this.authService.sendSms(normalizedPhone, SmsScene.REDEEM_PHONE_LOOKUP, {
|
||||
@@ -243,8 +256,8 @@ export class RedeemService {
|
||||
return { ok: true, maskedPhone: this.maskPhoneForStore(normalizedPhone) };
|
||||
}
|
||||
|
||||
async verifyPhoneAndGetBalance(storeAccountId: bigint, phone: string, code: string) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId);
|
||||
async verifyPhoneAndGetBalance(storeAccountId: bigint, storeId: bigint, phone: string, code: string) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId, storeId);
|
||||
const normalizedPhone = this.normalizeMobilePhone(phone);
|
||||
const user = await this.resolveUserByPhone(normalizedPhone);
|
||||
await this.authService.verifySmsCode(normalizedPhone, code, SmsScene.REDEEM_PHONE_LOOKUP);
|
||||
@@ -298,8 +311,8 @@ export class RedeemService {
|
||||
return session;
|
||||
}
|
||||
|
||||
async preparePhoneRedeem(storeAccountId: bigint, sessionId: string, amount: number) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId);
|
||||
async preparePhoneRedeem(storeAccountId: bigint, storeId: bigint, sessionId: string, amount: number) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId, storeId);
|
||||
const session = await this.loadPhoneSession(sessionId, storeAccountId);
|
||||
const userId = BigInt(session.userId);
|
||||
const { allocations } = await this.computeDirectAllocations(userId, amount);
|
||||
@@ -337,8 +350,8 @@ export class RedeemService {
|
||||
};
|
||||
}
|
||||
|
||||
async confirmPhoneRedeem(storeAccountId: bigint, sessionId: string, code: string) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId);
|
||||
async confirmPhoneRedeem(storeAccountId: bigint, storeId: bigint, sessionId: string, code: string) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId, storeId);
|
||||
const session = await this.loadPhoneSession(sessionId, storeAccountId);
|
||||
if (!session.confirmPrepared || session.amount == null || !session.allocations?.length) {
|
||||
throw new BadRequestException('请先选择核销金额并发送确认验证码');
|
||||
@@ -462,8 +475,8 @@ export class RedeemService {
|
||||
return { status: 'EXPIRED' as const };
|
||||
}
|
||||
|
||||
async previewRedeem(storeAccountId: bigint, token: string) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId);
|
||||
async previewRedeem(storeAccountId: bigint, storeId: bigint, token: string) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId, storeId);
|
||||
|
||||
const cached = await this.redis.getJson<TokenPayload>(`redeem:token:${token}`);
|
||||
if (!cached) throw new BadRequestException('核销码无效或已过期');
|
||||
@@ -516,8 +529,8 @@ export class RedeemService {
|
||||
});
|
||||
}
|
||||
|
||||
async confirmRedeem(storeAccountId: bigint, body: { token: string }) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId);
|
||||
async confirmRedeem(storeAccountId: bigint, storeId: bigint, body: { token: string }) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId, storeId);
|
||||
const token = body.token?.trim();
|
||||
if (!token) throw new BadRequestException('请提供核销码');
|
||||
|
||||
@@ -594,6 +607,7 @@ export class RedeemService {
|
||||
|
||||
async reportNetworkFailure(
|
||||
storeAccountId: bigint,
|
||||
storeId: bigint,
|
||||
body: {
|
||||
token: string;
|
||||
errorClass: 'NETWORK' | 'BUSINESS';
|
||||
@@ -601,9 +615,7 @@ export class RedeemService {
|
||||
step: 'preview' | 'confirm';
|
||||
},
|
||||
) {
|
||||
const account = await this.prisma.storeAccount.findUniqueOrThrow({
|
||||
where: { id: storeAccountId },
|
||||
});
|
||||
await this.loadOpenStoreAccount(storeAccountId, storeId);
|
||||
const token = body.token.trim();
|
||||
if (!token) throw new BadRequestException('请提供核销码');
|
||||
|
||||
@@ -618,7 +630,7 @@ export class RedeemService {
|
||||
const thresholdReached = failCount >= REDEEM_WEAKNET_FAIL_THRESHOLD;
|
||||
|
||||
this.analyticsService.trackStoreOneSafe(storeAccountId, ClientApp.SHOP_H5, {
|
||||
storeId: account.storeId,
|
||||
storeId,
|
||||
eventName: 'store_redeem_confirm_fail',
|
||||
extraJson: {
|
||||
token,
|
||||
@@ -632,7 +644,7 @@ export class RedeemService {
|
||||
|
||||
if (thresholdReached && body.errorClass === 'NETWORK') {
|
||||
this.analyticsService.trackStoreOneSafe(storeAccountId, ClientApp.SHOP_H5, {
|
||||
storeId: account.storeId,
|
||||
storeId,
|
||||
eventName: 'store_redeem_weaknet_threshold',
|
||||
extraJson: {
|
||||
token,
|
||||
@@ -670,9 +682,10 @@ export class RedeemService {
|
||||
|
||||
async submitPendingRedeem(
|
||||
storeAccountId: bigint,
|
||||
storeId: bigint,
|
||||
body: { token: string; photoResourceId: string; failCount?: number; remark?: string },
|
||||
) {
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId);
|
||||
const account = await this.loadOpenStoreAccount(storeAccountId, storeId);
|
||||
const token = body.token.trim();
|
||||
if (!token) throw new BadRequestException('请提供核销码');
|
||||
|
||||
@@ -831,13 +844,7 @@ export class RedeemService {
|
||||
throw new BadRequestException('待处理单状态不可补核销');
|
||||
}
|
||||
|
||||
const account = await this.prisma.storeAccount.findUniqueOrThrow({
|
||||
where: { id: pending.storeAccountId },
|
||||
include: { store: true },
|
||||
});
|
||||
if (account.store.status !== 'OPEN') {
|
||||
throw new BadRequestException('门店未营业,无法补核销');
|
||||
}
|
||||
const account = await this.loadOpenStoreAccount(pending.storeAccountId, pending.storeId);
|
||||
|
||||
const allocationsRaw = pending.allocationsJson as Array<{ couponId: string; amount: number }>;
|
||||
const normalizedAllocations = allocationsRaw.map((item) => ({
|
||||
@@ -941,42 +948,42 @@ export class RedeemService {
|
||||
return serializeBigInt(updated);
|
||||
}
|
||||
|
||||
async listShopRecords(storeAccountId: bigint, page = 1, pageSize = 20) {
|
||||
const account = await this.prisma.storeAccount.findUniqueOrThrow({
|
||||
where: { id: storeAccountId },
|
||||
async listShopRecords(storeAccountId: bigint, storeId: bigint, page = 1, pageSize = 20) {
|
||||
await this.prisma.storeAccountStore.findUniqueOrThrow({
|
||||
where: { storeAccountId_storeId: { storeAccountId, storeId } },
|
||||
});
|
||||
const [list, total] = await Promise.all([
|
||||
this.prisma.redeemRecord.findMany({
|
||||
where: { storeId: account.storeId },
|
||||
where: { storeId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: { payout: true },
|
||||
}),
|
||||
this.prisma.redeemRecord.count({ where: { storeId: account.storeId } }),
|
||||
this.prisma.redeemRecord.count({ where: { storeId } }),
|
||||
]);
|
||||
return { list: serializeBigInt(list), total, page, pageSize };
|
||||
}
|
||||
|
||||
async getShopDashboard(storeAccountId: bigint) {
|
||||
const account = await this.prisma.storeAccount.findUniqueOrThrow({
|
||||
where: { id: storeAccountId },
|
||||
async getShopDashboard(storeAccountId: bigint, storeId: bigint) {
|
||||
const binding = await this.prisma.storeAccountStore.findUniqueOrThrow({
|
||||
where: { storeAccountId_storeId: { storeAccountId, storeId } },
|
||||
include: { store: true },
|
||||
});
|
||||
const start = new Date();
|
||||
start.setHours(0, 0, 0, 0);
|
||||
const records = await this.prisma.redeemRecord.findMany({
|
||||
where: { storeId: account.storeId, createdAt: { gte: start } },
|
||||
where: { storeId, createdAt: { gte: start } },
|
||||
});
|
||||
const todayCount = records.length;
|
||||
const todayAmount = records.reduce((sum, r) => sum + Number(r.amount), 0);
|
||||
const recent = await this.prisma.redeemRecord.findMany({
|
||||
where: { storeId: account.storeId },
|
||||
where: { storeId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 3,
|
||||
});
|
||||
return serializeBigInt({
|
||||
store: account.store,
|
||||
store: binding.store,
|
||||
todayCount,
|
||||
todayAmount,
|
||||
recentRecords: recent,
|
||||
|
||||
Reference in New Issue
Block a user