feat;提交管理端和城市合伙人端
This commit is contained in:
@@ -13,6 +13,11 @@ MOCK_SMS_CODE=123456
|
||||
MOCK_PAY=true
|
||||
MOCK_DELIVERY_AUTO=true
|
||||
AUTO_APPROVE_STORE=true
|
||||
# preV1 Mock 微信授权登录:点击授权按钮走 Mock 流程直接登录(不接真实微信)
|
||||
MOCK_WECHAT=true
|
||||
|
||||
# C 端 H5 落地页(推广码二维码链接前缀)
|
||||
USER_H5_URL=http://localhost:5173
|
||||
|
||||
# 反向代理后提取真实客户端 IP(下单 IP 定位)
|
||||
TRUST_PROXY=true
|
||||
|
||||
@@ -208,6 +208,14 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.commonPromoCode.create({
|
||||
data: {
|
||||
code: 'DKHQ001',
|
||||
name: '总部品鉴会',
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PayWechatProvider } from './pay/pay.wechat.provider';
|
||||
import { DeliveryMockProvider } from './delivery/delivery.mock.provider';
|
||||
import { WechatApiProvider } from './wechat/wechat.api.provider';
|
||||
import { WechatDisabledProvider } from './wechat/wechat.disabled.provider';
|
||||
import { WechatMockProvider } from './wechat/wechat.mock.provider';
|
||||
import { OssMockProvider } from './oss/oss.mock.provider';
|
||||
import { OssAliyunProvider } from './oss/oss.aliyun.provider';
|
||||
import {
|
||||
@@ -27,16 +28,24 @@ import type { IOssProvider } from './oss/oss.interface';
|
||||
{ provide: SMS_PROVIDER, useClass: SmsMockProvider },
|
||||
WechatApiProvider,
|
||||
WechatDisabledProvider,
|
||||
WechatMockProvider,
|
||||
{
|
||||
provide: WECHAT_PROVIDER,
|
||||
useFactory: (api: WechatApiProvider, disabled: WechatDisabledProvider): IWechatProvider => {
|
||||
useFactory: (
|
||||
api: WechatApiProvider,
|
||||
disabled: WechatDisabledProvider,
|
||||
mock: WechatMockProvider,
|
||||
): IWechatProvider => {
|
||||
const cfg = loadAppConfig();
|
||||
const enabled =
|
||||
(cfg.wechatAuthEnabled || cfg.wechatPayEnabled) &&
|
||||
(!!cfg.wxAppId || !!process.env.WX_MCH_ID);
|
||||
return enabled ? api : disabled;
|
||||
if (enabled) return api;
|
||||
// preV1:真实微信未配置但开启 Mock 授权登录
|
||||
if (cfg.mockWechat) return mock;
|
||||
return disabled;
|
||||
},
|
||||
inject: [WechatApiProvider, WechatDisabledProvider],
|
||||
inject: [WechatApiProvider, WechatDisabledProvider, WechatMockProvider],
|
||||
},
|
||||
PayMockProvider,
|
||||
PayWechatProvider,
|
||||
|
||||
@@ -34,6 +34,10 @@ export class WechatApiProvider implements IWechatProvider {
|
||||
return this.config.wechatAuthEnabled && !!this.appId && !!this.appSecret;
|
||||
}
|
||||
|
||||
isMock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isPayEnabled() {
|
||||
return (
|
||||
this.config.wechatPayEnabled &&
|
||||
|
||||
@@ -7,6 +7,10 @@ export class WechatDisabledProvider implements IWechatProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
isMock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isPayEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ export type WechatPayNotifyResult = {
|
||||
export interface IWechatProvider {
|
||||
isEnabled(): boolean;
|
||||
|
||||
/** 是否为 preV1 Mock 实现(登录时可回落到演示账号) */
|
||||
isMock(): boolean;
|
||||
|
||||
/** 微信支付是否已配置(商户号 + 证书) */
|
||||
isPayEnabled(): boolean;
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { createHash } from 'crypto';
|
||||
import { Injectable, NotImplementedException } from '@nestjs/common';
|
||||
import type {
|
||||
IWechatProvider,
|
||||
WechatCodeSession,
|
||||
WechatOAuthSession,
|
||||
} from './wechat.interface';
|
||||
|
||||
/**
|
||||
* preV1 Mock 微信 Provider。
|
||||
*
|
||||
* 目的:让「微信授权登录」按钮在不接真实微信的情况下走通。前端仍按真实 OAuth 流程
|
||||
* (跳转 oauth-url → 回调携带 code),Mock 端将授权 URL 直接回跳并返回稳定 openId。
|
||||
* 后续填入 WX_APP_ID/WX_APP_SECRET 并置 MOCK_WECHAT=false 即切换到真实实现。
|
||||
*/
|
||||
@Injectable()
|
||||
export class WechatMockProvider implements IWechatProvider {
|
||||
isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
isMock() {
|
||||
return true;
|
||||
}
|
||||
|
||||
isPayEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getMchId() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/** 由 code 派生稳定 openId,保证同一 code 多次授权指向同一账号 */
|
||||
private openIdFromCode(code: string): string {
|
||||
return `mockwx_${createHash('md5').update(code).digest('hex').slice(0, 24)}`;
|
||||
}
|
||||
|
||||
async code2Session(code: string): Promise<WechatCodeSession> {
|
||||
return { openId: this.openIdFromCode(code), sessionKey: 'mock-session-key' };
|
||||
}
|
||||
|
||||
async oauth2AccessToken(code: string): Promise<WechatOAuthSession> {
|
||||
return { openId: this.openIdFromCode(code), accessToken: 'mock-access-token' };
|
||||
}
|
||||
|
||||
async createJssdkConfig(url: string) {
|
||||
return {
|
||||
appId: 'mock-appid',
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
nonceStr: 'mocknonce',
|
||||
signature: 'mocksignature',
|
||||
url,
|
||||
jsApiList: ['getLocation', 'scanQRCode', 'chooseImage'],
|
||||
} as unknown as Awaited<ReturnType<IWechatProvider['createJssdkConfig']>>;
|
||||
}
|
||||
|
||||
/** 直接把授权链接回跳到 redirectUri 并附带 mock code,模拟微信授权完成 */
|
||||
buildOAuthUrl(redirectUri: string, state: string): string {
|
||||
const sep = redirectUri.includes('?') ? '&' : '?';
|
||||
const code = `mockcode_${state || 'default'}`;
|
||||
return `${redirectUri}${sep}code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
|
||||
}
|
||||
|
||||
async getPhoneNumberByCode(): Promise<string> {
|
||||
throw new NotImplementedException('Mock 微信不支持获取手机号,请用短信绑定');
|
||||
}
|
||||
|
||||
createJsapiPrepay(): never {
|
||||
throw new NotImplementedException('FEATURE_DISABLED');
|
||||
}
|
||||
|
||||
parsePayNotification(): never {
|
||||
throw new NotImplementedException('FEATURE_DISABLED');
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,30 @@
|
||||
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
||||
import { AnalyticsService } from './analytics.service';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { PromoTouchDto } from './dto/promo.dto';
|
||||
import { ActorType } from '@dukang/shared-types';
|
||||
|
||||
@Controller('analytics')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class AnalyticsController {
|
||||
constructor(private readonly analyticsService: AnalyticsService) {}
|
||||
|
||||
@Post('events')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
track(@CurrentUser() user: AuthUser, @Body() body: { events: Array<{ eventName: string; params?: Record<string, unknown> }> }) {
|
||||
return this.analyticsService.trackBatch(user.actorId, user.clientApp, body.events);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('promo')
|
||||
export class PromoController {
|
||||
constructor(private readonly analyticsService: AnalyticsService) {}
|
||||
|
||||
@Post('touch')
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
touch(@CurrentUser() user: AuthUser | undefined, @Body() dto: PromoTouchDto) {
|
||||
const userId = user?.actorType === ActorType.USER ? user.actorId : undefined;
|
||||
return this.analyticsService.touchPromo(dto.promoCode, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { IamModule } from '../iam/iam.module';
|
||||
import { AnalyticsController } from './analytics.controller';
|
||||
import { AnalyticsController, PromoController } from './analytics.controller';
|
||||
import { AnalyticsService } from './analytics.service';
|
||||
|
||||
@Module({
|
||||
imports: [IamModule],
|
||||
controllers: [AnalyticsController],
|
||||
controllers: [AnalyticsController, PromoController],
|
||||
providers: [AnalyticsService],
|
||||
exports: [AnalyticsService],
|
||||
})
|
||||
export class AnalyticsModule {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import type { ClientApp } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class AnalyticsService {
|
||||
@@ -22,4 +23,43 @@ export class AnalyticsService {
|
||||
});
|
||||
return { count: events.length };
|
||||
}
|
||||
|
||||
/** 扫码归因:始终累加 scan_count;已登录用户首次写入 user_promo_attribution */
|
||||
async touchPromo(promoCode: string, userId?: bigint) {
|
||||
const code = promoCode.trim().toUpperCase();
|
||||
const promo = await this.prisma.commonPromoCode.findUnique({ where: { code } });
|
||||
if (!promo || promo.status !== 'ACTIVE') {
|
||||
throw new NotFoundException('推广码无效或已停用');
|
||||
}
|
||||
|
||||
await this.prisma.commonPromoCode.update({
|
||||
where: { id: promo.id },
|
||||
data: { scanCount: { increment: 1 } },
|
||||
});
|
||||
|
||||
let attributed = false;
|
||||
if (userId) {
|
||||
const existing = await this.prisma.userPromoAttribution.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
if (!existing) {
|
||||
await this.prisma.userPromoAttribution.create({
|
||||
data: {
|
||||
userId,
|
||||
promoCodeId: promo.id,
|
||||
channelName: promo.name,
|
||||
firstTouchAt: new Date(),
|
||||
},
|
||||
});
|
||||
attributed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return serializeBigInt({
|
||||
promoCode: promo.code,
|
||||
channelName: promo.name,
|
||||
attributed,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class PromoTouchDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
promoCode: string;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export class ClientConfigController {
|
||||
return {
|
||||
mockPay: cfg.mockPay,
|
||||
wechatPayEnabled: cfg.wechatPayEnabled,
|
||||
mockWechat: cfg.mockWechat,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginSmsDto, SendSmsDto } from './dto/auth.dto';
|
||||
import { LoginSmsDto, LoginWechatDto, 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';
|
||||
@@ -20,6 +20,11 @@ export class AdminAuthController {
|
||||
return this.authService.loginHq(dto.phone, dto.code, ClientApp.HQ_WEB);
|
||||
}
|
||||
|
||||
@Post('login/wechat')
|
||||
wechatLogin(@Body() dto: LoginWechatDto) {
|
||||
return this.authService.loginHqWechat(dto.code, ClientApp.HQ_WEB, dto.platform ?? 'h5');
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
me(@CurrentUser() user: AuthUser) {
|
||||
|
||||
@@ -268,6 +268,48 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
async loginHqWechat(code: string, clientApp: ClientApp, platform: 'h5' | 'mini' = 'h5') {
|
||||
this.assertWechatEnabled();
|
||||
const session =
|
||||
platform === 'mini'
|
||||
? await this.wechatProvider.code2Session(code)
|
||||
: await this.wechatProvider.oauth2AccessToken(code);
|
||||
|
||||
let account = await this.prisma.hqAccount.findFirst({
|
||||
where: { wxOpenId: session.openId },
|
||||
});
|
||||
|
||||
if (!account && this.wechatProvider.isMock()) {
|
||||
// preV1 Mock:无绑定微信时回落到演示超管账号
|
||||
account = await this.prisma.hqAccount.findFirst({
|
||||
where: { status: 'ACTIVE' },
|
||||
orderBy: [{ adminRole: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
throw new BadRequestException('首次登录请使用手机验证码,登录后将自动关联微信');
|
||||
}
|
||||
if (account.status !== 'ACTIVE') throw new BadRequestException('账号已停用');
|
||||
|
||||
account = await this.prisma.hqAccount.update({
|
||||
where: { id: account.id },
|
||||
data: {
|
||||
wxOpenId: session.openId,
|
||||
wxUnionId: session.unionId ?? account.wxUnionId,
|
||||
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);
|
||||
@@ -555,6 +597,15 @@ export class AuthService {
|
||||
include: { partner: true },
|
||||
});
|
||||
|
||||
if (!account && this.wechatProvider.isMock()) {
|
||||
// preV1 Mock:无绑定微信时回落到演示主账号,方便一键授权登录
|
||||
account = await this.prisma.partnerAccount.findFirst({
|
||||
where: { status: 'ACTIVE' },
|
||||
orderBy: [{ isPrimary: 'desc' }, { id: 'asc' }],
|
||||
include: { partner: true },
|
||||
});
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
throw new BadRequestException('首次登录请使用手机验证码,登录后将自动关联微信');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminPromoCodesService } from './admin-promo-codes.service';
|
||||
import { AdminPromoCodesQueryDto } from './dto/admin-query.dto';
|
||||
import { CreatePromoCodeDto, UpdatePromoCodeStatusDto } from './dto/admin-mutate.dto';
|
||||
|
||||
@Controller('admin/promo-codes')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminPromoCodesController {
|
||||
constructor(private readonly service: AdminPromoCodesService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminPromoCodesQueryDto) {
|
||||
return this.service.list(query);
|
||||
}
|
||||
|
||||
@Get(':id/stats')
|
||||
stats(@Param('id') id: string) {
|
||||
return this.service.stats(BigInt(id));
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(BigInt(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreatePromoCodeDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Put(':id/status')
|
||||
updateStatus(@Param('id') id: string, @Body() dto: UpdatePromoCodeStatusDto) {
|
||||
return this.service.updateStatus(BigInt(id), dto.status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { CreatePromoCodeDto } from './dto/admin-mutate.dto';
|
||||
import { AdminPromoCodesQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
function userH5Base(): string {
|
||||
return (process.env.USER_H5_URL || 'http://localhost:5173').replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function buildLandingUrl(code: string): string {
|
||||
return `${userH5Base()}/?promo=${encodeURIComponent(code)}`;
|
||||
}
|
||||
|
||||
function randomCode(): string {
|
||||
const n = Math.random().toString(36).slice(2, 8).toUpperCase();
|
||||
return `DK${n}`;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminPromoCodesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
private mapRow(row: {
|
||||
id: bigint;
|
||||
code: string;
|
||||
name: string;
|
||||
status: string;
|
||||
scanCount: number;
|
||||
orderCount: number;
|
||||
createdAt: Date;
|
||||
}) {
|
||||
return serializeBigInt({
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
status: row.status,
|
||||
scanCount: row.scanCount,
|
||||
orderCount: row.orderCount,
|
||||
landingUrl: buildLandingUrl(row.code),
|
||||
createdAt: row.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
async list(query: AdminPromoCodesQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: {
|
||||
status?: 'ACTIVE' | 'DISABLED';
|
||||
name?: { contains: string };
|
||||
code?: { contains: string };
|
||||
} = {};
|
||||
if (query.status) where.status = query.status as 'ACTIVE' | 'DISABLED';
|
||||
if (query.name) where.name = { contains: query.name };
|
||||
if (query.code) where.code = { contains: query.code };
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.commonPromoCode.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.commonPromoCode.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
items: items.map((r) => this.mapRow(r)),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const row = await this.prisma.commonPromoCode.findUnique({ where: { id } });
|
||||
if (!row) throw new NotFoundException('推广码不存在');
|
||||
const stats = this.statsFromRow(row);
|
||||
return serializeBigInt({ ...this.mapRow(row), stats });
|
||||
}
|
||||
|
||||
async create(dto: CreatePromoCodeDto) {
|
||||
let code = dto.code?.trim().toUpperCase();
|
||||
if (code) {
|
||||
const exists = await this.prisma.commonPromoCode.findUnique({ where: { code } });
|
||||
if (exists) throw new BadRequestException('推广码已存在');
|
||||
} else {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const candidate = randomCode();
|
||||
const exists = await this.prisma.commonPromoCode.findUnique({ where: { code: candidate } });
|
||||
if (!exists) {
|
||||
code = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!code) throw new BadRequestException('生成推广码失败,请重试');
|
||||
}
|
||||
|
||||
const row = await this.prisma.commonPromoCode.create({
|
||||
data: {
|
||||
code,
|
||||
name: dto.name.trim(),
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
return this.mapRow(row);
|
||||
}
|
||||
|
||||
async updateStatus(id: bigint, status: 'ACTIVE' | 'DISABLED') {
|
||||
const row = await this.prisma.commonPromoCode.update({
|
||||
where: { id },
|
||||
data: { status },
|
||||
});
|
||||
return this.mapRow(row);
|
||||
}
|
||||
|
||||
async stats(id: bigint) {
|
||||
const row = await this.prisma.commonPromoCode.findUnique({ where: { id } });
|
||||
if (!row) throw new NotFoundException('推广码不存在');
|
||||
return serializeBigInt(this.statsFromRow(row));
|
||||
}
|
||||
|
||||
private statsFromRow(row: { scanCount: number; orderCount: number }) {
|
||||
const scanCount = row.scanCount;
|
||||
const orderCount = row.orderCount;
|
||||
const conversionRate =
|
||||
scanCount > 0 ? Math.round((orderCount / scanCount) * 1000) / 10 : 0;
|
||||
return { scanCount, orderCount, conversionRate };
|
||||
}
|
||||
}
|
||||
@@ -426,3 +426,19 @@ export class UpdateProductDto {
|
||||
@IsString()
|
||||
coverUrl?: string;
|
||||
}
|
||||
|
||||
export class CreatePromoCodeDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export class UpdatePromoCodeStatusDto {
|
||||
@IsString()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status: 'ACTIVE' | 'DISABLED';
|
||||
}
|
||||
|
||||
@@ -236,3 +236,17 @@ export class AdminStoreMediaQueryDto extends PaginationQueryDto {
|
||||
@IsString()
|
||||
mediaType?: string;
|
||||
}
|
||||
|
||||
export class AdminPromoCodesQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
code?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status?: string;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import { AdminHqAccountsController } from './admin-hq-accounts.controller';
|
||||
import { AdminHqAccountsService } from './admin-hq-accounts.service';
|
||||
import { AdminProductsController } from './admin-products.controller';
|
||||
import { AdminProductsService } from './admin-products.service';
|
||||
import { AdminPromoCodesController } from './admin-promo-codes.controller';
|
||||
import { AdminPromoCodesService } from './admin-promo-codes.service';
|
||||
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
|
||||
@Module({
|
||||
@@ -41,6 +43,7 @@ import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
AdminDeliveriesController,
|
||||
AdminHqAccountsController,
|
||||
AdminProductsController,
|
||||
AdminPromoCodesController,
|
||||
],
|
||||
providers: [
|
||||
AdminDashboardService,
|
||||
@@ -54,6 +57,7 @@ import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
AdminDeliveriesService,
|
||||
AdminHqAccountsService,
|
||||
AdminProductsService,
|
||||
AdminPromoCodesService,
|
||||
SuperAdminGuard,
|
||||
],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user