feat(store): dual business hours, avg price, and status lock
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
Support 1-2 hour segments and optional avgPrice; show bank settlement on HQ store detail; enforce pickup min 2 bottles; Chinese order statuses; block shop reopen after permanent close. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1035,6 +1035,8 @@ model Store {
|
||||
auditedAt DateTime? @map("audited_at") @db.DateTime(3)
|
||||
openTime String? @map("open_time") @db.VarChar(8)
|
||||
closeTime String? @map("close_time") @db.VarChar(8)
|
||||
openTime2 String? @map("open_time_2") @db.VarChar(8)
|
||||
closeTime2 String? @map("close_time_2") @db.VarChar(8)
|
||||
settlementRate Decimal @default(0.60) @map("settlement_rate") @db.Decimal(5, 4)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
@@ -185,6 +185,11 @@ export class AdminStoresService {
|
||||
...(dto.address !== undefined ? { address: dto.address } : {}),
|
||||
...(dto.district !== undefined ? { district: dto.district } : {}),
|
||||
...(dto.settlementRate !== undefined ? { settlementRate: dto.settlementRate } : {}),
|
||||
...(dto.openTime !== undefined ? { openTime: dto.openTime } : {}),
|
||||
...(dto.closeTime !== undefined ? { closeTime: dto.closeTime } : {}),
|
||||
...(dto.openTime2 !== undefined ? { openTime2: dto.openTime2 || null } : {}),
|
||||
...(dto.closeTime2 !== undefined ? { closeTime2: dto.closeTime2 || null } : {}),
|
||||
...(dto.avgPrice !== undefined ? { avgPrice: dto.avgPrice } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -259,8 +264,11 @@ export class AdminStoresService {
|
||||
district: dto.district ?? '',
|
||||
address: dto.address,
|
||||
intro: dto.intro ?? null,
|
||||
openTime: '10:00',
|
||||
closeTime: '22:00',
|
||||
avgPrice: dto.avgPrice ?? null,
|
||||
openTime: dto.openTime?.trim() || '10:00',
|
||||
closeTime: dto.closeTime?.trim() || '22:00',
|
||||
openTime2: dto.openTime2?.trim() || null,
|
||||
closeTime2: dto.closeTime2?.trim() || null,
|
||||
status: 'OPEN',
|
||||
auditStatus: 'APPROVED',
|
||||
auditedAt: new Date(),
|
||||
|
||||
@@ -94,6 +94,27 @@ export class CreateStoreDto {
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
settlementRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
openTime?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
closeTime?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
openTime2?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
closeTime2?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
avgPrice?: number;
|
||||
}
|
||||
|
||||
export class UpdateStoreDto {
|
||||
@@ -125,6 +146,27 @@ export class UpdateStoreDto {
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
settlementRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
openTime?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
closeTime?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
openTime2?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
closeTime2?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
avgPrice?: number | null;
|
||||
}
|
||||
|
||||
export class CreateStoreAccountDto {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { loadAppConfig, ClientApp, SmsScene } from '@dukang/shared-types';
|
||||
import { PARTNER_STAFF_ROLE_LABELS, PartnerStaffRole, type PartnerLeaderboardPeriod } from '@dukang/shared-types';
|
||||
import { validateBusinessHours } from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { mapStoreCompat } from '../../common/compat/v31-compat';
|
||||
@@ -180,6 +181,21 @@ export class StoreService {
|
||||
const categoryId = parseBigIntParam(body.categoryId, '分类ID');
|
||||
await this.storeCategoryService.assertLeafCategoryId(categoryId);
|
||||
|
||||
const openTime = body.openTime ? String(body.openTime).trim() : '10:00';
|
||||
const closeTime = body.closeTime ? String(body.closeTime).trim() : '22:00';
|
||||
const openTime2 = body.openTime2 ? String(body.openTime2).trim() : '';
|
||||
const closeTime2 = body.closeTime2 ? String(body.closeTime2).trim() : '';
|
||||
const hoursCheck = validateBusinessHours([
|
||||
{ open: openTime, close: closeTime },
|
||||
...(openTime2 || closeTime2 ? [{ open: openTime2, close: closeTime2 }] : []),
|
||||
]);
|
||||
if (!hoursCheck.ok) throw new BadRequestException(hoursCheck.message);
|
||||
|
||||
const avgPriceRaw = body.avgPrice != null && body.avgPrice !== '' ? Number(body.avgPrice) : null;
|
||||
if (avgPriceRaw != null && (Number.isNaN(avgPriceRaw) || avgPriceRaw < 0)) {
|
||||
throw new BadRequestException('人均费用须为非负数字');
|
||||
}
|
||||
|
||||
const store = await this.prisma.store.create({
|
||||
data: {
|
||||
cityId: city.id,
|
||||
@@ -192,8 +208,11 @@ export class StoreService {
|
||||
district: String(body.district ?? ''),
|
||||
address: String(body.address),
|
||||
intro: body.intro ? String(body.intro) : null,
|
||||
openTime: body.openTime ? String(body.openTime) : '10:00',
|
||||
closeTime: body.closeTime ? String(body.closeTime) : '22:00',
|
||||
avgPrice: avgPriceRaw,
|
||||
openTime,
|
||||
closeTime,
|
||||
openTime2: openTime2 || null,
|
||||
closeTime2: closeTime2 || null,
|
||||
status: this.config.autoApproveStore ? 'OPEN' : 'PAUSED',
|
||||
auditStatus: this.config.autoApproveStore ? 'APPROVED' : 'PENDING',
|
||||
auditedAt: this.config.autoApproveStore ? new Date() : null,
|
||||
@@ -549,6 +568,9 @@ export class StoreService {
|
||||
where: { storeAccountId_storeId: { storeAccountId, storeId } },
|
||||
include: { store: true },
|
||||
});
|
||||
if (binding.store.status === 'CLOSED') {
|
||||
throw new BadRequestException('门店已永久关闭,无法在门店端开启或调整营业状态');
|
||||
}
|
||||
if (status === 'OPEN' && binding.store.auditStatus !== 'APPROVED') {
|
||||
throw new BadRequestException(
|
||||
binding.store.auditStatus === 'REJECTED'
|
||||
|
||||
@@ -96,7 +96,7 @@ export class TradeService {
|
||||
const freightPayType: FreightPayType | null = deliveryType === 'CROSS_CITY' ? 'COD' : null;
|
||||
const minQty =
|
||||
deliveryType === 'ON_SITE_PICKUP'
|
||||
? 1
|
||||
? city.localMinQty
|
||||
: deliveryType === 'LOCAL'
|
||||
? city.localMinQty
|
||||
: city.crossMinQty;
|
||||
|
||||
Reference in New Issue
Block a user