feat(store): dual business hours, avg price, and status lock
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:
2026-07-26 10:16:22 +08:00
parent 94d2a88581
commit 92cf51ba3d
25 changed files with 560 additions and 146 deletions
+2
View File
@@ -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;