This commit is contained in:
ditiqi 2025-02-24 19:33:18 +08:00
parent 5a5d4ec3ff
commit 7e482f8c53
11 changed files with 152 additions and 79 deletions

View File

@ -1,25 +1,27 @@
import { db, Prisma, PrismaClient } from "@nice/common"; import { db, Prisma, PrismaClient } from '@nice/common';
export type Operations = export type Operations =
| 'aggregate' | 'aggregate'
| 'count' | 'count'
| 'create' | 'create'
| 'createMany' | 'createMany'
| 'delete' | 'delete'
| 'deleteMany' | 'deleteMany'
| 'findFirst' | 'findFirst'
| 'findMany' | 'findMany'
| 'findUnique' | 'findUnique'
| 'update' | 'update'
| 'updateMany' | 'updateMany'
| 'upsert'; | 'upsert';
export type DelegateFuncs = { [K in Operations]: (args: any) => Promise<unknown> } export type DelegateFuncs = {
[K in Operations]: (args: any) => Promise<unknown>;
};
export type DelegateArgs<T> = { export type DelegateArgs<T> = {
[K in keyof T]: T[K] extends (args: infer A) => Promise<any> ? A : never; [K in keyof T]: T[K] extends (args: infer A) => Promise<any> ? A : never;
}; };
export type DelegateReturnTypes<T> = { export type DelegateReturnTypes<T> = {
[K in keyof T]: T[K] extends (args: any) => Promise<infer R> ? R : never; [K in keyof T]: T[K] extends (args: any) => Promise<infer R> ? R : never;
}; };
export type WhereArgs<T> = T extends { where?: infer W } ? W : never; export type WhereArgs<T> = T extends { where?: infer W } ? W : never;
@ -28,17 +30,17 @@ export type DataArgs<T> = T extends { data: infer D } ? D : never;
export type IncludeArgs<T> = T extends { include: infer I } ? I : never; export type IncludeArgs<T> = T extends { include: infer I } ? I : never;
export type OrderByArgs<T> = T extends { orderBy: infer O } ? O : never; export type OrderByArgs<T> = T extends { orderBy: infer O } ? O : never;
export type UpdateOrderArgs = { export type UpdateOrderArgs = {
id: string id: string;
overId: string overId: string;
} };
export interface FindManyWithCursorType<T extends DelegateFuncs> { export interface FindManyWithCursorType<T extends DelegateFuncs> {
cursor?: string; cursor?: string;
limit?: number; limit?: number;
where?: WhereArgs<DelegateArgs<T>['findUnique']>; where?: WhereArgs<DelegateArgs<T>['findUnique']>;
select?: SelectArgs<DelegateArgs<T>['findUnique']>; select?: SelectArgs<DelegateArgs<T>['findUnique']>;
orderBy?: OrderByArgs<DelegateArgs<T>['findMany']> orderBy?: OrderByArgs<DelegateArgs<T>['findMany']>;
} }
export type TransactionType = Omit< export type TransactionType = Omit<
PrismaClient, PrismaClient,
'$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends' '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'
>; >;

View File

@ -3,8 +3,10 @@ import { TrpcService } from '@server/trpc/trpc.service';
import { CourseMethodSchema, Prisma } from '@nice/common'; import { CourseMethodSchema, Prisma } from '@nice/common';
import { PostService } from './post.service'; import { PostService } from './post.service';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
import { UpdateOrderArgs } from '../base/base.type';
const PostCreateArgsSchema: ZodType<Prisma.PostCreateArgs> = z.any(); const PostCreateArgsSchema: ZodType<Prisma.PostCreateArgs> = z.any();
const PostUpdateArgsSchema: ZodType<Prisma.PostUpdateArgs> = z.any(); const PostUpdateArgsSchema: ZodType<Prisma.PostUpdateArgs> = z.any();
const PostUpdateOrderArgsSchema: ZodType<UpdateOrderArgs> = z.any();
const PostFindFirstArgsSchema: ZodType<Prisma.PostFindFirstArgs> = z.any(); const PostFindFirstArgsSchema: ZodType<Prisma.PostFindFirstArgs> = z.any();
const PostFindManyArgsSchema: ZodType<Prisma.PostFindManyArgs> = z.any(); const PostFindManyArgsSchema: ZodType<Prisma.PostFindManyArgs> = z.any();
const PostDeleteManyArgsSchema: ZodType<Prisma.PostDeleteManyArgs> = z.any(); const PostDeleteManyArgsSchema: ZodType<Prisma.PostDeleteManyArgs> = z.any();
@ -107,5 +109,21 @@ export class PostRouter {
.query(async ({ input }) => { .query(async ({ input }) => {
return await this.postService.findManyWithPagination(input); return await this.postService.findManyWithPagination(input);
}), }),
updateOrder: this.trpc.protectProcedure
.input(PostUpdateOrderArgsSchema)
.mutation(async ({ ctx, input }) => {
const { staff } = ctx;
return await this.postService.updateOrder(input);
}),
updateOrderByIds: this.trpc.protectProcedure
.input(
z.object({
ids: z.array(z.string()),
}),
)
.mutation(async ({ ctx, input }) => {
const { staff } = ctx;
return await this.postService.updateOrderByIds(input.ids);
}),
}); });
} }

