一、超管端设计优化 - 文档管理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>
250 lines
7.4 KiB
TypeScript
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 };
|
|
}
|
|
}
|