Skip to content

Commit a4eaf14

Browse files
feat: Enhance activity tracking with new models for activity logs and document visits, and implement search functionality for user documents.
1 parent c7eebf0 commit a4eaf14

73 files changed

Lines changed: 8412 additions & 561 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/launch.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"version": "0.0.1",
3+
"configurations": [
4+
{
5+
"name": "DocStudio Web (Next.js)",
6+
"runtimeExecutable": "pnpm",
7+
"runtimeArgs": ["dev:web"],
8+
"port": 3000,
9+
"autoPort": true
10+
},
11+
{
12+
"name": "DocStudio API (NestJS)",
13+
"runtimeExecutable": "pnpm",
14+
"runtimeArgs": ["dev:api"],
15+
"port": 3001
16+
}
17+
]
18+
}

.claude/settings.local.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(pnpm:*)",
5+
"Bash(ls /Users/jasonchen/Desktop/self/doc_studio/apps/web/src/app/\\\\\\(main\\\\\\)/spaces/[id]/documents/[documentId\\\\\\)/)",
6+
"Bash(ls /Users/jasonchen/Desktop/self/doc_studio/apps/web/src/app/\\\\\\(main\\\\\\)/spaces/[id]/documents/[documentId]/)",
7+
"Bash(cat \"/Users/jasonchen/Desktop/self/doc_studio/apps/web/src/app/\\(main\\)/spaces/[id]/documents/[documentId]/page.tsx\")",
8+
"Bash(npx tsc:*)",
9+
"Bash(ls /Users/jasonchen/Desktop/self/doc_studio/node_modules/@hocuspocus/server/dist/*.d.ts)",
10+
"Bash(find /Users/jasonchen/Desktop/self/doc_studio/node_modules/@hocuspocus/server -name *.d.ts -o -name *.d.mts)",
11+
"Bash(find /Users/jasonchen/Desktop/self/doc_studio/node_modules/@hocuspocus -maxdepth 3 -name *.js -o -name *.mjs)",
12+
"Bash(ls /Users/jasonchen/Desktop/self/doc_studio/apps/web/src/app/\\\\\\(main\\\\\\)/spaces/[id]/)",
13+
"mcp__Claude_Preview__preview_start",
14+
"Read(//Users/jasonchen/.claude/plugins/cache/ui-ux-pro-max-skill/ui-ux-pro-max/2.0.1/**)",
15+
"Bash(python3 skills/ui-ux-pro-max/scripts/search.py \"productivity documentation SaaS workspace dashboard modern minimal\" --design-system -p \"DocStudio\")",
16+
"Bash(python3 src/ui-ux-pro-max/scripts/search.py \"productivity documentation SaaS workspace modern\" --design-system -p \"DocStudio\")",
17+
"WebFetch(domain:www.reactbits.dev)"
18+
]
19+
}
20+
}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
node_modules/
33
.pnpm-store/
44

5+
.claude/
6+
57
# Build outputs
68
dist/
79
build/

