合伙人 H5 改造
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-07-16 15:04:56 +08:00
parent fd5427c7ea
commit f1dc23c54c
21 changed files with 817 additions and 614 deletions
@@ -0,0 +1,7 @@
import { SetMetadata } from '@nestjs/common';
import type { PartnerPermissionKey } from '@dukang/shared-types';
export const PARTNER_PERMISSIONS_KEY = 'partner_permissions';
export const RequirePartnerPermissions = (...permissions: PartnerPermissionKey[]) =>
SetMetadata(PARTNER_PERMISSIONS_KEY, permissions);
@@ -0,0 +1,45 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import type { PartnerPermissionKey } from '@dukang/shared-types';
import { PARTNER_PERMISSIONS_KEY } from '../decorators/partner-permission.decorator';
import { PrismaService } from '../prisma/prisma.module';
import { AuthUser, JwtAuthGuard } from './jwt-auth.guard';
@Injectable()
export class PartnerPermissionGuard implements CanActivate {
constructor(
private readonly jwtAuthGuard: JwtAuthGuard,
private readonly prisma: PrismaService,
private readonly reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
await this.jwtAuthGuard.canActivate(context);
const req = context.switchToHttp().getRequest();
const user = req.user as AuthUser;
if (user.actorType !== 'PARTNER') {
throw new ForbiddenException('仅合伙人可操作');
}
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
where: { id: user.actorId },
});
if (account.isPrimary === 1) return true;
const required = this.reflector.getAllAndOverride<PartnerPermissionKey[]>(
PARTNER_PERMISSIONS_KEY,
[context.getHandler(), context.getClass()],
);
if (!required?.length) return true;
const perms = Array.isArray(account.permissions)
? (account.permissions as string[])
: [];
if (required.some((p) => perms.includes(p))) return true;
throw new ForbiddenException('当前子账号无此操作权限');
}
}
@@ -21,6 +21,7 @@ import { PhoneVerifiedGuard } from '../../common/guards/phone-verified.guard';
import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guard';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
import { ShopStoreGuard } from '../../common/guards/shop-store.guard';
import { StoreMembershipService } from '../../common/guards/store-membership.service';
@@ -54,6 +55,7 @@ import { StoreMembershipService } from '../../common/guards/store-membership.ser
OptionalJwtAuthGuard,
HqAuthGuard,
PartnerPrimaryGuard,
PartnerPermissionGuard,
ShopStoreGuard,
],
exports: [
@@ -68,6 +70,7 @@ import { StoreMembershipService } from '../../common/guards/store-membership.ser
OptionalJwtAuthGuard,
HqAuthGuard,
PartnerPrimaryGuard,
PartnerPermissionGuard,
ShopStoreGuard,
],
})
@@ -263,6 +263,8 @@ export class PartnerMeController {
staffRole: account.staffRole ?? undefined,
permissions: Array.isArray(account.permissions) ? account.permissions : undefined,
primaryAccountId: primary.id.toString(),
primaryPhone: primary.phone,
primaryName: primary.name,
companyName: primary.companyName ?? undefined,
hasWechat: !!account.wxOpenId,
};
@@ -3,6 +3,8 @@ import { StoreService } from './store.service';
import { RedeemService } from '../redeem/redeem.service';
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
import { RequirePartnerPermissions } from '../../common/decorators/partner-permission.decorator';
import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
import { ShopStoreGuard } from '../../common/guards/shop-store.guard';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
@@ -76,7 +78,8 @@ export class PartnerStoreController {
}
@Controller('partner/dashboard')
@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
@UseGuards(JwtAuthGuard, PartnerPermissionGuard)
@RequirePartnerPermissions('store:create', 'store:manage', 'order:view', 'warehouse:manage')
export class PartnerDashboardController {
constructor(private readonly storeService: StoreService) {}
@@ -468,33 +468,56 @@ export class StoreService {
async partnerDashboard(partnerAccountId: bigint) {
const { account, primaryId } = await this.resolvePartnerScope(partnerAccountId);
this.assertPrimaryAccount(account);
const partnerStoreIds = await this.prisma.store.findMany({
where: { partnerAccountId: primaryId },
select: { id: true },
const primaryAccount = await this.prisma.partnerAccount.findUniqueOrThrow({
where: { id: primaryId },
});
const storeIds = partnerStoreIds.map((s) => s.id);
const [storeCount, orderCount, recentStores, pendingAuditCount, recentNotices] = await Promise.all([
this.prisma.store.count({ where: { partnerAccountId: primaryId } }),
this.prisma.order.count({
where: await this.partnerCityService.buildPartnerOrderWhere(primaryId),
}),
const orderCount = await this.prisma.order.count({
where: await this.partnerCityService.buildPartnerOrderWhere(primaryId),
});
let storeWhere: { partnerAccountId: bigint; id?: { in: bigint[] } } = {
partnerAccountId: primaryId,
};
if (this.isSubAccount(account)) {
const storeIds = await this.getStoreIdsCreatedByAccount(partnerAccountId);
if (storeIds.length === 0) {
return {
storeCount: 0,
orderCount,
companyName: primaryAccount.companyName ?? '',
recentStores: [],
pendingAuditCount: 0,
notifications: [],
};
}
storeWhere = { partnerAccountId: primaryId, id: { in: storeIds } };
}
const storeIdsForNotices = (
await this.prisma.store.findMany({
where: storeWhere,
select: { id: true },
})
).map((s) => s.id);
const [storeCount, recentStores, pendingAuditCount, recentNotices] = await Promise.all([
this.prisma.store.count({ where: storeWhere }),
this.prisma.store.findMany({
where: { partnerAccountId: primaryId },
where: storeWhere,
select: { id: true, name: true, status: true, auditStatus: true, createdAt: true },
orderBy: { createdAt: 'desc' },
take: 10,
}),
this.prisma.store.count({
where: { partnerAccountId: primaryId, auditStatus: 'PENDING' },
where: { ...storeWhere, auditStatus: 'PENDING' },
}),
storeIds.length === 0
storeIdsForNotices.length === 0
? Promise.resolve([])
: this.prisma.commonEvent.findMany({
where: {
eventType: 'STORE_AUDIT',
refType: 'STORE',
refId: { in: storeIds },
refId: { in: storeIdsForNotices },
status: { in: ['APPROVED', 'REJECTED'] },
actorType: 'HQ',
},
@@ -515,7 +538,7 @@ export class StoreService {
return {
storeCount,
orderCount,
companyName: account.companyName ?? '',
companyName: primaryAccount.companyName ?? '',
recentStores: serializeBigInt(recentStores),
pendingAuditCount,
notifications: serializeBigInt(
@@ -3,6 +3,8 @@ import type { Request } from 'express';
import { TradeService } from './trade.service';
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
import { RequirePartnerPermissions } from '../../common/decorators/partner-permission.decorator';
import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import {
PartnerProxyOrderCreateDto,
@@ -70,7 +72,8 @@ export class TradeController {
}
@Controller('partner/orders')
@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
@UseGuards(JwtAuthGuard, PartnerPermissionGuard)
@RequirePartnerPermissions('order:view', 'warehouse:manage')
export class PartnerOrderController {
constructor(private readonly tradeService: TradeService) {}