feat(v3.4.15): HQ store media edit and package images up to 20
Allow HQ to replace/delete store photos; support multi-image packages with a 20-image cap. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1325,6 +1325,8 @@ model StorePackage {
|
||||
usableTime String? @map("usable_time") @db.VarChar(256)
|
||||
otherNotes String? @map("other_notes") @db.VarChar(512)
|
||||
imageUrl String? @map("image_url") @db.VarChar(512)
|
||||
/// 套餐多图 URL 列表(JSON string[]),最多 20 张;imageUrl 同步为首图
|
||||
imageUrls Json? @map("image_urls")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
@@ -373,11 +373,24 @@ export class AdminStoresService {
|
||||
// 分实体手机号已废弃,忽略写入
|
||||
}
|
||||
|
||||
if (dto.coverUrl) {
|
||||
if (current.coverResourceId) {
|
||||
if (dto.coverUrl !== undefined) {
|
||||
const coverUrl = dto.coverUrl?.trim() || '';
|
||||
if (!coverUrl) {
|
||||
if (current.coverResourceId) {
|
||||
await tx.commonResource.update({
|
||||
where: { id: current.coverResourceId },
|
||||
data: { status: 'DELETED' },
|
||||
});
|
||||
await tx.store.update({ where: { id }, data: { coverResourceId: null } });
|
||||
}
|
||||
await tx.commonResource.updateMany({
|
||||
where: { ownerType: 'STORE', ownerId: id, bizType: 'COVER', status: 'ACTIVE' },
|
||||
data: { status: 'DELETED' },
|
||||
});
|
||||
} else if (current.coverResourceId) {
|
||||
await tx.commonResource.update({
|
||||
where: { id: current.coverResourceId },
|
||||
data: { url: dto.coverUrl, ossKey: dto.coverUrl },
|
||||
data: { url: coverUrl, ossKey: coverUrl, status: 'ACTIVE' },
|
||||
});
|
||||
} else {
|
||||
const cover = await tx.commonResource.create({
|
||||
@@ -387,14 +400,61 @@ export class AdminStoresService {
|
||||
bizType: 'COVER',
|
||||
mediaType: 'IMAGE',
|
||||
ossBucket: 'legacy',
|
||||
ossKey: dto.coverUrl,
|
||||
url: dto.coverUrl,
|
||||
ossKey: coverUrl,
|
||||
url: coverUrl,
|
||||
},
|
||||
});
|
||||
await tx.store.update({ where: { id }, data: { coverResourceId: cover.id } });
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.envPhotoUrls !== undefined) {
|
||||
const envUrls = [...new Set(
|
||||
(dto.envPhotoUrls ?? []).map((u) => String(u ?? '').trim()).filter(Boolean),
|
||||
)].slice(0, 20);
|
||||
await tx.commonResource.updateMany({
|
||||
where: { ownerType: 'STORE', ownerId: id, bizType: 'ENV', status: 'ACTIVE' },
|
||||
data: { status: 'DELETED' },
|
||||
});
|
||||
for (let i = 0; i < envUrls.length; i++) {
|
||||
await tx.commonResource.create({
|
||||
data: {
|
||||
ownerType: 'STORE',
|
||||
ownerId: id,
|
||||
bizType: 'ENV',
|
||||
mediaType: 'IMAGE',
|
||||
ossBucket: 'legacy',
|
||||
ossKey: envUrls[i],
|
||||
url: envUrls[i],
|
||||
sortOrder: i,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.contractUrl !== undefined) {
|
||||
const contractUrl = dto.contractUrl?.trim() || '';
|
||||
await tx.commonResource.updateMany({
|
||||
where: { ownerType: 'STORE', ownerId: id, bizType: 'CONTRACT', status: 'ACTIVE' },
|
||||
data: { status: 'DELETED' },
|
||||
});
|
||||
if (contractUrl) {
|
||||
const isPdf = /\.pdf(\?|$)/i.test(contractUrl);
|
||||
await tx.commonResource.create({
|
||||
data: {
|
||||
ownerType: 'STORE',
|
||||
ownerId: id,
|
||||
bizType: 'CONTRACT',
|
||||
mediaType: isPdf ? 'FILE' : 'IMAGE',
|
||||
ossBucket: 'legacy',
|
||||
ossKey: contractUrl,
|
||||
url: contractUrl,
|
||||
sortOrder: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (primaryBinding?.storeAccount) {
|
||||
const account = primaryBinding.storeAccount;
|
||||
const accountData: {
|
||||
@@ -545,7 +605,7 @@ export class AdminStoresService {
|
||||
await this.prisma.store.update({ where: { id: store.id }, data: { coverResourceId: cover.id } });
|
||||
}
|
||||
|
||||
const envUrls = (dto.envPhotoUrls ?? []).filter(Boolean);
|
||||
const envUrls = [...new Set((dto.envPhotoUrls ?? []).map((u) => String(u ?? '').trim()).filter(Boolean))].slice(0, 20);
|
||||
for (let i = 0; i < envUrls.length; i++) {
|
||||
await this.prisma.commonResource.create({
|
||||
data: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMaxSize,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
@@ -96,6 +97,7 @@ export class CreateStoreDto {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ArrayMaxSize(20)
|
||||
envPhotoUrls?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@@ -164,7 +166,17 @@ export class UpdateStoreDto {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
coverUrl?: string;
|
||||
coverUrl?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ArrayMaxSize(20)
|
||||
envPhotoUrls?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contractUrl?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -4,7 +4,12 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { STORE_PACKAGE_MAX_COUNT, type StorePackageItemDto } from '@dukang/shared-types';
|
||||
import {
|
||||
STORE_PACKAGE_IMAGE_MAX_COUNT,
|
||||
STORE_PACKAGE_MAX_COUNT,
|
||||
normalizeStorePackageImageUrls,
|
||||
type StorePackageItemDto,
|
||||
} from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { StoreService } from './store.service';
|
||||
@@ -44,9 +49,18 @@ export class StorePackageService {
|
||||
const otherNotes = item.otherNotes != null && String(item.otherNotes).trim()
|
||||
? String(item.otherNotes).trim()
|
||||
: null;
|
||||
const imageUrl = item.imageUrl != null && String(item.imageUrl).trim()
|
||||
? String(item.imageUrl).trim()
|
||||
: null;
|
||||
const imageUrls = normalizeStorePackageImageUrls({
|
||||
imageUrl: item.imageUrl as string | null | undefined,
|
||||
imageUrls: item.imageUrls,
|
||||
});
|
||||
if (
|
||||
Array.isArray(item.imageUrls) &&
|
||||
item.imageUrls.filter((u) => String(u ?? '').trim()).length > STORE_PACKAGE_IMAGE_MAX_COUNT
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`第 ${index + 1} 条套餐图片最多 ${STORE_PACKAGE_IMAGE_MAX_COUNT} 张`,
|
||||
);
|
||||
}
|
||||
const sortOrder = item.sortOrder != null ? Number(item.sortOrder) : index;
|
||||
return {
|
||||
name,
|
||||
@@ -54,7 +68,8 @@ export class StorePackageService {
|
||||
dishes,
|
||||
usableTime,
|
||||
otherNotes,
|
||||
imageUrl,
|
||||
imageUrl: imageUrls[0] ?? null,
|
||||
imageUrls: imageUrls.length ? imageUrls : null,
|
||||
sortOrder: Number.isFinite(sortOrder) ? sortOrder : index,
|
||||
};
|
||||
}
|
||||
@@ -67,8 +82,13 @@ export class StorePackageService {
|
||||
usableTime: string | null;
|
||||
otherNotes: string | null;
|
||||
imageUrl: string | null;
|
||||
imageUrls: Prisma.JsonValue | null;
|
||||
sortOrder: number;
|
||||
}) {
|
||||
const imageUrls = normalizeStorePackageImageUrls({
|
||||
imageUrl: row.imageUrl,
|
||||
imageUrls: row.imageUrls,
|
||||
});
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
name: row.name,
|
||||
@@ -76,7 +96,8 @@ export class StorePackageService {
|
||||
dishes: row.dishes,
|
||||
usableTime: row.usableTime,
|
||||
otherNotes: row.otherNotes,
|
||||
imageUrl: row.imageUrl,
|
||||
imageUrl: imageUrls[0] ?? null,
|
||||
imageUrls: imageUrls.length ? imageUrls : null,
|
||||
sortOrder: row.sortOrder,
|
||||
};
|
||||
}
|
||||
@@ -221,6 +242,9 @@ export class StorePackageService {
|
||||
usableTime: pkg.usableTime ?? null,
|
||||
otherNotes: pkg.otherNotes ?? null,
|
||||
imageUrl: pkg.imageUrl ?? null,
|
||||
imageUrls: pkg.imageUrls?.length
|
||||
? (pkg.imageUrls as Prisma.InputJsonValue)
|
||||
: Prisma.JsonNull,
|
||||
sortOrder: pkg.sortOrder ?? index,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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 { normalizeStorePackageImageUrls } from '@dukang/shared-types';
|
||||
import { validateBusinessHours } from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
@@ -230,15 +231,22 @@ export class StoreService {
|
||||
latitude: coords?.latitude ?? store.latitude,
|
||||
longitude: coords?.longitude ?? store.longitude,
|
||||
media,
|
||||
packages: packageRows.map((p) => ({
|
||||
name: p.name,
|
||||
price: p.price.toFixed(2),
|
||||
dishes: p.dishes,
|
||||
usableTime: p.usableTime,
|
||||
otherNotes: p.otherNotes,
|
||||
imageUrl: p.imageUrl,
|
||||
sortOrder: p.sortOrder,
|
||||
})),
|
||||
packages: packageRows.map((p) => {
|
||||
const imageUrls = normalizeStorePackageImageUrls({
|
||||
imageUrl: p.imageUrl,
|
||||
imageUrls: p.imageUrls,
|
||||
});
|
||||
return {
|
||||
name: p.name,
|
||||
price: p.price.toFixed(2),
|
||||
dishes: p.dishes,
|
||||
usableTime: p.usableTime,
|
||||
otherNotes: p.otherNotes,
|
||||
imageUrl: imageUrls[0] ?? null,
|
||||
imageUrls: imageUrls.length ? imageUrls : null,
|
||||
sortOrder: p.sortOrder,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1292,6 +1300,7 @@ export class StoreService {
|
||||
if (!url || seen.has(url)) continue;
|
||||
seen.add(url);
|
||||
urls.push(url);
|
||||
if (urls.length >= 20) break;
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user