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 ?? []}
|
subs={detail.children ?? []}
|
||||||
onAdd={() => {
|
onAdd={() => {
|
||||||
subForm.resetFields();
|
subForm.resetFields();
|
||||||
subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create'] });
|
subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create', 'store:manage'] });
|
||||||
setSubOpen(true);
|
setSubOpen(true);
|
||||||
}}
|
}}
|
||||||
onEdit={(row) => {
|
onEdit={(row) => {
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export default function CityPartnersPage() {
|
|||||||
async function openAddSubAccount(parentId: string) {
|
async function openAddSubAccount(parentId: string) {
|
||||||
await openPartner(parentId);
|
await openPartner(parentId);
|
||||||
subForm.resetFields();
|
subForm.resetFields();
|
||||||
subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create'] });
|
subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create', 'store:manage'] });
|
||||||
setSubOpen(true);
|
setSubOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,7 +482,7 @@ export default function CityPartnersPage() {
|
|||||||
subs={detail.children ?? []}
|
subs={detail.children ?? []}
|
||||||
onAdd={() => {
|
onAdd={() => {
|
||||||
subForm.resetFields();
|
subForm.resetFields();
|
||||||
subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create'] });
|
subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create', 'store:manage'] });
|
||||||
setSubOpen(true);
|
setSubOpen(true);
|
||||||
}}
|
}}
|
||||||
onEdit={(sub) => openSubEdit(sub, detail.id)}
|
onEdit={(sub) => openSubEdit(sub, detail.id)}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default function PartnerAccountsPage() {
|
|||||||
function openAddSub(parent: AccountTreeRow) {
|
function openAddSub(parent: AccountTreeRow) {
|
||||||
setSubParent(parent);
|
setSubParent(parent);
|
||||||
subForm.resetFields();
|
subForm.resetFields();
|
||||||
subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create'] });
|
subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create', 'store:manage'] });
|
||||||
setSubOpen(true);
|
setSubOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,14 +41,23 @@ export function canAccessPartnerStores(account: PartnerMe | null | undefined): b
|
|||||||
return hasAnyPartnerPermission(account, ['store:create', 'store:manage']);
|
return hasAnyPartnerPermission(account, ['store:create', 'store:manage']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 编辑资料 / 开闭店 / 重新上传:主账号或 store:manage / store:create */
|
/** 编辑资料 / 开闭店 / 重新上传:主账号、门店权限,或历史未配权限的门店类子账号 */
|
||||||
export function canManagePartnerStore(account: PartnerMe | null | undefined): boolean {
|
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 {
|
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 权限点一致 */
|
/** 与后端 GET /partner/orders 权限点一致 */
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { PartnerStaffRole, DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS } from '@dukang/shared-types';
|
||||||
PartnerStaffRole,
|
import type { CreatePartnerStaffRequest, PartnerStaffItem, UpdatePartnerStaffRequest } from '@dukang/shared-types';
|
||||||
type CreatePartnerStaffRequest,
|
|
||||||
type PartnerStaffItem,
|
|
||||||
type UpdatePartnerStaffRequest,
|
|
||||||
} from '@dukang/shared-types';
|
|
||||||
import { request } from './api';
|
import { request } from './api';
|
||||||
|
|
||||||
export function listPartnerStaff() {
|
export function listPartnerStaff() {
|
||||||
@@ -25,7 +21,10 @@ export function createPartnerStaff(body: CreatePartnerStaffRequest) {
|
|||||||
name: body.name,
|
name: body.name,
|
||||||
phone: body.phone,
|
phone: body.phone,
|
||||||
smsCode: body.smsCode,
|
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': '开店管理',
|
'store:create': '开店管理',
|
||||||
'order:view': '订单查看',
|
'order:view': '订单查看',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 门店类子账号默认权限:录入、开闭店、维护资料 */
|
||||||
|
export const DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS: PartnerPermissionKey[] = [
|
||||||
|
'store:create',
|
||||||
|
'store:manage',
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,240 +1,245 @@
|
|||||||
import {
|
import {
|
||||||
|
BadRequestException,
|
||||||
BadRequestException,
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
Injectable,
|
} from '@nestjs/common';
|
||||||
|
import { ClientApp, DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS, PartnerStaffRole, SmsScene } from '@dukang/shared-types';
|
||||||
NotFoundException,
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||||
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||||
} from '@nestjs/common';
|
import type { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||||
|
import { AnalyticsService } from '../analytics/analytics.service';
|
||||||
import { ClientApp, PartnerStaffRole, SmsScene } from '@dukang/shared-types';
|
import { AuthService } from './auth.service';
|
||||||
|
import { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto';
|
||||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
|
||||||
|
@Injectable()
|
||||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
export class PartnerStaffService {
|
||||||
|
constructor(
|
||||||
import type { AuthUser } from '../../common/guards/jwt-auth.guard';
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly analytics: AnalyticsService,
|
||||||
import { AnalyticsService } from '../analytics/analytics.service';
|
private readonly authService: AuthService,
|
||||||
|
) {}
|
||||||
import { AuthService } from './auth.service';
|
|
||||||
|
async listStaff(parentAccountId: bigint) {
|
||||||
import { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto';
|
const rows = await this.prisma.partnerAccount.findMany({
|
||||||
|
where: { parentAccountId },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
@Injectable()
|
return rows.map((row) => this.toStaffItem(row));
|
||||||
|
}
|
||||||
export class PartnerStaffService {
|
|
||||||
|
async sendStaffPhoneSms(actor: AuthUser, phone: string) {
|
||||||
constructor(
|
const parentAccountId = actor.actorId;
|
||||||
|
const parent = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||||
private readonly prisma: PrismaService,
|
where: { id: parentAccountId },
|
||||||
|
});
|
||||||
private readonly analytics: AnalyticsService,
|
if (parent.isPrimary !== 1) {
|
||||||
|
throw new BadRequestException('仅主账号可添加子账号');
|
||||||
private readonly authService: AuthService,
|
}
|
||||||
|
|
||||||
) {}
|
const normalized = phone.trim();
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(normalized)) {
|
||||||
|
throw new BadRequestException('请输入正确的手机号码');
|
||||||
|
}
|
||||||
async listStaff(parentAccountId: bigint) {
|
|
||||||
|
const existing = await this.prisma.partnerAccount.findUnique({ where: { phone: normalized } });
|
||||||
const rows = await this.prisma.partnerAccount.findMany({
|
if (existing) throw new BadRequestException('该手机号已被使用');
|
||||||
|
|
||||||
where: { parentAccountId },
|
const masked = this.maskPhone(normalized);
|
||||||
|
try {
|
||||||
orderBy: { createdAt: 'desc' },
|
await this.authService.sendSms(normalized, SmsScene.PARTNER_STAFF_ADD, {
|
||||||
|
clientApp: ClientApp.PARTNER_H5,
|
||||||
});
|
});
|
||||||
|
this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_send', parent.id, {
|
||||||
return rows.map((row) => this.toStaffItem(row));
|
phone: masked,
|
||||||
|
scene: SmsScene.PARTNER_STAFF_ADD,
|
||||||
}
|
status: 'success',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof BadRequestException) {
|
||||||
async sendStaffPhoneSms(actor: AuthUser, phone: string) {
|
this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_send', parent.id, {
|
||||||
|
phone: masked,
|
||||||
const parentAccountId = actor.actorId;
|
scene: SmsScene.PARTNER_STAFF_ADD,
|
||||||
|
status: 'failed',
|
||||||
const parent = await this.prisma.partnerAccount.findUniqueOrThrow({
|
reason: err.message,
|
||||||
|
});
|
||||||
where: { id: parentAccountId },
|
}
|
||||||
|
throw err;
|
||||||
});
|
}
|
||||||
|
|
||||||
if (parent.isPrimary !== 1) {
|
return { ok: true, maskedPhone: masked };
|
||||||
|
}
|
||||||
throw new BadRequestException('仅主账号可添加子账号');
|
|
||||||
|
async createStaff(actor: AuthUser, dto: CreatePartnerStaffDto) {
|
||||||
}
|
const parentAccountId = actor.actorId;
|
||||||
|
const parent = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||||
|
where: { id: parentAccountId },
|
||||||
|
});
|
||||||
const normalized = phone.trim();
|
if (parent.isPrimary !== 1) {
|
||||||
|
throw new BadRequestException('仅主账号可添加子账号');
|
||||||
if (!/^1[3-9]\d{9}$/.test(normalized)) {
|
}
|
||||||
|
|
||||||
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 } });
|
||||||
const existing = await this.prisma.partnerAccount.findUnique({ where: { phone: normalized } });
|
if (existing) throw new BadRequestException('该手机号已被使用');
|
||||||
|
|
||||||
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);
|
||||||
const masked = this.maskPhone(normalized);
|
} catch (err) {
|
||||||
|
this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_verify_fail', parentAccountId, {
|
||||||
try {
|
phone: this.maskPhone(phone),
|
||||||
|
reason: err instanceof BadRequestException ? err.message : '验证码错误',
|
||||||
await this.authService.sendSms(normalized, SmsScene.PARTNER_STAFF_ADD, {
|
});
|
||||||
|
throw err;
|
||||||
clientApp: ClientApp.PARTNER_H5,
|
}
|
||||||
|
|
||||||
});
|
const name = dto.name.trim();
|
||||||
|
if (!name) throw new BadRequestException('请填写真实姓名');
|
||||||
this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_send', parent.id, {
|
|
||||||
|
const staffRole = (dto.staffRole as PartnerStaffRole | undefined) ?? PartnerStaffRole.INTERNAL;
|
||||||
phone: masked,
|
const permissions =
|
||||||
|
dto.permissions && dto.permissions.length > 0
|
||||||
scene: SmsScene.PARTNER_STAFF_ADD,
|
? dto.permissions
|
||||||
|
: [...DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS];
|
||||||
status: 'success',
|
|
||||||
|
const account = await this.prisma.partnerAccount.create({
|
||||||
});
|
data: {
|
||||||
|
phone,
|
||||||
} catch (err) {
|
name,
|
||||||
|
staffRole,
|
||||||
if (err instanceof BadRequestException) {
|
permissions,
|
||||||
|
isPrimary: 0,
|
||||||
this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_send', parent.id, {
|
parentAccountId: parent.id,
|
||||||
|
status: 'DISABLED',
|
||||||
phone: masked,
|
},
|
||||||
|
});
|
||||||
scene: SmsScene.PARTNER_STAFF_ADD,
|
|
||||||
|
this.trackStaffEvent(actor, parent.id, 'partner_staff_create', account.id, {
|
||||||
status: 'failed',
|
name,
|
||||||
|
phone: this.maskPhone(phone),
|
||||||
reason: err.message,
|
staffRole,
|
||||||
|
permissions,
|
||||||
});
|
status: account.status,
|
||||||
|
phoneVerified: true,
|
||||||
}
|
});
|
||||||
|
|
||||||
throw err;
|
return this.toStaffItem(account);
|
||||||
|
}
|
||||||
}
|
|
||||||
|
async updateStaff(actor: AuthUser, staffId: bigint, dto: UpdatePartnerStaffDto) {
|
||||||
|
const parentAccountId = actor.actorId;
|
||||||
|
const staff = await this.assertStaffOwned(parentAccountId, staffId);
|
||||||
return { ok: true, maskedPhone: masked };
|
const before = {
|
||||||
|
name: staff.name,
|
||||||
}
|
staffRole: staff.staffRole,
|
||||||
|
status: staff.status,
|
||||||
|
};
|
||||||
|
const data: Record<string, unknown> = {};
|
||||||
async createStaff(actor: AuthUser, dto: CreatePartnerStaffDto) {
|
if (dto.name !== undefined) {
|
||||||
|
const name = dto.name.trim();
|
||||||
const parentAccountId = actor.actorId;
|
if (!name) throw new BadRequestException('请填写真实姓名');
|
||||||
|
data.name = name;
|
||||||
const parent = await this.prisma.partnerAccount.findUniqueOrThrow({
|
}
|
||||||
|
if (dto.staffRole !== undefined) {
|
||||||
where: { id: parentAccountId },
|
data.staffRole = dto.staffRole as PartnerStaffRole;
|
||||||
|
}
|
||||||
});
|
if (dto.permissions !== undefined) {
|
||||||
|
data.permissions = dto.permissions;
|
||||||
if (parent.isPrimary !== 1) {
|
}
|
||||||
|
if (dto.status !== undefined) {
|
||||||
throw new BadRequestException('仅主账号可添加子账号');
|
data.status = dto.status;
|
||||||
|
}
|
||||||
}
|
const updated = await this.prisma.partnerAccount.update({
|
||||||
|
where: { id: staff.id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
const phone = dto.phone.trim();
|
|
||||||
|
const onlyRoleChange =
|
||||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
(dto.staffRole !== undefined || dto.permissions !== undefined) &&
|
||||||
|
dto.name === undefined &&
|
||||||
throw new BadRequestException('请输入正确的手机号码');
|
dto.status === undefined;
|
||||||
|
const eventName = onlyRoleChange ? 'partner_staff_permission_update' : 'partner_staff_update';
|
||||||
}
|
|
||||||
|
const primaryId = parentAccountId;
|
||||||
|
this.trackStaffEvent(actor, primaryId, eventName, staff.id, {
|
||||||
|
before,
|
||||||
const existing = await this.prisma.partnerAccount.findUnique({ where: { phone } });
|
after: {
|
||||||
|
name: updated.name,
|
||||||
if (existing) throw new BadRequestException('该手机号已被使用');
|
staffRole: updated.staffRole,
|
||||||
|
status: updated.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
const smsCode = dto.smsCode.trim();
|
|
||||||
|
return this.toStaffItem(updated);
|
||||||
if (!smsCode) throw new BadRequestException('请输入手机号验证码');
|
}
|
||||||
|
|
||||||
try {
|
async deleteStaff(actor: AuthUser, staffId: bigint) {
|
||||||
|
const parentAccountId = actor.actorId;
|
||||||
await this.authService.verifySmsCode(phone, smsCode, SmsScene.PARTNER_STAFF_ADD);
|
const staff = await this.assertStaffOwned(parentAccountId, staffId);
|
||||||
|
|
||||||
} catch (err) {
|
this.trackStaffEvent(actor, parentAccountId, 'partner_staff_delete', staff.id, {
|
||||||
|
name: staff.name,
|
||||||
this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_verify_fail', parentAccountId, {
|
phone: this.maskPhone(staff.phone),
|
||||||
|
staffRole: staff.staffRole,
|
||||||
phone: this.maskPhone(phone),
|
status: staff.status,
|
||||||
|
});
|
||||||
reason: err instanceof BadRequestException ? err.message : '验证码错误',
|
|
||||||
|
await this.prisma.partnerAccount.delete({ where: { id: staff.id } });
|
||||||
});
|
return { ok: true };
|
||||||
|
}
|
||||||
throw err;
|
|
||||||
|
private trackStaffEvent(
|
||||||
}
|
actor: AuthUser,
|
||||||
|
primaryAccountId: bigint,
|
||||||
|
eventName: string,
|
||||||
|
refId: bigint,
|
||||||
const name = dto.name.trim();
|
extraJson?: Record<string, unknown>,
|
||||||
|
) {
|
||||||
if (!name) throw new BadRequestException('请填写真实姓名');
|
this.analytics.trackPartnerOneSafe(actor.actorId, actor.clientApp, {
|
||||||
|
partnerAccountId: primaryAccountId,
|
||||||
|
eventName,
|
||||||
|
refType: 'PARTNER_ACCOUNT',
|
||||||
const staffRole = (dto.staffRole as PartnerStaffRole | undefined) ?? PartnerStaffRole.INTERNAL;
|
refId,
|
||||||
|
extraJson,
|
||||||
|
});
|
||||||
|
}
|
||||||
const account = await this.prisma.partnerAccount.create({
|
|
||||||
|
private async assertStaffOwned(parentAccountId: bigint, staffId: bigint) {
|
||||||
data: {
|
const staff = await this.prisma.partnerAccount.findFirst({
|
||||||
|
where: { id: staffId, parentAccountId },
|
||||||
phone,
|
});
|
||||||
|
if (!staff) throw new NotFoundException('子账号不存在');
|
||||||
name,
|
return staff;
|
||||||
|
}
|
||||||
staffRole,
|
|
||||||
|
private toStaffItem(row: {
|
||||||
permissions: dto.permissions ?? undefined,
|
id: bigint;
|
||||||
|
name: string;
|
||||||
isPrimary: 0,
|
phone: string;
|
||||||
|
staffRole: string | null;
|
||||||
parentAccountId: parent.id,
|
permissions?: unknown;
|
||||||
|
status: string;
|
||||||
status: 'DISABLED',
|
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,
|
||||||
this.trackStaffEvent(actor, parent.id, 'partner_staff_create', account.id, {
|
status: row.status,
|
||||||
|
lastLoginAt: row.lastLoginAt?.toISOString(),
|
||||||
name,
|
});
|
||||||
|
}
|
||||||
phone: this.maskPhone(phone),
|
|
||||||
|
private maskPhone(phone: string): string {
|
||||||
|
if (phone.length !== 11) return phone;
|
||||||
|
return `${phone.slice(0, 3)} **** ${phone.slice(7)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BadRequestException, Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
import { BadRequestException, Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||||
import { IsOptional, IsString } from 'class-validator';
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
import { DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS } from '@dukang/shared-types';
|
||||||
import { SettlementService } from './settlement.service';
|
import { SettlementService } from './settlement.service';
|
||||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||||
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
|
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
|
||||||
@@ -269,7 +270,7 @@ export class PartnerMeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async buildPartnerMe(actorId: bigint) {
|
private async buildPartnerMe(actorId: bigint) {
|
||||||
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
let account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||||
where: { id: actorId },
|
where: { id: actorId },
|
||||||
});
|
});
|
||||||
let primary = account;
|
let primary = account;
|
||||||
@@ -278,6 +279,23 @@ export class PartnerMeController {
|
|||||||
where: { id: account.parentAccountId },
|
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);
|
const hasWarehouseAccess = await this.partnerCityService.hasManagedWarehouse(primary.id);
|
||||||
return {
|
return {
|
||||||
id: account.id.toString(),
|
id: account.id.toString(),
|
||||||
|
|||||||
@@ -992,7 +992,7 @@ export class StoreService {
|
|||||||
return Array.isArray(account.permissions) ? (account.permissions as string[]) : [];
|
return Array.isArray(account.permissions) ? (account.permissions as string[]) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 主账号,或具备 store:manage / store:create 的子账号可改门店 */
|
/** 主账号,或门店类子账号(含历史空权限)可改门店 */
|
||||||
private async assertCanMutateStore(
|
private async assertCanMutateStore(
|
||||||
account: { isPrimary: number; permissions?: unknown },
|
account: { isPrimary: number; permissions?: unknown },
|
||||||
partnerAccountId: bigint,
|
partnerAccountId: bigint,
|
||||||
@@ -1002,10 +1002,17 @@ export class StoreService {
|
|||||||
const perms = this.partnerPermissionList(account);
|
const perms = this.partnerPermissionList(account);
|
||||||
const canManage = perms.includes('store:manage');
|
const canManage = perms.includes('store:manage');
|
||||||
const canCreate = perms.includes('store:create');
|
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('子账号无门店管理权限');
|
throw new ForbiddenException('子账号无门店管理权限');
|
||||||
}
|
}
|
||||||
// store:manage 可管团队门店;仅 store:create 只能改自己录入的店
|
// store:manage 可管团队门店;仅 store:create / 历史空权限只能改自己录入的店
|
||||||
if (canManage) return;
|
if (canManage) return;
|
||||||
await this.assertStoreOwnedByAccount(partnerAccountId, storeId);
|
await this.assertStoreOwnedByAccount(partnerAccountId, storeId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user