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:
2026-08-05 00:08:40 +08:00
parent 16c386f165
commit 02d89e6385
18 changed files with 677 additions and 24 deletions
@@ -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));