v4.0.9版本更新-门店分类支持多选
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-09-02 11:12:51 +08:00
parent 77e9917a18
commit fe557c912d
18 changed files with 742 additions and 239 deletions
@@ -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);
}
}