library-picturebook-activity/backend/src/public/content-review.service.ts
aid 418aa57ea8 Day4: 超管端设计优化 + UGC绘本创作社区P0实现
一、超管端设计优化
- 文档管理SOP体系建立,docs目录重组
- 统一用户管理:跨租户全局视角,合并用户管理+公众用户
- 活动监管全模块重构:全部活动(统计卡片+阶段筛选+SuperDetail详情页)、报名数据/作品数据/评审进度(两层合一扁平列表)、成果发布(去Tab+统计+隐藏写操作)
- 菜单精简:移除评委管理/评审规则/通知管理
- Bug修复:租户编辑丢失隐藏菜单、pageSize限制、主色统一

二、UGC绘本创作社区P0
- 数据库:10张新表(user_works/user_work_pages/work_tags等)
- 子女账号独立化:Child升级为独立User,家长切换+独立登录
- 用户作品库:CRUD+发布审核,8个API
- AI创作流程:提交→生成→保存到作品库,4个API
- 作品广场:首页改造为推荐流,标签+搜索+排序
- 内容审核(超管端):作品审核+作品管理+标签管理
- 活动联动:WorkSelector作品选择器
- 布局改造:底部5Tab(发现/创作/活动/作品库/我的)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:20:25 +08:00

250 lines
7.4 KiB
TypeScript

