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
@@ -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'