feat(ops): kb doc edit, validation auto-tickets, support batch ops
Add knowledge document GET/PUT and admin edit UI; auto-create support tickets on client 400 validation errors across user/shop/partner apps; batch create tasks and publish from support tickets; restore mini-user store env single-column layout. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import type {
|
||||
CreateKnowledgeBaseRequest,
|
||||
CreateKnowledgeDocumentRequest,
|
||||
UpdateKnowledgeBaseRequest,
|
||||
UpdateKnowledgeDocumentRequest,
|
||||
} from '@dukang/shared-types';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import {
|
||||
@@ -119,6 +120,33 @@ export class AdminKnowledgeBasesController {
|
||||
return this.service.addDocument(actor, BigInt(id), body);
|
||||
}
|
||||
|
||||
@Get(':id/documents/:docId')
|
||||
async getDocument(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Param('docId') docId: string,
|
||||
) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.getDocument(actor, BigInt(id), BigInt(docId));
|
||||
}
|
||||
|
||||
@Put(':id/documents/:docId')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.KNOWLEDGE_DOC_UPDATE,
|
||||
refType: 'KNOWLEDGE_DOCUMENT',
|
||||
refIdField: 'docId',
|
||||
includeBody: true,
|
||||
})
|
||||
async updateDocument(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Param('docId') docId: string,
|
||||
@Body() body: UpdateKnowledgeDocumentRequest,
|
||||
) {
|
||||
const actor = await this.service.resolveActor(user.actorId);
|
||||
return this.service.updateDocument(actor, BigInt(id), BigInt(docId), body);
|
||||
}
|
||||
|
||||
@Delete(':id/documents/:docId')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.KNOWLEDGE_DOC_DELETE,
|
||||
|
||||
@@ -9,8 +9,10 @@ import type {
|
||||
CreateKnowledgeDocumentRequest,
|
||||
KnowledgeBaseDto,
|
||||
KnowledgeBaseOptionDto,
|
||||
KnowledgeDocumentDetailDto,
|
||||
KnowledgeDocumentDto,
|
||||
UpdateKnowledgeBaseRequest,
|
||||
UpdateKnowledgeDocumentRequest,
|
||||
} from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
@@ -228,6 +230,113 @@ export class AdminKnowledgeBasesService {
|
||||
return this.toDocDto(row);
|
||||
}
|
||||
|
||||
async getDocument(actor: ActorCtx, kbId: bigint, docId: bigint): Promise<KnowledgeDocumentDetailDto> {
|
||||
await this.requireKb(actor, kbId);
|
||||
const doc = await this.prisma.knowledgeDocument.findFirst({
|
||||
where: { id: docId, knowledgeBaseId: kbId },
|
||||
});
|
||||
if (!doc) throw new NotFoundException('文档不存在');
|
||||
return {
|
||||
...this.toDocDto(doc),
|
||||
contentText: doc.contentText,
|
||||
};
|
||||
}
|
||||
|
||||
async updateDocument(
|
||||
actor: ActorCtx,
|
||||
kbId: bigint,
|
||||
docId: bigint,
|
||||
dto: UpdateKnowledgeDocumentRequest,
|
||||
) {
|
||||
const kb = await this.requireKb(actor, kbId);
|
||||
this.requireWrite(actor, kb);
|
||||
const doc = await this.prisma.knowledgeDocument.findFirst({
|
||||
where: { id: docId, knowledgeBaseId: kbId },
|
||||
});
|
||||
if (!doc) throw new NotFoundException('文档不存在');
|
||||
|
||||
const data: {
|
||||
title?: string;
|
||||
fileName?: string | null;
|
||||
fileUrl?: string | null;
|
||||
mimeType?: string | null;
|
||||
sizeBytes?: number | null;
|
||||
contentText?: string | null;
|
||||
status?: 'READY' | 'EMPTY' | 'FAILED';
|
||||
errorMessage?: string | null;
|
||||
} = {};
|
||||
|
||||
if (dto.title !== undefined) {
|
||||
const title = dto.title.trim();
|
||||
if (!title) throw new BadRequestException('请填写标题');
|
||||
data.title = title;
|
||||
}
|
||||
|
||||
const hasContentInput =
|
||||
dto.contentText !== undefined ||
|
||||
dto.fileUrl !== undefined ||
|
||||
dto.fileName !== undefined ||
|
||||
dto.mimeType !== undefined;
|
||||
|
||||
if (hasContentInput) {
|
||||
let contentText = dto.contentText !== undefined ? dto.contentText?.trim() || '' : doc.contentText?.trim() || '';
|
||||
let status: 'READY' | 'EMPTY' | 'FAILED' = doc.status as 'READY' | 'EMPTY' | 'FAILED';
|
||||
let errorMessage: string | null = doc.errorMessage;
|
||||
|
||||
if (dto.contentText !== undefined) {
|
||||
if (contentText) {
|
||||
status = 'READY';
|
||||
errorMessage = null;
|
||||
} else if (!dto.fileUrl?.trim() && !doc.fileUrl) {
|
||||
status = 'EMPTY';
|
||||
errorMessage = null;
|
||||
}
|
||||
data.contentText = contentText || null;
|
||||
}
|
||||
|
||||
if (dto.fileUrl !== undefined) data.fileUrl = dto.fileUrl?.trim() || null;
|
||||
if (dto.fileName !== undefined) data.fileName = dto.fileName?.trim() || null;
|
||||
if (dto.mimeType !== undefined) data.mimeType = dto.mimeType?.trim() || null;
|
||||
if (dto.sizeBytes !== undefined) data.sizeBytes = dto.sizeBytes ?? null;
|
||||
|
||||
const fileUrl = dto.fileUrl?.trim() || doc.fileUrl;
|
||||
const fileName = dto.fileName?.trim() || doc.fileName || '';
|
||||
const mimeType = dto.mimeType?.trim() || doc.mimeType;
|
||||
|
||||
if (dto.fileUrl?.trim() && dto.contentText === undefined) {
|
||||
if (TEXT_EXT.test(fileName) || isLikelyTextMime(mimeType)) {
|
||||
try {
|
||||
contentText = await fetchText(dto.fileUrl.trim());
|
||||
data.contentText = contentText || null;
|
||||
status = contentText.trim() ? 'READY' : 'EMPTY';
|
||||
errorMessage = contentText.trim() ? null : '文件内容为空';
|
||||
} catch (e) {
|
||||
status = 'FAILED';
|
||||
errorMessage = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
} else {
|
||||
status = 'EMPTY';
|
||||
errorMessage = '非文本文件未抽取正文,请粘贴文本或上传 .txt/.md';
|
||||
}
|
||||
} else if (dto.contentText !== undefined && contentText) {
|
||||
status = 'READY';
|
||||
errorMessage = null;
|
||||
} else if (dto.contentText !== undefined && !contentText && !fileUrl) {
|
||||
status = 'EMPTY';
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
data.status = status;
|
||||
data.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
const updated = await this.prisma.knowledgeDocument.update({
|
||||
where: { id: docId },
|
||||
data,
|
||||
});
|
||||
return this.toDocDto(updated);
|
||||
}
|
||||
|
||||
async removeDocument(actor: ActorCtx, kbId: bigint, docId: bigint) {
|
||||
const kb = await this.requireKb(actor, kbId);
|
||||
this.requireWrite(actor, kb);
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
SupportTicketRemarkDto,
|
||||
UpdateSupportTicketDto,
|
||||
BatchUpdateSupportTicketStatusDto,
|
||||
BatchCreateSupportTicketTasksDto,
|
||||
BatchPublishSupportTicketsDto,
|
||||
} from '../common/dto/support-ticket.dto';
|
||||
import {
|
||||
BatchReviewConfirmDto,
|
||||
@@ -109,6 +111,26 @@ export class AdminSupportTicketsController {
|
||||
);
|
||||
}
|
||||
|
||||
@Post('batch-create-tasks')
|
||||
@UseGuards(SuperAdminGuard)
|
||||
async batchCreateTasks(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Body() body: BatchCreateSupportTicketTasksDto,
|
||||
) {
|
||||
const account = await this.resolveHqAccount(user);
|
||||
return this.service.batchCreateTasks(body.ticketIds.map(BigInt), account);
|
||||
}
|
||||
|
||||
@Post('batch-publish')
|
||||
@UseGuards(SuperAdminGuard)
|
||||
async batchPublish(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Body() body: BatchPublishSupportTicketsDto,
|
||||
) {
|
||||
const account = await this.resolveHqAccount(user);
|
||||
return this.service.batchPublish(body.ticketIds.map(BigInt), body, account);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(BigInt(id));
|
||||
|
||||
Reference in New Issue
Block a user