library-picturebook-activity/backend/src/public/gallery.service.ts
aid f246b38fc1 Day5: 超管端内容管理模块全面优化 + 广场推荐作品展示
作品审核:
- 批量通过/批量拒绝 + 撤销审核机制
- 默认筛选待审核,表格加描述预览+审核时间列
- 详情Drawer加上一个/下一个导航,审核后自动跳下一个
- 操作日志时间线展示,筛选下拉自动查询

作品管理:
- 修复筛选/排序失效,新增推荐中筛选
- 下架改为弹窗选择原因,取消推荐二次确认
- 详情Drawer补全描述/标签/操作按钮/操作日志
- 统计卡片可点击筛选,下架自动取消推荐

标签管理:
- 按分类分组卡片式展示,分类改为下拉选择
- 新增标签颜色字段(预设色+自定义)
- 上移/下移排序按钮,使用次数可点击跳转作品管理
- 新增/编辑时实时预览用户端标签效果

广场推荐:
- 新增推荐作品列表接口 GET /public/gallery/recommended
- 广场顶部新增「编辑推荐」横向滚动栏

文档更新:内容管理设计文档补充实施记录,UGC开发计划P1-1标记已完成

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:21:21 +08:00

137 lines
3.7 KiB
TypeScript

import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class GalleryService {
constructor(private prisma: PrismaService) {}
/** 作品广场列表(已审核通过的公开作品) */
async getGalleryList(params: {
page?: number;
pageSize?: number;
tagId?: number;
category?: string;
sortBy?: string;
keyword?: string;
}) {
const { page = 1, pageSize = 12, tagId, category, sortBy = 'latest', keyword } = params;
const skip = (page - 1) * pageSize;
const where: any = {
status: 'published',
visibility: 'public',
isDeleted: 0,
};
if (keyword) {
where.OR = [
{ title: { contains: keyword } },
{ description: { contains: keyword } },
];
}
if (tagId) {
where.tags = { some: { tagId } };
}
if (category) {
where.tags = {
...where.tags,
some: { ...where.tags?.some, tag: { category } },
};
}
const orderBy: any = sortBy === 'hot'
? [{ likeCount: 'desc' }, { viewCount: 'desc' }]
: sortBy === 'views'
? { viewCount: 'desc' }
: { publishTime: 'desc' };
const [list, total] = await Promise.all([
this.prisma.userWork.findMany({
where,
skip,
take: pageSize,
orderBy,
include: {
creator: { select: { id: true, nickname: true, avatar: true } },
tags: { include: { tag: { select: { id: true, name: true } } } },
_count: { select: { pages: true, likes: true, comments: true } },
},
}),
this.prisma.userWork.count({ where }),
]);
return { list, total, page, pageSize };
}
/** 广场作品详情(增加浏览量) */
async getGalleryDetail(id: number) {
const work = await this.prisma.userWork.findFirst({
where: { id, status: 'published', visibility: 'public', isDeleted: 0 },
include: {
pages: { orderBy: { pageNo: 'asc' } },
creator: { select: { id: true, nickname: true, avatar: true, username: true } },
tags: { include: { tag: true } },
_count: { select: { pages: true, likes: true, favorites: true, comments: true } },
},
});
if (!work) throw new NotFoundException('作品不存在');
// 异步增加浏览量
this.prisma.userWork.update({
where: { id },
data: { viewCount: { increment: 1 } },
}).catch(() => {});
return work;
}
/** 推荐作品列表 */
async getRecommendedWorks(limit = 10) {
return this.prisma.userWork.findMany({
where: {
isRecommended: true,
status: 'published',
visibility: 'public',
isDeleted: 0,
},
take: limit,
orderBy: [{ likeCount: 'desc' }, { publishTime: 'desc' }],
include: {
creator: { select: { id: true, nickname: true, avatar: true } },
},
});
}
/** 某用户的公开作品列表 */
async getUserPublicWorks(userId: number, params: { page?: number; pageSize?: number }) {
const { page = 1, pageSize = 12 } = params;
const skip = (page - 1) * pageSize;
const where = {
userId,
status: 'published',
visibility: 'public',
isDeleted: 0,
};
const [list, total] = await Promise.all([
this.prisma.userWork.findMany({
where,
skip,
take: pageSize,
orderBy: { publishTime: 'desc' },
include: {
tags: { include: { tag: { select: { id: true, name: true } } } },
_count: { select: { pages: true, likes: true } },
},
}),
this.prisma.userWork.count({ where }),
]);
return { list, total, page, pageSize };
}
}