diff --git a/apps/h5-partner/src/styles.css b/apps/h5-partner/src/styles.css
index c24aff5..09ad919 100644
--- a/apps/h5-partner/src/styles.css
+++ b/apps/h5-partner/src/styles.css
@@ -3067,6 +3067,68 @@ body {
border-top: 1px dashed rgba(166, 29, 36, 0.15);
}
+.partner-home--flush-top .partner-home-body,
+.partner-home--flush-top.partner-store-page,
+.partner-home--flush-top.partner-center-page {
+ padding-top: 0;
+}
+
+.partner-home--flush-top .partner-sticky-filter {
+ padding-top: 12px;
+}
+
+.partner-leaderboard-row--self {
+ background: rgba(166, 29, 36, 0.08);
+ border: 1px solid rgba(166, 29, 36, 0.18);
+ border-radius: 10px;
+}
+
+.partner-leaderboard-self-tag {
+ display: inline-block;
+ margin-left: 6px;
+ padding: 1px 6px;
+ font-size: 10px;
+ font-weight: 700;
+ color: var(--color-heritage-red);
+ background: rgba(166, 29, 36, 0.12);
+ border-radius: 4px;
+ vertical-align: middle;
+}
+
+.partner-order-card-wrap {
+ margin: 0 var(--space-page) 12px;
+}
+
+.partner-order-ship-btn {
+ display: block;
+ width: 100%;
+ margin-top: -4px;
+ padding: 10px 16px;
+ border: none;
+ border-radius: 0 0 var(--radius-md) var(--radius-md);
+ background: var(--color-heritage-red);
+ color: #fff;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.partner-order-ship-btn:disabled {
+ opacity: 0.6;
+}
+
+.partner-order-footer-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ width: 100%;
+}
+
+.partner-order-ship-btn--footer {
+ width: 100%;
+ justify-content: center;
+}
+
/* 微信 H5 系统标题已展示:隐藏页内重复标题,保留返回键与操作区 */
.app-page-title {
display: none !important;
diff --git a/packages/shared-types/src/partner.ts b/packages/shared-types/src/partner.ts
index 847e132..28f339d 100644
--- a/packages/shared-types/src/partner.ts
+++ b/packages/shared-types/src/partner.ts
@@ -10,6 +10,9 @@ export interface PartnerMe {
staffRole?: PartnerStaffRole;
permissions?: string[];
primaryAccountId?: string;
+ /** 子账号联系主账号用 */
+ primaryPhone?: string;
+ primaryName?: string;
}
export interface UpdatePartnerMeRequest {
diff --git a/server/dukang-api/src/common/decorators/partner-permission.decorator.ts b/server/dukang-api/src/common/decorators/partner-permission.decorator.ts
new file mode 100644
index 0000000..98c1dd0
--- /dev/null
+++ b/server/dukang-api/src/common/decorators/partner-permission.decorator.ts
@@ -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);
diff --git a/server/dukang-api/src/common/guards/partner-permission.guard.ts b/server/dukang-api/src/common/guards/partner-permission.guard.ts
new file mode 100644
index 0000000..12ac3c9
--- /dev/null
+++ b/server/dukang-api/src/common/guards/partner-permission.guard.ts
@@ -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
{
+ 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(
+ 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('当前子账号无此操作权限');
+ }
+}
diff --git a/server/dukang-api/src/modules/iam/iam.module.ts b/server/dukang-api/src/modules/iam/iam.module.ts
index 8702871..45ecd70 100644
--- a/server/dukang-api/src/modules/iam/iam.module.ts
+++ b/server/dukang-api/src/modules/iam/iam.module.ts
@@ -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,
],
})
diff --git a/server/dukang-api/src/modules/settlement/settlement.controller.ts b/server/dukang-api/src/modules/settlement/settlement.controller.ts
index f346dcb..1cdf07c 100644
--- a/server/dukang-api/src/modules/settlement/settlement.controller.ts
+++ b/server/dukang-api/src/modules/settlement/settlement.controller.ts
@@ -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,
};
diff --git a/server/dukang-api/src/modules/store/store.controller.ts b/server/dukang-api/src/modules/store/store.controller.ts
index 5e14a48..d9d44db 100644
--- a/server/dukang-api/src/modules/store/store.controller.ts
+++ b/server/dukang-api/src/modules/store/store.controller.ts
@@ -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) {}
diff --git a/server/dukang-api/src/modules/store/store.service.ts b/server/dukang-api/src/modules/store/store.service.ts
index a50a30f..36a2c13 100644
--- a/server/dukang-api/src/modules/store/store.service.ts
+++ b/server/dukang-api/src/modules/store/store.service.ts
@@ -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(
diff --git a/server/dukang-api/src/modules/trade/trade.controller.ts b/server/dukang-api/src/modules/trade/trade.controller.ts
index 4dcc1f0..2713874 100644
--- a/server/dukang-api/src/modules/trade/trade.controller.ts
+++ b/server/dukang-api/src/modules/trade/trade.controller.ts
@@ -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) {}