|
|
@@ -1,29 +1,346 @@
|
|
|
// apps/box-mgnt-api/src/mgnt-backend/feature/video-media/video-media.service.ts
|
|
|
-import { Injectable } from '@nestjs/common';
|
|
|
+import {
|
|
|
+ Injectable,
|
|
|
+ NotFoundException,
|
|
|
+ BadRequestException,
|
|
|
+} from '@nestjs/common';
|
|
|
import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
|
|
|
-import { CreateVideoMediaDto, UpdateVideoMediaDto } from './video-media.dto';
|
|
|
+import {
|
|
|
+ VideoMediaListQueryDto,
|
|
|
+ VideoMediaDetailDto,
|
|
|
+ UpdateVideoMediaManageDto,
|
|
|
+ UpdateVideoMediaStatusDto,
|
|
|
+ BatchUpdateVideoMediaStatusDto,
|
|
|
+} from './video-media.dto';
|
|
|
|
|
|
@Injectable()
|
|
|
export class VideoMediaService {
|
|
|
constructor(private readonly prisma: MongoPrismaService) {}
|
|
|
|
|
|
- create(createVideoMediaDto: CreateVideoMediaDto) {
|
|
|
- return 'This action adds a new videoMedia';
|
|
|
+ async findAll(query: VideoMediaListQueryDto) {
|
|
|
+ const page = query.page ?? 1;
|
|
|
+ const pageSize = query.size ?? 20;
|
|
|
+ const skip = (page - 1) * pageSize;
|
|
|
+ const take = pageSize;
|
|
|
+
|
|
|
+ const where: any = {};
|
|
|
+
|
|
|
+ if (query.keyword) {
|
|
|
+ where.OR = [
|
|
|
+ { title: { contains: query.keyword, mode: 'insensitive' } },
|
|
|
+ { filename: { contains: query.keyword, mode: 'insensitive' } },
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (query.categoryId) {
|
|
|
+ where.categoryId = query.categoryId;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (query.tagId) {
|
|
|
+ where.tagIds = { has: query.tagId };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof query.listStatus === 'number') {
|
|
|
+ where.listStatus = query.listStatus;
|
|
|
+ }
|
|
|
+
|
|
|
+ const [total, rows] = await Promise.all([
|
|
|
+ this.prisma.videoMedia.count({ where }),
|
|
|
+ this.prisma.videoMedia.findMany({
|
|
|
+ where,
|
|
|
+ skip,
|
|
|
+ take,
|
|
|
+ orderBy: { addedTime: 'desc' },
|
|
|
+ }),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return {
|
|
|
+ total,
|
|
|
+ page,
|
|
|
+ pageSize,
|
|
|
+ items: rows.map((row) => ({
|
|
|
+ id: row.id,
|
|
|
+ title: row.title,
|
|
|
+ filename: row.filename,
|
|
|
+ videoTime: row.videoTime,
|
|
|
+ size: row.size?.toString?.() ?? '0',
|
|
|
+ coverImg: row.coverImg ?? '',
|
|
|
+ categoryId: row.categoryId ?? null,
|
|
|
+ tagIds: row.tagIds ?? [],
|
|
|
+ listStatus: row.listStatus ?? 0,
|
|
|
+ editedAt: row.editedAt?.toString?.() ?? '0',
|
|
|
+ })),
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ async findOne(id: string): Promise<VideoMediaDetailDto> {
|
|
|
+ const video = await this.prisma.videoMedia.findUnique({
|
|
|
+ where: { id },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!video) {
|
|
|
+ throw new NotFoundException('Video not found');
|
|
|
+ }
|
|
|
+
|
|
|
+ const [category, tags] = await Promise.all([
|
|
|
+ video.categoryId
|
|
|
+ ? this.prisma.category.findUnique({
|
|
|
+ where: { id: video.categoryId },
|
|
|
+ })
|
|
|
+ : null,
|
|
|
+ video.tagIds && video.tagIds.length
|
|
|
+ ? this.prisma.tag.findMany({
|
|
|
+ where: { id: { in: video.tagIds } },
|
|
|
+ orderBy: { seq: 'asc' },
|
|
|
+ })
|
|
|
+ : [],
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: video.id,
|
|
|
+ title: video.title,
|
|
|
+ filename: video.filename,
|
|
|
+ videoTime: video.videoTime,
|
|
|
+ size: video.size?.toString?.() ?? '0',
|
|
|
+ coverImg: video.coverImg ?? '',
|
|
|
+ type: video.type,
|
|
|
+ formatType: video.formatType,
|
|
|
+ contentType: video.contentType,
|
|
|
+ country: video.country,
|
|
|
+ status: video.status,
|
|
|
+ desc: video.desc ?? '',
|
|
|
+ categoryId: video.categoryId ?? null,
|
|
|
+ tagIds: video.tagIds ?? [],
|
|
|
+ listStatus: video.listStatus ?? 0,
|
|
|
+ editedAt: video.editedAt?.toString?.() ?? '0',
|
|
|
+ categoryName: category?.name ?? null,
|
|
|
+ tags: tags.map((t) => ({ id: t.id, name: t.name })),
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ async updateManage(id: string, dto: UpdateVideoMediaManageDto) {
|
|
|
+ const video = await this.prisma.videoMedia.findUnique({
|
|
|
+ where: { id },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!video) {
|
|
|
+ throw new NotFoundException('Video not found');
|
|
|
+ }
|
|
|
+
|
|
|
+ const updateData: any = {};
|
|
|
+
|
|
|
+ if (typeof dto.title === 'string') {
|
|
|
+ updateData.title = dto.title.trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ let categoryId: string | null | undefined = dto.categoryId;
|
|
|
+ const tagIds: string[] | undefined = dto.tagIds;
|
|
|
+
|
|
|
+ if (dto.categoryId === null) {
|
|
|
+ categoryId = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof categoryId !== 'undefined' || typeof tagIds !== 'undefined') {
|
|
|
+ const { finalCategoryId, finalTagIds, tagsFlat } =
|
|
|
+ await this.validateCategoryAndTags(categoryId, tagIds);
|
|
|
+
|
|
|
+ updateData.categoryId = finalCategoryId;
|
|
|
+ updateData.tagIds = finalTagIds;
|
|
|
+ updateData.tagsFlat = tagsFlat; // 👈 new
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof dto.listStatus === 'number') {
|
|
|
+ if (dto.listStatus !== 0 && dto.listStatus !== 1) {
|
|
|
+ throw new BadRequestException('Invalid listStatus value');
|
|
|
+ }
|
|
|
+ updateData.listStatus = dto.listStatus;
|
|
|
+ }
|
|
|
+
|
|
|
+ updateData.editedAt = BigInt(Date.now());
|
|
|
+
|
|
|
+ await this.prisma.videoMedia.update({
|
|
|
+ where: { id },
|
|
|
+ data: updateData,
|
|
|
+ });
|
|
|
+
|
|
|
+ return this.findOne(id);
|
|
|
}
|
|
|
|
|
|
- findAll() {
|
|
|
- return `This action returns all videoMedias`;
|
|
|
+ async updateStatus(id: string, dto: UpdateVideoMediaStatusDto) {
|
|
|
+ const video = await this.prisma.videoMedia.findUnique({
|
|
|
+ where: { id },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!video) {
|
|
|
+ throw new NotFoundException('Video not found');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dto.listStatus !== 0 && dto.listStatus !== 1) {
|
|
|
+ throw new BadRequestException('Invalid listStatus value');
|
|
|
+ }
|
|
|
+
|
|
|
+ const editedAt = BigInt(Date.now());
|
|
|
+
|
|
|
+ await this.prisma.videoMedia.update({
|
|
|
+ where: { id },
|
|
|
+ data: {
|
|
|
+ listStatus: dto.listStatus,
|
|
|
+ editedAt,
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ id,
|
|
|
+ listStatus: dto.listStatus,
|
|
|
+ editedAt: editedAt.toString(),
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- findOne(id: number) {
|
|
|
- return `This action returns a #${id} videoMedia`;
|
|
|
+ async batchUpdateStatus(dto: BatchUpdateVideoMediaStatusDto) {
|
|
|
+ if (!dto.ids?.length) {
|
|
|
+ throw new BadRequestException('ids cannot be empty');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dto.listStatus !== 0 && dto.listStatus !== 1) {
|
|
|
+ throw new BadRequestException('Invalid listStatus value');
|
|
|
+ }
|
|
|
+
|
|
|
+ const editedAt = BigInt(Date.now());
|
|
|
+
|
|
|
+ const result = await this.prisma.videoMedia.updateMany({
|
|
|
+ where: { id: { in: dto.ids } },
|
|
|
+ data: {
|
|
|
+ listStatus: dto.listStatus,
|
|
|
+ editedAt,
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ affected: result.count,
|
|
|
+ listStatus: dto.listStatus,
|
|
|
+ editedAt: editedAt.toString(),
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- update(id: number, updateVideoMediaDto: UpdateVideoMediaDto) {
|
|
|
- return `This action updates a #${id} videoMedia`;
|
|
|
+ /**
|
|
|
+ * 封面更新逻辑占位(S3 上传完成后更新 coverImg)
|
|
|
+ */
|
|
|
+ async updateCover(id: string, coverImg: string) {
|
|
|
+ const video = await this.prisma.videoMedia.findUnique({
|
|
|
+ where: { id },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!video) {
|
|
|
+ throw new NotFoundException('Video not found');
|
|
|
+ }
|
|
|
+
|
|
|
+ const editedAt = BigInt(Date.now());
|
|
|
+
|
|
|
+ await this.prisma.videoMedia.update({
|
|
|
+ where: { id },
|
|
|
+ data: {
|
|
|
+ coverImg,
|
|
|
+ editedAt,
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ id,
|
|
|
+ coverImg,
|
|
|
+ editedAt: editedAt.toString(),
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- remove(id: number) {
|
|
|
- return `This action removes a #${id} videoMedia`;
|
|
|
+ private async validateCategoryAndTags(
|
|
|
+ categoryId: string | null | undefined,
|
|
|
+ tagIds: string[] | undefined,
|
|
|
+ ): Promise<{
|
|
|
+ finalCategoryId: string | null;
|
|
|
+ finalTagIds: string[];
|
|
|
+ tagsFlat: string;
|
|
|
+ }> {
|
|
|
+ let finalCategoryId: string | null =
|
|
|
+ typeof categoryId === 'undefined' ? (undefined as any) : categoryId;
|
|
|
+ let finalTagIds: string[] = [];
|
|
|
+ let tagsFlat = '';
|
|
|
+
|
|
|
+ // Normalize tagIds: remove duplicates
|
|
|
+ if (Array.isArray(tagIds)) {
|
|
|
+ const unique = [...new Set(tagIds)];
|
|
|
+ if (unique.length > 5) {
|
|
|
+ throw new BadRequestException('Tag count cannot exceed 5');
|
|
|
+ }
|
|
|
+ finalTagIds = unique;
|
|
|
+ }
|
|
|
+
|
|
|
+ // If tags are provided but categoryId is null/undefined -> error
|
|
|
+ if (finalTagIds.length > 0 && !finalCategoryId) {
|
|
|
+ throw new BadRequestException(
|
|
|
+ 'Category is required when tags are provided.',
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate category if present
|
|
|
+ if (typeof finalCategoryId !== 'undefined' && finalCategoryId !== null) {
|
|
|
+ const category = await this.prisma.category.findUnique({
|
|
|
+ where: { id: finalCategoryId },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!category) {
|
|
|
+ throw new BadRequestException('Category not found');
|
|
|
+ }
|
|
|
+ if (category.status !== 1) {
|
|
|
+ throw new BadRequestException('Category is disabled');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (finalTagIds.length > 0) {
|
|
|
+ const tags = await this.prisma.tag.findMany({
|
|
|
+ where: { id: { in: finalTagIds } },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (tags.length !== finalTagIds.length) {
|
|
|
+ throw new BadRequestException('Some tags do not exist');
|
|
|
+ }
|
|
|
+
|
|
|
+ const distinctCategoryIds = [
|
|
|
+ ...new Set(tags.map((t) => t.categoryId.toString())),
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (distinctCategoryIds.length > 1) {
|
|
|
+ throw new BadRequestException(
|
|
|
+ 'All tags must belong to the same category',
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ const tagCategoryId = distinctCategoryIds[0];
|
|
|
+
|
|
|
+ if (finalCategoryId && tagCategoryId !== finalCategoryId) {
|
|
|
+ throw new BadRequestException(
|
|
|
+ 'Tags do not belong to the specified category',
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // If categoryId was not provided but tags exist, infer from tags
|
|
|
+ if (!finalCategoryId) {
|
|
|
+ finalCategoryId = tagCategoryId;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Build tagsFlat: lowercased names joined by space
|
|
|
+ tagsFlat = tags
|
|
|
+ .map((t) => t.name.trim().toLowerCase())
|
|
|
+ .filter(Boolean)
|
|
|
+ .join(' ');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof finalCategoryId === 'undefined') {
|
|
|
+ finalCategoryId = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ finalCategoryId,
|
|
|
+ finalTagIds,
|
|
|
+ tagsFlat,
|
|
|
+ };
|
|
|
}
|
|
|
}
|