fix:提交修复6个问题
This commit is contained in:
@@ -433,6 +433,7 @@ model Partner {
|
||||
bankAccountName String? @map("bank_account_name") @db.VarChar(64)
|
||||
bankAccountNo String? @map("bank_account_no") @db.VarChar(32)
|
||||
bankBranch String? @map("bank_branch") @db.VarChar(128)
|
||||
weeklyStoreTarget Int? @default(20) @map("weekly_store_target")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ async function main() {
|
||||
bankAccountName: '郑州合伙人公司',
|
||||
bankAccountNo: '6222021234567890',
|
||||
bankBranch: '工商银行郑州分行',
|
||||
weeklyStoreTarget: 20,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -150,6 +151,22 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
const primaryAccount = await prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { phone: '13700000001' },
|
||||
});
|
||||
|
||||
await prisma.partnerAccount.create({
|
||||
data: {
|
||||
partnerId: partner.id,
|
||||
phone: '13700000002',
|
||||
name: '拓店员小李',
|
||||
isPrimary: 0,
|
||||
parentAccountId: primaryAccount.id,
|
||||
staffRole: 'INTERNAL',
|
||||
status: 'DISABLED',
|
||||
},
|
||||
});
|
||||
|
||||
const storeDefs = [
|
||||
{
|
||||
name: '郑州老城店',
|
||||
@@ -173,6 +190,7 @@ async function main() {
|
||||
},
|
||||
];
|
||||
|
||||
const createdStores: { id: bigint; name: string }[] = [];
|
||||
for (const def of storeDefs) {
|
||||
const store = await prisma.store.create({
|
||||
data: {
|
||||
@@ -201,8 +219,42 @@ async function main() {
|
||||
data: { storeId: store.id, phone: def.phone, name: def.name },
|
||||
});
|
||||
}
|
||||
createdStores.push({ id: store.id, name: def.name });
|
||||
}
|
||||
|
||||
const weekStart = (() => {
|
||||
const d = new Date();
|
||||
d.setHours(12, 0, 0, 0);
|
||||
const day = d.getDay();
|
||||
const diff = day === 0 ? 6 : day - 1;
|
||||
d.setDate(d.getDate() - diff);
|
||||
d.setHours(10, 0, 0, 0);
|
||||
return d;
|
||||
})();
|
||||
|
||||
const newWeekStore = await prisma.store.create({
|
||||
data: {
|
||||
cityId: city.id,
|
||||
partnerId: partner.id,
|
||||
categoryId: categories[0].id,
|
||||
name: '本周新签体验店',
|
||||
phone: '13910000003',
|
||||
province: '河南省',
|
||||
cityName: '郑州市',
|
||||
district: '中原区',
|
||||
address: '建设路88号',
|
||||
intro: '本周新签约门店',
|
||||
status: 'OPEN',
|
||||
openTime: '10:00',
|
||||
closeTime: '22:00',
|
||||
bankAccountName: '本周新签体验店',
|
||||
bankAccountNo: '6222029876543211',
|
||||
bankBranch: '农业银行郑州分行',
|
||||
createdAt: new Date(weekStart.getTime() + 2 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
});
|
||||
createdStores.push({ id: newWeekStore.id, name: newWeekStore.name });
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
userNo: 'DK88293401',
|
||||
@@ -244,6 +296,91 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findUniqueOrThrow({ where: { phone: '13800000001' } });
|
||||
const product = products[0];
|
||||
|
||||
const orderDefs = [
|
||||
{ dayOffset: 0, payAmount: 1198, quantity: 2 },
|
||||
{ dayOffset: 1, payAmount: 599, quantity: 1 },
|
||||
{ dayOffset: 2, payAmount: 1760, quantity: 2 },
|
||||
{ dayOffset: 4, payAmount: 1299, quantity: 1 },
|
||||
{ dayOffset: 5, payAmount: 880, quantity: 1 },
|
||||
];
|
||||
|
||||
const coupons: { id: bigint; balance: number }[] = [];
|
||||
for (const [index, def] of orderDefs.entries()) {
|
||||
const paidAt = new Date(weekStart.getTime() + def.dayOffset * 24 * 60 * 60 * 1000 + 14 * 60 * 60 * 1000);
|
||||
const listAmount = def.payAmount;
|
||||
const order = await prisma.order.create({
|
||||
data: {
|
||||
orderNo: `WK${Date.now()}${index}`,
|
||||
userId: user.id,
|
||||
cityId: city.id,
|
||||
status: 'COMPLETED',
|
||||
payStatus: 'PAID',
|
||||
deliveryType: 'LOCAL',
|
||||
productId: product.id,
|
||||
barcode69: product.barcode69,
|
||||
productName: product.name,
|
||||
productSpec: product.spec,
|
||||
quantity: def.quantity,
|
||||
listUnitPrice: product.price,
|
||||
listAmount,
|
||||
productAmount: listAmount,
|
||||
payAmount: listAmount,
|
||||
benefitAmount: listAmount,
|
||||
receiverName: '测试用户',
|
||||
receiverPhone: user.phone,
|
||||
receiverAddress: '郑州市金水区测试路1号',
|
||||
receiverProvince: '河南省',
|
||||
receiverCity: '郑州市',
|
||||
receiverDistrict: '金水区',
|
||||
paidAt,
|
||||
completedAt: paidAt,
|
||||
},
|
||||
});
|
||||
const coupon = await prisma.benefitCoupon.create({
|
||||
data: {
|
||||
couponNo: `CPN${Date.now()}${index}`,
|
||||
userId: user.id,
|
||||
orderId: order.id,
|
||||
totalAmount: listAmount,
|
||||
balance: listAmount,
|
||||
sourceProduct: product.name,
|
||||
},
|
||||
});
|
||||
coupons.push({ id: coupon.id, balance: listAmount });
|
||||
}
|
||||
|
||||
const redeemDefs = [
|
||||
{ storeIndex: 0, amount: 42800, dayOffset: 1 },
|
||||
{ storeIndex: 1, amount: 38400, dayOffset: 2 },
|
||||
{ storeIndex: 2, amount: 31200, dayOffset: 3 },
|
||||
];
|
||||
for (const [index, def] of redeemDefs.entries()) {
|
||||
const coupon = coupons[index];
|
||||
const store = createdStores[def.storeIndex];
|
||||
const createdAt = new Date(weekStart.getTime() + def.dayOffset * 24 * 60 * 60 * 1000 + 16 * 60 * 60 * 1000);
|
||||
await prisma.redeemRecord.create({
|
||||
data: {
|
||||
redeemNo: `RD${Date.now()}${index}`,
|
||||
userId: user.id,
|
||||
couponId: coupon.id,
|
||||
storeId: store.id,
|
||||
amount: def.amount,
|
||||
settleAmount: Math.round(def.amount * 0.6 * 100) / 100,
|
||||
createdAt,
|
||||
},
|
||||
});
|
||||
await prisma.benefitCoupon.update({
|
||||
where: { id: coupon.id },
|
||||
data: {
|
||||
usedAmount: def.amount,
|
||||
balance: Math.max(0, coupon.balance - def.amount),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.module';
|
||||
import { AuthUser, JwtAuthGuard } from './jwt-auth.guard';
|
||||
|
||||
@Injectable()
|
||||
export class PartnerPrimaryGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly jwtAuthGuard: JwtAuthGuard,
|
||||
private readonly prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
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) {
|
||||
throw new ForbiddenException('仅主账号可操作');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -217,8 +217,14 @@ export class AuthService {
|
||||
if (existing) throw new BadRequestException('该手机号已绑定门店');
|
||||
return;
|
||||
}
|
||||
if (scene === SmsScene.PARTNER_LOGIN || scene === SmsScene.PARTNER_STAFF_ADD) {
|
||||
if (scene === SmsScene.PARTNER_LOGIN) {
|
||||
await this.assertPartnerAccountByPhone(phone);
|
||||
return;
|
||||
}
|
||||
if (scene === SmsScene.PARTNER_STAFF_ADD) {
|
||||
const existing = await this.prisma.partnerAccount.findUnique({ where: { phone } });
|
||||
if (existing) throw new BadRequestException('该手机号已被使用');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,6 +608,7 @@ export class AuthService {
|
||||
name: account.name,
|
||||
phone: account.phone,
|
||||
isPrimary: account.isPrimary === 1,
|
||||
staffRole: account.staffRole ?? undefined,
|
||||
companyName: account.partner.companyName,
|
||||
});
|
||||
}
|
||||
@@ -1152,6 +1159,7 @@ export class AuthService {
|
||||
name: account.name,
|
||||
phone: account.phone,
|
||||
isPrimary: account.isPrimary === 1,
|
||||
staffRole: account.staffRole ?? undefined,
|
||||
companyName: account.partner.companyName,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { AccountStatus, PartnerStaffRole } from '@dukang/shared-types';
|
||||
|
||||
export class CreatePartnerStaffDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(Object.values(PartnerStaffRole))
|
||||
staffRole: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
code: string;
|
||||
}
|
||||
|
||||
export class UpdatePartnerStaffDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(Object.values(PartnerStaffRole))
|
||||
@IsOptional()
|
||||
staffRole?: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(Object.values(AccountStatus))
|
||||
@IsOptional()
|
||||
status?: string;
|
||||
}
|
||||
@@ -11,11 +11,14 @@ import {
|
||||
} from './auth.controller';
|
||||
import { UserAddressController } from './user-address.controller';
|
||||
import { UserAddressService } from './user-address.service';
|
||||
import { PartnerStaffController } from './partner-staff.controller';
|
||||
import { PartnerStaffService } from './partner-staff.service';
|
||||
import { AdminAuthController } from './admin-auth.controller';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -30,11 +33,12 @@ import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
UserAuthController,
|
||||
ShopAuthController,
|
||||
PartnerAuthController,
|
||||
PartnerStaffController,
|
||||
UserProfileController,
|
||||
UserAddressController,
|
||||
AdminAuthController,
|
||||
],
|
||||
providers: [AuthService, UserAddressService, JwtAuthGuard, PhoneVerifiedGuard, OptionalJwtAuthGuard, HqAuthGuard],
|
||||
exports: [AuthService, UserAddressService, JwtModule, JwtAuthGuard, PhoneVerifiedGuard, OptionalJwtAuthGuard, HqAuthGuard],
|
||||
providers: [AuthService, UserAddressService, PartnerStaffService, JwtAuthGuard, PhoneVerifiedGuard, OptionalJwtAuthGuard, HqAuthGuard, PartnerPrimaryGuard],
|
||||
exports: [AuthService, UserAddressService, PartnerStaffService, JwtModule, JwtAuthGuard, PhoneVerifiedGuard, OptionalJwtAuthGuard, HqAuthGuard, PartnerPrimaryGuard],
|
||||
})
|
||||
export class IamModule {}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { PartnerStaffService } from './partner-staff.service';
|
||||
import { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto';
|
||||
|
||||
@Controller('partner/staff')
|
||||
@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
|
||||
export class PartnerStaffController {
|
||||
constructor(private readonly staffService: PartnerStaffService) {}
|
||||
|
||||
@Get()
|
||||
list(@CurrentUser() user: AuthUser) {
|
||||
return this.staffService.listStaff(user.actorId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@CurrentUser() user: AuthUser, @Body() dto: CreatePartnerStaffDto) {
|
||||
return this.staffService.createStaff(user.actorId, dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdatePartnerStaffDto,
|
||||
) {
|
||||
return this.staffService.updateStaff(user.actorId, BigInt(id), dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.staffService.deleteStaff(user.actorId, BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PartnerStaffRole, SmsScene } from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { AuthService } from './auth.service';
|
||||
import { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto';
|
||||
|
||||
@Injectable()
|
||||
export class PartnerStaffService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
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 createStaff(parentAccountId: bigint, dto: CreatePartnerStaffDto) {
|
||||
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('该手机号已被使用');
|
||||
|
||||
await this.authService.verifySmsCode(phone, dto.code, SmsScene.PARTNER_STAFF_ADD);
|
||||
|
||||
const name = dto.name.trim();
|
||||
if (!name) throw new BadRequestException('请填写真实姓名');
|
||||
|
||||
const account = await this.prisma.partnerAccount.create({
|
||||
data: {
|
||||
partnerId: parent.partnerId,
|
||||
phone,
|
||||
name,
|
||||
staffRole: dto.staffRole as PartnerStaffRole,
|
||||
isPrimary: 0,
|
||||
parentAccountId: parent.id,
|
||||
status: 'DISABLED',
|
||||
},
|
||||
});
|
||||
|
||||
return this.toStaffItem(account);
|
||||
}
|
||||
|
||||
async updateStaff(parentAccountId: bigint, staffId: bigint, dto: UpdatePartnerStaffDto) {
|
||||
const staff = await this.assertStaffOwned(parentAccountId, staffId);
|
||||
const data: Record<string, unknown> = {};
|
||||
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.status !== undefined) {
|
||||
data.status = dto.status;
|
||||
}
|
||||
const updated = await this.prisma.partnerAccount.update({
|
||||
where: { id: staff.id },
|
||||
data,
|
||||
});
|
||||
return this.toStaffItem(updated);
|
||||
}
|
||||
|
||||
async deleteStaff(parentAccountId: bigint, staffId: bigint) {
|
||||
const staff = await this.assertStaffOwned(parentAccountId, staffId);
|
||||
await this.prisma.partnerAccount.delete({ where: { id: staff.id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
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;
|
||||
status: string;
|
||||
lastLoginAt: Date | null;
|
||||
}) {
|
||||
return serializeBigInt({
|
||||
id: row.id.toString(),
|
||||
name: row.name,
|
||||
phone: this.maskPhone(row.phone),
|
||||
staffRole: row.staffRole ?? PartnerStaffRole.INTERNAL,
|
||||
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)}`;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { SettlementService } from './settlement.service';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||
@@ -8,7 +9,7 @@ import { HqOperationAction } from '../../common/hq-operation/hq-operation.consta
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
|
||||
@Controller('partner/settlement')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
|
||||
export class SettlementController {
|
||||
constructor(private readonly settlementService: SettlementService) {}
|
||||
|
||||
@@ -158,6 +159,7 @@ export class PartnerMeController {
|
||||
name: account.name,
|
||||
phone: account.phone,
|
||||
isPrimary: account.isPrimary === 1,
|
||||
staffRole: account.staffRole ?? undefined,
|
||||
companyName: account.partner.companyName,
|
||||
hasWechat: !!account.wxOpenId,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nest
|
||||
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 { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
@Controller('stores')
|
||||
@@ -34,6 +35,11 @@ export class PartnerStoreController {
|
||||
return this.storeService.partnerListCities(user.actorId);
|
||||
}
|
||||
|
||||
@Get('phone-available')
|
||||
phoneAvailable(@Query('phone') phone?: string) {
|
||||
return this.storeService.partnerCheckStorePhone(phone ?? '');
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.storeService.partnerGetStore(user.actorId, BigInt(id));
|
||||
@@ -64,7 +70,7 @@ export class PartnerStoreController {
|
||||
}
|
||||
|
||||
@Controller('partner/dashboard')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
|
||||
export class PartnerDashboardController {
|
||||
constructor(private readonly storeService: StoreService) {}
|
||||
|
||||
@@ -72,6 +78,30 @@ export class PartnerDashboardController {
|
||||
dashboard(@CurrentUser() user: AuthUser) {
|
||||
return this.storeService.partnerDashboard(user.actorId);
|
||||
}
|
||||
|
||||
@Get('leaderboard')
|
||||
leaderboard(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Query('period') period?: string,
|
||||
) {
|
||||
const normalized =
|
||||
period === 'month' || period === 'lastMonth' || period === 'total' ? period : 'total';
|
||||
return this.storeService.partnerLeaderboard(user.actorId, normalized);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('partner/reports')
|
||||
@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
|
||||
export class PartnerReportController {
|
||||
constructor(private readonly storeService: StoreService) {}
|
||||
|
||||
@Get('weekly')
|
||||
weekly(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Query('startDate') startDate?: string,
|
||||
) {
|
||||
return this.storeService.partnerWeeklyReport(user.actorId, startDate);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('shop/store')
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AnalyticsModule } from '../analytics/analytics.module';
|
||||
import { StoreService } from './store.service';
|
||||
import {
|
||||
PartnerDashboardController,
|
||||
PartnerReportController,
|
||||
PartnerStoreController,
|
||||
PublicStoreController,
|
||||
ShopDashboardController,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
PublicStoreController,
|
||||
PartnerStoreController,
|
||||
PartnerDashboardController,
|
||||
PartnerReportController,
|
||||
ShopStoreController,
|
||||
ShopDashboardController,
|
||||
],
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { loadAppConfig } from '@dukang/shared-types';
|
||||
import { PARTNER_STAFF_ROLE_LABELS, PartnerStaffRole, type PartnerLeaderboardPeriod } from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { mapStoreCompat } from '../../common/compat/v31-compat';
|
||||
@@ -47,8 +49,14 @@ export class StoreService {
|
||||
|
||||
async partnerListStores(partnerAccountId: bigint) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
const where: { partnerId: bigint; id?: { in: bigint[] } } = { partnerId: account.partnerId };
|
||||
if (this.isSubAccount(account)) {
|
||||
const storeIds = await this.getStoreIdsCreatedByAccount(partnerAccountId);
|
||||
if (storeIds.length === 0) return [];
|
||||
where.id = { in: storeIds };
|
||||
}
|
||||
const stores = await this.prisma.store.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
where,
|
||||
include: { category: true, coverResource: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
@@ -62,6 +70,9 @@ export class StoreService {
|
||||
include: { category: true, coverResource: true },
|
||||
});
|
||||
if (!store) throw new NotFoundException('门店不存在');
|
||||
if (this.isSubAccount(account)) {
|
||||
await this.assertStoreOwnedByAccount(partnerAccountId, storeId);
|
||||
}
|
||||
|
||||
const media = await this.prisma.commonResource.findMany({
|
||||
where: {
|
||||
@@ -85,16 +96,27 @@ export class StoreService {
|
||||
return serializeBigInt(cities);
|
||||
}
|
||||
|
||||
async createStore(partnerAccountId: bigint, body: Record<string, unknown>) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
const normalizedPhone = String(body.phone).trim();
|
||||
async partnerCheckStorePhone(phone: string) {
|
||||
const normalizedPhone = phone.trim();
|
||||
if (!normalizedPhone) {
|
||||
return { available: false, message: '请填写联系电话' };
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(normalizedPhone)) {
|
||||
throw new BadRequestException('联系电话须为11位手机号');
|
||||
return { available: false, message: '联系电话须为11位手机号' };
|
||||
}
|
||||
const existingAccount = await this.prisma.storeAccount.findUnique({
|
||||
where: { phone: normalizedPhone },
|
||||
});
|
||||
if (existingAccount) throw new BadRequestException('该手机号已绑定门店');
|
||||
if (existingAccount) {
|
||||
return { available: false, message: '该手机号已绑定门店' };
|
||||
}
|
||||
return { available: true };
|
||||
}
|
||||
|
||||
async createStore(partnerAccountId: bigint, body: Record<string, unknown>) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
const normalizedPhone = String(body.phone).trim();
|
||||
await this.assertStorePhoneAvailable(normalizedPhone);
|
||||
|
||||
const city = await this.resolvePartnerCity(account.partnerId, body.cityId);
|
||||
const coverUrl = body.coverUrl ? String(body.coverUrl).trim() : '';
|
||||
@@ -103,6 +125,10 @@ export class StoreService {
|
||||
: [];
|
||||
const contractUrl = body.contractUrl ? String(body.contractUrl).trim() : '';
|
||||
|
||||
if (!coverUrl) throw new BadRequestException('请上传门头照');
|
||||
if (envPhotoUrls.length < 3) throw new BadRequestException('请上传至少 3 张环境照片');
|
||||
if (!contractUrl) throw new BadRequestException('请上传签约合同');
|
||||
|
||||
const store = await this.prisma.store.create({
|
||||
data: {
|
||||
cityId: city.id,
|
||||
@@ -209,6 +235,7 @@ export class StoreService {
|
||||
status: 'OPEN' | 'PAUSED' | 'CLOSED',
|
||||
) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
this.assertPrimaryAccount(account);
|
||||
const store = await this.prisma.store.findFirst({
|
||||
where: { id: storeId, partnerId: account.partnerId },
|
||||
});
|
||||
@@ -244,6 +271,7 @@ export class StoreService {
|
||||
body: Record<string, unknown>,
|
||||
) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
this.assertPrimaryAccount(account);
|
||||
const store = await this.prisma.store.findFirst({
|
||||
where: { id: storeId, partnerId: account.partnerId },
|
||||
});
|
||||
@@ -310,7 +338,13 @@ export class StoreService {
|
||||
|
||||
async partnerDashboard(partnerAccountId: bigint) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
const [storeCount, orderCount, recentStores] = await Promise.all([
|
||||
this.assertPrimaryAccount(account);
|
||||
const partnerStoreIds = await this.prisma.store.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
select: { id: true },
|
||||
});
|
||||
const storeIds = partnerStoreIds.map((s) => s.id);
|
||||
const [storeCount, orderCount, recentStores, pendingAuditCount] = await Promise.all([
|
||||
this.prisma.store.count({ where: { partnerId: account.partnerId } }),
|
||||
this.prisma.order.count({
|
||||
where: { city: { partnerId: account.partnerId } },
|
||||
@@ -321,15 +355,322 @@ export class StoreService {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10,
|
||||
}),
|
||||
storeIds.length === 0
|
||||
? Promise.resolve(0)
|
||||
: this.prisma.commonEvent.count({
|
||||
where: {
|
||||
eventType: 'STORE_AUDIT',
|
||||
status: 'PENDING',
|
||||
refType: 'STORE',
|
||||
refId: { in: storeIds },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
return {
|
||||
storeCount,
|
||||
orderCount,
|
||||
companyName: account.partner.companyName,
|
||||
recentStores: serializeBigInt(recentStores),
|
||||
pendingAuditCount,
|
||||
};
|
||||
}
|
||||
|
||||
async partnerLeaderboard(partnerAccountId: bigint, period: PartnerLeaderboardPeriod = 'total') {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
this.assertPrimaryAccount(account);
|
||||
|
||||
const accounts = await this.prisma.partnerAccount.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
orderBy: { id: 'asc' },
|
||||
});
|
||||
|
||||
const { periodStart, periodEnd } = this.resolveLeaderboardPeriodRange(period);
|
||||
const entries = await Promise.all(
|
||||
accounts.map(async (row) => {
|
||||
const totalStores = await this.prisma.commonEvent.count({
|
||||
where: {
|
||||
eventType: 'STORE_AUDIT',
|
||||
param1: 'NEW',
|
||||
actorType: 'PARTNER',
|
||||
actorId: row.id,
|
||||
refType: 'STORE',
|
||||
},
|
||||
});
|
||||
const periodStores =
|
||||
period === 'total'
|
||||
? totalStores
|
||||
: await this.prisma.commonEvent.count({
|
||||
where: {
|
||||
eventType: 'STORE_AUDIT',
|
||||
param1: 'NEW',
|
||||
actorType: 'PARTNER',
|
||||
actorId: row.id,
|
||||
refType: 'STORE',
|
||||
createdAt: { gte: periodStart, lt: periodEnd },
|
||||
},
|
||||
});
|
||||
const staffRole = row.staffRole as PartnerStaffRole | null;
|
||||
const roleLabel =
|
||||
staffRole != null
|
||||
? PARTNER_STAFF_ROLE_LABELS[staffRole]
|
||||
: row.isPrimary === 1
|
||||
? PARTNER_STAFF_ROLE_LABELS.PARTNER
|
||||
: PARTNER_STAFF_ROLE_LABELS.INTERNAL;
|
||||
return {
|
||||
accountId: row.id.toString(),
|
||||
name: row.name,
|
||||
staffRole: staffRole ?? undefined,
|
||||
roleLabel,
|
||||
totalStores,
|
||||
periodStores,
|
||||
isSelf: row.id === partnerAccountId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
entries.sort((a, b) => {
|
||||
if (b.periodStores !== a.periodStores) return b.periodStores - a.periodStores;
|
||||
if (b.totalStores !== a.totalStores) return b.totalStores - a.totalStores;
|
||||
return a.accountId.localeCompare(b.accountId);
|
||||
});
|
||||
|
||||
const list = entries.map((entry, index) => ({
|
||||
...entry,
|
||||
rank: index + 1,
|
||||
}));
|
||||
|
||||
const selfEntry = list.find((entry) => entry.isSelf);
|
||||
let self: (typeof list)[number] & { beatPercent?: number } | undefined;
|
||||
if (selfEntry) {
|
||||
const below = list.filter((entry) => entry.rank > selfEntry.rank).length;
|
||||
const beatPercent =
|
||||
list.length <= 1 ? 0 : Math.round((below / (list.length - 1)) * 100);
|
||||
self = { ...selfEntry, beatPercent };
|
||||
}
|
||||
|
||||
return { period, list, self };
|
||||
}
|
||||
|
||||
async partnerWeeklyReport(partnerAccountId: bigint, startDate?: string) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
this.assertPrimaryAccount(account);
|
||||
const partnerId = account.partnerId;
|
||||
|
||||
const currentWeekStart = this.startOfWeekMonday(new Date());
|
||||
const periodStart =
|
||||
startDate && /^\d{4}-\d{2}-\d{2}$/.test(startDate)
|
||||
? this.parseLocalDate(startDate)
|
||||
: currentWeekStart;
|
||||
const periodEnd = this.addDays(periodStart, 7);
|
||||
const prevPeriodStart = this.addDays(periodStart, -7);
|
||||
const prevPeriodEnd = periodStart;
|
||||
|
||||
const newStoreTarget =
|
||||
account.partner.weeklyStoreTarget ??
|
||||
Number(process.env.PARTNER_WEEKLY_STORE_TARGET ?? 20);
|
||||
|
||||
const orderWhere = {
|
||||
city: { partnerId },
|
||||
payStatus: 'PAID' as const,
|
||||
paidAt: { gte: periodStart, lt: periodEnd },
|
||||
};
|
||||
const prevOrderWhere = {
|
||||
city: { partnerId },
|
||||
payStatus: 'PAID' as const,
|
||||
paidAt: { gte: prevPeriodStart, lt: prevPeriodEnd },
|
||||
};
|
||||
|
||||
const [
|
||||
gmvAgg,
|
||||
orderCount,
|
||||
totalStoreCount,
|
||||
newStoreCount,
|
||||
prevGmvAgg,
|
||||
redeemGroups,
|
||||
activeRedeems,
|
||||
paidOrders,
|
||||
] = await Promise.all([
|
||||
this.prisma.order.aggregate({ where: orderWhere, _sum: { payAmount: true } }),
|
||||
this.prisma.order.count({ where: orderWhere }),
|
||||
this.prisma.store.count({ where: { partnerId } }),
|
||||
this.prisma.store.count({
|
||||
where: { partnerId, createdAt: { gte: periodStart, lt: periodEnd } },
|
||||
}),
|
||||
this.prisma.order.aggregate({ where: prevOrderWhere, _sum: { payAmount: true } }),
|
||||
this.prisma.redeemRecord.groupBy({
|
||||
by: ['storeId'],
|
||||
where: {
|
||||
createdAt: { gte: periodStart, lt: periodEnd },
|
||||
store: { partnerId },
|
||||
},
|
||||
_sum: { amount: true },
|
||||
orderBy: { _sum: { amount: 'desc' } },
|
||||
take: 10,
|
||||
}),
|
||||
this.prisma.redeemRecord.findMany({
|
||||
where: {
|
||||
createdAt: { gte: periodStart, lt: periodEnd },
|
||||
store: { partnerId },
|
||||
},
|
||||
select: { storeId: true },
|
||||
distinct: ['storeId'],
|
||||
}),
|
||||
this.prisma.order.findMany({
|
||||
where: orderWhere,
|
||||
select: { payAmount: true, paidAt: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const gmv = Number(gmvAgg._sum.payAmount ?? 0);
|
||||
const prevGmv = Number(prevGmvAgg._sum.payAmount ?? 0);
|
||||
const gmvGrowthPercent =
|
||||
prevGmv === 0 ? 0 : Math.round(((gmv - prevGmv) / prevGmv) * 1000) / 10;
|
||||
const newStoreProgressPercent =
|
||||
newStoreTarget <= 0
|
||||
? 0
|
||||
: Math.min(100, Math.round((newStoreCount / newStoreTarget) * 100));
|
||||
|
||||
const dailyGmvMap = new Map<string, number>();
|
||||
for (let i = 0; i < 7; i += 1) {
|
||||
dailyGmvMap.set(this.formatDateKey(this.addDays(periodStart, i)), 0);
|
||||
}
|
||||
for (const order of paidOrders) {
|
||||
if (!order.paidAt) continue;
|
||||
const key = this.formatDateKey(order.paidAt);
|
||||
if (dailyGmvMap.has(key)) {
|
||||
dailyGmvMap.set(key, (dailyGmvMap.get(key) ?? 0) + Number(order.payAmount));
|
||||
}
|
||||
}
|
||||
const dailyGmv = Array.from({ length: 7 }, (_, i) => {
|
||||
const day = this.addDays(periodStart, i);
|
||||
const date = this.formatDateKey(day);
|
||||
return {
|
||||
date,
|
||||
weekdayLabel: this.weekdayLabel(i),
|
||||
amount: dailyGmvMap.get(date) ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
const rankStoreIds = redeemGroups.map((group) => group.storeId);
|
||||
const rankStores =
|
||||
rankStoreIds.length > 0
|
||||
? await this.prisma.store.findMany({
|
||||
where: { id: { in: rankStoreIds } },
|
||||
select: { id: true, name: true, intro: true, address: true },
|
||||
})
|
||||
: [];
|
||||
const storeMap = new Map(rankStores.map((store) => [store.id.toString(), store]));
|
||||
const storeRanking = redeemGroups.map((group, index) => {
|
||||
const store = storeMap.get(group.storeId.toString());
|
||||
const subtitleSource = store?.intro?.trim() || store?.address?.trim() || '';
|
||||
const subtitle =
|
||||
subtitleSource.length > 30 ? `${subtitleSource.slice(0, 30)}…` : subtitleSource;
|
||||
return {
|
||||
rank: index + 1,
|
||||
storeId: group.storeId.toString(),
|
||||
name: store?.name ?? '未知门店',
|
||||
subtitle: subtitle || undefined,
|
||||
redeemAmount: Number(group._sum.amount ?? 0),
|
||||
};
|
||||
});
|
||||
|
||||
const topStore = storeRanking[0];
|
||||
let insight: string;
|
||||
if (topStore && topStore.redeemAmount > 0) {
|
||||
if (gmvGrowthPercent > 0) {
|
||||
insight = `本周 GMV 增长 ${gmvGrowthPercent}%,主要得益于「${topStore.name}」的核销表现。建议关注同类高潜力门店。`;
|
||||
} else if (gmvGrowthPercent < 0) {
|
||||
insight = `本周 GMV 较上期下降 ${Math.abs(gmvGrowthPercent)}%,「${topStore.name}」仍为核销领先门店。建议复盘低效门店并复制头部经验。`;
|
||||
} else {
|
||||
insight = `本周 GMV 与上期持平,「${topStore.name}」核销表现领先。建议持续推动门店活跃。`;
|
||||
}
|
||||
} else {
|
||||
insight = '本周暂无核销数据,建议关注门店培训与权益推广,激活辖区门店。';
|
||||
}
|
||||
|
||||
const availablePeriods = [0, 1, 2, 3].map((offset) => {
|
||||
const start = this.addDays(currentWeekStart, -7 * offset);
|
||||
const end = this.addDays(start, 7);
|
||||
return {
|
||||
startDate: this.formatDateKey(start),
|
||||
endDate: this.formatDateKey(this.addDays(end, -1)),
|
||||
label: this.formatPeriodLabel(start, end),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
period: {
|
||||
startDate: this.formatDateKey(periodStart),
|
||||
endDate: this.formatDateKey(this.addDays(periodEnd, -1)),
|
||||
label: this.formatPeriodLabel(periodStart, periodEnd),
|
||||
},
|
||||
availablePeriods,
|
||||
summary: {
|
||||
gmv,
|
||||
gmvGrowthPercent,
|
||||
activeStoreCount: activeRedeems.length,
|
||||
totalStoreCount,
|
||||
orderCount,
|
||||
newStoreCount,
|
||||
newStoreTarget,
|
||||
newStoreProgressPercent,
|
||||
},
|
||||
dailyGmv,
|
||||
storeRanking,
|
||||
insight,
|
||||
};
|
||||
}
|
||||
|
||||
private resolveLeaderboardPeriodRange(period: PartnerLeaderboardPeriod) {
|
||||
const now = new Date();
|
||||
if (period === 'month') {
|
||||
const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
return { periodStart, periodEnd: now };
|
||||
}
|
||||
if (period === 'lastMonth') {
|
||||
const periodStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const periodEnd = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
return { periodStart, periodEnd };
|
||||
}
|
||||
return { periodStart: new Date(0), periodEnd: now };
|
||||
}
|
||||
|
||||
private startOfWeekMonday(date: Date): Date {
|
||||
const d = new Date(date);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
const day = d.getDay();
|
||||
const diff = day === 0 ? 6 : day - 1;
|
||||
d.setDate(d.getDate() - diff);
|
||||
return d;
|
||||
}
|
||||
|
||||
private addDays(date: Date, days: number): Date {
|
||||
const d = new Date(date);
|
||||
d.setDate(d.getDate() + days);
|
||||
return d;
|
||||
}
|
||||
|
||||
private parseLocalDate(dateKey: string): Date {
|
||||
const [year, month, day] = dateKey.split('-').map(Number);
|
||||
return new Date(year, month - 1, day, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
private formatDateKey(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
private formatPeriodLabel(start: Date, endExclusive: Date): string {
|
||||
const end = this.addDays(endExclusive, -1);
|
||||
return `${start.getMonth() + 1}月${start.getDate()}日 - ${end.getMonth() + 1}月${end.getDate()}日`;
|
||||
}
|
||||
|
||||
private weekdayLabel(index: number): string {
|
||||
return ['周一', '周二', '周三', '周四', '周五', '周六', '周日'][index] ?? '';
|
||||
}
|
||||
|
||||
private async getPartnerAccount(partnerAccountId: bigint) {
|
||||
return this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: partnerAccountId },
|
||||
@@ -349,4 +690,49 @@ export class StoreService {
|
||||
if (!city) throw new BadRequestException('合伙人未绑定开城');
|
||||
return city;
|
||||
}
|
||||
|
||||
private isSubAccount(account: { isPrimary: number }) {
|
||||
return account.isPrimary !== 1;
|
||||
}
|
||||
|
||||
private assertPrimaryAccount(account: { isPrimary: number }) {
|
||||
if (this.isSubAccount(account)) {
|
||||
throw new ForbiddenException('子账号无权执行此操作');
|
||||
}
|
||||
}
|
||||
|
||||
private async assertStorePhoneAvailable(phone: string) {
|
||||
const result = await this.partnerCheckStorePhone(phone);
|
||||
if (!result.available) {
|
||||
throw new BadRequestException(result.message ?? '该手机号已绑定门店');
|
||||
}
|
||||
}
|
||||
|
||||
private async getStoreIdsCreatedByAccount(partnerAccountId: bigint): Promise<bigint[]> {
|
||||
const events = await this.prisma.commonEvent.findMany({
|
||||
where: {
|
||||
eventType: 'STORE_AUDIT',
|
||||
param1: 'NEW',
|
||||
actorType: 'PARTNER',
|
||||
actorId: partnerAccountId,
|
||||
refType: 'STORE',
|
||||
},
|
||||
select: { refId: true },
|
||||
});
|
||||
return events.map((event) => event.refId).filter((id): id is bigint => id != null);
|
||||
}
|
||||
|
||||
private async assertStoreOwnedByAccount(partnerAccountId: bigint, storeId: bigint) {
|
||||
const event = await this.prisma.commonEvent.findFirst({
|
||||
where: {
|
||||
eventType: 'STORE_AUDIT',
|
||||
param1: 'NEW',
|
||||
actorType: 'PARTNER',
|
||||
actorId: partnerAccountId,
|
||||
refType: 'STORE',
|
||||
refId: storeId,
|
||||
},
|
||||
});
|
||||
if (!event) throw new ForbiddenException('无权查看该门店');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, Post, Put, Query, Req, UseGuards } from '
|
||||
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 { PhoneVerifiedGuard } from '../../common/guards/phone-verified.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
@@ -67,7 +68,7 @@ export class TradeController {
|
||||
}
|
||||
|
||||
@Controller('partner/orders')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
|
||||
export class PartnerOrderController {
|
||||
constructor(private readonly tradeService: TradeService) {}
|
||||
|
||||
@@ -96,7 +97,7 @@ export class PartnerOrderController {
|
||||
}
|
||||
|
||||
@Controller('partner/reshipments')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
|
||||
export class PartnerReshipmentController {
|
||||
constructor(private readonly tradeService: TradeService) {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user