diff --git a/src/domain/mark.ts b/src/domain/mark.ts index 85d526a..263c02d 100644 --- a/src/domain/mark.ts +++ b/src/domain/mark.ts @@ -8,8 +8,6 @@ import { ServerError, ShareActionNotAllowedError, ShareCodeNotFoundError, - ShareCollectionNotAllowedError, - ShareCollectionNotFoundError, ShareDisabledError } from '../const/err' import { markDetailPO, MarkRepo, markType } from '../infra/repository/dbMark' @@ -20,6 +18,8 @@ import { UserRepo } from '../infra/repository/dbUser' export interface markResponse { id: number root_id: number + uuid: string + root_uid?: string } export interface markPathItem { @@ -33,6 +33,7 @@ export interface markRequest { source: markPathItem[] select_content: markSelectContent[] parent_id: number + parent_uid?: string comment?: string bm_id?: number bookmark_uid?: string @@ -57,6 +58,16 @@ export interface markSelectContent { src: string } +export type markIdParams = { uuid: string } | { id: number } + +export interface markMetadata { + root_id?: string | null + parent_id?: string | null + user_id?: string + source_id?: string + bookmark_id?: string +} + export interface markInfo { id: number user_id: number @@ -87,6 +98,9 @@ export interface markCommentItem { source_type: 'share' | 'bookmark' source_id: string approx_source?: markApproxSource + uuid: string + parent_uid?: string + root_uid?: string } @injectable() @@ -110,11 +124,19 @@ export class MarkService { return res } - assertMarkBookmark = async (ctx: ContextManager, bmId: number, parentId: number) => { - if (parentId < 1) throw ErrorParam() - - const mark = await this.markRepo.get(parentId) + protected resolveMark = async (params: markIdParams) => { + if ('uuid' in params) { + const mark = await this.markRepo.getByUuid(params.uuid) + if (!mark) throw ErrorParam() + return mark + } + const mark = await this.markRepo.get(params.id) if (!mark) throw ErrorParam() + return mark + } + + assertMarkBookmark = async (ctx: ContextManager, bmId: number, params: markIdParams) => { + const mark = await this.resolveMark(params) if (mark.user_bookmark_id !== bmId) throw ShareActionNotAllowedError() if (mark.is_deleted) throw ShareActionNotAllowedError() return mark @@ -168,7 +190,7 @@ export class MarkService { bmId = res.bookmark_id } - if (data.bm_id) { + if (data.bm_id || data.bookmark_uid) { await bookmarkHandle() } else if (data.share_code) { await shareHandle() @@ -195,11 +217,11 @@ export class MarkService { let parentId = 0 let replyComment: markDetailPO | undefined = undefined if (data.type === markType.REPLY) { - parentId = ctx.hashIds.decodeId(data.parent_id) - if (parentId < 1) throw ErrorParam() - const res = await this.assertMarkBookmark(ctx, userBookmark.id, parentId) + const parentParams: markIdParams = data.parent_uid ? { uuid: data.parent_uid } : { id: ctx.hashIds.decodeId(data.parent_id) } + const res = await this.assertMarkBookmark(ctx, userBookmark.id, parentParams) replyComment = res rootId = res.root_id + parentId = res.id data.source = [] } @@ -215,7 +237,7 @@ export class MarkService { sourceId = `${data.collection_code!}/${cbId}` } else { sourceType = 'bookmark' - sourceId = ctx.hashIds.decodeId(data.bm_id || 0) + sourceId = data.bm_id ? ctx.hashIds.decodeId(data.bm_id) : userBookmark.bookmark_id } // 创建实体 @@ -234,6 +256,7 @@ export class MarkService { root_id: rootId, approx_source: data.approx_source }) + if (!res) throw ServerError() // 如果是父级评论,多update一次追加root_id,后续delete的时候不需要重新查询 @@ -242,10 +265,13 @@ export class MarkService { } ctx.execution.waitUntil(callback()) + const metadata = res.metadata as markMetadata return { response: { id: ctx.hashIds.encodeId(res.id), - root_id: ctx.hashIds.encodeId(res.root_id > 0 ? res.root_id : res.id) + root_id: ctx.hashIds.encodeId(res.root_id > 0 ? res.root_id : res.id), + uuid: res.uuid, + root_uid: metadata?.root_id ?? undefined }, mark: res, userBookmark, @@ -253,33 +279,33 @@ export class MarkService { } } - public async deleteMark(ctx: ContextManager, markId: number): Promise { - const userId = ctx.getUserId() + public async deleteMark(ctx: ContextManager, params: markIdParams): Promise { + const mark = await this.resolveMark(params) + if (mark.is_deleted) throw ErrorParam() - const mark = await this.markRepo.get(markId) - if (!mark || mark.is_deleted) throw ErrorParam() + const userId = ctx.getUserId() if (mark.user_id !== userId) { - // 非本人则去校验文章所有权 const ubm = await this.bookmarkRepo.getUserBookmarkById(mark.user_bookmark_id) - if (!ubm) throw ShareActionNotAllowedError() - if (ubm.user_id !== userId) throw ShareActionNotAllowedError() + if (!ubm || ubm.user_id !== userId) throw ShareActionNotAllowedError() } - // 如果root_id的评论底下有任意子评论,则软删除当前评论 - if ([markType.COMMENT, markType.ORIGIN_COMMENT, markType.REPLY].includes(mark.type)) { - const res = await this.markRepo.existsCommentMarkChild(mark.user_bookmark_id, mark.root_id) - // 如果有子评论,把评论标记为删除 - if (res && res > 1) { - await this.markRepo.updateCommentMarkDeleted(markId) + const isComment = [markType.COMMENT, markType.ORIGIN_COMMENT, markType.REPLY].includes(mark.type) + + if (isComment) { + const childCount = mark.root_uid + ? await this.markRepo.existsCommentMarkChildByRootUid(mark.user_bookmark_id, mark.root_uid) + : await this.markRepo.existsCommentMarkChild(mark.user_bookmark_id, mark.root_id) + if (childCount && childCount > 1) { + await this.softDeleteMark(mark) return 'ok' } } - // 硬删除评论 + try { - if ([markType.COMMENT, markType.ORIGIN_COMMENT, markType.REPLY].includes(mark.type)) { - await this.markRepo.deleteByRootId(mark.user_bookmark_id, mark.root_id) - } else if ([markType.LINE, markType.ORIGIN_LINE].includes(mark.type)) { - await this.markRepo.del(markId) + if (isComment) { + await this.hardDeleteComment(mark) + } else { + await this.hardDeleteLine(mark) } } catch (e) { console.error(`delete mark failed: ${e}`) @@ -289,16 +315,48 @@ export class MarkService { return 'ok' } - public async getBookmarkMarkList(ctx: ContextManager, userBmId: number, isShowMarks: boolean) { + private async softDeleteMark(mark: markDetailPO) { + if (mark.uuid) { + await this.markRepo.updateCommentMarkDeletedByUid(mark.uuid) + } else { + await this.markRepo.updateCommentMarkDeleted(mark.id) + } + } + + private async hardDeleteComment(mark: markDetailPO) { + if (mark.root_uid) { + await this.markRepo.deleteByRootUid(mark.user_bookmark_id, mark.root_uid) + } else { + await this.markRepo.deleteByRootId(mark.user_bookmark_id, mark.root_id) + } + } + + private async hardDeleteLine(mark: markDetailPO) { + if (mark.uuid) { + await this.markRepo.delByUid(mark.uuid) + } else { + await this.markRepo.del(mark.id) + } + } + + public async getBookmarkMarkList(ctx: ContextManager, params: markIdParams & { isShowMarks: boolean }) { const defaultResult = { mark_list: [], user_list: [] } - if (!isShowMarks) return defaultResult - const markRepo = this.markRepo - const userRepo = this.userRepo + if (!params.isShowMarks) return defaultResult - const marks = await markRepo.list(userBmId) + let userBmId: number | undefined + if ('uuid' in params) { + const ub = await this.bookmarkRepo.getUserBookmarkByUuid(params.uuid) + if (!ub) return defaultResult + userBmId = ub.id + } else { + userBmId = params.id + } + if (!userBmId) return defaultResult + + const marks = await this.markRepo.list(userBmId) if (marks.length === 0) return defaultResult - const users = await userRepo.getUserInfoList(marks.map(m => m.user_id)) + const users = await this.userRepo.getUserInfoList(marks.map(m => m.user_id)) const markList: markInfo[] = marks.map(m => { return { id: ctx.hashIds.encodeId(m.id), @@ -310,7 +368,10 @@ export class MarkService { root_id: ctx.hashIds.encodeId(m.root_id), created_at: m.created_at, is_deleted: m.is_deleted, - approx_source: m.approx_source + approx_source: m.approx_source, + uuid: m.uuid, + parent_uid: m.parent_uid, + root_uid: m.root_uid } }) const userMap: Record = { @@ -335,7 +396,6 @@ export class MarkService { } public async getMarkList(ctx: ContextManager, page: number, size: number): Promise { - const markRepo = this.markRepo const markTypeMap: Record = { [markType.COMMENT]: 'comment', [markType.LINE]: 'mark', @@ -343,7 +403,7 @@ export class MarkService { [markType.ORIGIN_LINE]: 'mark', [markType.ORIGIN_COMMENT]: 'comment' } - const marks = await markRepo.listUserMark(ctx.getUserId(), page, size) + const marks = await this.markRepo.listUserMark(ctx.getUserId(), page, size) if (marks.length === 0) return [] const bookmarkIdList: number[] = [] @@ -388,8 +448,12 @@ export class MarkService { const [collectionCode, cbId] = mark.source_id.split('/') sourceId = `${collectionCode}/${ctx.hashIds.encodeId(parseInt(cbId))}` } + + const metadata = mark.metadata as markMetadata + res.push({ id: ctx.hashIds.encodeId(mark.id), + uuid: mark.uuid, type: markTypeMap[mark.type as markType], content: JSON.parse(mark.content) as markSelectContent[], created_at: mark.created_at, @@ -400,7 +464,9 @@ export class MarkService { comment: mark.comment, source_type: mark.source_type as 'share' | 'bookmark', source_id: sourceId, - approx_source: JSON.parse(mark.approx_source) + approx_source: JSON.parse(mark.approx_source), + parent_uid: metadata?.parent_id ?? undefined, + root_uid: metadata?.root_id ?? undefined }) } catch (e) { console.log(`get mark list failed: ${e}, content: ${mark.content}`) diff --git a/src/domain/orchestrator/bookmark.ts b/src/domain/orchestrator/bookmark.ts index 59a8a6a..78fe8ef 100644 --- a/src/domain/orchestrator/bookmark.ts +++ b/src/domain/orchestrator/bookmark.ts @@ -19,7 +19,7 @@ export class BookmarkOrchestrator { if (!res || !res.bookmark) throw BookmarkNotFoundError() if (res.bookmark.private_user > 0 && res.bookmark.private_user !== userId) throw BookmarkNotFoundError() - const marksResult = await this.markService.getBookmarkMarkList(ctx, res.id, true) + const marksResult = await this.markService.getBookmarkMarkList(ctx, { id: res.id, isShowMarks: true }) return marksResult } @@ -29,7 +29,7 @@ export class BookmarkOrchestrator { if (res.bookmark.private_user > 0 && res.bookmark.private_user !== ctx.getUserId()) throw BookmarkNotFoundError() const [marksResult, overviewResult, tagsResult] = await Promise.allSettled([ - this.markService.getBookmarkMarkList(ctx, res.id, true), + this.markService.getBookmarkMarkList(ctx, { id: res.id, isShowMarks: true }), this.bookmarkService.getUserBookmarkOverview(ctx.getUserId(), bmId), this.tagService.getBookmarkTags(ctx, ctx.getUserId(), bmId) ]) @@ -59,7 +59,7 @@ export class BookmarkOrchestrator { const [contentResult, marksResult, tagsResult, overviewResult] = await Promise.allSettled([ this.bookmarkService.getBookmarkContent(res.bookmark.content_key), - this.markService.getBookmarkMarkList(ctx, res.id, true), + this.markService.getBookmarkMarkList(ctx, { id: res.id, isShowMarks: true }), this.tagService.getBookmarkTags(ctx, userId, bmId), this.bookmarkService.getUserBookmarkOverview(userId, bmId) ]) diff --git a/src/domain/orchestrator/share.ts b/src/domain/orchestrator/share.ts index 9a4fae5..289317b 100644 --- a/src/domain/orchestrator/share.ts +++ b/src/domain/orchestrator/share.ts @@ -47,7 +47,7 @@ export class ShareOrchestrator { const [userInfo, bookmark, marks] = await Promise.all([ this.userService.getUserBriefInfo(share.show_userinfo, share.user_id), this.bookmarkService.getBookmarkById(share.bookmark_id), - this.markService.getBookmarkMarkList(ctx, userBm.id, share.show_comment && share.show_line) + this.markService.getBookmarkMarkList(ctx, { id: userBm.id, isShowMarks: share.show_comment && share.show_line }) ]) if (!bookmark) throw BookmarkNotFoundError() @@ -111,6 +111,6 @@ export class ShareOrchestrator { const userBm = await this.bookmarkService.getUserBookmark(share.bookmark_id, share.user_id) if (!userBm) return { mark_list: [], user_list: [] } - return await this.markService.getBookmarkMarkList(ctx, userBm.id, share.show_comment && share.show_line) + return await this.markService.getBookmarkMarkList(ctx, { id: userBm.id, isShowMarks: share.show_comment && share.show_line }) } } diff --git a/src/handler/http/markController.ts b/src/handler/http/markController.ts index 8ddf70c..aa1e01d 100644 --- a/src/handler/http/markController.ts +++ b/src/handler/http/markController.ts @@ -1,6 +1,6 @@ import { ErrorMarkTypeError, ErrorParam } from '../../const/err' import { markType } from '../../infra/repository/dbMark' -import { markRequest } from '../../domain/mark' +import { markRequest, markIdParams } from '../../domain/mark' import { RequestUtils } from '../../utils/requestUtils' import { Failed, Successed } from '../../utils/responseUtils' import { Controller } from '../../decorators/controller' @@ -23,7 +23,7 @@ export class MarkController { @Post('/create') public async createMark(ctx: ContextManager, request: Request) { const req = await RequestUtils.json(request) - if (!req || !req.source || (!req.bm_id && !req.share_code && !req.collection_code && !req.cb_id)) { + if (!req || !req.source || (!req.bm_id && !req.bookmark_uid && !req.share_code && !req.collection_code && !req.cb_id)) { return Failed(ErrorParam()) } @@ -34,14 +34,16 @@ export class MarkController { const sourceType = typeof req.source if (req.type === markType.LINE && (req.comment || sourceType !== 'object')) return Failed(ErrorMarkTypeError()) if (req.type === markType.COMMENT && (!req.comment || req.comment.length < 1 || sourceType !== 'object')) return Failed(ErrorMarkTypeError()) - if (req.type === markType.REPLY && (!req.comment || req.comment.length < 1)) return Failed(ErrorMarkTypeError()) + if (req.type === markType.REPLY && (!req.comment || req.comment.length < 1 || (!req.parent_id && !req.parent_uid))) return Failed(ErrorMarkTypeError()) if ([markType.ORIGIN_COMMENT, markType.ORIGIN_LINE].includes(req.type) && !req.approx_source) return Failed(ErrorMarkTypeError()) const createResult = await this.markOrchestrator.createMark(ctx, req) return Successed({ mark_id: createResult.id, - root_id: createResult.root_id + root_id: createResult.root_id, + mark_uid: createResult.uuid, + root_uid: createResult.root_uid }) } @@ -50,11 +52,17 @@ export class MarkController { */ @Post('/delete') public async deleteMark(ctx: ContextManager, request: Request) { - const req = await RequestUtils.json<{ mark_id: number }>(request) - if (!req || !req.mark_id) { + const req = await RequestUtils.json<{ mark_id?: number; mark_uid?: string }>(request) + if (!req || (!req.mark_id && !req.mark_uid)) { return Failed(ErrorParam()) } - const deleteResult = await this.markService.deleteMark(ctx, ctx.hashIds.decodeId(req.mark_id)) + let params: markIdParams + if (req.mark_uid) { + params = { uuid: req.mark_uid } + } else { + params = { id: ctx.hashIds.decodeId(req.mark_id!) } + } + const deleteResult = await this.markService.deleteMark(ctx, params) return Successed(deleteResult) } diff --git a/src/infra/repository/dbMark.ts b/src/infra/repository/dbMark.ts index 692b554..205777f 100644 --- a/src/infra/repository/dbMark.ts +++ b/src/infra/repository/dbMark.ts @@ -1,9 +1,10 @@ -import { markSelectContent } from '../../domain/mark' +import { markSelectContent, markMetadata } from '../../domain/mark' import { inject, injectable } from '../../decorators/di' import { PRISIMA_CLIENT, PRISIMA_HYPERDRIVE_CLIENT } from '../../const/symbol' import type { LazyInstance } from '../../decorators/lazy' import { PrismaClient as HyperdrivePrismaClient } from '@prisma/hyperdrive-client' import { PrismaClient } from '@prisma/client' +import { JsonValue } from '@prisma/client/runtime/client' export enum markType { LINE = 1, @@ -63,6 +64,8 @@ export interface markPOWithId { is_deleted: boolean created_at: Date updated_at: Date + uuid: string + metadata: JsonValue // { parent_id?: string; root_id?: string } } export interface markDetailPO { @@ -77,6 +80,9 @@ export interface markDetailPO { is_deleted: boolean parent_id: number root_id: number + uuid: string + parent_uid?: string + root_uid?: string } @injectable() @@ -116,8 +122,10 @@ export class MarkRepo { } }) ).map(item => { + const metadata = item.metadata as markMetadata return { id: item.id, + uuid: item.uuid, user_id: item.is_deleted ? 0 : item.user_id, user_bookmark_id: item.bookmark_id, type: item.type, @@ -128,6 +136,8 @@ export class MarkRepo { is_deleted: item.is_deleted, parent_id: item.parent_id, root_id: item.root_id, + parent_uid: metadata?.parent_id ?? undefined, + root_uid: metadata?.root_id ?? undefined, approx_source: JSON.parse(item.approx_source.length > 0 ? item.approx_source : '{}') } }) @@ -136,8 +146,12 @@ export class MarkRepo { async get(id: number): Promise { const res = await this.prismaPg().sr_bookmark_comment.findFirst({ where: { id } }) if (!res) return null + + const metadata = res.metadata as markMetadata + return { id: res.id, + uuid: res.uuid, user_id: res.is_deleted ? 0 : res.user_id, user_bookmark_id: res.bookmark_id, type: res.type, @@ -147,7 +161,33 @@ export class MarkRepo { updated_at: res.updated_at, is_deleted: res.is_deleted, parent_id: res.parent_id, - root_id: res.root_id + root_id: res.root_id, + parent_uid: metadata?.parent_id ?? undefined, + root_uid: metadata?.root_id ?? undefined + } + } + + async getByUuid(uuid: string): Promise { + const res = await this.prismaPg().sr_bookmark_comment.findFirst({ where: { uuid } }) + if (!res) return null + + const metadata = res.metadata as markMetadata + + return { + id: res.id, + uuid: res.uuid, + user_id: res.is_deleted ? 0 : res.user_id, + user_bookmark_id: res.bookmark_id, + type: res.type, + source: JSON.parse(res.source), + comment: res.is_deleted ? '' : res.comment, + created_at: res.created_at, + updated_at: res.updated_at, + is_deleted: res.is_deleted, + parent_id: res.parent_id, + root_id: res.root_id, + parent_uid: metadata?.parent_id ?? undefined, + root_uid: metadata?.root_id ?? undefined } } @@ -155,10 +195,20 @@ export class MarkRepo { return await this.prismaPg().sr_bookmark_comment.delete({ where: { id } }) } + async delByUid(uid: string) { + return await this.prismaPg().sr_bookmark_comment.delete({ where: { uuid: uid } }) + } + async deleteByRootId(bookmarkId: number, rootId: number) { return await this.prismaPg().sr_bookmark_comment.deleteMany({ where: { bookmark_id: bookmarkId, root_id: rootId } }) } + async deleteByRootUid(bookmarkId: number, rootUid: string) { + const rootMark = await this.getByUuid(rootUid) + if (!rootMark) return + return await this.prismaPg().sr_bookmark_comment.deleteMany({ where: { bookmark_id: bookmarkId, root_id: rootMark.id } }) + } + async existsCommentMarkChild(bookmarkId: number, rootId: number) { const res = await this.prismaPg().sr_bookmark_comment.count({ where: { @@ -173,14 +223,30 @@ export class MarkRepo { return Number(res || 0) } + async existsCommentMarkChildByRootUid(bookmarkId: number, rootUid: string) { + const rootMark = await this.getByUuid(rootUid) + if (!rootMark) return 0 + return await this.existsCommentMarkChild(bookmarkId, rootMark.id) + } + async updateCommentMarkDeleted(id: number) { return await this.prismaPg().sr_bookmark_comment.update({ where: { id }, data: { is_deleted: true, updated_at: new Date() } }) } + async updateCommentMarkDeletedByUid(uid: string) { + return await this.prismaPg().sr_bookmark_comment.update({ where: { uuid: uid }, data: { is_deleted: true, updated_at: new Date() } }) + } + async updateCommentRootId(id: number, rootId: number) { return await this.prismaPg().sr_bookmark_comment.update({ where: { id }, data: { root_id: rootId, updated_at: new Date() } }) } + async updateCommentRootUid(uid: string, rootUid: string) { + const rootMark = await this.getByUuid(rootUid) + if (!rootMark) return + return await this.prismaPg().sr_bookmark_comment.update({ where: { uuid: uid }, data: { root_id: rootMark.id, updated_at: new Date() } }) + } + async deleteByBookmarkId(bookmarkId: number) { return await this.prismaPg().sr_bookmark_comment.deleteMany({ where: { bookmark_id: bookmarkId } }) }