@@ -18,6 +18,12 @@ import { AnalyticsService } from '../analytics/analytics.service';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import { AuthService } from '../iam/auth.service';
|
||||
import { StoreCategoryService } from './store-category.service';
|
||||
import {
|
||||
attachStoreCategories,
|
||||
parseUniqueCategoryIds,
|
||||
storeCategoryLinkInclude,
|
||||
syncStoreCategoryLinks,
|
||||
} from './store-category-link.util';
|
||||
import { TencentLbsProvider } from '../../integrations/map/tencent-lbs.provider';
|
||||
import {
|
||||
TestWhitelistService,
|
||||
@@ -185,6 +191,7 @@ export class StoreService {
|
||||
where: where as never,
|
||||
include: {
|
||||
category: { include: { parent: true } },
|
||||
...storeCategoryLinkInclude,
|
||||
coverResource: true,
|
||||
},
|
||||
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
|
||||
@@ -226,11 +233,11 @@ export class StoreService {
|
||||
const coords = await this.ensureStoreCoordinates(store);
|
||||
const { visibilityWhitelistEnabled: _wl, ...rest } = store;
|
||||
const mapped = mapStoreCompat(
|
||||
{
|
||||
attachStoreCategories({
|
||||
...rest,
|
||||
latitude: coords?.latitude ?? store.latitude,
|
||||
longitude: coords?.longitude ?? store.longitude,
|
||||
},
|
||||
}),
|
||||
{ publicDial: true },
|
||||
);
|
||||
const distanceMeters =
|
||||
@@ -263,6 +270,7 @@ export class StoreService {
|
||||
where: { id, status: 'OPEN' },
|
||||
include: {
|
||||
category: { include: { parent: true } },
|
||||
...storeCategoryLinkInclude,
|
||||
coverResource: true,
|
||||
},
|
||||
});
|
||||
@@ -287,7 +295,7 @@ export class StoreService {
|
||||
const { visibilityWhitelistEnabled: _wl, ...rest } = store;
|
||||
return serializeBigInt(
|
||||
mapStoreCompat(
|
||||
{
|
||||
attachStoreCategories({
|
||||
...rest,
|
||||
latitude: coords?.latitude ?? store.latitude,
|
||||
longitude: coords?.longitude ?? store.longitude,
|
||||
@@ -310,7 +318,7 @@ export class StoreService {
|
||||
sortOrder: p.sortOrder,
|
||||
};
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{ publicDial: true },
|
||||
),
|
||||
);
|
||||
@@ -332,17 +340,17 @@ export class StoreService {
|
||||
}
|
||||
const stores = await this.prisma.store.findMany({
|
||||
where,
|
||||
include: { category: true, coverResource: true },
|
||||
include: { category: true, ...storeCategoryLinkInclude, coverResource: true },
|
||||
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
return serializeBigInt(stores.map((s) => mapStoreCompat(s)));
|
||||
return serializeBigInt(stores.map((s) => mapStoreCompat(attachStoreCategories(s))));
|
||||
}
|
||||
|
||||
async partnerGetStore(partnerAccountId: bigint, storeId: bigint) {
|
||||
const { account, primaryId } = await this.resolvePartnerScope(partnerAccountId);
|
||||
const store = await this.prisma.store.findFirst({
|
||||
where: { id: storeId, partnerAccountId: primaryId },
|
||||
include: { category: true, coverResource: true },
|
||||
include: { category: true, ...storeCategoryLinkInclude, coverResource: true },
|
||||
});
|
||||
if (!store) throw new NotFoundException('门店不存在');
|
||||
if (this.isSubAccount(account)) {
|
||||
@@ -350,7 +358,7 @@ export class StoreService {
|
||||
}
|
||||
|
||||
const media = await this.loadPartnerStoreMedia(storeId);
|
||||
return serializeBigInt(mapStoreCompat({ ...store, media }));
|
||||
return serializeBigInt(mapStoreCompat(attachStoreCategories({ ...store, media })));
|
||||
}
|
||||
|
||||
async partnerListCities(partnerAccountId: bigint) {
|
||||
@@ -444,11 +452,20 @@ export class StoreService {
|
||||
const bankAccountNo = body.bankAccountNo ? String(body.bankAccountNo) : null;
|
||||
const bankBranch = body.bankBranch ? String(body.bankBranch) : null;
|
||||
|
||||
if (!body.categoryId) {
|
||||
if (!body.categoryId && !Array.isArray(body.categoryIds)) {
|
||||
throw new BadRequestException('请选择店铺类型');
|
||||
}
|
||||
const categoryId = parseBigIntParam(body.categoryId, '分类ID');
|
||||
await this.storeCategoryService.assertLeafCategoryId(categoryId);
|
||||
const categoryIds = parseUniqueCategoryIds(body.categoryIds);
|
||||
if (!categoryIds.length && body.categoryId) {
|
||||
categoryIds.push(parseBigIntParam(body.categoryId, '分类ID'));
|
||||
}
|
||||
if (!categoryIds.length) {
|
||||
throw new BadRequestException('请选择店铺类型');
|
||||
}
|
||||
for (const id of categoryIds) {
|
||||
await this.storeCategoryService.assertLeafCategoryId(id);
|
||||
}
|
||||
const categoryId = categoryIds[0];
|
||||
|
||||
const openTime = body.openTime ? String(body.openTime).trim() : '10:00';
|
||||
const closeTime = body.closeTime ? String(body.closeTime).trim() : '22:00';
|
||||
@@ -507,6 +524,8 @@ export class StoreService {
|
||||
},
|
||||
});
|
||||
|
||||
await syncStoreCategoryLinks(this.prisma, store.id, categoryIds);
|
||||
|
||||
if (latitude == null || longitude == null) {
|
||||
await this.ensureStoreCoordinates(store);
|
||||
}
|
||||
@@ -1515,4 +1534,138 @@ export class StoreService {
|
||||
});
|
||||
if (!event) throw new ForbiddenException('无权查看该门店');
|
||||
}
|
||||
|
||||
async getStoreCategories(storeId: bigint) {
|
||||
const store = await this.prisma.store.findUnique({
|
||||
where: { id: storeId },
|
||||
include: {
|
||||
category: { include: { parent: true } },
|
||||
...storeCategoryLinkInclude,
|
||||
},
|
||||
});
|
||||
if (!store) throw new NotFoundException('门店不存在');
|
||||
return serializeBigInt(attachStoreCategories(store).categories);
|
||||
}
|
||||
|
||||
async partnerGetStoreCategories(partnerAccountId: bigint, storeId: bigint) {
|
||||
await this.partnerGetStore(partnerAccountId, storeId);
|
||||
return this.getStoreCategories(storeId);
|
||||
}
|
||||
|
||||
async assignCategoryToStore(storeId: bigint, categoryIdRaw: string, priority?: number) {
|
||||
const categoryId = parseBigIntParam(categoryIdRaw, '分类ID');
|
||||
await this.storeCategoryService.assertLeafCategoryId(categoryId);
|
||||
const store = await this.prisma.store.findUnique({ where: { id: storeId } });
|
||||
if (!store) throw new NotFoundException('门店不存在');
|
||||
|
||||
const existing = await this.prisma.storeCategoryLink.findUnique({
|
||||
where: { storeId_categoryId: { storeId, categoryId } },
|
||||
});
|
||||
if (existing) {
|
||||
if (priority != null) {
|
||||
await this.prisma.storeCategoryLink.update({
|
||||
where: { id: existing.id },
|
||||
data: { priority },
|
||||
});
|
||||
}
|
||||
return this.getStoreCategories(storeId);
|
||||
}
|
||||
|
||||
const nextPriority =
|
||||
priority ??
|
||||
((await this.prisma.storeCategoryLink.count({ where: { storeId } })) || 0);
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.storeCategoryLink.create({
|
||||
data: { storeId, categoryId, priority: nextPriority },
|
||||
});
|
||||
if (!store.categoryId) {
|
||||
await tx.store.update({ where: { id: storeId }, data: { categoryId } });
|
||||
}
|
||||
});
|
||||
return this.getStoreCategories(storeId);
|
||||
}
|
||||
|
||||
async partnerAssignCategoryToStore(
|
||||
partnerAccountId: bigint,
|
||||
storeId: bigint,
|
||||
categoryId: string,
|
||||
priority?: number,
|
||||
) {
|
||||
await this.partnerGetStore(partnerAccountId, storeId);
|
||||
return this.assignCategoryToStore(storeId, categoryId, priority);
|
||||
}
|
||||
|
||||
async removeCategoryFromStore(storeId: bigint, categoryIdRaw: string) {
|
||||
const categoryId = parseBigIntParam(categoryIdRaw, '分类ID');
|
||||
const store = await this.prisma.store.findUnique({ where: { id: storeId } });
|
||||
if (!store) throw new NotFoundException('门店不存在');
|
||||
|
||||
const linkCount = await this.prisma.storeCategoryLink.count({ where: { storeId } });
|
||||
const legacyOnly = linkCount === 0 && store.categoryId?.toString() === categoryId.toString();
|
||||
if (linkCount <= 1 && !legacyOnly) {
|
||||
throw new BadRequestException('门店至少保留一个分类');
|
||||
}
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.storeCategoryLink.deleteMany({ where: { storeId, categoryId } });
|
||||
const remaining = await tx.storeCategoryLink.findMany({
|
||||
where: { storeId },
|
||||
orderBy: [{ priority: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
const nextPrimary = remaining[0]?.categoryId ?? null;
|
||||
await tx.store.update({
|
||||
where: { id: storeId },
|
||||
data: { categoryId: nextPrimary },
|
||||
});
|
||||
});
|
||||
return this.getStoreCategories(storeId);
|
||||
}
|
||||
|
||||
async partnerRemoveCategoryFromStore(
|
||||
partnerAccountId: bigint,
|
||||
storeId: bigint,
|
||||
categoryId: string,
|
||||
) {
|
||||
await this.partnerGetStore(partnerAccountId, storeId);
|
||||
return this.removeCategoryFromStore(storeId, categoryId);
|
||||
}
|
||||
|
||||
async replaceStoreCategories(
|
||||
storeId: bigint,
|
||||
categoryIdsRaw: string[],
|
||||
priorities?: Record<string, number>,
|
||||
) {
|
||||
if (!categoryIdsRaw?.length) {
|
||||
throw new BadRequestException('请至少选择一个门店分类');
|
||||
}
|
||||
const parsed = parseUniqueCategoryIds(categoryIdsRaw);
|
||||
if (!parsed.length) {
|
||||
throw new BadRequestException('请至少选择一个门店分类');
|
||||
}
|
||||
for (const id of parsed) {
|
||||
await this.storeCategoryService.assertLeafCategoryId(id);
|
||||
}
|
||||
const sorted = [...parsed].sort((a, b) => {
|
||||
const pa = priorities?.[a.toString()] ?? 0;
|
||||
const pb = priorities?.[b.toString()] ?? 0;
|
||||
if (pa !== pb) return pa - pb;
|
||||
return Number(a - b);
|
||||
});
|
||||
const store = await this.prisma.store.findUnique({ where: { id: storeId } });
|
||||
if (!store) throw new NotFoundException('门店不存在');
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await syncStoreCategoryLinks(tx, storeId, sorted);
|
||||
});
|
||||
return this.getStoreCategories(storeId);
|
||||
}
|
||||
|
||||
async partnerReplaceStoreCategories(
|
||||
partnerAccountId: bigint,
|
||||
storeId: bigint,
|
||||
categoryIds: string[],
|
||||
priorities?: Record<string, number>,
|
||||
) {
|
||||
await this.partnerGetStore(partnerAccountId, storeId);
|
||||
return this.replaceStoreCategories(storeId, categoryIds, priorities);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user