apps/api/eslint.config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export default tseslint.config(
2929
'@typescript-eslint/no-explicit-any': 'off',
3030
'@typescript-eslint/no-floating-promises': 'warn',
3131
'@typescript-eslint/no-unsafe-argument': 'warn',
32+
'@typescript-eslint/no-unsafe-assignment': 'warn',
33+
'@typescript-eslint/no-unsafe-call': 'warn',
34+
'@typescript-eslint/no-unsafe-member-access': 'warn',
35+
'@typescript-eslint/no-unsafe-return': 'warn',
36+
'@typescript-eslint/no-redundant-type-constituents': 'warn',
3237
"prettier/prettier": ["error", { endOfLine: "auto" }],
3338
},
3439
},
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
-- CreateEnum
2+
CREATE TYPE "ActivityAction" AS ENUM ('CREATE', 'UPDATE', 'DELETE', 'VIEW', 'MOVE', 'RESTORE', 'SHARE', 'JOIN', 'LEAVE', 'INVITE', 'ROLE_CHANGE');
3+
4+
-- CreateEnum
5+
CREATE TYPE "EntityType" AS ENUM ('DOCUMENT', 'SPACE', 'SNAPSHOT', 'SHARE_LINK', 'MEMBER');
6+
7+
-- CreateTable
8+
CREATE TABLE "activity_logs" (
9+
"id" TEXT NOT NULL,
10+
"userId" TEXT NOT NULL,
11+
"action" "ActivityAction" NOT NULL,
12+
"entityType" "EntityType" NOT NULL,
13+
"entityId" TEXT NOT NULL,
14+
"entityName" TEXT,
15+
"spaceId" TEXT,
16+
"spaceName" TEXT,
17+
"metadata" TEXT,
18+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
19+
20+
CONSTRAINT "activity_logs_pkey" PRIMARY KEY ("id")
21+
);
22+
23+
-- CreateTable
24+
CREATE TABLE "document_visits" (
25+
"id" TEXT NOT NULL,
26+
"userId" TEXT NOT NULL,
27+
"documentId" TEXT NOT NULL,
28+
"spaceId" TEXT NOT NULL,
29+
"visitCount" INTEGER NOT NULL DEFAULT 1,
30+
"lastVisitAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
31+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
32+
33+
CONSTRAINT "document_visits_pkey" PRIMARY KEY ("id")
34+
);
35+
36+
-- CreateIndex
37+
CREATE INDEX "activity_logs_userId_createdAt_idx" ON "activity_logs"("userId", "createdAt" DESC);
38+
39+
-- CreateIndex
40+
CREATE INDEX "activity_logs_spaceId_createdAt_idx" ON "activity_logs"("spaceId", "createdAt" DESC);
41+
42+
-- CreateIndex
43+
CREATE INDEX "activity_logs_entityType_entityId_idx" ON "activity_logs"("entityType", "entityId");
44+
45+
-- CreateIndex
46+
CREATE INDEX "document_visits_userId_lastVisitAt_idx" ON "document_visits"("userId", "lastVisitAt" DESC);
47+
48+
-- CreateIndex
49+
CREATE INDEX "document_visits_spaceId_idx" ON "document_visits"("spaceId");
50+
51+
-- CreateIndex
52+
CREATE UNIQUE INDEX "document_visits_userId_documentId_key" ON "document_visits"("userId", "documentId");
53+
54+
-- AddForeignKey
55+
ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
56+
57+
-- AddForeignKey
58+
ALTER TABLE "document_visits" ADD CONSTRAINT "document_visits_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
59+
60+
-- AddForeignKey
61+
ALTER TABLE "document_visits" ADD CONSTRAINT "document_visits_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "documents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
-- CreateEnum
2+
CREATE TYPE "TemplateScope" AS ENUM ('SYSTEM', 'SPACE', 'USER');
3+
4+
-- CreateEnum
5+
CREATE TYPE "TemplateCategory" AS ENUM ('MEETING', 'TECH', 'REPORT', 'REQUIREMENT', 'GUIDE', 'OTHER');
6+
7+
-- CreateTable
8+
CREATE TABLE "document_templates" (
9+
"id" TEXT NOT NULL,
10+
"name" TEXT NOT NULL,
11+
"description" TEXT,
12+
"content" TEXT NOT NULL,
13+
"icon" TEXT NOT NULL DEFAULT '📄',
14+
"category" "TemplateCategory" NOT NULL DEFAULT 'OTHER',
15+
"scope" "TemplateScope" NOT NULL DEFAULT 'SYSTEM',
16+
"spaceId" TEXT,
17+
"createdBy" TEXT NOT NULL,
18+
"sortOrder" INTEGER NOT NULL DEFAULT 0,
19+
"isActive" BOOLEAN NOT NULL DEFAULT true,
20+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
21+
"updatedAt" TIMESTAMP(3) NOT NULL,
22+
23+
CONSTRAINT "document_templates_pkey" PRIMARY KEY ("id")
24+
);
25+
26+
-- CreateIndex
27+
CREATE INDEX "document_templates_scope_category_idx" ON "document_templates"("scope", "category");
28+
29+
-- CreateIndex
30+
CREATE INDEX "document_templates_spaceId_idx" ON "document_templates"("spaceId");
31+
32+
-- CreateIndex
33+
CREATE INDEX "document_templates_createdBy_idx" ON "document_templates"("createdBy");
34+
35+
-- AddForeignKey
36+
ALTER TABLE "document_templates" ADD CONSTRAINT "document_templates_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
37+
38+
-- AddForeignKey
39+
ALTER TABLE "document_templates" ADD CONSTRAINT "document_templates_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;

