This commit is contained in:
2026-07-01 08:27:26 +08:00
parent 9f4577d3d8
commit 25f0d8e97b
56 changed files with 5298 additions and 2 deletions
+2
View File
@@ -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 {}