import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class ContentReviewService {
constructor(private prisma: PrismaService) {}
/** 审核统计 */
async getWorkStats() {
const today = new Date();
today.setHours(0, 0, 0, 0);
const [pending, todayReviewed, todayApproved, todayRejected] = await Promise.all([
this.prisma.userWork.count({ where: { status: 'pending_review', isDeleted: 0 } }),
this.prisma.contentReviewLog.count({ where: { targetType: 'work', createTime: { gte: today } } }),
this.prisma.contentReviewLog.count({ where: { targetType: 'work', action: 'approve', createTime: { gte: today } } }),
this.prisma.contentReviewLog.count({ where: { targetType: 'work', action: 'reject', createTime: { gte: today } } }),
]);
return { pending, todayReviewed, todayApproved, todayRejected };
}
/** 审核队列列表 */
async getWorkQueue(params: {
page?: number;
pageSize?: number;
status?: string;
keyword?: string;
startTime?: string;
endTime?: string;
}) {
const { page = 1, pageSize = 10, status, keyword, startTime, endTime } = params;
const skip = (page - 1) * pageSize;
const where: any = { isDeleted: 0 };
if (status) {
where.status = status;
} else {
where.status = { in: ['pending_review', 'published', 'rejected', 'taken_down'] };
}
if (keyword) {
where.OR = [
{ title: { contains: keyword } },
{ creator: { nickname: { contains: keyword } } },
];
}
if (startTime) where.createTime = { ...where.createTime, gte: new Date(startTime) };
if (endTime) where.createTime = { ...where.createTime, lte: new Date(endTime + ' 23:59:59') };
const [list, total] = await Promise.all([
this.prisma.userWork.findMany({
where,
skip,
take: pageSize,
orderBy: { createTime: 'desc' },
include: {
creator: { select: { id: true, nickname: true, avatar: true, username: true, userType: true } },
tags: { include: { tag: { select: { id: true, name: true } } } },
_count: { select: { pages: true } },
},
}),
this.prisma.userWork.count({ where }),
]);
return { list, total, page, pageSize };
}
/** 审核详情(含绘本分页) */
async getWorkDetail(workId: number) {
const work = await this.prisma.userWork.findUnique({
where: { id: workId },
include: {
pages: { orderBy: { pageNo: 'asc' } },
creator: { select: { id: true, nickname: true, avatar: true, username: true, userType: true } },
tags: { include: { tag: true } },
_count: { select: { pages: true, likes: true, favorites: true, comments: true } },
},
});
if (!work) throw new NotFoundException('作品不存在');
return work;
}
/** 通过 */
async approve(workId: number, operatorId: number, note?: string) {
const work = await this.prisma.userWork.findUnique({ where: { id: workId } });
if (!work) throw new NotFoundException('作品不存在');
return this.prisma.$transaction(async (tx) => {
await tx.userWork.update({
where: { id: workId },
data: {
status: 'published',
reviewTime: new Date(),
reviewerId: operatorId,
reviewNote: note || null,
publishTime: new Date(),
},
});
await tx.contentReviewLog.create({
data: {
targetType: 'work',
targetId: workId,
workId,
action: 'approve',
note,
operatorId,
},
});
return { success: true };
});
}
/** 拒绝 */
async reject(workId: number, operatorId: number, reason: string, note?: string) {
const work = await this.prisma.userWork.findUnique({ where: { id: workId } });
if (!work) throw new NotFoundException('作品不存在');
return this.prisma.$transaction(async (tx) => {
await tx.userWork.update({
where: { id: workId },
data: {
status: 'rejected',
reviewTime: new Date(),
reviewerId: operatorId,
reviewNote: reason,
},
});
await tx.contentReviewLog.create({
data: {
targetType: 'work',
targetId: workId,
workId,
action: 'reject',
reason,
note,
operatorId,
},
});
return { success: true };
});
}
/** 下架 */
async takedown(workId: number, operatorId: number, reason: string) {
const work = await this.prisma.userWork.findUnique({ where: { id: workId } });
if (!work) throw new NotFoundException('作品不存在');
return this.prisma.$transaction(async (tx) => {
await tx.userWork.update({
where: { id: workId },
data: { status: 'taken_down', reviewNote: reason },
});
await tx.contentReviewLog.create({
data: {
targetType: 'work',
targetId: workId,
workId,
action: 'takedown',
reason,
operatorId,
},
});
return { success: true };
});
}
/** 恢复 */
async restore(workId: number, operatorId: number) {
const work = await this.prisma.userWork.findUnique({ where: { id: workId } });
if (!work) throw new NotFoundException('作品不存在');
return this.prisma.$transaction(async (tx) => {
await tx.userWork.update({
where: { id: workId },
data: { status: 'published' },
});
await tx.contentReviewLog.create({
data: {
targetType: 'work',
targetId: workId,
workId,
action: 'restore',
operatorId,
},
});
return { success: true };
});
}
/** 推荐/取消推荐 */
async toggleRecommend(workId: number) {
const work = await this.prisma.userWork.findUnique({ where: { id: workId } });
if (!work) throw new NotFoundException('作品不存在');
return this.prisma.userWork.update({
where: { id: workId },
data: { isRecommended: !work.isRecommended },
});
}
/** 作品管理统计 */
async getManagementStats() {
const today = new Date();
today.setHours(0, 0, 0, 0);
const [total, todayNew, totalViews, takenDown] = await Promise.all([
this.prisma.userWork.count({ where: { status: 'published', isDeleted: 0 } }),
this.prisma.userWork.count({ where: { status: 'published', publishTime: { gte: today }, isDeleted: 0 } }),
this.prisma.userWork.aggregate({ where: { status: 'published', isDeleted: 0 }, _sum: { viewCount: true } }),
this.prisma.userWork.count({ where: { status: 'taken_down', isDeleted: 0 } }),
]);
return { total, todayNew, totalViews: totalViews._sum.viewCount || 0, takenDown };
}
/** 审核日志 */
async getLogs(params: { page?: number; pageSize?: number; workId?: number }) {
const { page = 1, pageSize = 20, workId } = params;
const skip = (page - 1) * pageSize;
const where: any = {};
if (workId) where.workId = workId;
const [list, total] = await Promise.all([
this.prisma.contentReviewLog.findMany({
where,
skip,
take: pageSize,
orderBy: { createTime: 'desc' },
include: {
operator: { select: { id: true, nickname: true } },
},
}),
this.prisma.contentReviewLog.count({ where }),
]);
return { list, total, page, pageSize };
}
}