hqweb端
This commit is contained in:
@@ -14,6 +14,7 @@ import { RedeemModule } from './modules/redeem/redeem.module';
|
||||
import { SettlementModule } from './modules/settlement/settlement.module';
|
||||
import { AnalyticsModule } from './modules/analytics/analytics.module';
|
||||
import { JobsModule } from './jobs/jobs.module';
|
||||
import { OpsModule } from './modules/ops/ops.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -36,6 +37,7 @@ import { JobsModule } from './jobs/jobs.module';
|
||||
SettlementModule,
|
||||
AnalyticsModule,
|
||||
JobsModule,
|
||||
OpsModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { ClientApp } from '@dukang/shared-types';
|
||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||
|
||||
@Injectable()
|
||||
export class HqAuthGuard extends JwtAuthGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const ok = super.canActivate(context);
|
||||
if (!ok) return false;
|
||||
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const clientApp = req.headers['x-client-app'] as ClientApp;
|
||||
if (clientApp !== ClientApp.HQ_WEB) {
|
||||
throw new UnauthorizedException('Invalid client app for admin');
|
||||
}
|
||||
if (req.user?.actorType !== 'HQ') {
|
||||
throw new UnauthorizedException('HQ access required');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.module';
|
||||
import type { AuthUser } from './jwt-auth.guard';
|
||||
|
||||
@Injectable()
|
||||
export class SuperAdminGuard implements CanActivate {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const user = req.user as AuthUser | undefined;
|
||||
if (!user || user.actorType !== 'HQ') {
|
||||
throw new ForbiddenException('需要 HQ 权限');
|
||||
}
|
||||
const account = await this.prisma.hqAccount.findUnique({
|
||||
where: { id: user.actorId },
|
||||
select: { adminRole: true, status: true },
|
||||
});
|
||||
if (!account || account.status !== 'ACTIVE' || account.adminRole !== 'SUPER_ADMIN') {
|
||||
throw new ForbiddenException('需要超级管理员权限');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginSmsDto, SendSmsDto } from './dto/auth.dto';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { ClientApp } from '@dukang/shared-types';
|
||||
|
||||
@Controller('admin/auth')
|
||||
export class AdminAuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('sms/send')
|
||||
sendSms(@Body() dto: SendSmsDto) {
|
||||
return this.authService.sendSms(dto.phone, dto.scene);
|
||||
}
|
||||
|
||||
@Post('login/sms')
|
||||
login(@Body() dto: LoginSmsDto) {
|
||||
return this.authService.loginHq(dto.phone, dto.code, ClientApp.HQ_WEB);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
me(@CurrentUser() user: AuthUser) {
|
||||
return this.authService.getMe(user.actorType, user.actorId);
|
||||
}
|
||||
}
|
||||
@@ -227,6 +227,24 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
async loginHq(phone: string, code: string, clientApp: ClientApp) {
|
||||
await this.smsProvider.verify(phone, code, SmsScene.HQ_LOGIN);
|
||||
const account = await this.prisma.hqAccount.findUnique({ where: { phone } });
|
||||
if (!account) throw new BadRequestException('HQ账号不存在');
|
||||
if (account.status !== 'ACTIVE') throw new BadRequestException('账号已停用');
|
||||
await this.prisma.hqAccount.update({
|
||||
where: { id: account.id },
|
||||
data: { lastLoginAt: new Date() },
|
||||
});
|
||||
return this.issueToken('HQ', account.id, clientApp, false, undefined, undefined, undefined, undefined, {
|
||||
id: account.id.toString(),
|
||||
phone: account.phone,
|
||||
name: account.name,
|
||||
adminRole: account.adminRole,
|
||||
status: account.status,
|
||||
});
|
||||
}
|
||||
|
||||
async getMe(actorType: string, actorId: bigint) {
|
||||
if (actorType === 'USER') {
|
||||
const user = await this.assertActiveUser(actorId);
|
||||
@@ -246,6 +264,10 @@ export class AuthService {
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
}
|
||||
if (actorType === 'HQ') {
|
||||
const account = await this.prisma.hqAccount.findUnique({ where: { id: actorId } });
|
||||
return serializeBigInt(account);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -358,6 +380,7 @@ export class AuthService {
|
||||
store?: Record<string, unknown>,
|
||||
partner?: Record<string, unknown>,
|
||||
deviceKey?: string | null,
|
||||
hq?: Record<string, unknown>,
|
||||
) {
|
||||
const payload = {
|
||||
sub: actorId.toString(),
|
||||
@@ -378,6 +401,7 @@ export class AuthService {
|
||||
user,
|
||||
store,
|
||||
partner,
|
||||
hq,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@ import {
|
||||
} from './auth.controller';
|
||||
import { UserAddressController } from './user-address.controller';
|
||||
import { UserAddressService } from './user-address.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';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -28,8 +30,9 @@ import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guar
|
||||
PartnerAuthController,
|
||||
UserProfileController,
|
||||
UserAddressController,
|
||||
AdminAuthController,
|
||||
],
|
||||
providers: [AuthService, UserAddressService, JwtAuthGuard, PhoneVerifiedGuard, OptionalJwtAuthGuard],
|
||||
exports: [AuthService, JwtModule, JwtAuthGuard, PhoneVerifiedGuard, OptionalJwtAuthGuard],
|
||||
providers: [AuthService, UserAddressService, JwtAuthGuard, PhoneVerifiedGuard, OptionalJwtAuthGuard, HqAuthGuard],
|
||||
exports: [AuthService, JwtModule, JwtAuthGuard, PhoneVerifiedGuard, OptionalJwtAuthGuard, HqAuthGuard],
|
||||
})
|
||||
export class IamModule {}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminBenefitService } from './admin-benefit.service';
|
||||
import { AdminBenefitCouponsQueryDto, AdminBenefitLedgersQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
@Controller('admin/benefit/coupons')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminBenefitCouponsController {
|
||||
constructor(private readonly service: AdminBenefitService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminBenefitCouponsQueryDto) {
|
||||
return this.service.listCoupons(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detailCoupon(BigInt(id));
|
||||
}
|
||||
|
||||
@Post(':id/void')
|
||||
voidCoupon(@Param('id') id: string) {
|
||||
return this.service.voidCoupon(BigInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('admin/benefit/ledgers')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminBenefitLedgersController {
|
||||
constructor(private readonly service: AdminBenefitService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminBenefitLedgersQueryDto) {
|
||||
return this.service.listLedgers(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { AdminBenefitCouponsQueryDto, AdminBenefitLedgersQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminBenefitService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async listCoupons(query: AdminBenefitCouponsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.BenefitCouponWhereInput = {};
|
||||
if (query.couponNo) where.couponNo = { contains: query.couponNo };
|
||||
if (query.userId) where.userId = BigInt(query.userId);
|
||||
if (query.status) where.status = query.status as Prisma.EnumBenefitCouponStatusFilter['equals'];
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.benefitCoupon.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
user: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
||||
order: { select: { id: true, orderNo: true, status: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.benefitCoupon.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
}
|
||||
|
||||
async detailCoupon(id: bigint) {
|
||||
const coupon = await this.prisma.benefitCoupon.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
||||
order: { select: { id: true, orderNo: true, status: true, payAmount: true } },
|
||||
ledgers: { orderBy: { createdAt: 'desc' }, take: 20 },
|
||||
redeemRecords: { orderBy: { createdAt: 'desc' }, take: 10, include: { store: { select: { id: true, name: true } } } },
|
||||
},
|
||||
});
|
||||
if (!coupon) throw new NotFoundException('权益券不存在');
|
||||
return serializeBigInt(coupon);
|
||||
}
|
||||
|
||||
async voidCoupon(id: bigint) {
|
||||
const coupon = await this.prisma.benefitCoupon.findUnique({ where: { id } });
|
||||
if (!coupon) throw new NotFoundException('权益券不存在');
|
||||
if (coupon.status === 'VOID') throw new BadRequestException('权益券已作废');
|
||||
|
||||
const updated = await this.prisma.$transaction(async (tx) => {
|
||||
const row = await tx.benefitCoupon.update({
|
||||
where: { id },
|
||||
data: { status: 'VOID', balance: 0 },
|
||||
});
|
||||
if (Number(coupon.balance) > 0) {
|
||||
await tx.benefitLedger.create({
|
||||
data: {
|
||||
userId: coupon.userId,
|
||||
couponId: coupon.id,
|
||||
type: 'ADJUST',
|
||||
amount: -Number(coupon.balance),
|
||||
balanceAfter: 0,
|
||||
refType: 'ADMIN_VOID',
|
||||
remark: 'HQ 手动作废',
|
||||
},
|
||||
});
|
||||
}
|
||||
return row;
|
||||
});
|
||||
return serializeBigInt(updated);
|
||||
}
|
||||
|
||||
async listLedgers(query: AdminBenefitLedgersQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.BenefitLedgerWhereInput = {};
|
||||
if (query.userId) where.userId = BigInt(query.userId);
|
||||
if (query.couponId) where.couponId = BigInt(query.couponId);
|
||||
if (query.type) where.type = query.type as Prisma.EnumBenefitLedgerTypeFilter['equals'];
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.benefitLedger.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
user: { select: { id: true, userNo: true } },
|
||||
coupon: { select: { id: true, couponNo: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.benefitLedger.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminCitiesService } from './admin-cities.service';
|
||||
import { AdminCitiesQueryDto } from './dto/admin-query.dto';
|
||||
import { CreateCityDto, UpdateCityDto } from './dto/admin-mutate.dto';
|
||||
import { Body } from '@nestjs/common';
|
||||
|
||||
@Controller('admin/cities')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminCitiesController {
|
||||
constructor(private readonly service: AdminCitiesService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminCitiesQueryDto) {
|
||||
return this.service.list(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(BigInt(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateCityDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateCityDto) {
|
||||
return this.service.update(BigInt(id), dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { AdminCitiesQueryDto } from './dto/admin-query.dto';
|
||||
import type { CreateCityDto, UpdateCityDto } from './dto/admin-mutate.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminCitiesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(query: AdminCitiesQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.CityWhereInput = {};
|
||||
if (query.name) where.name = { contains: query.name };
|
||||
if (query.code) where.code = { contains: query.code };
|
||||
if (query.status) where.status = query.status as Prisma.EnumCityStatusFilter['equals'];
|
||||
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.city.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
partner: { select: { id: true, companyName: true } },
|
||||
_count: { select: { stores: true, orders: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.city.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
items: items.map((c) => ({
|
||||
...c,
|
||||
storeCount: c._count.stores,
|
||||
orderCount: c._count.orders,
|
||||
_count: undefined,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const city = await this.prisma.city.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
partner: true,
|
||||
commissionRule: true,
|
||||
_count: { select: { stores: true, orders: true } },
|
||||
},
|
||||
});
|
||||
if (!city) throw new NotFoundException('开城城市不存在');
|
||||
return serializeBigInt(city);
|
||||
}
|
||||
|
||||
async create(dto: CreateCityDto) {
|
||||
const exists = await this.prisma.city.findUnique({ where: { code: dto.code } });
|
||||
if (exists) throw new BadRequestException('城市编码已存在');
|
||||
const city = await this.prisma.city.create({
|
||||
data: {
|
||||
code: dto.code,
|
||||
name: dto.name,
|
||||
province: dto.province,
|
||||
partnerId: dto.partnerId ? BigInt(dto.partnerId) : null,
|
||||
status: (dto.status ?? 'PENDING') as 'PENDING' | 'ACTIVE' | 'PAUSED',
|
||||
commissionRule: {
|
||||
create: {
|
||||
orderCommissionRate: 0.05,
|
||||
redeemCommissionRate: 0.03,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return serializeBigInt(city);
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdateCityDto) {
|
||||
const city = await this.prisma.city.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
||||
...(dto.province !== undefined ? { province: dto.province } : {}),
|
||||
...(dto.partnerId !== undefined
|
||||
? { partnerId: dto.partnerId ? BigInt(dto.partnerId) : null }
|
||||
: {}),
|
||||
...(dto.status !== undefined ? { status: dto.status as 'PENDING' | 'ACTIVE' | 'PAUSED' } : {}),
|
||||
...(dto.localMinQty !== undefined ? { localMinQty: dto.localMinQty } : {}),
|
||||
...(dto.crossMinQty !== undefined ? { crossMinQty: dto.crossMinQty } : {}),
|
||||
},
|
||||
});
|
||||
return serializeBigInt(city);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminDashboardService } from './admin-dashboard.service';
|
||||
|
||||
@Controller('admin/dashboard')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminDashboardController {
|
||||
constructor(private readonly dashboardService: AdminDashboardService) {}
|
||||
|
||||
@Get('stats')
|
||||
stats() {
|
||||
return this.dashboardService.getStats();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
|
||||
@Injectable()
|
||||
export class AdminDashboardService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async getStats() {
|
||||
const todayStart = new Date();
|
||||
todayStart.setHours(0, 0, 0, 0);
|
||||
|
||||
const [
|
||||
usersTotal,
|
||||
guestUsers,
|
||||
verifiedUsers,
|
||||
mergedUsers,
|
||||
ordersToday,
|
||||
ordersByStatus,
|
||||
storesTotal,
|
||||
partnersTotal,
|
||||
redeemToday,
|
||||
deliveriesTotal,
|
||||
] = await Promise.all([
|
||||
this.prisma.user.count({ where: { status: 1, mergedIntoUserId: null } }),
|
||||
this.prisma.user.count({
|
||||
where: { status: 1, mergedIntoUserId: null, phoneVerifiedAt: null },
|
||||
}),
|
||||
this.prisma.user.count({
|
||||
where: { status: 1, mergedIntoUserId: null, phoneVerifiedAt: { not: null } },
|
||||
}),
|
||||
this.prisma.user.count({ where: { mergedIntoUserId: { not: null } } }),
|
||||
this.prisma.order.count({ where: { createdAt: { gte: todayStart } } }),
|
||||
this.prisma.order.groupBy({
|
||||
by: ['status'],
|
||||
_count: { status: true },
|
||||
}),
|
||||
this.prisma.store.count(),
|
||||
this.prisma.partner.count(),
|
||||
this.prisma.redeemRecord.count({ where: { createdAt: { gte: todayStart } } }),
|
||||
this.prisma.orderDelivery.count(),
|
||||
]);
|
||||
|
||||
return {
|
||||
usersTotal,
|
||||
guestUsers,
|
||||
verifiedUsers,
|
||||
mergedUsers,
|
||||
ordersToday,
|
||||
storesTotal,
|
||||
partnersTotal,
|
||||
redeemToday,
|
||||
deliveriesTotal,
|
||||
ordersByStatus: ordersByStatus.map((row) => ({
|
||||
status: row.status,
|
||||
count: row._count.status,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
import { AdminHqAccountsService } from './admin-hq-accounts.service';
|
||||
import { AdminHqAccountsQueryDto } from './dto/admin-query.dto';
|
||||
import { CreateHqAccountDto, UpdateHqAccountDto } from './dto/admin-mutate.dto';
|
||||
|
||||
@Controller('admin/hq-accounts')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminHqAccountsController {
|
||||
constructor(private readonly service: AdminHqAccountsService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminHqAccountsQueryDto) {
|
||||
return this.service.list(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(BigInt(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(SuperAdminGuard)
|
||||
create(@Body() dto: CreateHqAccountDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(SuperAdminGuard)
|
||||
update(@Param('id') id: string, @Body() dto: UpdateHqAccountDto) {
|
||||
return this.service.update(BigInt(id), dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { AdminHqAccountsQueryDto } from './dto/admin-query.dto';
|
||||
import type { CreateHqAccountDto, UpdateHqAccountDto } from './dto/admin-mutate.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminHqAccountsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(query: AdminHqAccountsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.HqAccountWhereInput = {};
|
||||
if (query.phone) where.phone = { contains: query.phone };
|
||||
if (query.adminRole) where.adminRole = query.adminRole as Prisma.EnumHqAdminRoleFilter['equals'];
|
||||
if (query.status) where.status = query.status as Prisma.EnumAccountStatusFilter['equals'];
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.hqAccount.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.hqAccount.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const account = await this.prisma.hqAccount.findUnique({ where: { id } });
|
||||
if (!account) throw new NotFoundException('HQ 账号不存在');
|
||||
return serializeBigInt(account);
|
||||
}
|
||||
|
||||
async create(dto: CreateHqAccountDto) {
|
||||
const exists = await this.prisma.hqAccount.findUnique({ where: { phone: dto.phone } });
|
||||
if (exists) throw new BadRequestException('手机号已存在');
|
||||
const account = await this.prisma.hqAccount.create({
|
||||
data: {
|
||||
phone: dto.phone,
|
||||
name: dto.name,
|
||||
adminRole: (dto.adminRole ?? 'OPS') as 'SUPER_ADMIN' | 'OPS' | 'FINANCE' | 'CUSTOMER_SERVICE',
|
||||
},
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdateHqAccountDto) {
|
||||
const account = await this.prisma.hqAccount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
||||
...(dto.adminRole !== undefined
|
||||
? { adminRole: dto.adminRole as 'SUPER_ADMIN' | 'OPS' | 'FINANCE' | 'CUSTOMER_SERVICE' }
|
||||
: {}),
|
||||
...(dto.status !== undefined ? { status: dto.status as 'ACTIVE' | 'DISABLED' } : {}),
|
||||
},
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminOrdersService } from './admin-orders.service';
|
||||
import { AdminOrdersQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
@Controller('admin/orders')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminOrdersController {
|
||||
constructor(private readonly ordersService: AdminOrdersService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminOrdersQueryDto) {
|
||||
return this.ordersService.list(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.ordersService.detail(BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { AdminOrdersQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminOrdersService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(query: AdminOrdersQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.OrderWhereInput = {};
|
||||
|
||||
if (query.orderNo) where.orderNo = { contains: query.orderNo };
|
||||
if (query.status) where.status = query.status as Prisma.EnumOrderStatusFilter['equals'];
|
||||
if (query.userId) where.userId = BigInt(query.userId);
|
||||
if (query.receiverPhone) where.receiverPhone = { contains: query.receiverPhone };
|
||||
if (query.createdFrom || query.createdTo) {
|
||||
where.createdAt = {};
|
||||
if (query.createdFrom) where.createdAt.gte = new Date(query.createdFrom);
|
||||
if (query.createdTo) where.createdAt.lte = new Date(query.createdTo);
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.order.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
user: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
||||
delivery: { select: { provider: true, trackingNo: true, providerOrderNo: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.order.count({ where }),
|
||||
]);
|
||||
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const order = await this.prisma.order.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
userNo: true,
|
||||
phone: true,
|
||||
nickname: true,
|
||||
deviceKey: true,
|
||||
phoneVerifiedAt: true,
|
||||
},
|
||||
},
|
||||
items: true,
|
||||
delivery: true,
|
||||
payment: true,
|
||||
statusLogs: { orderBy: { createdAt: 'asc' } },
|
||||
benefitCoupons: {
|
||||
select: { id: true, couponNo: true, balance: true, status: true },
|
||||
},
|
||||
city: { select: { id: true, name: true, code: true } },
|
||||
},
|
||||
});
|
||||
if (!order) throw new NotFoundException('订单不存在');
|
||||
return serializeBigInt(order);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminPartnersService } from './admin-partners.service';
|
||||
import { AdminPartnerAccountsQueryDto, AdminPartnersQueryDto } from './dto/admin-query.dto';
|
||||
import {
|
||||
CreatePartnerAccountDto,
|
||||
CreatePartnerDto,
|
||||
UpdatePartnerAccountDto,
|
||||
UpdatePartnerDto,
|
||||
} from './dto/admin-mutate.dto';
|
||||
|
||||
@Controller('admin/partners')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminPartnersController {
|
||||
constructor(private readonly service: AdminPartnersService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminPartnersQueryDto) {
|
||||
return this.service.listPartners(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detailPartner(BigInt(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreatePartnerDto) {
|
||||
return this.service.createPartner(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdatePartnerDto) {
|
||||
return this.service.updatePartner(BigInt(id), dto);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('admin/partner-accounts')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminPartnerAccountsController {
|
||||
constructor(private readonly service: AdminPartnersService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminPartnerAccountsQueryDto) {
|
||||
return this.service.listPartnerAccounts(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detailPartnerAccount(BigInt(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreatePartnerAccountDto) {
|
||||
return this.service.createPartnerAccount(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdatePartnerAccountDto) {
|
||||
return this.service.updatePartnerAccount(BigInt(id), dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { AdminPartnerAccountsQueryDto, AdminPartnersQueryDto } from './dto/admin-query.dto';
|
||||
import type {
|
||||
CreatePartnerAccountDto,
|
||||
CreatePartnerDto,
|
||||
UpdatePartnerAccountDto,
|
||||
UpdatePartnerDto,
|
||||
} from './dto/admin-mutate.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminPartnersService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async listPartners(query: AdminPartnersQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.PartnerWhereInput = {};
|
||||
if (query.companyName) where.companyName = { contains: query.companyName };
|
||||
if (query.contactPhone) where.contactPhone = { contains: query.contactPhone };
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.partner.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
_count: { select: { stores: true, accounts: true, cities: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.partner.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
items: items.map((p) => ({
|
||||
...p,
|
||||
storeCount: p._count.stores,
|
||||
accountCount: p._count.accounts,
|
||||
cityCount: p._count.cities,
|
||||
_count: undefined,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detailPartner(id: bigint) {
|
||||
const partner = await this.prisma.partner.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
cities: { select: { id: true, code: true, name: true, status: true } },
|
||||
accounts: { select: { id: true, phone: true, name: true, isPrimary: true, status: true } },
|
||||
stores: { select: { id: true, name: true, status: true }, take: 10, orderBy: { createdAt: 'desc' } },
|
||||
_count: { select: { stores: true, accounts: true } },
|
||||
},
|
||||
});
|
||||
if (!partner) throw new NotFoundException('开城合伙人不存在');
|
||||
return serializeBigInt(partner);
|
||||
}
|
||||
|
||||
async createPartner(dto: CreatePartnerDto) {
|
||||
const partner = await this.prisma.partner.create({ data: dto });
|
||||
return serializeBigInt(partner);
|
||||
}
|
||||
|
||||
async updatePartner(id: bigint, dto: UpdatePartnerDto) {
|
||||
const partner = await this.prisma.partner.update({ where: { id }, data: dto });
|
||||
return serializeBigInt(partner);
|
||||
}
|
||||
|
||||
async listPartnerAccounts(query: AdminPartnerAccountsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.PartnerAccountWhereInput = {};
|
||||
if (query.phone) where.phone = { contains: query.phone };
|
||||
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
||||
if (query.status) where.status = query.status as Prisma.EnumAccountStatusFilter['equals'];
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.partnerAccount.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
partner: { select: { id: true, companyName: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.partnerAccount.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
}
|
||||
|
||||
async detailPartnerAccount(id: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id },
|
||||
include: { partner: true },
|
||||
});
|
||||
if (!account) throw new NotFoundException('开城合伙人账号不存在');
|
||||
|
||||
const [bills, orders] = await Promise.all([
|
||||
this.prisma.partnerBill.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
}),
|
||||
this.prisma.order.findMany({
|
||||
where: { city: { partnerId: account.partnerId } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
select: {
|
||||
id: true,
|
||||
orderNo: true,
|
||||
status: true,
|
||||
payAmount: true,
|
||||
createdAt: true,
|
||||
user: { select: { userNo: true, phone: true } },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return serializeBigInt({ ...account, bills, orders });
|
||||
}
|
||||
|
||||
async createPartnerAccount(dto: CreatePartnerAccountDto) {
|
||||
const partner = await this.prisma.partner.findUnique({ where: { id: BigInt(dto.partnerId) } });
|
||||
if (!partner) throw new BadRequestException('开城合伙人不存在');
|
||||
const account = await this.prisma.partnerAccount.create({
|
||||
data: {
|
||||
partnerId: partner.id,
|
||||
phone: dto.phone,
|
||||
name: dto.name,
|
||||
staffRole: dto.staffRole ? (dto.staffRole as 'PARTNER' | 'INTERNAL' | 'PROMOTER') : undefined,
|
||||
isPrimary: 0,
|
||||
},
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
}
|
||||
|
||||
async updatePartnerAccount(id: bigint, dto: UpdatePartnerAccountDto) {
|
||||
const account = await this.prisma.partnerAccount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
||||
...(dto.status !== undefined ? { status: dto.status as 'ACTIVE' | 'DISABLED' } : {}),
|
||||
},
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Body, Controller, Get, Param, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminRedeemService, AdminDeliveriesService } from './admin-redeem.service';
|
||||
import { AdminDeliveriesQueryDto, AdminRedeemRecordsQueryDto } from './dto/admin-query.dto';
|
||||
import { UpdateDeliveryDto } from './dto/admin-mutate.dto';
|
||||
|
||||
@Controller('admin/redeem-records')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminRedeemRecordsController {
|
||||
constructor(private readonly service: AdminRedeemService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminRedeemRecordsQueryDto) {
|
||||
return this.service.listRecords(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detailRecord(BigInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('admin/deliveries')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminDeliveriesController {
|
||||
constructor(private readonly service: AdminDeliveriesService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminDeliveriesQueryDto) {
|
||||
return this.service.list(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(BigInt(id));
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateDeliveryDto) {
|
||||
return this.service.update(BigInt(id), dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { AdminDeliveriesQueryDto, AdminRedeemRecordsQueryDto } from './dto/admin-query.dto';
|
||||
import type { UpdateDeliveryDto } from './dto/admin-mutate.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminRedeemService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async listRecords(query: AdminRedeemRecordsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.RedeemRecordWhereInput = {};
|
||||
if (query.redeemNo) where.redeemNo = { contains: query.redeemNo };
|
||||
if (query.storeId) where.storeId = BigInt(query.storeId);
|
||||
if (query.userId) where.userId = BigInt(query.userId);
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.redeemRecord.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
user: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
||||
store: { select: { id: true, name: true, cityName: true } },
|
||||
coupon: { select: { id: true, couponNo: true, balance: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.redeemRecord.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
}
|
||||
|
||||
async detailRecord(id: bigint) {
|
||||
const record = await this.prisma.redeemRecord.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: true,
|
||||
store: { include: { partner: { select: { id: true, companyName: true } } } },
|
||||
coupon: true,
|
||||
payout: true,
|
||||
commissions: true,
|
||||
},
|
||||
});
|
||||
if (!record) throw new NotFoundException('核销记录不存在');
|
||||
return serializeBigInt(record);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminDeliveriesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(query: AdminDeliveriesQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.OrderDeliveryWhereInput = {};
|
||||
if (query.provider) where.provider = query.provider;
|
||||
if (query.trackingNo) where.trackingNo = { contains: query.trackingNo };
|
||||
if (query.orderNo) {
|
||||
where.order = { orderNo: { contains: query.orderNo } };
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.orderDelivery.findMany({
|
||||
where,
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
order: {
|
||||
select: {
|
||||
id: true,
|
||||
orderNo: true,
|
||||
status: true,
|
||||
receiverName: true,
|
||||
receiverPhone: true,
|
||||
deliveryType: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
this.prisma.orderDelivery.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const delivery = await this.prisma.orderDelivery.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
order: {
|
||||
include: {
|
||||
user: { select: { id: true, userNo: true, phone: true } },
|
||||
items: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!delivery) throw new NotFoundException('配送单不存在');
|
||||
return serializeBigInt(delivery);
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdateDeliveryDto) {
|
||||
const delivery = await this.prisma.orderDelivery.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.provider !== undefined ? { provider: dto.provider } : {}),
|
||||
...(dto.providerOrderNo !== undefined ? { providerOrderNo: dto.providerOrderNo } : {}),
|
||||
...(dto.trackingNo !== undefined ? { trackingNo: dto.trackingNo } : {}),
|
||||
},
|
||||
include: { order: { select: { orderNo: true, status: true } } },
|
||||
});
|
||||
return serializeBigInt(delivery);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminStoresService } from './admin-stores.service';
|
||||
import {
|
||||
AdminStoreAccountsQueryDto,
|
||||
AdminStoreMediaQueryDto,
|
||||
AdminStoresQueryDto,
|
||||
} from './dto/admin-query.dto';
|
||||
import {
|
||||
CreateStoreAccountDto,
|
||||
CreateStoreDto,
|
||||
CreateStoreMediaDto,
|
||||
UpdateStoreAccountDto,
|
||||
UpdateStoreDto,
|
||||
UpdateStoreMediaDto,
|
||||
UpdateStoreStatusDto,
|
||||
} from './dto/admin-mutate.dto';
|
||||
|
||||
@Controller('admin/stores')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminStoresController {
|
||||
constructor(private readonly service: AdminStoresService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminStoresQueryDto) {
|
||||
return this.service.listStores(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detailStore(BigInt(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateStoreDto) {
|
||||
return this.service.createStore(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateStoreDto) {
|
||||
return this.service.updateStore(BigInt(id), dto);
|
||||
}
|
||||
|
||||
@Put(':id/status')
|
||||
updateStatus(@Param('id') id: string, @Body() dto: UpdateStoreStatusDto) {
|
||||
return this.service.updateStoreStatus(BigInt(id), dto);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('admin/store-accounts')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminStoreAccountsController {
|
||||
constructor(private readonly service: AdminStoresService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminStoreAccountsQueryDto) {
|
||||
return this.service.listStoreAccounts(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detailStoreAccount(BigInt(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateStoreAccountDto) {
|
||||
return this.service.createStoreAccount(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateStoreAccountDto) {
|
||||
return this.service.updateStoreAccount(BigInt(id), dto);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('admin/store-media')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminStoreMediaController {
|
||||
constructor(private readonly service: AdminStoresService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminStoreMediaQueryDto) {
|
||||
return this.service.listStoreMedia(query);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateStoreMediaDto) {
|
||||
return this.service.createStoreMedia(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateStoreMediaDto) {
|
||||
return this.service.updateStoreMedia(BigInt(id), dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.deleteStoreMedia(BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { AdminStoreAccountsQueryDto, AdminStoreMediaQueryDto, AdminStoresQueryDto } from './dto/admin-query.dto';
|
||||
import type {
|
||||
CreateStoreAccountDto,
|
||||
CreateStoreDto,
|
||||
CreateStoreMediaDto,
|
||||
UpdateStoreAccountDto,
|
||||
UpdateStoreDto,
|
||||
UpdateStoreMediaDto,
|
||||
UpdateStoreStatusDto,
|
||||
} from './dto/admin-mutate.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminStoresService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async listStores(query: AdminStoresQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.StoreWhereInput = {};
|
||||
if (query.name) where.name = { contains: query.name };
|
||||
if (query.status) where.status = query.status as Prisma.EnumStoreStatusFilter['equals'];
|
||||
if (query.cityId) where.cityId = BigInt(query.cityId);
|
||||
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
||||
if (query.phone) where.phone = { contains: query.phone };
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.store.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
cityRef: { select: { id: true, name: true, code: true } },
|
||||
partner: { select: { id: true, companyName: true } },
|
||||
account: { select: { id: true, phone: true, name: true, status: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.store.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
}
|
||||
|
||||
async detailStore(id: bigint) {
|
||||
const store = await this.prisma.store.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
cityRef: true,
|
||||
partner: true,
|
||||
category: true,
|
||||
account: true,
|
||||
media: { orderBy: { sortOrder: 'asc' } },
|
||||
audits: { orderBy: { submittedAt: 'desc' }, take: 5 },
|
||||
_count: { select: { redeemRecords: true, ratings: true } },
|
||||
},
|
||||
});
|
||||
if (!store) throw new NotFoundException('门店不存在');
|
||||
return serializeBigInt({
|
||||
...store,
|
||||
redeemCount: store._count.redeemRecords,
|
||||
ratingCount: store._count.ratings,
|
||||
_count: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async updateStoreStatus(id: bigint, dto: UpdateStoreStatusDto) {
|
||||
const store = await this.prisma.store.update({
|
||||
where: { id },
|
||||
data: { status: dto.status as 'OPEN' | 'PAUSED' | 'CLOSED' },
|
||||
});
|
||||
return serializeBigInt(store);
|
||||
}
|
||||
|
||||
async updateStore(id: bigint, dto: UpdateStoreDto) {
|
||||
const store = await this.prisma.store.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
||||
...(dto.phone !== undefined ? { phone: dto.phone } : {}),
|
||||
...(dto.intro !== undefined ? { intro: dto.intro } : {}),
|
||||
...(dto.coverUrl !== undefined ? { coverUrl: dto.coverUrl } : {}),
|
||||
...(dto.address !== undefined ? { address: dto.address } : {}),
|
||||
...(dto.district !== undefined ? { district: dto.district } : {}),
|
||||
},
|
||||
});
|
||||
return serializeBigInt(store);
|
||||
}
|
||||
|
||||
async createStore(dto: CreateStoreDto) {
|
||||
const partner = await this.prisma.partner.findUnique({ where: { id: BigInt(dto.partnerId) } });
|
||||
if (!partner) throw new BadRequestException('开城合伙人不存在');
|
||||
const city = await this.prisma.city.findUnique({ where: { id: BigInt(dto.cityId) } });
|
||||
if (!city) throw new BadRequestException('开城城市不存在');
|
||||
|
||||
const store = await this.prisma.store.create({
|
||||
data: {
|
||||
cityId: city.id,
|
||||
partnerId: partner.id,
|
||||
categoryId: dto.categoryId ? BigInt(dto.categoryId) : null,
|
||||
name: dto.name,
|
||||
phone: dto.phone,
|
||||
province: dto.province ?? city.province,
|
||||
cityName: dto.city ?? city.name,
|
||||
district: dto.district ?? '',
|
||||
address: dto.address,
|
||||
intro: dto.intro ?? null,
|
||||
coverUrl: dto.coverUrl ?? null,
|
||||
status: 'OPEN',
|
||||
},
|
||||
});
|
||||
|
||||
await this.prisma.storeAccount.create({
|
||||
data: {
|
||||
storeId: store.id,
|
||||
phone: dto.accountPhone ?? dto.phone,
|
||||
name: dto.accountName ?? dto.name,
|
||||
},
|
||||
});
|
||||
|
||||
return serializeBigInt(store);
|
||||
}
|
||||
|
||||
async createStoreAccount(dto: CreateStoreAccountDto) {
|
||||
const store = await this.prisma.store.findUnique({
|
||||
where: { id: BigInt(dto.storeId) },
|
||||
include: { account: true },
|
||||
});
|
||||
if (!store) throw new BadRequestException('门店不存在');
|
||||
if (store.account) throw new BadRequestException('门店已有账户');
|
||||
const account = await this.prisma.storeAccount.create({
|
||||
data: { storeId: store.id, phone: dto.phone, name: dto.name },
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
}
|
||||
|
||||
async listStoreMedia(query: AdminStoreMediaQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.StoreMediaWhereInput = {};
|
||||
if (query.storeId) where.storeId = BigInt(query.storeId);
|
||||
if (query.mediaType) where.mediaType = query.mediaType;
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.storeMedia.findMany({
|
||||
where,
|
||||
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: { store: { select: { id: true, name: true } } },
|
||||
}),
|
||||
this.prisma.storeMedia.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
}
|
||||
|
||||
async createStoreMedia(dto: CreateStoreMediaDto) {
|
||||
const store = await this.prisma.store.findUnique({ where: { id: BigInt(dto.storeId) } });
|
||||
if (!store) throw new BadRequestException('门店不存在');
|
||||
const media = await this.prisma.storeMedia.create({
|
||||
data: {
|
||||
storeId: store.id,
|
||||
mediaType: dto.mediaType,
|
||||
url: dto.url,
|
||||
sortOrder: dto.sortOrder ?? 0,
|
||||
},
|
||||
});
|
||||
return serializeBigInt(media);
|
||||
}
|
||||
|
||||
async updateStoreMedia(id: bigint, dto: UpdateStoreMediaDto) {
|
||||
const media = await this.prisma.storeMedia.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.url !== undefined ? { url: dto.url } : {}),
|
||||
...(dto.mediaType !== undefined ? { mediaType: dto.mediaType } : {}),
|
||||
...(dto.sortOrder !== undefined ? { sortOrder: dto.sortOrder } : {}),
|
||||
},
|
||||
});
|
||||
return serializeBigInt(media);
|
||||
}
|
||||
|
||||
async deleteStoreMedia(id: bigint) {
|
||||
await this.prisma.storeMedia.delete({ where: { id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async listStoreAccounts(query: AdminStoreAccountsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.StoreAccountWhereInput = {};
|
||||
if (query.phone) where.phone = { contains: query.phone };
|
||||
if (query.storeId) where.storeId = BigInt(query.storeId);
|
||||
if (query.status) where.status = query.status as Prisma.EnumAccountStatusFilter['equals'];
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.storeAccount.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
store: { select: { id: true, name: true, status: true, cityName: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.storeAccount.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
}
|
||||
|
||||
async detailStoreAccount(id: bigint) {
|
||||
const account = await this.prisma.storeAccount.findUnique({
|
||||
where: { id },
|
||||
include: { store: { include: { cityRef: true, partner: true } } },
|
||||
});
|
||||
if (!account) throw new NotFoundException('门店账号不存在');
|
||||
return serializeBigInt(account);
|
||||
}
|
||||
|
||||
async updateStoreAccount(id: bigint, dto: UpdateStoreAccountDto) {
|
||||
const account = await this.prisma.storeAccount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
||||
...(dto.phone !== undefined ? { phone: dto.phone } : {}),
|
||||
...(dto.status !== undefined ? { status: dto.status as 'ACTIVE' | 'DISABLED' } : {}),
|
||||
},
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminUsersService } from './admin-users.service';
|
||||
import { AdminUsersQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
@Controller('admin/users')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminUsersController {
|
||||
constructor(private readonly usersService: AdminUsersService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminUsersQueryDto) {
|
||||
return this.usersService.list(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.usersService.detail(BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { AdminUsersQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminUsersService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(query: AdminUsersQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.UserWhereInput = {};
|
||||
|
||||
if (query.phone) where.phone = { contains: query.phone };
|
||||
if (query.userNo) where.userNo = { contains: query.userNo };
|
||||
if (query.deviceKey) where.deviceKey = query.deviceKey;
|
||||
if (query.status !== undefined) where.status = query.status;
|
||||
if (query.phoneVerified === '1') where.phoneVerifiedAt = { not: null };
|
||||
if (query.phoneVerified === '0') where.phoneVerifiedAt = null;
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.user.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
select: {
|
||||
id: true,
|
||||
userNo: true,
|
||||
deviceKey: true,
|
||||
phone: true,
|
||||
phoneVerifiedAt: true,
|
||||
mergedIntoUserId: true,
|
||||
nickname: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
_count: { select: { orders: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.user.count({ where }),
|
||||
]);
|
||||
|
||||
return serializeBigInt({
|
||||
items: items.map((u) => ({
|
||||
...u,
|
||||
orderCount: u._count.orders,
|
||||
_count: undefined,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
cityPref: true,
|
||||
mergedInto: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
||||
orders: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 5,
|
||||
select: {
|
||||
id: true,
|
||||
orderNo: true,
|
||||
status: true,
|
||||
payAmount: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
_count: { select: { mergedFrom: true, orders: true, addresses: true } },
|
||||
},
|
||||
});
|
||||
if (!user) throw new NotFoundException('用户不存在');
|
||||
|
||||
return serializeBigInt({
|
||||
...user,
|
||||
mergedFromCount: user._count.mergedFrom,
|
||||
orderCount: user._count.orders,
|
||||
addressCount: user._count.addresses,
|
||||
_count: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class UpdateStoreStatusDto {
|
||||
@IsString()
|
||||
@IsIn(['OPEN', 'PAUSED', 'CLOSED'])
|
||||
status: string;
|
||||
}
|
||||
|
||||
export class CreateStoreDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
partnerId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
cityId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
categoryId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
province?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
city?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
district?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
address: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
intro?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
coverUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
accountPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
accountName?: string;
|
||||
}
|
||||
|
||||
export class UpdateStoreDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
intro?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
coverUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
district?: string;
|
||||
}
|
||||
|
||||
export class CreateStoreAccountDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
storeId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class UpdateStoreAccountDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class CreatePartnerDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
companyName: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
address: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
contactPhone: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankAccountName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankAccountNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankBranch?: string;
|
||||
}
|
||||
|
||||
export class UpdatePartnerDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
companyName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankAccountName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankAccountNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankBranch?: string;
|
||||
}
|
||||
|
||||
export class UpdatePartnerAccountDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class CreatePartnerAccountDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
partnerId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['PARTNER', 'INTERNAL', 'PROMOTER'])
|
||||
staffRole?: string;
|
||||
}
|
||||
|
||||
export class CreateCityDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
code: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
province: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['PENDING', 'ACTIVE', 'PAUSED'])
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class UpdateCityDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
province?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['PENDING', 'ACTIVE', 'PAUSED'])
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
localMinQty?: number;
|
||||
|
||||
@IsOptional()
|
||||
crossMinQty?: number;
|
||||
}
|
||||
|
||||
export class CreateStoreMediaDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
storeId: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['IMAGE', 'VIDEO'])
|
||||
mediaType: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
url: string;
|
||||
|
||||
@IsOptional()
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export class UpdateStoreMediaDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
url?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['IMAGE', 'VIDEO'])
|
||||
mediaType?: string;
|
||||
|
||||
@IsOptional()
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export class UpdateDeliveryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
provider?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
providerOrderNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
trackingNo?: string;
|
||||
}
|
||||
|
||||
export class CreateHqAccountDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['SUPER_ADMIN', 'OPS', 'FINANCE', 'CUSTOMER_SERVICE'])
|
||||
adminRole?: string;
|
||||
}
|
||||
|
||||
export class UpdateHqAccountDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['SUPER_ADMIN', 'OPS', 'FINANCE', 'CUSTOMER_SERVICE'])
|
||||
adminRole?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status?: string;
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
|
||||
|
||||
export class PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
pageSize?: number = 20;
|
||||
}
|
||||
|
||||
export class AdminUsersQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
deviceKey?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['0', '1'])
|
||||
phoneVerified?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsIn([0, 1])
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export class AdminOrdersQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
orderNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
receiverPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
createdFrom?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
createdTo?: string;
|
||||
}
|
||||
|
||||
export class AdminStoresQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cityId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export class AdminStoreAccountsQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
storeId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class AdminPartnersQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
companyName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactPhone?: string;
|
||||
}
|
||||
|
||||
export class AdminPartnerAccountsQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class AdminBenefitCouponsQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
couponNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class AdminBenefitLedgersQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
couponId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export class AdminRedeemRecordsQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
redeemNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
storeId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export class AdminDeliveriesQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
provider?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
trackingNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
orderNo?: string;
|
||||
}
|
||||
|
||||
export class AdminHqAccountsQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
adminRole?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class AdminCitiesQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
code?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerId?: string;
|
||||
}
|
||||
|
||||
export class AdminStoreMediaQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
storeId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mediaType?: string;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { IamModule } from '../iam/iam.module';
|
||||
import { AdminDashboardController } from './admin-dashboard.controller';
|
||||
import { AdminDashboardService } from './admin-dashboard.service';
|
||||
import { AdminUsersController } from './admin-users.controller';
|
||||
import { AdminUsersService } from './admin-users.service';
|
||||
import { AdminOrdersController } from './admin-orders.controller';
|
||||
import { AdminOrdersService } from './admin-orders.service';
|
||||
import { AdminStoresController, AdminStoreAccountsController, AdminStoreMediaController } from './admin-stores.controller';
|
||||
import { AdminStoresService } from './admin-stores.service';
|
||||
import { AdminPartnersController, AdminPartnerAccountsController } from './admin-partners.controller';
|
||||
import { AdminPartnersService } from './admin-partners.service';
|
||||
import { AdminCitiesController } from './admin-cities.controller';
|
||||
import { AdminCitiesService } from './admin-cities.service';
|
||||
import { AdminBenefitCouponsController, AdminBenefitLedgersController } from './admin-benefit.controller';
|
||||
import { AdminBenefitService } from './admin-benefit.service';
|
||||
import { AdminRedeemRecordsController, AdminDeliveriesController } from './admin-redeem.controller';
|
||||
import { AdminRedeemService, AdminDeliveriesService } from './admin-redeem.service';
|
||||
import { AdminHqAccountsController } from './admin-hq-accounts.controller';
|
||||
import { AdminHqAccountsService } from './admin-hq-accounts.service';
|
||||
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
|
||||
@Module({
|
||||
imports: [IamModule],
|
||||
controllers: [
|
||||
AdminDashboardController,
|
||||
AdminUsersController,
|
||||
AdminOrdersController,
|
||||
AdminStoresController,
|
||||
AdminStoreAccountsController,
|
||||
AdminStoreMediaController,
|
||||
AdminPartnersController,
|
||||
AdminPartnerAccountsController,
|
||||
AdminCitiesController,
|
||||
AdminBenefitCouponsController,
|
||||
AdminBenefitLedgersController,
|
||||
AdminRedeemRecordsController,
|
||||
AdminDeliveriesController,
|
||||
AdminHqAccountsController,
|
||||
],
|
||||
providers: [
|
||||
AdminDashboardService,
|
||||
AdminUsersService,
|
||||
AdminOrdersService,
|
||||
AdminStoresService,
|
||||
AdminPartnersService,
|
||||
AdminCitiesService,
|
||||
AdminBenefitService,
|
||||
AdminRedeemService,
|
||||
AdminDeliveriesService,
|
||||
AdminHqAccountsService,
|
||||
SuperAdminGuard,
|
||||
],
|
||||
})
|
||||
export class OpsModule {}
|
||||
Reference in New Issue
Block a user