('PARTNER_H5', '/partner/store-categories')
+ .then((list) => {
+ const tree = Array.isArray(list) ? list : [];
+ setCategoryTree(tree);
+ if (form.categoryId && !form.categoryParentId) {
+ const parent = tree.find((root) =>
+ (root.children ?? []).some((child) => child.id === form.categoryId),
+ );
+ if (parent) patchForm({ categoryParentId: parent.id });
+ }
+ })
+ .catch(() => setCategoryTree([]));
+ }, []);
+
+ const categoryChildren = useMemo(() => {
+ const parent = categoryTree.find((item) => item.id === form.categoryParentId);
+ return Array.isArray(parent?.children) ? parent!.children! : [];
+ }, [categoryTree, form.categoryParentId]);
+
useEffect(() => {
@@ -491,6 +514,8 @@ export default function StoreCreatePage() {
closeTime: form.closeTime.trim(),
+ categoryId: form.categoryId.trim(),
+
intro: form.intro.trim() || undefined,
coverUrl: form.coverUrl.trim() || undefined,
@@ -653,6 +678,62 @@ export default function StoreCreatePage() {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/dukang-api/prisma/schema.prisma b/server/dukang-api/prisma/schema.prisma
index 0004882..521a622 100644
--- a/server/dukang-api/prisma/schema.prisma
+++ b/server/dukang-api/prisma/schema.prisma
@@ -477,12 +477,18 @@ model CommonProductDetailTemplate {
}
model CommonStoreCategory {
- id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
- code String @unique @db.VarChar(32)
- name String @db.VarChar(64)
- sort Int @default(0)
- stores Store[]
+ id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
+ code String @unique @db.VarChar(32)
+ name String @db.VarChar(64)
+ sort Int @default(0)
+ parentId BigInt? @map("parent_id") @db.UnsignedBigInt
+ status String @default("ACTIVE") @db.VarChar(16)
+ parent CommonStoreCategory? @relation("StoreCategoryTree", fields: [parentId], references: [id], onDelete: Restrict)
+ children CommonStoreCategory[] @relation("StoreCategoryTree")
+ stores Store[]
+ @@index([parentId, sort])
+ @@index([status])
@@map("common_store_category")
}
diff --git a/server/dukang-api/prisma/schema.v31.prisma b/server/dukang-api/prisma/schema.v31.prisma
index e5f3b83..2ebe59f 100644
--- a/server/dukang-api/prisma/schema.v31.prisma
+++ b/server/dukang-api/prisma/schema.v31.prisma
@@ -335,12 +335,18 @@ model CommonProductItem {
}
model CommonStoreCategory {
- id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
- code String @unique @db.VarChar(32)
- name String @db.VarChar(64)
- sort Int @default(0)
- stores Store[]
+ id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
+ code String @unique @db.VarChar(32)
+ name String @db.VarChar(64)
+ sort Int @default(0)
+ parentId BigInt? @map("parent_id") @db.UnsignedBigInt
+ status String @default("ACTIVE") @db.VarChar(16)
+ parent CommonStoreCategory? @relation("StoreCategoryTree", fields: [parentId], references: [id], onDelete: Restrict)
+ children CommonStoreCategory[] @relation("StoreCategoryTree")
+ stores Store[]
+ @@index([parentId, sort])
+ @@index([status])
@@map("common_store_category")
}
diff --git a/server/dukang-api/prisma/seed-v31.ts b/server/dukang-api/prisma/seed-v31.ts
index 026dd37..0010a31 100644
--- a/server/dukang-api/prisma/seed-v31.ts
+++ b/server/dukang-api/prisma/seed-v31.ts
@@ -266,13 +266,36 @@ async function main() {
- const categories = await Promise.all([
-
- prisma.commonStoreCategory.create({ data: { code: 'HOTPOT', name: '火锅', sort: 1 } }),
-
- prisma.commonStoreCategory.create({ data: { code: 'LOCAL', name: '地方菜', sort: 2 } }),
-
- ]);
+ const { DEFAULT_STORE_CATEGORY_TREE } = await import('../src/modules/store/store-category.defaults');
+ const categoryByCode = new Map();
+ for (const root of DEFAULT_STORE_CATEGORY_TREE) {
+ const parent = await prisma.commonStoreCategory.create({
+ data: {
+ code: root.code,
+ name: root.name,
+ sort: root.sort,
+ parentId: null,
+ status: 'ACTIVE',
+ },
+ });
+ categoryByCode.set(root.code, parent);
+ for (const child of root.children) {
+ const row = await prisma.commonStoreCategory.create({
+ data: {
+ code: child.code,
+ name: child.name,
+ sort: child.sort,
+ parentId: parent.id,
+ status: 'ACTIVE',
+ },
+ });
+ categoryByCode.set(child.code, row);
+ }
+ }
+ const categories = [
+ categoryByCode.get('HOTPOT')!,
+ categoryByCode.get('LOCAL')!,
+ ];
diff --git a/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts b/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts
index f490360..079ffaa 100644
--- a/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts
+++ b/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts
@@ -31,6 +31,10 @@ export const HqOperationAction = {
STORE_MEDIA_CREATE: 'STORE_MEDIA_CREATE',
STORE_MEDIA_UPDATE: 'STORE_MEDIA_UPDATE',
STORE_MEDIA_DELETE: 'STORE_MEDIA_DELETE',
+ STORE_CATEGORY_CREATE: 'STORE_CATEGORY_CREATE',
+ STORE_CATEGORY_UPDATE: 'STORE_CATEGORY_UPDATE',
+ STORE_CATEGORY_DELETE: 'STORE_CATEGORY_DELETE',
+ STORE_CATEGORY_ENSURE: 'STORE_CATEGORY_ENSURE',
PRODUCT_CREATE: 'PRODUCT_CREATE',
PRODUCT_UPDATE: 'PRODUCT_UPDATE',
PRODUCT_DELETE: 'PRODUCT_DELETE',
@@ -94,6 +98,10 @@ export const HQ_OPERATION_ACTION_LABELS: Record = {
[HqOperationAction.STORE_MEDIA_CREATE]: '新增门店资源',
[HqOperationAction.STORE_MEDIA_UPDATE]: '编辑门店资源',
[HqOperationAction.STORE_MEDIA_DELETE]: '删除门店资源',
+ [HqOperationAction.STORE_CATEGORY_CREATE]: '新增门店分类',
+ [HqOperationAction.STORE_CATEGORY_UPDATE]: '编辑门店分类',
+ [HqOperationAction.STORE_CATEGORY_DELETE]: '删除门店分类',
+ [HqOperationAction.STORE_CATEGORY_ENSURE]: '初始化默认门店分类',
[HqOperationAction.PRODUCT_CREATE]: '新增商品',
[HqOperationAction.PRODUCT_UPDATE]: '编辑商品',
[HqOperationAction.PRODUCT_DELETE]: '删除商品',
diff --git a/server/dukang-api/src/modules/ops/admin-store-categories.controller.ts b/server/dukang-api/src/modules/ops/admin-store-categories.controller.ts
new file mode 100644
index 0000000..0285678
--- /dev/null
+++ b/server/dukang-api/src/modules/ops/admin-store-categories.controller.ts
@@ -0,0 +1,68 @@
+import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
+import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
+import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
+import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
+import { StoreCategoryService } from '../store/store-category.service';
+import {
+ CreateStoreCategoryDto,
+ UpdateStoreCategoryDto,
+} from './dto/admin-mutate.dto';
+
+@Controller('admin/store-categories')
+@UseGuards(HqAuthGuard)
+export class AdminStoreCategoriesController {
+ constructor(private readonly categories: StoreCategoryService) {}
+
+ @Get()
+ list() {
+ return this.categories.listTree({ includeDisabled: true, ensure: true });
+ }
+
+ @Get('flat')
+ listFlat() {
+ return this.categories.listFlat({ includeDisabled: true, ensure: true });
+ }
+
+ @Post('ensure-defaults')
+ @HqOperation({
+ action: HqOperationAction.STORE_CATEGORY_ENSURE,
+ refType: 'STORE_CATEGORY',
+ includeBody: false,
+ })
+ async ensureDefaults() {
+ await this.categories.ensureDefaults();
+ return this.categories.listTree({ includeDisabled: true, ensure: false });
+ }
+
+ @Post()
+ @HqOperation({
+ action: HqOperationAction.STORE_CATEGORY_CREATE,
+ refType: 'STORE_CATEGORY',
+ refIdField: 'id',
+ includeBody: true,
+ })
+ create(@Body() dto: CreateStoreCategoryDto) {
+ return this.categories.create(dto);
+ }
+
+ @Put(':id')
+ @HqOperation({
+ action: HqOperationAction.STORE_CATEGORY_UPDATE,
+ refType: 'STORE_CATEGORY',
+ refIdParam: 'id',
+ includeBody: true,
+ })
+ update(@Param('id') id: string, @Body() dto: UpdateStoreCategoryDto) {
+ return this.categories.update(BigInt(id), dto);
+ }
+
+ @Delete(':id')
+ @HqOperation({
+ action: HqOperationAction.STORE_CATEGORY_DELETE,
+ refType: 'STORE_CATEGORY',
+ refIdParam: 'id',
+ })
+ remove(@Param('id') id: string) {
+ return this.categories.remove(BigInt(id));
+ }
+}
diff --git a/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts b/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts
index e8fe9c5..72dd471 100644
--- a/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts
+++ b/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts
@@ -1150,3 +1150,53 @@ export class AdminBenefitGrantDto {
@IsString()
remark?: string;
}
+
+export class CreateStoreCategoryDto {
+ @IsString()
+ @IsNotEmpty()
+ code: string;
+
+ @IsString()
+ @IsNotEmpty()
+ name: string;
+
+ @IsOptional()
+ @Type(() => Number)
+ @IsNumber()
+ sort?: number;
+
+ /** 空/不传 = 一级分类;传入一级 id = 二级分类 */
+ @IsOptional()
+ @IsString()
+ parentId?: string | null;
+
+ @IsOptional()
+ @IsIn(['ACTIVE', 'DISABLED'])
+ status?: string;
+}
+
+export class UpdateStoreCategoryDto {
+ @IsOptional()
+ @IsString()
+ @IsNotEmpty()
+ code?: string;
+
+ @IsOptional()
+ @IsString()
+ @IsNotEmpty()
+ name?: string;
+
+ @IsOptional()
+ @Type(() => Number)
+ @IsNumber()
+ sort?: number;
+
+ @IsOptional()
+ @ValidateIf((_, v) => v !== null && v !== undefined)
+ @IsString()
+ parentId?: string | null;
+
+ @IsOptional()
+ @IsIn(['ACTIVE', 'DISABLED'])
+ status?: string;
+}
diff --git a/server/dukang-api/src/modules/ops/ops.module.ts b/server/dukang-api/src/modules/ops/ops.module.ts
index b936a10..84fe912 100644
--- a/server/dukang-api/src/modules/ops/ops.module.ts
+++ b/server/dukang-api/src/modules/ops/ops.module.ts
@@ -3,6 +3,7 @@ import { CityScopeModule } from '../city-scope/city-scope.module';
import { IamModule } from '../iam/iam.module';
import { TradeModule } from '../trade/trade.module';
import { FulfillmentModule } from '../fulfillment/fulfillment.module';
+import { StoreModule } from '../store/store.module';
import { AdminDashboardController } from './admin-dashboard.controller';
import { AdminDashboardService } from './admin-dashboard.service';
import { AdminUsersController } from './admin-users.controller';
@@ -10,6 +11,7 @@ import { AdminUsersService } from './admin-users.service';
import { AdminOrdersController } from './admin-orders.controller';
import { AdminOrdersService } from './admin-orders.service';
import { AdminStoresController, AdminStoreAccountsController, AdminStoreMediaController } from './admin-stores.controller';
+import { AdminStoreCategoriesController } from './admin-store-categories.controller';
import { AdminStoresService } from './admin-stores.service';
import { AdminPartnersController, AdminPartnerAccountsController } from './admin-partners.controller';
import { AdminPartnersService } from './admin-partners.service';
@@ -59,7 +61,7 @@ import { AdminSystemConfigController } from './admin-system-config.controller';
import { AdminFulfillmentProvidersController } from './admin-fulfillment-providers.controller';
@Module({
- imports: [CityScopeModule, IamModule, TradeModule, FulfillmentModule, BenefitModule, CommonModule, IntegrationsModule, RedeemModule],
+ imports: [CityScopeModule, IamModule, TradeModule, FulfillmentModule, BenefitModule, CommonModule, IntegrationsModule, RedeemModule, StoreModule],
controllers: [
AdminDashboardController,
AdminDeployController,
@@ -68,6 +70,7 @@ import { AdminFulfillmentProvidersController } from './admin-fulfillment-provide
AdminStoresController,
AdminStoreAccountsController,
AdminStoreMediaController,
+ AdminStoreCategoriesController,
AdminPartnersController,
AdminPartnerAccountsController,
AdminCitiesController,
diff --git a/server/dukang-api/src/modules/store/store-category.defaults.ts b/server/dukang-api/src/modules/store/store-category.defaults.ts
new file mode 100644
index 0000000..c7527a0
--- /dev/null
+++ b/server/dukang-api/src/modules/store/store-category.defaults.ts
@@ -0,0 +1,33 @@
+/** 门店两级分类默认树(一级 + 二级) */
+export const DEFAULT_STORE_CATEGORY_TREE = [
+ {
+ code: 'DINING',
+ name: '餐饮',
+ sort: 1,
+ children: [
+ { code: 'LOCAL', name: '地方菜', sort: 1 },
+ { code: 'WESTERN', name: '西餐', sort: 2 },
+ { code: 'BBQ', name: '烧烤', sort: 3 },
+ { code: 'HOTPOT', name: '火锅', sort: 4 },
+ ],
+ },
+ {
+ code: 'LODGING',
+ name: '住宿',
+ sort: 2,
+ children: [
+ { code: 'BUDGET_HOTEL', name: '快捷酒店', sort: 1 },
+ { code: 'INN', name: '旅馆', sort: 2 },
+ { code: 'LUXURY_HOTEL', name: '豪华酒店', sort: 3 },
+ ],
+ },
+ {
+ code: 'ENTERTAINMENT',
+ name: '娱乐',
+ sort: 3,
+ children: [
+ { code: 'KTV', name: 'KTV', sort: 1 },
+ { code: 'CLUB', name: '会所', sort: 2 },
+ ],
+ },
+] as const;
diff --git a/server/dukang-api/src/modules/store/store-category.service.ts b/server/dukang-api/src/modules/store/store-category.service.ts
new file mode 100644
index 0000000..efd2cf3
--- /dev/null
+++ b/server/dukang-api/src/modules/store/store-category.service.ts
@@ -0,0 +1,267 @@
+import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
+import { Prisma } from '@prisma/client';
+import { PrismaService } from '../../common/prisma/prisma.module';
+import { serializeBigInt } from '../../common/decorators/current-user.decorator';
+import { DEFAULT_STORE_CATEGORY_TREE } from './store-category.defaults';
+
+export type StoreCategoryNode = {
+ id: string;
+ code: string;
+ name: string;
+ sort: number;
+ parentId: string | null;
+ status: string;
+ children: StoreCategoryNode[];
+};
+
+@Injectable()
+export class StoreCategoryService {
+ constructor(private readonly prisma: PrismaService) {}
+
+ /**
+ * 仅补齐缺失的默认分类,绝不 update 已有行(避免改动线上历史数据)。
+ * 已存在的 HOTPOT/LOCAL 等保持原样;缺的一级/二级才 create。
+ */
+ async ensureDefaults() {
+ for (const root of DEFAULT_STORE_CATEGORY_TREE) {
+ let parent = await this.prisma.commonStoreCategory.findUnique({
+ where: { code: root.code },
+ });
+ if (!parent) {
+ parent = await this.prisma.commonStoreCategory.create({
+ data: {
+ code: root.code,
+ name: root.name,
+ sort: root.sort,
+ parentId: null,
+ status: 'ACTIVE',
+ },
+ });
+ }
+ for (const child of root.children) {
+ const existing = await this.prisma.commonStoreCategory.findUnique({
+ where: { code: child.code },
+ });
+ if (existing) continue;
+ await this.prisma.commonStoreCategory.create({
+ data: {
+ code: child.code,
+ name: child.name,
+ sort: child.sort,
+ parentId: parent.id,
+ status: 'ACTIVE',
+ },
+ });
+ }
+ }
+ }
+
+ private mapNode(
+ row: {
+ id: bigint;
+ code: string;
+ name: string;
+ sort: number;
+ parentId: bigint | null;
+ status: string;
+ children?: Array<{
+ id: bigint;
+ code: string;
+ name: string;
+ sort: number;
+ parentId: bigint | null;
+ status: string;
+ }>;
+ },
+ includeDisabledChildren: boolean,
+ ): StoreCategoryNode {
+ const children = (row.children ?? [])
+ .filter((c) => includeDisabledChildren || c.status === 'ACTIVE')
+ .sort((a, b) => a.sort - b.sort || Number(a.id - b.id))
+ .map((c) => this.mapNode(c, includeDisabledChildren));
+ return {
+ id: String(row.id),
+ code: row.code,
+ name: row.name,
+ sort: row.sort,
+ parentId: row.parentId != null ? String(row.parentId) : null,
+ status: row.status,
+ children,
+ };
+ }
+
+ async listTree(options?: { includeDisabled?: boolean; ensure?: boolean }) {
+ if (options?.ensure !== false) {
+ await this.ensureDefaults();
+ }
+ const includeDisabled = options?.includeDisabled === true;
+ const roots = await this.prisma.commonStoreCategory.findMany({
+ where: {
+ parentId: null,
+ ...(includeDisabled ? {} : { status: 'ACTIVE' }),
+ },
+ include: {
+ children: {
+ orderBy: [{ sort: 'asc' }, { id: 'asc' }],
+ },
+ },
+ orderBy: [{ sort: 'asc' }, { id: 'asc' }],
+ });
+ return serializeBigInt(roots.map((r) => this.mapNode(r, includeDisabled)));
+ }
+
+ async listFlat(options?: { includeDisabled?: boolean; ensure?: boolean }) {
+ if (options?.ensure !== false) {
+ await this.ensureDefaults();
+ }
+ const includeDisabled = options?.includeDisabled === true;
+ const rows = await this.prisma.commonStoreCategory.findMany({
+ where: includeDisabled ? undefined : { status: 'ACTIVE' },
+ orderBy: [{ parentId: 'asc' }, { sort: 'asc' }, { id: 'asc' }],
+ });
+ return serializeBigInt(
+ rows.map((row) => ({
+ id: String(row.id),
+ code: row.code,
+ name: row.name,
+ sort: row.sort,
+ parentId: row.parentId != null ? String(row.parentId) : null,
+ status: row.status,
+ level: row.parentId == null ? 1 : 2,
+ })),
+ );
+ }
+
+ async assertLeafCategoryId(categoryId: bigint) {
+ const row = await this.prisma.commonStoreCategory.findUnique({ where: { id: categoryId } });
+ if (!row || row.status !== 'ACTIVE') {
+ throw new BadRequestException('店铺类型不存在或已停用');
+ }
+ if (row.parentId == null) {
+ throw new BadRequestException('请选择二级店铺类型');
+ }
+ return row;
+ }
+
+ async create(dto: {
+ code: string;
+ name: string;
+ sort?: number;
+ parentId?: string | null;
+ status?: string;
+ }) {
+ const code = dto.code.trim().toUpperCase();
+ const name = dto.name.trim();
+ if (!code) throw new BadRequestException('请填写分类编码');
+ if (!name) throw new BadRequestException('请填写分类名称');
+ let parentId: bigint | null = null;
+ if (dto.parentId) {
+ const parent = await this.prisma.commonStoreCategory.findUnique({
+ where: { id: BigInt(dto.parentId) },
+ });
+ if (!parent || parent.parentId != null) {
+ throw new BadRequestException('父级必须是一级分类');
+ }
+ parentId = parent.id;
+ }
+ try {
+ const row = await this.prisma.commonStoreCategory.create({
+ data: {
+ code,
+ name,
+ sort: dto.sort ?? 0,
+ parentId,
+ status: dto.status === 'DISABLED' ? 'DISABLED' : 'ACTIVE',
+ },
+ });
+ return serializeBigInt(row);
+ } catch (e) {
+ if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
+ throw new BadRequestException('分类编码已存在');
+ }
+ throw e;
+ }
+ }
+
+ async update(
+ id: bigint,
+ dto: {
+ code?: string;
+ name?: string;
+ sort?: number;
+ parentId?: string | null;
+ status?: string;
+ },
+ ) {
+ const existing = await this.prisma.commonStoreCategory.findUnique({ where: { id } });
+ if (!existing) throw new NotFoundException('分类不存在');
+
+ let parentId: bigint | null | undefined = undefined;
+ if (dto.parentId !== undefined) {
+ if (dto.parentId == null || dto.parentId === '') {
+ parentId = null;
+ } else {
+ if (BigInt(dto.parentId) === id) {
+ throw new BadRequestException('不能将分类设为自己的子级');
+ }
+ const parent = await this.prisma.commonStoreCategory.findUnique({
+ where: { id: BigInt(dto.parentId) },
+ });
+ if (!parent || parent.parentId != null) {
+ throw new BadRequestException('父级必须是一级分类');
+ }
+ // 一级分类若已有子级,不允许变成二级
+ if (existing.parentId == null) {
+ const childCount = await this.prisma.commonStoreCategory.count({
+ where: { parentId: id },
+ });
+ if (childCount > 0) {
+ throw new BadRequestException('该一级分类下仍有二级分类,不能改为二级');
+ }
+ }
+ parentId = parent.id;
+ }
+ }
+
+ try {
+ const row = await this.prisma.commonStoreCategory.update({
+ where: { id },
+ data: {
+ ...(dto.code !== undefined ? { code: dto.code.trim().toUpperCase() } : {}),
+ ...(dto.name !== undefined ? { name: dto.name.trim() } : {}),
+ ...(dto.sort !== undefined ? { sort: dto.sort } : {}),
+ ...(parentId !== undefined ? { parentId } : {}),
+ ...(dto.status !== undefined
+ ? { status: dto.status === 'DISABLED' ? 'DISABLED' : 'ACTIVE' }
+ : {}),
+ },
+ });
+ return serializeBigInt(row);
+ } catch (e) {
+ if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
+ throw new BadRequestException('分类编码已存在');
+ }
+ throw e;
+ }
+ }
+
+ async remove(id: bigint) {
+ const existing = await this.prisma.commonStoreCategory.findUnique({ where: { id } });
+ if (!existing) throw new NotFoundException('分类不存在');
+ const childCount = await this.prisma.commonStoreCategory.count({ where: { parentId: id } });
+ if (childCount > 0) {
+ throw new BadRequestException('请先删除或停用下级分类');
+ }
+ const storeCount = await this.prisma.store.count({ where: { categoryId: id } });
+ if (storeCount > 0) {
+ // 软停用,避免破坏已有门店关联
+ const row = await this.prisma.commonStoreCategory.update({
+ where: { id },
+ data: { status: 'DISABLED' },
+ });
+ return serializeBigInt({ ...row, softDisabled: true });
+ }
+ await this.prisma.commonStoreCategory.delete({ where: { id } });
+ return { id: String(id), deleted: true };
+ }
+}
diff --git a/server/dukang-api/src/modules/store/store.controller.ts b/server/dukang-api/src/modules/store/store.controller.ts
index c14ea9c..b11064a 100644
--- a/server/dukang-api/src/modules/store/store.controller.ts
+++ b/server/dukang-api/src/modules/store/store.controller.ts
@@ -1,5 +1,6 @@
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
import { StoreService } from './store.service';
+import { StoreCategoryService } from './store-category.service';
import { RedeemService } from '../redeem/redeem.service';
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
@@ -23,6 +24,17 @@ export class PublicStoreController {
}
}
+@Controller('partner/store-categories')
+@UseGuards(JwtAuthGuard)
+export class PartnerStoreCategoriesController {
+ constructor(private readonly categories: StoreCategoryService) {}
+
+ @Get()
+ list() {
+ return this.categories.listTree({ includeDisabled: false, ensure: true });
+ }
+}
+
@Controller('partner/stores')
@UseGuards(JwtAuthGuard)
export class PartnerStoreController {
diff --git a/server/dukang-api/src/modules/store/store.module.ts b/server/dukang-api/src/modules/store/store.module.ts
index 30db0ec..11a9739 100644
--- a/server/dukang-api/src/modules/store/store.module.ts
+++ b/server/dukang-api/src/modules/store/store.module.ts
@@ -4,9 +4,11 @@ import { RedeemModule } from '../redeem/redeem.module';
import { AnalyticsModule } from '../analytics/analytics.module';
import { CityScopeModule } from '../city-scope/city-scope.module';
import { StoreService } from './store.service';
+import { StoreCategoryService } from './store-category.service';
import {
PartnerDashboardController,
PartnerReportController,
+ PartnerStoreCategoriesController,
PartnerStoreController,
PublicStoreController,
ShopDashboardController,
@@ -17,13 +19,14 @@ import {
imports: [IamModule, AnalyticsModule, CityScopeModule, forwardRef(() => RedeemModule)],
controllers: [
PublicStoreController,
+ PartnerStoreCategoriesController,
PartnerStoreController,
PartnerDashboardController,
PartnerReportController,
ShopStoreController,
ShopDashboardController,
],
- providers: [StoreService],
- exports: [StoreService],
+ providers: [StoreService, StoreCategoryService],
+ exports: [StoreService, StoreCategoryService],
})
export class StoreModule {}
diff --git a/server/dukang-api/src/modules/store/store.service.ts b/server/dukang-api/src/modules/store/store.service.ts
index ff95efc..27a9242 100644
--- a/server/dukang-api/src/modules/store/store.service.ts
+++ b/server/dukang-api/src/modules/store/store.service.ts
@@ -13,6 +13,7 @@ import { parseBigIntParam } from '../../common/parse-bigint';
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';
@Injectable()
export class StoreService {
@@ -23,6 +24,7 @@ export class StoreService {
private readonly analyticsService: AnalyticsService,
private readonly partnerCityService: PartnerCityService,
private readonly authService: AuthService,
+ private readonly storeCategoryService: StoreCategoryService,
) {}
async listOpenStores(cityCode?: string) {
@@ -172,11 +174,17 @@ export class StoreService {
const bankAccountNo = body.bankAccountNo ? String(body.bankAccountNo) : null;
const bankBranch = body.bankBranch ? String(body.bankBranch) : null;
+ if (!body.categoryId) {
+ throw new BadRequestException('请选择店铺类型');
+ }
+ const categoryId = parseBigIntParam(body.categoryId, '分类ID');
+ await this.storeCategoryService.assertLeafCategoryId(categoryId);
+
const store = await this.prisma.store.create({
data: {
cityId: city.id,
partnerAccountId: primaryId,
- categoryId: body.categoryId ? parseBigIntParam(body.categoryId, '分类ID') : null,
+ categoryId,
name: String(body.name),
phone: normalizedPhone,
province: String(body.province ?? city.province ?? '河南省'),