门店账户多账号

This commit is contained in:
2026-07-12 12:24:34 +08:00
parent 06b1cb22e0
commit 54a15d6da7
39 changed files with 1962 additions and 311 deletions
@@ -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,