apps/api/prisma/schema.prisma

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ model User {
2828
createdDocuments Document[] @relation("DocumentCreator")
2929
invitationsSent SpaceInvitation[] @relation("InvitationSender")
3030
snapshots DocumentSnapshot[]
31+
activityLogs ActivityLog[]
32+
documentVisits DocumentVisit[]
33+
templates DocumentTemplate[]
3134
3235
@@map("users")
3336
}
@@ -47,6 +50,7 @@ model Space {
4750
documents Document[]
4851
permissions SpacePermission[]
4952
invitations SpaceInvitation[]
53+
templates DocumentTemplate[]
5054
5155
@@index([ownerId])
5256
@@index([isPublic])
@@ -75,6 +79,7 @@ model Document {
7579
creator User @relation("DocumentCreator", fields: [createdBy], references: [id])
7680
shareLinks ShareLink[]
7781
snapshots DocumentSnapshot[]
82+
visits DocumentVisit[]
7883
7984
@@index([spaceId])
8085
@@index([parentId])
@@ -169,6 +174,72 @@ enum Permission {
169174
WRITE // 可写
170175
}
171176

177+
// ==================== 活动日志模型 ====================
178+
model ActivityLog {
179+
id String @id @default(cuid())
180+
userId String
181+
action ActivityAction
182+
entityType EntityType
183+
entityId String
184+
entityName String? // 冗余存储实体名称,避免关联查询(实体删除后仍可显示)
185+
spaceId String? // 冗余存储,方便按空间查询
186+
spaceName String? // 冗余存储空间名称
187+
metadata String? @db.Text // JSON 额外信息(如旧标题、变更字段等)
188+
createdAt DateTime @default(now())
189+
190+
// 关系
191+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
192+
193+
@@index([userId, createdAt(sort: Desc)])
194+
@@index([spaceId, createdAt(sort: Desc)])
195+
@@index([entityType, entityId])
196+
@@map("activity_logs")
197+
}
198+
199+
// 活动动作枚举
200+
enum ActivityAction {
201+
CREATE
202+
UPDATE
203+
DELETE
204+
VIEW
205+
MOVE
206+
RESTORE
207+
SHARE
208+
JOIN
209+
LEAVE
210+
INVITE
211+
ROLE_CHANGE
212+
}
213+
214+
// 实体类型枚举
215+
enum EntityType {
216+
DOCUMENT
217+
SPACE
218+
SNAPSHOT
219+
SHARE_LINK
220+
MEMBER
221+
}
222+
223+
// ==================== 最近访问模型(独立优化表) ====================
224+
model DocumentVisit {
225+
id String @id @default(cuid())
226+
userId String
227+
documentId String
228+
spaceId String // 冗余存储,方便查询
229+
visitCount Int @default(1)
230+
lastVisitAt DateTime @default(now())
231+
createdAt DateTime @default(now())
232+
233+
// 关系
234+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
235+
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
236+
237+
@@unique([userId, documentId])
238+
@@index([userId, lastVisitAt(sort: Desc)])
239+
@@index([spaceId])
240+
@@map("document_visits")
241+
}
242+
172243
// ==================== 版本历史快照模型 ====================
173244
model DocumentSnapshot {
174245
id String @id @default(cuid())
@@ -184,3 +255,43 @@ model DocumentSnapshot {
184255
@@index([docId, createdAt])
185256
@@map("document_snapshots")
186257
}
258+
259+
// ==================== 文档模板模型 ====================
260+
model DocumentTemplate {
261+
id String @id @default(cuid())
262+
name String
263+
description String?
264+
content String @db.Text
265+
icon String @default("📄")
266+
category TemplateCategory @default(OTHER)
267+
scope TemplateScope @default(SYSTEM)
268+
spaceId String?
269+
createdBy String
270+
sortOrder Int @default(0)
271+
isActive Boolean @default(true)
272+
createdAt DateTime @default(now())
273+
updatedAt DateTime @updatedAt
274+
275+
creator User @relation(fields: [createdBy], references: [id])
276+
space Space? @relation(fields: [spaceId], references: [id], onDelete: Cascade)
277+
278+
@@index([scope, category])
279+
@@index([spaceId])
280+
@@index([createdBy])
281+
@@map("document_templates")
282+
}
283+
284+
enum TemplateScope {
285+
SYSTEM
286+
SPACE
287+
USER
288+
}
289+
290+
enum TemplateCategory {
291+
MEETING
292+
TECH
293+
REPORT
294+
REQUIREMENT
295+
GUIDE
296+
OTHER
297+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {
2+
Controller,
3+
Get,
4+
Param,
5+
Query,
6+
UseGuards,
7+
Req,
8+
} from '@nestjs/common';
9+
import { ActivityService } from './activity.service';
10+
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
11+
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
12+
13+
@ApiTags('activity')
14+
@ApiBearerAuth()
15+
@UseGuards(JwtAuthGuard)
16+
@Controller('activity')
17+
export class ActivityController {
18+
constructor(private readonly activityService: ActivityService) {}
19+
20+
@Get('recent-documents')
21+
@ApiOperation({ summary: '获取当前用户最近访问的文档列表' })
22+
getRecentDocuments(
23+
@Req() req: any,
24+
@Query('limit') limit?: string,
25+
@Query('page') page?: string,
26+
) {
27+
const l = limit ? parseInt(limit, 10) : 20;
28+
const p = page ? parseInt(page, 10) : 1;
29+
return this.activityService.getRecentDocuments(
30+
req.user.id,
31+
l,
32+
(p - 1) * l,
33+
);
34+
}
35+
36+
@Get('my')
37+
@ApiOperation({ summary: '获取当前用户的活动流' })
38+
getMyActivity(
39+
@Req() req: any,
40+
@Query('page') page?: string,
41+
@Query('limit') limit?: string,
42+
) {
43+
return this.activityService.getMyActivity(
44+
req.user.id,
45+
page ? parseInt(page, 10) : 1,
46+
limit ? parseInt(limit, 10) : 30,
47+
);
48+
}
49+
50+
@Get('space/:spaceId')
51+
@ApiOperation({ summary: '获取空间内的活动流' })
52+
getSpaceActivity(
53+
@Param('spaceId') spaceId: string,
54+
@Query('page') page?: string,
55+
@Query('limit') limit?: string,
56+
) {
57+
return this.activityService.getSpaceActivity(
58+
spaceId,
59+
page ? parseInt(page, 10) : 1,
60+
limit ? parseInt(limit, 10) : 30,
61+
);
62+
}
63+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Global, Module } from '@nestjs/common';
2+
import { ActivityService } from './activity.service';
3+
import { ActivityController } from './activity.controller';
4+
5+
@Global() // 全局模块,让所有模块都可以注入 ActivityService
6+
@Module({
7+
controllers: [ActivityController],
8+
providers: [ActivityService],
9+
exports: [ActivityService],
10+
})
11+
export class ActivityModule {}

0 commit comments

Comments
 (0)