feat: 完成分类、worksheet 接口开发,资源迁移到七牛
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
HttpException,
|
||||
} from '@nestjs/common';
|
||||
import type { ContentStatus } from '@doodle/contracts';
|
||||
import { Prisma } from '../generated/prisma/client';
|
||||
import { requireCurrentAccount } from '../auth/account-policy';
|
||||
import type { AuthenticatedUser } from '../common/http-request';
|
||||
import { ApiException } from '../common/api-exception';
|
||||
|
||||
/** 只从 active 根节点展开;草稿祖先、失联或成环的分支均不对外可见。 */
|
||||
export const visibleCategoryIds = Prisma.sql`WITH RECURSIVE visible_categories AS (
|
||||
SELECT id FROM public.categories WHERE parent_id IS NULL AND status = 'active'
|
||||
UNION
|
||||
SELECT c.id FROM public.categories c JOIN visible_categories p ON c.parent_id = p.id
|
||||
WHERE c.status = 'active'
|
||||
) SELECT id FROM visible_categories`;
|
||||
|
||||
/** 题型列表、详情和管理预览共用规则,不能只过滤左侧分类菜单。SQL 中题型别名固定为 w。 */
|
||||
export const visibleWorksheet = Prisma.sql`w.status = 'active' AND w.category_id IN (${visibleCategoryIds})`;
|
||||
|
||||
/** 发布状态显式校验;不接受布尔值、NULL 或客户端自行添加的测试状态。 */
|
||||
export function contentStatus(value: unknown): ContentStatus {
|
||||
if (value !== 'draft' && value !== 'active' && value !== 'hidden')
|
||||
throw new BadRequestException('发布状态不合法');
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 写事务重新核对当前账户并持锁,避免守卫之后被禁用或降权仍然提交。 */
|
||||
export async function lockContentAdmin(
|
||||
tx: Prisma.TransactionClient,
|
||||
actor: AuthenticatedUser,
|
||||
): Promise<void> {
|
||||
const rows = await tx.$queryRaw<
|
||||
Array<{
|
||||
id: string;
|
||||
status: 'active' | 'disabled';
|
||||
role: 'user' | 'admin';
|
||||
authVersion: number;
|
||||
}>
|
||||
>`SELECT id, status, role, auth_version AS "authVersion" FROM public.users WHERE id = ${actor.userId} FOR UPDATE`;
|
||||
const account = requireCurrentAccount(
|
||||
rows[0] ?? null,
|
||||
actor.userId,
|
||||
actor.authVersion,
|
||||
);
|
||||
if (account.role !== 'admin') throw new ForbiddenException();
|
||||
}
|
||||
|
||||
/** 先撤销数据库展示快照与发布标记;外部已分发的静态文件须由后续发布链路撤销/重建。 */
|
||||
export async function invalidateContentSnapshots(
|
||||
tx: Prisma.TransactionClient,
|
||||
): Promise<void> {
|
||||
await tx.pageConfig.updateMany({
|
||||
data: {
|
||||
home: Prisma.DbNull,
|
||||
category: Prisma.DbNull,
|
||||
age: Prisma.DbNull,
|
||||
version: { increment: 1 },
|
||||
publishedVersion: null,
|
||||
publishedAt: null,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 保留业务 HTTP 错误;连接/约束诊断不原样输出给调用者。 */
|
||||
export async function runContentQuery<T>(
|
||||
operation: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new ApiException(503, 'DATABASE_UNAVAILABLE');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user