View File

@ -20,6 +20,7 @@ import EventBus, { CrudOperation } from '@server/utils/event-bus';
import { BaseTreeService } from '../base/base.tree.service'; import { BaseTreeService } from '../base/base.tree.service';
import { z } from 'zod'; import { z } from 'zod';
import { DefaultArgs } from '@prisma/client/runtime/library'; import { DefaultArgs } from '@prisma/client/runtime/library';
import dayjs from 'dayjs';
@Injectable() @Injectable()
export class PostService extends BaseTreeService<Prisma.PostDelegate> { export class PostService extends BaseTreeService<Prisma.PostDelegate> {
@ -43,6 +44,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
content: content, content: content,
title: title, title: title,
authorId: params?.staff?.id, authorId: params?.staff?.id,
updatedAt: dayjs().toDate(),
resources: { resources: {
connect: resourceIds.map((fileId) => ({ fileId })), connect: resourceIds.map((fileId) => ({ fileId })),
}, },
@ -71,6 +73,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
parentId: courseId, parentId: courseId,
title: title, title: title,
authorId: staff?.id, authorId: staff?.id,
updatedAt: dayjs().toDate(),
} as any, } as any,
}, },
{ tx }, { tx },
@ -152,6 +155,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
params?: { staff?: UserProfile; tx?: Prisma.TransactionClient }, params?: { staff?: UserProfile; tx?: Prisma.TransactionClient },
) { ) {
args.data.authorId = params?.staff?.id; args.data.authorId = params?.staff?.id;
args.data.updatedAt = dayjs().toDate();
const result = await super.create(args); const result = await super.create(args);
EventBus.emit('dataChanged', { EventBus.emit('dataChanged', {
type: ObjectType.POST, type: ObjectType.POST,
@ -162,6 +166,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
} }
async update(args: Prisma.PostUpdateArgs, staff?: UserProfile) { async update(args: Prisma.PostUpdateArgs, staff?: UserProfile) {
args.data.authorId = staff?.id; args.data.authorId = staff?.id;
args.data.updatedAt = dayjs().toDate();
const result = await super.update(args); const result = await super.update(args);
EventBus.emit('dataChanged', { EventBus.emit('dataChanged', {
type: ObjectType.POST, type: ObjectType.POST,
@ -246,8 +251,38 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
}[]; }[];
totalPages: number; totalPages: number;
}> { }> {
// super.updateOrder;
return super.findManyWithPagination(args); return super.findManyWithPagination(args);
} }
async updateOrderByIds(ids: string[]) {
const posts = await db.post.findMany({
where: { id: { in: ids } },
select: { id: true, order: true },
});
const postMap = new Map(posts.map((post) => [post.id, post]));
const orderedPosts = ids
.map((id) => postMap.get(id))
.filter((post): post is { id: string; order: number } => !!post);
// 生成仅需更新的操作
const updates = orderedPosts
.map((post, index) => ({
id: post.id,
newOrder: index, // 按数组索引设置新顺序
currentOrder: post.order,
}))
.filter(({ newOrder, currentOrder }) => newOrder !== currentOrder)
.map(({ id, newOrder }) =>
db.post.update({
where: { id },
data: { order: newOrder },
}),
);
// 批量执行更新
return updates.length > 0 ? await db.$transaction(updates) : [];
}
protected async setPerms(data: Post, staff?: UserProfile) { protected async setPerms(data: Post, staff?: UserProfile) {
if (!staff) return; if (!staff) return;
const perms: ResPerm = { const perms: ResPerm = {

View File

@ -37,5 +37,4 @@ export class PostQueueService implements OnModuleInit {
debounce: { id: `${QueueJobType.UPDATE_POST_STATE}_${data.id}` }, debounce: { id: `${QueueJobType.UPDATE_POST_STATE}_${data.id}` },
}); });
} }
} }

View File

@ -7,6 +7,7 @@ import {
Department, Department,
getRandomElement, getRandomElement,
getRandomElements, getRandomElements,
PostType,
Staff, Staff,
TaxonomySlug, TaxonomySlug,
Term, Term,
@ -14,6 +15,7 @@ import {
import EventBus from '@server/utils/event-bus'; import EventBus from '@server/utils/event-bus';
import { capitalizeFirstLetter, DevDataCounts, getCounts } from './utils'; import { capitalizeFirstLetter, DevDataCounts, getCounts } from './utils';
import { StaffService } from '@server/models/staff/staff.service'; import { StaffService } from '@server/models/staff/staff.service';
import dayjs from 'dayjs';
@Injectable() @Injectable()
export class GenDevService { export class GenDevService {
private readonly logger = new Logger(GenDevService.name); private readonly logger = new Logger(GenDevService.name);
@ -22,7 +24,7 @@ export class GenDevService {
terms: Record<TaxonomySlug, Term[]> = { terms: Record<TaxonomySlug, Term[]> = {
[TaxonomySlug.CATEGORY]: [], [TaxonomySlug.CATEGORY]: [],
[TaxonomySlug.TAG]: [], [TaxonomySlug.TAG]: [],
[TaxonomySlug.LEVEL]: [] [TaxonomySlug.LEVEL]: [],
}; };
depts: Department[] = []; depts: Department[] = [];
domains: Department[] = []; domains: Department[] = [];
@ -35,7 +37,7 @@ export class GenDevService {
private readonly departmentService: DepartmentService, private readonly departmentService: DepartmentService,
private readonly staffService: StaffService, private readonly staffService: StaffService,
private readonly termService: TermService, private readonly termService: TermService,
) { } ) {}
async genDataEvent() { async genDataEvent() {
EventBus.emit('genDataEvent', { type: 'start' }); EventBus.emit('genDataEvent', { type: 'start' });
try { try {
@ -58,6 +60,7 @@ export class GenDevService {
if (this.counts.termCount === 0) { if (this.counts.termCount === 0) {
this.logger.log('Generate terms'); this.logger.log('Generate terms');
await this.createTerms(null, TaxonomySlug.CATEGORY, depth, count); await this.createTerms(null, TaxonomySlug.CATEGORY, depth, count);
await this.createLevelTerm();
const domains = this.depts.filter((item) => item.isDomain); const domains = this.depts.filter((item) => item.isDomain);
for (const domain of domains) { for (const domain of domains) {
await this.createTerms(domain, TaxonomySlug.CATEGORY, depth, count); await this.createTerms(domain, TaxonomySlug.CATEGORY, depth, count);
@ -174,7 +177,54 @@ export class GenDevService {
} }
} }
} }
private async createLevelTerm() {
try {
// 1. 获取分类时添加异常处理
const taxLevel = await db.taxonomy.findFirst({
where: { slug: TaxonomySlug.LEVEL },
});
if (!taxLevel) {
throw new Error('LEVEL taxonomy not found');
}
// 2. 使用数组定义初始化数据 + 名称去重
const termsToCreate = [
{ name: '初级', taxonomyId: taxLevel.id },
{ name: '中级', taxonomyId: taxLevel.id },
{ name: '高级', taxonomyId: taxLevel.id }, // 改为高级更合理
];
await this.termService.createMany({
data: termsToCreate,
});
console.log('created level terms');
} catch (error) {
console.error('Failed to create level terms:', error);
throw error; // 向上抛出错误供上层处理
}
}
private async createCourse(
title: string,
deptId: string,
cateId: string,
levelId: string,
) {
const course = await db.post.create({
data: {
type: PostType.COURSE,
title: title,
updatedAt: dayjs().toDate(),
depts: {
connect: {
id: deptId,
},
},
terms:{
connect:[cateId,levelId].map
}
},
});
}
private async createDepartment( private async createDepartment(
name: string, name: string,
parentId?: string | null, parentId?: string | null,

View File

@ -115,7 +115,7 @@ export function CourseFormProvider({
}); });
message.success("课程更新成功!"); message.success("课程更新成功!");
} else { } else {
const result = await createCourse.mutateAsync({ const result = await create.mutateAsync({
courseDetail: { courseDetail: {
data: { data: {
title: formattedValues.title || "12345", title: formattedValues.title || "12345",

View File

@ -48,7 +48,7 @@ export const LectureList: React.FC<LectureListProps> = ({
field, field,
sectionId, sectionId,
}) => { }) => {
const { softDeleteByIds } = usePost(); const { softDeleteByIds, updateOrderByIds } = usePost();
const { data: lectures = [], isLoading } = ( const { data: lectures = [], isLoading } = (
api.post.findMany as any api.post.findMany as any
).useQuery( ).useQuery(
@ -87,11 +87,19 @@ export const LectureList: React.FC<LectureListProps> = ({
const handleDragEnd = (event: DragEndEvent) => { const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event; const { active, over } = event;
if (!over || active.id === over.id) return; if (!over || active.id === over.id) return;
// updateOrder.mutateAsync({
// id: active.id,
// overId: over.id,
// });
let newItems = [];
setItems((items) => { setItems((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id); const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id); const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex); newItems = arrayMove(items, oldIndex, newIndex);
return newItems;
});
updateOrderByIds.mutateAsync({
ids: newItems.map((item) => item.id),
}); });
}; };

View File

@ -1,19 +0,0 @@
// import { FormArrayField } from "@web/src/components/common/form/FormArrayField";
// import { useFormContext } from "react-hook-form";
// import { CourseFormData } from "../context/CourseEditorContext";
// import InputList from "@web/src/components/common/input/InputList";
// import { useState } from "react";
// import { Form } from "antd";
// export function CourseGoalForm() {
// return (
// <div className="max-w-2xl mx-auto space-y-6 p-6">
// <Form.Item name="requirements" label="前置要求">
// <InputList placeholder="请输入前置要求"></InputList>
// </Form.Item>
// <Form.Item name="objectives" label="学习目标">
// <InputList placeholder="请输入学习目标"></InputList>
// </Form.Item>
// </div>
// );
// }

View File

@ -1,26 +0,0 @@
// import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader";
// import { Form, Input } from "antd";
// export default function CourseSettingForm() {
// return (
// <div className="max-w-2xl mx-auto space-y-6 p-6">
// <Form.Item
// name="title"
// label="课程预览图"
// >
// <AvatarUploader
// style={
// {
// width: "120px",
// height: "120px",
// margin:" 0 10px"
// }
// }
// onChange={(value) => {
// console.log(value);
// }}
// ></AvatarUploader>
// </Form.Item>
// </div>
// )
// }

View File

@ -113,5 +113,8 @@ export function useEntity<T extends keyof RouterInputs>(
T, T,
"updateOrder" "updateOrder"
>, // 更新实体顺序的 mutation 函数 >, // 更新实体顺序的 mutation 函数
updateOrderByIds: createMutationHandler(
"updateOrderByIds"
) as MutationResult<T, "updateOrderByIds">, // 更新实体顺序的 mutation 函数
}; };
} }

View File

@ -110,6 +110,7 @@ model Department {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
order Float? order Float?
posts Post[] @relation("post_dept")
ancestors DeptAncestry[] @relation("DescendantToAncestor") ancestors DeptAncestry[] @relation("DescendantToAncestor")
descendants DeptAncestry[] @relation("AncestorToDescendant") descendants DeptAncestry[] @relation("AncestorToDescendant")
parentId String? @map("parent_id") parentId String? @map("parent_id")
@ -200,6 +201,8 @@ model Post {
order Float? @default(0) @map("order") order Float? @default(0) @map("order")
duration Int? duration Int?
rating Int? @default(0) rating Int? @default(0)
depts Department[] @relation("post_dept")
// 索引 // 索引
// 日期时间类型字段 // 日期时间类型字段
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")