门店账户多账号

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
@@ -116,17 +116,34 @@ export class StoreService {
}
const existingAccount = await this.prisma.storeAccount.findUnique({
where: { phone: normalizedPhone },
include: { _count: { select: { bindings: true } } },
});
if (existingAccount) {
return { available: false, message: '该手机号已绑定门店' };
if (!existingAccount) {
return { available: true, existingStoreCount: 0, needConfirm: false };
}
return { available: true };
if (existingAccount.isPrimary !== 1) {
return { available: false, message: '该手机号已是门店子账号,不可作为负责人' };
}
if (existingAccount.status !== 'ACTIVE') {
return { available: false, message: '该手机号对应门店账号已停用' };
}
const existingStoreCount = existingAccount._count.bindings;
return {
available: true,
existingStoreCount,
needConfirm: existingStoreCount > 0,
message:
existingStoreCount > 0
? `该手机号已是门店主账号(已绑 ${existingStoreCount} 家店),确认后将追加绑定新店`
: undefined,
};
}
async createStore(partnerAccountId: bigint, body: Record<string, unknown>) {
const { primaryId } = await this.resolvePartnerScope(partnerAccountId);
const normalizedPhone = String(body.phone).trim();
await this.assertStorePhoneAvailable(normalizedPhone);
const confirmBindExisting = body.confirmBindExisting === true || body.confirmBindExisting === 'true';
await this.assertStorePhoneAvailable(normalizedPhone, confirmBindExisting);
const city = await this.resolvePartnerCity(partnerAccountId, body.cityId);
const coverUrl = body.coverUrl ? String(body.coverUrl).trim() : '';
@@ -139,6 +156,10 @@ export class StoreService {
if (envPhotoUrls.length < 3) throw new BadRequestException('请上传至少 3 张环境照片');
if (!contractUrl) throw new BadRequestException('请上传签约合同');
const bankAccountName = body.bankAccountName ? String(body.bankAccountName) : null;
const bankAccountNo = body.bankAccountNo ? String(body.bankAccountNo) : null;
const bankBranch = body.bankBranch ? String(body.bankBranch) : null;
const store = await this.prisma.store.create({
data: {
cityId: city.id,
@@ -151,9 +172,6 @@ export class StoreService {
district: String(body.district ?? ''),
address: String(body.address),
intro: body.intro ? String(body.intro) : null,
bankAccountName: body.bankAccountName ? String(body.bankAccountName) : null,
bankAccountNo: body.bankAccountNo ? String(body.bankAccountNo) : null,
bankBranch: body.bankBranch ? String(body.bankBranch) : null,
openTime: body.openTime ? String(body.openTime) : '10:00',
closeTime: body.closeTime ? String(body.closeTime) : '22:00',
status: this.config.autoApproveStore ? 'OPEN' : 'PAUSED',
@@ -219,14 +237,38 @@ export class StoreService {
extraJson: body as never,
},
});
void audit;
await this.prisma.storeAccount.create({
data: {
storeId: store.id,
phone: normalizedPhone,
name: String(body.name),
},
const existingPrimary = await this.prisma.storeAccount.findUnique({
where: { phone: normalizedPhone },
});
if (existingPrimary && existingPrimary.isPrimary === 1) {
await this.prisma.storeAccountStore.create({
data: { storeAccountId: existingPrimary.id, storeId: store.id },
});
if (bankAccountName || bankAccountNo || bankBranch) {
await this.prisma.storeAccount.update({
where: { id: existingPrimary.id },
data: {
...(bankAccountName != null ? { bankAccountName } : {}),
...(bankAccountNo != null ? { bankAccountNo } : {}),
...(bankBranch != null ? { bankBranch } : {}),
},
});
}
} else {
await this.prisma.storeAccount.create({
data: {
phone: normalizedPhone,
name: String(body.name),
isPrimary: 1,
bankAccountName,
bankAccountNo,
bankBranch,
bindings: { create: [{ storeId: store.id }] },
},
});
}
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
partnerAccountId: primaryId,
@@ -321,26 +363,26 @@ export class StoreService {
return this.partnerGetStore(partnerAccountId, storeId);
}
async getShopStore(storeAccountId: bigint) {
const account = await this.prisma.storeAccount.findUniqueOrThrow({
where: { id: storeAccountId },
async getShopStore(storeAccountId: bigint, storeId: bigint) {
const binding = await this.prisma.storeAccountStore.findUniqueOrThrow({
where: { storeAccountId_storeId: { storeAccountId, storeId } },
include: { store: { include: { category: true, coverResource: true } } },
});
return serializeBigInt(mapStoreCompat(account.store));
return serializeBigInt(mapStoreCompat(binding.store));
}
async updateShopStatus(storeAccountId: bigint, status: 'OPEN' | 'PAUSED') {
const account = await this.prisma.storeAccount.findUniqueOrThrow({
where: { id: storeAccountId },
async updateShopStatus(storeAccountId: bigint, storeId: bigint, status: 'OPEN' | 'PAUSED') {
const binding = await this.prisma.storeAccountStore.findUniqueOrThrow({
where: { storeAccountId_storeId: { storeAccountId, storeId } },
include: { store: true },
});
const previousStatus = account.store.status;
const previousStatus = binding.store.status;
const store = await this.prisma.store.update({
where: { id: account.storeId },
where: { id: storeId },
data: { status },
});
this.analyticsService.trackStoreOneSafe(storeAccountId, 'SHOP_H5', {
storeId: account.storeId,
storeId,
eventName: 'store_status_change',
extraJson: {
status,
@@ -721,10 +763,13 @@ export class StoreService {
}
}
private async assertStorePhoneAvailable(phone: string) {
private async assertStorePhoneAvailable(phone: string, confirmBindExisting = false) {
const result = await this.partnerCheckStorePhone(phone);
if (!result.available) {
throw new BadRequestException(result.message ?? '该手机号已绑定门店');
throw new BadRequestException(result.message ?? '该手机号不可用');
}
if (result.needConfirm && !confirmBindExisting) {
throw new BadRequestException(result.message ?? '该手机号已绑定门店,请确认后重试');
}
}