diff --git a/apps/admin-web/src/components/CityPartnersPanel.tsx b/apps/admin-web/src/components/CityPartnersPanel.tsx index d00cde4..3f02579 100644 --- a/apps/admin-web/src/components/CityPartnersPanel.tsx +++ b/apps/admin-web/src/components/CityPartnersPanel.tsx @@ -292,7 +292,7 @@ export default function CityPartnersPanel({ cityId, maxPartnerCommissionRate = 0 subs={detail.children ?? []} onAdd={() => { subForm.resetFields(); - subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create'] }); + subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create', 'store:manage'] }); setSubOpen(true); }} onEdit={(row) => { diff --git a/apps/admin-web/src/pages/CityPartnersPage.tsx b/apps/admin-web/src/pages/CityPartnersPage.tsx index b14eb01..55c9914 100644 --- a/apps/admin-web/src/pages/CityPartnersPage.tsx +++ b/apps/admin-web/src/pages/CityPartnersPage.tsx @@ -224,7 +224,7 @@ export default function CityPartnersPage() { async function openAddSubAccount(parentId: string) { await openPartner(parentId); subForm.resetFields(); - subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create'] }); + subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create', 'store:manage'] }); setSubOpen(true); } @@ -482,7 +482,7 @@ export default function CityPartnersPage() { subs={detail.children ?? []} onAdd={() => { subForm.resetFields(); - subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create'] }); + subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create', 'store:manage'] }); setSubOpen(true); }} onEdit={(sub) => openSubEdit(sub, detail.id)} diff --git a/apps/admin-web/src/pages/PartnerAccountsPage.tsx b/apps/admin-web/src/pages/PartnerAccountsPage.tsx index 3aba5a7..698133e 100644 --- a/apps/admin-web/src/pages/PartnerAccountsPage.tsx +++ b/apps/admin-web/src/pages/PartnerAccountsPage.tsx @@ -154,7 +154,7 @@ export default function PartnerAccountsPage() { function openAddSub(parent: AccountTreeRow) { setSubParent(parent); subForm.resetFields(); - subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create'] }); + subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create', 'store:manage'] }); setSubOpen(true); } diff --git a/apps/h5-partner/src/lib/partnerAccess.ts b/apps/h5-partner/src/lib/partnerAccess.ts index dc8d2c7..5b29587 100644 --- a/apps/h5-partner/src/lib/partnerAccess.ts +++ b/apps/h5-partner/src/lib/partnerAccess.ts @@ -41,14 +41,23 @@ export function canAccessPartnerStores(account: PartnerMe | null | undefined): b return hasAnyPartnerPermission(account, ['store:create', 'store:manage']); } -/** 编辑资料 / 开闭店 / 重新上传:主账号或 store:manage / store:create */ +/** 编辑资料 / 开闭店 / 重新上传:主账号、门店权限,或历史未配权限的门店类子账号 */ export function canManagePartnerStore(account: PartnerMe | null | undefined): boolean { - return hasAnyPartnerPermission(account, ['store:manage', 'store:create']); + if (!account) return false; + if (isPrimaryAccount(account)) return true; + if (isWarehouseStaff(account)) return false; + if (hasAnyPartnerPermission(account, ['store:manage', 'store:create'])) return true; + // 合伙人端早期创建的子账号可能 permissions 为空,按门店员工放开 + return !Array.isArray(account.permissions) || account.permissions.length === 0; } /** 录入新店 */ export function canCreatePartnerStore(account: PartnerMe | null | undefined): boolean { - return hasPartnerPermission(account, 'store:create'); + if (!account) return false; + if (isPrimaryAccount(account)) return true; + if (isWarehouseStaff(account)) return false; + if (hasPartnerPermission(account, 'store:create')) return true; + return !Array.isArray(account.permissions) || account.permissions.length === 0; } /** 与后端 GET /partner/orders 权限点一致 */ diff --git a/apps/h5-partner/src/lib/staff.ts b/apps/h5-partner/src/lib/staff.ts index 083cb6c..ca33c3b 100644 --- a/apps/h5-partner/src/lib/staff.ts +++ b/apps/h5-partner/src/lib/staff.ts @@ -1,9 +1,5 @@ -import { - PartnerStaffRole, - type CreatePartnerStaffRequest, - type PartnerStaffItem, - type UpdatePartnerStaffRequest, -} from '@dukang/shared-types'; +import { PartnerStaffRole, DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS } from '@dukang/shared-types'; +import type { CreatePartnerStaffRequest, PartnerStaffItem, UpdatePartnerStaffRequest } from '@dukang/shared-types'; import { request } from './api'; export function listPartnerStaff() { @@ -25,7 +21,10 @@ export function createPartnerStaff(body: CreatePartnerStaffRequest) { name: body.name, phone: body.phone, smsCode: body.smsCode, - staffRole: PartnerStaffRole.INTERNAL, + staffRole: body.staffRole ?? PartnerStaffRole.INTERNAL, + permissions: body.permissions?.length + ? body.permissions + : [...DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS], }), }); } diff --git a/packages/shared-types/src/city-partner.ts b/packages/shared-types/src/city-partner.ts index 6a1520b..107ae64 100644 --- a/packages/shared-types/src/city-partner.ts +++ b/packages/shared-types/src/city-partner.ts @@ -89,3 +89,9 @@ export const PARTNER_PERMISSION_LABELS: Record = { 'store:create': '开店管理', 'order:view': '订单查看', }; + +/** 门店类子账号默认权限:录入、开闭店、维护资料 */ +export const DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS: PartnerPermissionKey[] = [ + 'store:create', + 'store:manage', +]; diff --git a/server/dukang-api/src/modules/iam/partner-staff.service.ts b/server/dukang-api/src/modules/iam/partner-staff.service.ts index 7cc0671..1808bfc 100644 --- a/server/dukang-api/src/modules/iam/partner-staff.service.ts +++ b/server/dukang-api/src/modules/iam/partner-staff.service.ts @@ -1,240 +1,245 @@ -import { - BadRequestException, - Injectable, - NotFoundException, -} from '@nestjs/common'; -import { ClientApp, PartnerStaffRole, SmsScene } from '@dukang/shared-types'; -import { PrismaService } from '../../common/prisma/prisma.module'; -import { serializeBigInt } from '../../common/decorators/current-user.decorator'; -import type { AuthUser } from '../../common/guards/jwt-auth.guard'; -import { AnalyticsService } from '../analytics/analytics.service'; -import { AuthService } from './auth.service'; -import { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto'; - -@Injectable() -export class PartnerStaffService { - constructor( - private readonly prisma: PrismaService, - private readonly analytics: AnalyticsService, - private readonly authService: AuthService, - ) {} - - async listStaff(parentAccountId: bigint) { - const rows = await this.prisma.partnerAccount.findMany({ - where: { parentAccountId }, - orderBy: { createdAt: 'desc' }, - }); - return rows.map((row) => this.toStaffItem(row)); - } - - async sendStaffPhoneSms(actor: AuthUser, phone: string) { - const parentAccountId = actor.actorId; - const parent = await this.prisma.partnerAccount.findUniqueOrThrow({ - where: { id: parentAccountId }, - }); - if (parent.isPrimary !== 1) { - throw new BadRequestException('仅主账号可添加子账号'); - } - - const normalized = phone.trim(); - if (!/^1[3-9]\d{9}$/.test(normalized)) { - throw new BadRequestException('请输入正确的手机号码'); - } - - const existing = await this.prisma.partnerAccount.findUnique({ where: { phone: normalized } }); - if (existing) throw new BadRequestException('该手机号已被使用'); - - const masked = this.maskPhone(normalized); - try { - await this.authService.sendSms(normalized, SmsScene.PARTNER_STAFF_ADD, { - clientApp: ClientApp.PARTNER_H5, - }); - this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_send', parent.id, { - phone: masked, - scene: SmsScene.PARTNER_STAFF_ADD, - status: 'success', - }); - } catch (err) { - if (err instanceof BadRequestException) { - this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_send', parent.id, { - phone: masked, - scene: SmsScene.PARTNER_STAFF_ADD, - status: 'failed', - reason: err.message, - }); - } - throw err; - } - - return { ok: true, maskedPhone: masked }; - } - - async createStaff(actor: AuthUser, dto: CreatePartnerStaffDto) { - const parentAccountId = actor.actorId; - const parent = await this.prisma.partnerAccount.findUniqueOrThrow({ - where: { id: parentAccountId }, - }); - if (parent.isPrimary !== 1) { - throw new BadRequestException('仅主账号可添加子账号'); - } - - const phone = dto.phone.trim(); - if (!/^1[3-9]\d{9}$/.test(phone)) { - throw new BadRequestException('请输入正确的手机号码'); - } - - const existing = await this.prisma.partnerAccount.findUnique({ where: { phone } }); - if (existing) throw new BadRequestException('该手机号已被使用'); - - const smsCode = dto.smsCode.trim(); - if (!smsCode) throw new BadRequestException('请输入手机号验证码'); - try { - await this.authService.verifySmsCode(phone, smsCode, SmsScene.PARTNER_STAFF_ADD); - } catch (err) { - this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_verify_fail', parentAccountId, { - phone: this.maskPhone(phone), - reason: err instanceof BadRequestException ? err.message : '验证码错误', - }); - throw err; - } - - const name = dto.name.trim(); - if (!name) throw new BadRequestException('请填写真实姓名'); - - const staffRole = (dto.staffRole as PartnerStaffRole | undefined) ?? PartnerStaffRole.INTERNAL; - - const account = await this.prisma.partnerAccount.create({ - data: { - phone, - name, - staffRole, - permissions: dto.permissions ?? undefined, - isPrimary: 0, - parentAccountId: parent.id, - status: 'DISABLED', - }, - }); - - this.trackStaffEvent(actor, parent.id, 'partner_staff_create', account.id, { - name, - phone: this.maskPhone(phone), - staffRole, - status: account.status, - phoneVerified: true, - }); - - return this.toStaffItem(account); - } - - async updateStaff(actor: AuthUser, staffId: bigint, dto: UpdatePartnerStaffDto) { - const parentAccountId = actor.actorId; - const staff = await this.assertStaffOwned(parentAccountId, staffId); - const before = { - name: staff.name, - staffRole: staff.staffRole, - status: staff.status, - }; - const data: Record = {}; - if (dto.name !== undefined) { - const name = dto.name.trim(); - if (!name) throw new BadRequestException('请填写真实姓名'); - data.name = name; - } - if (dto.staffRole !== undefined) { - data.staffRole = dto.staffRole as PartnerStaffRole; - } - if (dto.permissions !== undefined) { - data.permissions = dto.permissions; - } - if (dto.status !== undefined) { - data.status = dto.status; - } - const updated = await this.prisma.partnerAccount.update({ - where: { id: staff.id }, - data, - }); - - const onlyRoleChange = - (dto.staffRole !== undefined || dto.permissions !== undefined) && - dto.name === undefined && - dto.status === undefined; - const eventName = onlyRoleChange ? 'partner_staff_permission_update' : 'partner_staff_update'; - - const primaryId = parentAccountId; - this.trackStaffEvent(actor, primaryId, eventName, staff.id, { - before, - after: { - name: updated.name, - staffRole: updated.staffRole, - status: updated.status, - }, - }); - - return this.toStaffItem(updated); - } - - async deleteStaff(actor: AuthUser, staffId: bigint) { - const parentAccountId = actor.actorId; - const staff = await this.assertStaffOwned(parentAccountId, staffId); - - this.trackStaffEvent(actor, parentAccountId, 'partner_staff_delete', staff.id, { - name: staff.name, - phone: this.maskPhone(staff.phone), - staffRole: staff.staffRole, - status: staff.status, - }); - - await this.prisma.partnerAccount.delete({ where: { id: staff.id } }); - return { ok: true }; - } - - private trackStaffEvent( - actor: AuthUser, - primaryAccountId: bigint, - eventName: string, - refId: bigint, - extraJson?: Record, - ) { - this.analytics.trackPartnerOneSafe(actor.actorId, actor.clientApp, { - partnerAccountId: primaryAccountId, - eventName, - refType: 'PARTNER_ACCOUNT', - refId, - extraJson, - }); - } - - private async assertStaffOwned(parentAccountId: bigint, staffId: bigint) { - const staff = await this.prisma.partnerAccount.findFirst({ - where: { id: staffId, parentAccountId }, - }); - if (!staff) throw new NotFoundException('子账号不存在'); - return staff; - } - - private toStaffItem(row: { - id: bigint; - name: string; - phone: string; - staffRole: string | null; - permissions?: unknown; - status: string; - lastLoginAt: Date | null; - }) { - return serializeBigInt({ - id: row.id.toString(), - name: row.name, - phone: this.maskPhone(row.phone), - staffRole: row.staffRole ?? PartnerStaffRole.INTERNAL, - permissions: Array.isArray(row.permissions) ? row.permissions : undefined, - status: row.status, - lastLoginAt: row.lastLoginAt?.toISOString(), - }); - } - - private maskPhone(phone: string): string { - if (phone.length !== 11) return phone; - return `${phone.slice(0, 3)} **** ${phone.slice(7)}`; - } -} - \ No newline at end of file +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { ClientApp, DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS, PartnerStaffRole, SmsScene } from '@dukang/shared-types'; +import { PrismaService } from '../../common/prisma/prisma.module'; +import { serializeBigInt } from '../../common/decorators/current-user.decorator'; +import type { AuthUser } from '../../common/guards/jwt-auth.guard'; +import { AnalyticsService } from '../analytics/analytics.service'; +import { AuthService } from './auth.service'; +import { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto'; + +@Injectable() +export class PartnerStaffService { + constructor( + private readonly prisma: PrismaService, + private readonly analytics: AnalyticsService, + private readonly authService: AuthService, + ) {} + + async listStaff(parentAccountId: bigint) { + const rows = await this.prisma.partnerAccount.findMany({ + where: { parentAccountId }, + orderBy: { createdAt: 'desc' }, + }); + return rows.map((row) => this.toStaffItem(row)); + } + + async sendStaffPhoneSms(actor: AuthUser, phone: string) { + const parentAccountId = actor.actorId; + const parent = await this.prisma.partnerAccount.findUniqueOrThrow({ + where: { id: parentAccountId }, + }); + if (parent.isPrimary !== 1) { + throw new BadRequestException('仅主账号可添加子账号'); + } + + const normalized = phone.trim(); + if (!/^1[3-9]\d{9}$/.test(normalized)) { + throw new BadRequestException('请输入正确的手机号码'); + } + + const existing = await this.prisma.partnerAccount.findUnique({ where: { phone: normalized } }); + if (existing) throw new BadRequestException('该手机号已被使用'); + + const masked = this.maskPhone(normalized); + try { + await this.authService.sendSms(normalized, SmsScene.PARTNER_STAFF_ADD, { + clientApp: ClientApp.PARTNER_H5, + }); + this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_send', parent.id, { + phone: masked, + scene: SmsScene.PARTNER_STAFF_ADD, + status: 'success', + }); + } catch (err) { + if (err instanceof BadRequestException) { + this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_send', parent.id, { + phone: masked, + scene: SmsScene.PARTNER_STAFF_ADD, + status: 'failed', + reason: err.message, + }); + } + throw err; + } + + return { ok: true, maskedPhone: masked }; + } + + async createStaff(actor: AuthUser, dto: CreatePartnerStaffDto) { + const parentAccountId = actor.actorId; + const parent = await this.prisma.partnerAccount.findUniqueOrThrow({ + where: { id: parentAccountId }, + }); + if (parent.isPrimary !== 1) { + throw new BadRequestException('仅主账号可添加子账号'); + } + + const phone = dto.phone.trim(); + if (!/^1[3-9]\d{9}$/.test(phone)) { + throw new BadRequestException('请输入正确的手机号码'); + } + + const existing = await this.prisma.partnerAccount.findUnique({ where: { phone } }); + if (existing) throw new BadRequestException('该手机号已被使用'); + + const smsCode = dto.smsCode.trim(); + if (!smsCode) throw new BadRequestException('请输入手机号验证码'); + try { + await this.authService.verifySmsCode(phone, smsCode, SmsScene.PARTNER_STAFF_ADD); + } catch (err) { + this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_verify_fail', parentAccountId, { + phone: this.maskPhone(phone), + reason: err instanceof BadRequestException ? err.message : '验证码错误', + }); + throw err; + } + + const name = dto.name.trim(); + if (!name) throw new BadRequestException('请填写真实姓名'); + + const staffRole = (dto.staffRole as PartnerStaffRole | undefined) ?? PartnerStaffRole.INTERNAL; + const permissions = + dto.permissions && dto.permissions.length > 0 + ? dto.permissions + : [...DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS]; + + const account = await this.prisma.partnerAccount.create({ + data: { + phone, + name, + staffRole, + permissions, + isPrimary: 0, + parentAccountId: parent.id, + status: 'DISABLED', + }, + }); + + this.trackStaffEvent(actor, parent.id, 'partner_staff_create', account.id, { + name, + phone: this.maskPhone(phone), + staffRole, + permissions, + status: account.status, + phoneVerified: true, + }); + + return this.toStaffItem(account); + } + + async updateStaff(actor: AuthUser, staffId: bigint, dto: UpdatePartnerStaffDto) { + const parentAccountId = actor.actorId; + const staff = await this.assertStaffOwned(parentAccountId, staffId); + const before = { + name: staff.name, + staffRole: staff.staffRole, + status: staff.status, + }; + const data: Record = {}; + if (dto.name !== undefined) { + const name = dto.name.trim(); + if (!name) throw new BadRequestException('请填写真实姓名'); + data.name = name; + } + if (dto.staffRole !== undefined) { + data.staffRole = dto.staffRole as PartnerStaffRole; + } + if (dto.permissions !== undefined) { + data.permissions = dto.permissions; + } + if (dto.status !== undefined) { + data.status = dto.status; + } + const updated = await this.prisma.partnerAccount.update({ + where: { id: staff.id }, + data, + }); + + const onlyRoleChange = + (dto.staffRole !== undefined || dto.permissions !== undefined) && + dto.name === undefined && + dto.status === undefined; + const eventName = onlyRoleChange ? 'partner_staff_permission_update' : 'partner_staff_update'; + + const primaryId = parentAccountId; + this.trackStaffEvent(actor, primaryId, eventName, staff.id, { + before, + after: { + name: updated.name, + staffRole: updated.staffRole, + status: updated.status, + }, + }); + + return this.toStaffItem(updated); + } + + async deleteStaff(actor: AuthUser, staffId: bigint) { + const parentAccountId = actor.actorId; + const staff = await this.assertStaffOwned(parentAccountId, staffId); + + this.trackStaffEvent(actor, parentAccountId, 'partner_staff_delete', staff.id, { + name: staff.name, + phone: this.maskPhone(staff.phone), + staffRole: staff.staffRole, + status: staff.status, + }); + + await this.prisma.partnerAccount.delete({ where: { id: staff.id } }); + return { ok: true }; + } + + private trackStaffEvent( + actor: AuthUser, + primaryAccountId: bigint, + eventName: string, + refId: bigint, + extraJson?: Record, + ) { + this.analytics.trackPartnerOneSafe(actor.actorId, actor.clientApp, { + partnerAccountId: primaryAccountId, + eventName, + refType: 'PARTNER_ACCOUNT', + refId, + extraJson, + }); + } + + private async assertStaffOwned(parentAccountId: bigint, staffId: bigint) { + const staff = await this.prisma.partnerAccount.findFirst({ + where: { id: staffId, parentAccountId }, + }); + if (!staff) throw new NotFoundException('子账号不存在'); + return staff; + } + + private toStaffItem(row: { + id: bigint; + name: string; + phone: string; + staffRole: string | null; + permissions?: unknown; + status: string; + lastLoginAt: Date | null; + }) { + return serializeBigInt({ + id: row.id.toString(), + name: row.name, + phone: this.maskPhone(row.phone), + staffRole: row.staffRole ?? PartnerStaffRole.INTERNAL, + permissions: Array.isArray(row.permissions) ? row.permissions : undefined, + status: row.status, + lastLoginAt: row.lastLoginAt?.toISOString(), + }); + } + + private maskPhone(phone: string): string { + if (phone.length !== 11) return phone; + return `${phone.slice(0, 3)} **** ${phone.slice(7)}`; + } +} + diff --git a/server/dukang-api/src/modules/settlement/settlement.controller.ts b/server/dukang-api/src/modules/settlement/settlement.controller.ts index 011d715..994772f 100644 --- a/server/dukang-api/src/modules/settlement/settlement.controller.ts +++ b/server/dukang-api/src/modules/settlement/settlement.controller.ts @@ -1,5 +1,6 @@ import { BadRequestException, Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { IsOptional, IsString } from 'class-validator'; +import { DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS } from '@dukang/shared-types'; import { SettlementService } from './settlement.service'; import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard'; import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard'; @@ -269,7 +270,7 @@ export class PartnerMeController { } private async buildPartnerMe(actorId: bigint) { - const account = await this.prisma.partnerAccount.findUniqueOrThrow({ + let account = await this.prisma.partnerAccount.findUniqueOrThrow({ where: { id: actorId }, }); let primary = account; @@ -278,6 +279,23 @@ export class PartnerMeController { where: { id: account.parentAccountId }, }); } + + // 门店类子账号若未配置权限,补齐开店/门店管理,便于开闭店与重传资料 + if (account.isPrimary !== 1) { + const perms = Array.isArray(account.permissions) ? (account.permissions as string[]) : []; + const hasStorePerm = perms.includes('store:create') || perms.includes('store:manage'); + const warehouseOnly = + !hasStorePerm && + perms.length > 0 && + (perms.includes('warehouse:manage') || perms.includes('order:view')); + if (!hasStorePerm && !warehouseOnly) { + account = await this.prisma.partnerAccount.update({ + where: { id: account.id }, + data: { permissions: [...DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS] }, + }); + } + } + const hasWarehouseAccess = await this.partnerCityService.hasManagedWarehouse(primary.id); return { id: account.id.toString(), diff --git a/server/dukang-api/src/modules/store/store.service.ts b/server/dukang-api/src/modules/store/store.service.ts index 5abd6ef..ff95efc 100644 --- a/server/dukang-api/src/modules/store/store.service.ts +++ b/server/dukang-api/src/modules/store/store.service.ts @@ -992,7 +992,7 @@ export class StoreService { return Array.isArray(account.permissions) ? (account.permissions as string[]) : []; } - /** 主账号,或具备 store:manage / store:create 的子账号可改门店 */ + /** 主账号,或门店类子账号(含历史空权限)可改门店 */ private async assertCanMutateStore( account: { isPrimary: number; permissions?: unknown }, partnerAccountId: bigint, @@ -1002,10 +1002,17 @@ export class StoreService { const perms = this.partnerPermissionList(account); const canManage = perms.includes('store:manage'); const canCreate = perms.includes('store:create'); - if (!canManage && !canCreate) { + const legacyStoreStaff = perms.length === 0; + const warehouseOnly = + !canManage && + !canCreate && + !legacyStoreStaff && + (perms.includes('warehouse:manage') || + (perms.includes('order:view') && !perms.includes('store:create') && !perms.includes('store:manage'))); + if (warehouseOnly || (!canManage && !canCreate && !legacyStoreStaff)) { throw new ForbiddenException('子账号无门店管理权限'); } - // store:manage 可管团队门店;仅 store:create 只能改自己录入的店 + // store:manage 可管团队门店;仅 store:create / 历史空权限只能改自己录入的店 if (canManage) return; await this.assertStoreOwnedByAccount(partnerAccountId, storeId); }