fix(partner): grant store staff open/close and media permissions by default
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
Default new sub-accounts to store:create+store:manage, backfill empty permissions on /partner/me, and treat legacy store staff as allowed to mutate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 权限点一致 */
|
||||
|
||||
@@ -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],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,3 +89,9 @@ export const PARTNER_PERMISSION_LABELS: Record<PartnerPermissionKey, string> = {
|
||||
'store:create': '开店管理',
|
||||
'order:view': '订单查看',
|
||||
};
|
||||
|
||||
/** 门店类子账号默认权限:录入、开闭店、维护资料 */
|
||||
export const DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS: PartnerPermissionKey[] = [
|
||||
'store:create',
|
||||
'store:manage',
|
||||
];
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
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';
|
||||
@@ -102,13 +102,17 @@ export class PartnerStaffService {
|
||||
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',
|
||||
@@ -119,6 +123,7 @@ export class PartnerStaffService {
|
||||
name,
|
||||
phone: this.maskPhone(phone),
|
||||
staffRole,
|
||||
permissions,
|
||||
status: account.status,
|
||||
phoneVerified: true,
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user