diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..d288680 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Read(./.env.example)" + ], + "deny": [ + "Read(./.env)", + "Read(./.env.development)", + "Read(./.env.production)" + ] + } +} \ No newline at end of file diff --git a/.cloud-dev/Dockerfile b/.cloud-dev/Dockerfile index ccb810d..ff44178 100644 --- a/.cloud-dev/Dockerfile +++ b/.cloud-dev/Dockerfile @@ -44,9 +44,11 @@ RUN apt-get update && apt-get install -y \ cmake \ telnet \ redis-tools \ + iputils-ping \ potrace \ imagemagick \ zsh \ + vim \ && rm -rf /var/lib/apt/lists/* # 安装 Python 3.12 @@ -108,11 +110,11 @@ RUN mkdir -p /var/run/sshd && \ RUN code-server --install-extension ms-ceintl.vscode-language-pack-zh-hans \ && code-server --install-extension bierner.markdown-mermaid \ && code-server --install-extension ms-python.python \ - && code-server --install-extension rooveterinaryinc.roo-cline \ && code-server --install-extension dbaeumer.vscode-eslint \ && code-server --install-extension prisma.prisma \ && code-server --install-extension ecmel.vscode-html-css \ - && code-server --install-extension cweijan.vscode-redis-client + && code-server --install-extension cweijan.vscode-redis-client \ + && code-server --install-extension anthropic.claude-code # 配置 code-server (密码将在启动时设置) RUN mkdir -p /root/.config/code-server && \ diff --git a/.cloud-dev/README.md b/.cloud-dev/README.md index e4d0012..534e43c 100644 --- a/.cloud-dev/README.md +++ b/.cloud-dev/README.md @@ -29,6 +29,7 @@ - **cmake**: 构建工具 - **telnet**: 网络调试 - **redis-tools**: Redis 命令行工具 +- **ping**: 网络连通性测试 - **potrace**: 位图转矢量图 - **imagemagick**: 图像处理工具 - **uv**: 快速 Python 包管理器 diff --git a/.cloud-dev/docker-compose.yml b/.cloud-dev/docker-compose.yml index ca25a48..541448d 100644 --- a/.cloud-dev/docker-compose.yml +++ b/.cloud-dev/docker-compose.yml @@ -11,14 +11,18 @@ services: - "7681:7681" # ttyd (Web Terminal) - "3000:3000" # Next.js Dev Server volumes: - # 项目代码映射(使用 cached 模式提高性能) + # 项目代码映射(如果映射到Windows/Mac宿主机的本地目录,使用 cached 模式提高性能) - ../:/workspace:cached - # node_modules 使用命名卷以提高性能 - - node_modules:/workspace/node_modules - # pnpm store 缓存 - - pnpm_store:/root/.local/share/pnpm/store - # Git 配置(可选,如果需要保留 Git 配置) - - ~/.gitconfig:/root/.gitconfig:ro + # Code Server 配置(包含 config.yaml 和密码) + - code-server-config:/root/.config/code-server + # Code Server 数据(插件、用户设置、扩展数据) + - code-server-data:/root/.local/share/code-server + # Claude Code 配置和保存数据 + - claude:/root/.claude + # Claude Code Router 配置和保存数据 + - claude-code-router:/root/.claude-code-router + # SSH 配置 + - ssh:/root/.ssh environment: - NODE_ENV=development - TZ=Asia/Shanghai diff --git a/.env.example b/.env.example index 9baef90..8e49704 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,7 @@ POSTGRESQL_PASSWORD= POSTGRESQL_PORT= DATABASE_URL= +REDIS_HOST= REDIS_PORT= REDIS_PASSWORD= @@ -40,10 +41,12 @@ MINIO_BUCKET= # 应用相关 SUPER_ADMIN_PASSWORD= +USER_DEFAULT_PASSWORD= +## 数据库批次操作默认并发数 +DB_PARALLEL_LIMIT = # NextAuth.js Configuration NEXTAUTH_SECRET= -NEXTAUTH_URL= PKUAI_API_KEY= PKUAI_API_BASE= @@ -52,7 +55,9 @@ PKUAI_API_BASE= # 仅在开发环境加载(写在.env.development中) +PORT= NEXT_PUBLIC_DEV_TERMINAL_DEFAULT_PORT= +NEXT_PUBLIC_DEV_TERMINAL_URL= DEV_TERMINAL= diff --git a/.roo/mcp.json b/.roo/mcp.json deleted file mode 100644 index 7a61061..0000000 --- a/.roo/mcp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mcpServers":{ - "ai-elements": { - "command": "npx", - "args": [ - "-y", - "mcp-remote", - "https://registry.ai-sdk.dev/api/mcp" - ] - } - } -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..42e6293 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "prisma.pinToPrisma6": true +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f073e3a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,94 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目说明 +本项目模板(Hair Keeper v1.1.0)是一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑。 + +Hair Keeper是个诙谐有趣的名称,和项目内容毫无关系。 + +开发者直接在本项目模板的基础上进行开发,本项目源代码完全对开发者可见并可以随时修改、扩展功能、增加新的组件和模块,开发者尽量遵从如下文表述的约定和项目文件组织规则。 + +## 主要依赖库 +本项目使用pnpm作为包管理器 +- 基础:next + react + trpc + prisma +- UI基础框架:tailwindcss + radix-ui(基于shadcn/ui库添加组件) + lucide-react + sonner(toast) +- 图表等高级UI:recharts(图表) + xyflow/react(节点图 dagre自动布局) + embla-carousel-react + dnd-kit/sortable +- 用户交互增强:motion(动画) + framer-motion(动画) + use-gesture/react(手势) +- Headless UI:react-hook-form + tanstack/react-table + headless-tree/react +- 数据和存储:pg(PostgreSQL) + ioredis + minio +- 后台任务及消息队列:bullmq +- AI大模型交互: ai + ai-sdk/react + ai-elements(基于shadcn/ui库添加组件) +- 辅助性库:lodash + zod + date-fns + nanoid + zustand + p-limit +- 其他:next-auth + bcryptjs + nuqs + superjson(前后端序列化类型安全) + copy-to-clipboard + +## 项目约定 +### 前端 +- 可使用`pnpx shadcn@latest add`添加UI控件(会添加到components/ui中) +- 表单输入组件提供value、onChange和disabled三种属性的支持,可方便的集成到react-hook-form中 +- z-index约定:常规对话框及其遮罩z-50,表单控件的Popover为z-60,全屏预览及遮罩z-70 +- tailwindcss v4支持直接编写如 w-[10px] 这样的任意值,非必要不写style,支持样式类合并`import { cn } from "@/lib/utils"` +- 用`import { useCallbackRef } from "@/hooks/use-callback-ref"`这个钩子构建引用不变但逻辑总是最新的函数,解决闭包陷阱 + +### 后端 +- tRPC接口报错时可使用TRPCError,例如throw new TRPCError({ code: 'NOT_FOUND', message: '' }) +- server/trpc.ts中导出了createTRPCRouter(本质是t.router)用来创建路由,预定义了publicProcedure用于创建无权限限制、也不需要登录的api +- server/trpc.ts中预定义了permissionRequiredProcedure,用来创建限制特定权限访问的api,例如permissionRequiredProcedure(Permissions.USER_MANAGE);空字符串表示无权限要求,但是需要用户登录;约定用permissionRequiredProcedure('SUPER_ADMIN_ONLY')限制超级管理员才能访问,该权限不在Permissions中定义,只有超级管理员才能绕过授权限制访问所有接口,因此SUPER_ADMIN_ONLY这个字符串只是一个通用的约定。 +- 数据库批次操作时,使用`const dbParallelLimit = pLimit(parseInt(process.env.DB_PARALLEL_LIMIT || '16', 10))`控制最大并发数 + +### 数据和存储 +- Prisma 生成的客户端输出为默认路径,导入时使用`@prisma/client`,可以从中导入定义的数据表的ts类型,例如`import type { User } from '@prisma/client'` +- 数据库连接使用 `server/db.ts` 中的全局单例 `db`,不要直接实例化 PrismaClient +- 时间字段统一使用 `@db.Timestamptz` 类型 +- 前后端参数传递尽量使用扁平结构而非嵌套结构 +- 文件的上传和下载采用“客户端直传”架构(基于MinIO),服务器端只负责授权和生成预签名URL + +### 开发模式 +为了方便开发,本项目模板内置了在开发模式下可用的大量页面和功能 +- 仅在开发阶段使用的页面、布局、api等会被NextJS识别并处理的文件,以dev.tsx、dev.ts、dev.jsx、dev.js为后缀,不会被打包到生产环境 +- 仅在开发阶段使用的数据模型以Dev开头,对应的数据表以dev_开头 + +## 重要目录和文件 +### 前端 +- `components/common/`:进一步封装的高级通用控件,例如下拉菜单、对话框表单、响应式tabs等控件 +- `components/features/`:进一步封装的控件,通常更重或者与业务关联强需要在不同的页面中复用 +- `components/ai-elements/`:ai对话相关的组件 +- `components/data-details/`:专注于数据展示的可复用控件,例如detail-badge-list、detail-copyable、detail-list、detail-timeline等控件 +- `components/data-table/`:专注于数据表格的可复用控件,本项目模板自带了基础的data-table、过滤器、排序、分页、列可见性切换等功能 +- `components/icons/`:项目的自定义图标可以写在这个文件夹 +- `components/layout/`:应用的完整布局框架和导航系统以及可复用的布局容器 +- `components/ui/`:高度可定制可复用的基础UI组件,通常源自第三方库 +- `app/(main)/`:开发者在这里自行实现的所有业务的页面 +- `app/(main)/dev/`:辅助开发的页面,本项目模板在其中实现了许多功能,代码在实现业务时也可以借鉴参考 +- `app/(main)/settings/`:全局设置,由开发者根据业务需求进行补充和实现 +- `app/(main)/users/`:用户管理模块,提供用户CRUD、角色管理、批量授权等完整的用户管理功能的页面和组件实现 +- `hooks/`:可复用React Hooks库,部分复杂的组件也通过hook实现Headless UI逻辑与样式分离,组件中可复用的逻辑都可以放在这 +- `lib/trpc.ts`:创建并导出tRPC React客户端实例,用于前端与后端API通信 +- `lib/stores/`:通过zustand管理的全局的状态 + +### 后端 +- `server/routers/`:项目trpc api定义文件,开发者主要在这里定义和实现业务的后端API +- `server/routers/_app.ts`:`appRouter`根路由定义,需要添加子路由时在此处注册 +- `server/routers/common.ts`:定义需要在多个模块中复用的通用业务接口路由 +- `server/routers/jobs.ts`:tRPC任务进度订阅路由 +- `server/routers/selection.ts`:用于记录用户选择的选项或者输入的内容,优化用户的输入体验 +- `server/routers/global.ts`:系统全局和特定业务关联不大的一些api +- `server/routers/dev/`:开发模式下的辅助功能需要的trpc api +- `server/queues/`:消息队列和worker,通过其中的index.ts统一导出,任务状态更新采用trpc SSE subscription,接口定义在`server/routers/jobs.ts`中 +- `server/agents`:LLM的对接和使用 +- `server/service/`:服务层模块集合,封装后端业务逻辑和系统服务 +- `server/service/dev/`:开发模式下的辅助功能需要的后台服务 +- `server/utils/`:服务端专用工具函数库,为后端业务逻辑提供基础设施支持 +- `api/dev/`:开发模式下的辅助功能需要的api + +### 其他 +- `constants/`:项目全局常量管理 +- `constants/permissions.ts`:权限定义,支持前后端一致的权限控制,支持解析复杂的权限表达式(如"A&B|(C&D)") +- `lib/schema/`:集中管理数据验证schema,定义前后端统一的数据结构和验证规则,前端对默认值等其他要求写在表单组件中,后端对默认值等其他要求写在接口文件中,使用z.input而不是z.infer来获取Schema的输入类型 +- `lib/algorithom.ts`:通用计算机算法实现,例如拓扑排序 +- `lib/format.ts`:数据格式化工具函数库 + +## 非标准命令 +- `pnpm run dev:attach`:这会使用tmux在名为nextdev的session中启动pnpm run dev,便于在开发页面或其他地方与开发服务器交互 +- `pnpm run db:seed`:数据库种子数据,`prisma/init_data/`存放种子初始化数据 +- `pnpm run build:analyze`:打包项目并使用next/bundle-analyzer进行分析 diff --git a/README.md b/README.md index 9c61bbb..30493ce 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ## 项目说明 -本项目模板(Hair Keeper v1.0.0)是一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑。 +本项目模板(Hair Keeper v1.1.0)是一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑。 Hair Keeper是个诙谐有趣的名称,和项目内容毫无关系。 @@ -14,7 +14,7 @@ Hair Keeper是个诙谐有趣的名称,和项目内容毫无关系。 - 数据和存储:pg(PostgreSQL) + ioredis + minio - 后台任务及消息队列:bullmq - AI大模型交互: ai + ai-sdk/react + ai-elements(基于shadcn/ui库添加组件) -- 辅助性库:lodash + zod + date-fns + nanoid +- 辅助性库:lodash + zod + date-fns + nanoid + zustand + p-limit - 其他:next-auth + bcryptjs + nuqs + superjson(前后端序列化类型安全) + copy-to-clipboard ## 项目约定 @@ -27,11 +27,12 @@ Hair Keeper是个诙谐有趣的名称,和项目内容毫无关系。 ### 后端 - tRPC接口报错时可使用TRPCError,例如throw new TRPCError({ code: 'NOT_FOUND', message: '' }) -- server/trpc.ts中预定义了publicProcedure用于创建无权限限制、也不需要登录的api -- server/trpc.ts中预定义了publicProcedurepermissionRequiredProcedure,用来创建限制特定权限访问的路由,例如permissionRequiredProcedure(Permissions.USER_MANAGE);空字符串表示无权限要求,但是需要用户登录;约定用permissionRequiredProcedure('SUPER_ADMIN_ONLY')限制超级管理员才能访问,该权限不在Permissions中定义,只有超级管理员才能绕过授权限制访问所有接口,因此SUPER_ADMIN_ONLY这个字符串只是一个通用的约定。 +- server/trpc.ts中导出了createTRPCRouter(本质是t.router)用来创建路由,预定义了publicProcedure用于创建无权限限制、也不需要登录的api +- server/trpc.ts中预定义了permissionRequiredProcedure,用来创建限制特定权限访问的api,例如permissionRequiredProcedure(Permissions.USER_MANAGE);空字符串表示无权限要求,但是需要用户登录;约定用permissionRequiredProcedure('SUPER_ADMIN_ONLY')限制超级管理员才能访问,该权限不在Permissions中定义,只有超级管理员才能绕过授权限制访问所有接口,因此SUPER_ADMIN_ONLY这个字符串只是一个通用的约定。 +- 数据库批次操作时,使用`const dbParallelLimit = pLimit(parseInt(process.env.DB_PARALLEL_LIMIT || '16', 10))`控制最大并发数 ### 数据和存储 -- Prisma 生成的客户端输出为默认路径,导入时使用`@prisma/client` +- Prisma 生成的客户端输出为默认路径,导入时使用`@prisma/client`,可以从中导入定义的数据表的ts类型,例如`import type { User } from '@prisma/client'` - 数据库连接使用 `server/db.ts` 中的全局单例 `db`,不要直接实例化 PrismaClient - 时间字段统一使用 `@db.Timestamptz` 类型 - 前后端参数传递尽量使用扁平结构而非嵌套结构 @@ -58,6 +59,7 @@ Hair Keeper是个诙谐有趣的名称,和项目内容毫无关系。 - `app/(main)/users/`:用户管理模块,提供用户CRUD、角色管理、批量授权等完整的用户管理功能的页面和组件实现 - `hooks/`:可复用React Hooks库,部分复杂的组件也通过hook实现Headless UI逻辑与样式分离,组件中可复用的逻辑都可以放在这 - `lib/trpc.ts`:创建并导出tRPC React客户端实例,用于前端与后端API通信 +- `lib/stores/`:通过zustand管理的全局的状态 ### 后端 - `server/routers/`:项目trpc api定义文件,开发者主要在这里定义和实现业务的后端API @@ -77,6 +79,7 @@ Hair Keeper是个诙谐有趣的名称,和项目内容毫无关系。 ### 其他 - `constants/`:项目全局常量管理 - `constants/permissions.ts`:权限定义,支持前后端一致的权限控制,支持解析复杂的权限表达式(如"A&B|(C&D)") +- `constants/menu.ts`: 菜单管理工具库,提供菜单项定义、查询、权限过滤等功能 - `lib/schema/`:集中管理数据验证schema,定义前后端统一的数据结构和验证规则,前端对默认值等其他要求写在表单组件中,后端对默认值等其他要求写在接口文件中,使用z.input而不是z.infer来获取Schema的输入类型 - `lib/algorithom.ts`:通用计算机算法实现,例如拓扑排序 - `lib/format.ts`:数据格式化工具函数库 diff --git a/docker-compose.yml b/docker-compose.yml index 0fa09b9..267f2a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,8 @@ +name: hair-keeper + services: postgresql: image: bitnami/postgresql:17 # https://hub.docker.com/r/bitnami/postgresql - container_name: hair-keeper-dev-pg restart: always ports: - "${POSTGRESQL_PORT}:5432" @@ -10,21 +11,19 @@ services: POSTGRESQL_PASSWORD: ${POSTGRESQL_PASSWORD} POSTGRESQL_INITDB_ARGS: "--encoding=UTF-8 --locale=C" volumes: - - hair_keeper_postgresql_data:/bitnami/postgresql + - postgresql_data:/bitnami/postgresql redis: image: redis:8-alpine # https://hub.docker.com/_/redis - container_name: hair-keeper-dev-redis ports: - "${REDIS_PORT}:6379" command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes volumes: - - hair_keeper_redis_data:/data + - redis_data:/data restart: always minio: image: minio/minio:RELEASE.2025-07-23T15-54-02Z - container_name: hair-keeper-dev-minio ports: - "${MINIO_API_PORT}:9000" # API端口 - "${MINIO_CONSOLE_PORT}:9001" # Console端口 @@ -33,11 +32,11 @@ services: MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} MINIO_SERVER_URL: ${MINIO_SERVER_URL} volumes: - - hair_keeper_minio_data:/data + - minio_data:/data command: server /data --console-address ":9001" restart: always volumes: - hair_keeper_postgresql_data: - hair_keeper_redis_data: - hair_keeper_minio_data: + postgresql_data: + redis_data: + minio_data: diff --git a/package.json b/package.json index c544a63..894fcdd 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "hair-keeper", - "version": "0.1.0", + "version": "1.1.0", "private": true, "scripts": { "dev": "next dev -p 3000 --turbo", "dev:attach": "DEV_TERMINAL=nextdev;tmux new-session -A -s $DEV_TERMINAL\\; send-keys \"pnpm run dev\" ^M", "build": "next build", "start": "next start -p 3000", - "lint": "eslint", + "lint": "next lint && tsc --noEmit", "db:seed": "tsx prisma/seed.ts", "build:analyze": "ANALYZE=true next build" }, @@ -74,10 +74,11 @@ "minio": "^8.0.6", "motion": "^12.23.22", "nanoid": "^5.1.6", - "next": "~15.4.0", + "next": "~15.4.8", "next-auth": "^4.24.11", "next-themes": "^0.4.6", "nuqs": "^2.6.0", + "p-limit": "^7.2.0", "pg": "^8.16.3", "prism-react-renderer": "^2.4.1", "prisma": "^6.15.0", @@ -95,7 +96,8 @@ "tailwind-merge": "^3.3.1", "use-stick-to-bottom": "^1.1.1", "vaul": "^1.1.2", - "zod": "^4.1.9" + "zod": "^4.1.9", + "zustand": "^5.0.8" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -107,7 +109,6 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.5.2", - "react-icons": "^5.5.0", "react-live": "^4.1.8", "shadcn": "^3.5.0", "tailwindcss": "^4", diff --git a/prisma/migrations/20251118103418_add_cascade_delete_to_selection_log/migration.sql b/prisma/migrations/20251118103418_add_cascade_delete_to_selection_log/migration.sql new file mode 100644 index 0000000..0e8419e --- /dev/null +++ b/prisma/migrations/20251118103418_add_cascade_delete_to_selection_log/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "selection_log" DROP CONSTRAINT "selection_log_userId_fkey"; + +-- AddForeignKey +ALTER TABLE "selection_log" ADD CONSTRAINT "selection_log_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20251118105141_add_dept_admin_and_managed_dept_switch/migration.sql b/prisma/migrations/20251118105141_add_dept_admin_and_managed_dept_switch/migration.sql new file mode 100644 index 0000000..fb7fafd --- /dev/null +++ b/prisma/migrations/20251118105141_add_dept_admin_and_managed_dept_switch/migration.sql @@ -0,0 +1,26 @@ +-- AlterTable +ALTER TABLE "user" ADD COLUMN "current_managed_dept" VARCHAR(5); + +-- CreateTable +CREATE TABLE "dept_admin" ( + "id" SERIAL NOT NULL, + "uid" VARCHAR(30) NOT NULL, + "dept_code" VARCHAR(5) NOT NULL, + "admin_email" VARCHAR(100), + "admin_line_phone" VARCHAR(100), + "admin_mobile_phone" VARCHAR(100), + "note" VARCHAR(1000), + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL, + + CONSTRAINT "dept_admin_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "dept_admin_uid_dept_code_key" ON "dept_admin"("uid", "dept_code"); + +-- AddForeignKey +ALTER TABLE "dept_admin" ADD CONSTRAINT "dept_admin_uid_fkey" FOREIGN KEY ("uid") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "dept_admin" ADD CONSTRAINT "dept_admin_dept_code_fkey" FOREIGN KEY ("dept_code") REFERENCES "dept"("code") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8ed525a..71c2229 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -19,6 +19,7 @@ model User { name String? status String? // 在校/减离/NULL deptCode String? @map("dept_code") // 所属院系代码(外键) + currentManagedDept String? @map("current_managed_dept") @db.VarChar(5) // 当前正在管理的院系代码,用于支持院系管理员类型的角色 createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz password String @@ -29,6 +30,7 @@ model User { dept Dept? @relation(fields: [deptCode], references: [code]) roles Role[] // 多对多关联角色 selectionLogs SelectionLog[] // 选择日志 + deptAdmins DeptAdmin[] // 作为院系管理员的信息 @@map("user") } @@ -41,10 +43,31 @@ model Dept { // 关联 users User[] + deptAdmins DeptAdmin[] // 院系管理员 @@map("dept") } +// 院系管理员表 +model DeptAdmin { + id Int @id @default(autoincrement()) + uid String @db.VarChar(30) // 管理员用户ID(外键) + deptCode String @map("dept_code") @db.VarChar(5) // 院系代码(外键) + adminEmail String? @map("admin_email") @db.VarChar(100) // 管理员邮箱 + adminLinePhone String? @map("admin_line_phone") @db.VarChar(100) // 管理员座机 + adminMobilePhone String? @map("admin_mobile_phone") @db.VarChar(100) // 管理员手机 + note String? @db.VarChar(1000) // 备注 + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz + + // 关联 + user User @relation(fields: [uid], references: [id]) + dept Dept @relation(fields: [deptCode], references: [code]) + + @@unique([uid, deptCode], name: "uidx_uid_dept_code") + @@map("dept_admin") +} + // 角色表 model Role { id Int @id @default(autoincrement()) @@ -68,7 +91,7 @@ model Permission { model SelectionLog { id Int @id @default(autoincrement()) userId String // 关联到用户 - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) // 用于标识是哪个的选项,用.进行分隔,例如"user.filter.dept" context String diff --git a/prisma/seed.ts b/prisma/seed.ts index 3901899..07faba3 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -3,9 +3,13 @@ import bcrypt from 'bcryptjs' import { Permissions, ALL_PERMISSIONS } from '../src/constants/permissions' import fs from 'fs' import path from 'path' +import pLimit from 'p-limit' const prisma = new PrismaClient() +// 从环境变量获取并发限制,默认为16 +const dbParallelLimit = pLimit(parseInt(process.env.DB_PARALLEL_LIMIT || '16', 10)) + // 解析 JSON 文件并导入院系数据 async function importDepartments() { const jsonPath = path.join(__dirname, 'init_data', '院系.json') @@ -16,7 +20,7 @@ async function importDepartments() { await Promise.all( departments.map((dept: any) => { - return prisma.dept.upsert({ + return dbParallelLimit(() => prisma.dept.upsert({ where: { code: dept.id }, update: { name: dept.name, @@ -27,7 +31,7 @@ async function importDepartments() { name: dept.name, fullName: dept.full_name, }, - }) + })) }) ) console.log('院系数据导入完成') @@ -37,13 +41,15 @@ async function main() { console.log('开始数据库初始化...') // 插入权限 - for (const permName of ALL_PERMISSIONS) { - await prisma.permission.upsert({ - where: { name: permName }, - update: {}, - create: { name: permName }, + await Promise.all( + ALL_PERMISSIONS.map((permName) => { + return dbParallelLimit(() => prisma.permission.upsert({ + where: { name: permName }, + update: {}, + create: { name: permName }, + })) }) - } + ) // 角色与权限映射 const rolePermissionsMap: Record = { @@ -51,18 +57,20 @@ async function main() { } // 插入角色 - for (const [roleName, perms] of Object.entries(rolePermissionsMap)) { - await prisma.role.upsert({ - where: { name: roleName }, - update: {}, - create: { - name: roleName, - permissions: { - connect: perms.map((name) => ({ name })), + await Promise.all( + Object.entries(rolePermissionsMap).map(([roleName, perms]) => { + return dbParallelLimit(() => prisma.role.upsert({ + where: { name: roleName }, + update: {}, + create: { + name: roleName, + permissions: { + connect: perms.map((name) => ({ name })), + }, }, - }, + })) }) - } + ) await importDepartments() @@ -74,33 +82,37 @@ async function main() { { id: 'unknown', name: '未知用户', status: '在校', deptCode: '00001', roleNames: [] }, ] - for (const u of usersToCreate) { - const password = await bcrypt.hash(u.password ?? '123456', 12) - await prisma.user.upsert({ - where: { id: u.id }, - update: { - name: u.name, - status: u.status, - deptCode: u.deptCode, - password, - isSuperAdmin: u.isSuperAdmin ?? false, - roles: { - set: u.roleNames.map((name) => ({ name })), - }, - }, - create: { - id: u.id, - name: u.name, - status: u.status, - deptCode: u.deptCode, - password, - isSuperAdmin: u.isSuperAdmin ?? false, - roles: { - connect: u.roleNames.map((name) => ({ name })), - }, - }, + await Promise.all( + usersToCreate.map((u) => { + return dbParallelLimit(async () => { + const password = await bcrypt.hash(u.password || process.env.USER_DEFAULT_PASSWORD || 'jeep4ahxahx7ee7U', 12) + await prisma.user.upsert({ + where: { id: u.id }, + update: { + name: u.name, + status: u.status, + deptCode: u.deptCode, + password, + isSuperAdmin: u.isSuperAdmin ?? false, + roles: { + set: u.roleNames.map((name) => ({ name })), + }, + }, + create: { + id: u.id, + name: u.name, + status: u.status, + deptCode: u.deptCode, + password, + isSuperAdmin: u.isSuperAdmin ?? false, + roles: { + connect: u.roleNames.map((name) => ({ name })), + }, + }, + }) + }) }) - } + ) // 插入文件类型(仅开发环境) const fileTypes = [ @@ -125,18 +137,19 @@ async function main() { { id: 'OTHER', name: '其他', description: '无法归入以上分类的文件' }, ] - for (let index = 0; index < fileTypes.length; index++) { - const fileType = fileTypes[index] - await prisma.devFileType.upsert({ - where: { id: fileType.id }, - update: { - name: fileType.name, - description: fileType.description, - order: (index + 1) * 10, - }, - create: {...fileType, order: (index + 1) * 10}, + await Promise.all( + fileTypes.map((fileType, index) => { + return dbParallelLimit(() => prisma.devFileType.upsert({ + where: { id: fileType.id }, + update: { + name: fileType.name, + description: fileType.description, + order: (index + 1) * 10, + }, + create: {...fileType, order: (index + 1) * 10}, + })) }) - } + ) console.log('文件类型数据初始化完成') // 插入依赖包类型(仅开发环境) @@ -152,18 +165,19 @@ async function main() { { id: 'NODEJS_CORE', name: 'Node.js核心', description: 'Node.js运行时内置的模块,用于底层操作如文件系统、路径处理等,例如fs、path、child_process、util、module、os、http、crypto、events、stream、process、net、url、assert。' }, ] - for (let index = 0; index < pkgTypes.length; index++) { - const pkgType = pkgTypes[index] - await prisma.devPkgType.upsert({ - where: { id: pkgType.id }, - update: { - name: pkgType.name, - description: pkgType.description, - order: (index + 1) * 10, - }, - create: {...pkgType, order: (index + 1) * 10}, + await Promise.all( + pkgTypes.map((pkgType, index) => { + return dbParallelLimit(() => prisma.devPkgType.upsert({ + where: { id: pkgType.id }, + update: { + name: pkgType.name, + description: pkgType.description, + order: (index + 1) * 10, + }, + create: {...pkgType, order: (index + 1) * 10}, + })) }) - } + ) console.log('依赖包类型数据初始化完成') console.log('数据库初始化完成') @@ -177,4 +191,4 @@ main() console.error(e) await prisma.$disconnect() process.exit(1) - }) \ No newline at end of file + }) diff --git a/quickstart.sh b/quickstart.sh new file mode 100755 index 0000000..10a4627 --- /dev/null +++ b/quickstart.sh @@ -0,0 +1,450 @@ +#!/bin/bash + +# Hair Keeper 模板项目快速配置脚本 +# 用于帮助用户快速配置环境变量和开发工具 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# 打印带颜色的信息 +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_header() { + echo "" + echo -e "${CYAN}========================================${NC}" + echo -e "${CYAN} $1${NC}" + echo -e "${CYAN}========================================${NC}" + echo "" +} + +# 读取用户输入,支持默认值 +read_input() { + local prompt="$1" + local default="$2" + local allow_empty="$3" + local result + + if [ -n "$default" ]; then + echo -n "$prompt [默认: $default]: " >&2 + read result + result="${result:-$default}" + else + if [ "$allow_empty" = "true" ]; then + echo -n "$prompt (可为空): " >&2 + read result + else + while [ -z "$result" ]; do + echo -n "$prompt: " >&2 + read result + if [ -z "$result" ]; then + print_warning "此项不能为空,请重新输入" + fi + done + fi + fi + echo "$result" +} + +# 读取密码输入(隐藏输入) +read_password() { + local prompt="$1" + local allow_empty="$2" + local result + + if [ "$allow_empty" = "true" ]; then + echo -n "$prompt (可为空): " >&2 + read -s result + echo "" >&2 + else + while [ -z "$result" ]; do + echo -n "$prompt: " >&2 + read -s result + echo "" >&2 + if [ -z "$result" ]; then + print_warning "此项不能为空,请重新输入" + fi + done + fi + echo "$result" +} + +# 生成随机密码 +generate_password() { + local length="$1" + if command -v pwgen &> /dev/null; then + pwgen -s "$length" 1 + else + # 如果没有 pwgen,使用 openssl 或 /dev/urandom + if command -v openssl &> /dev/null; then + openssl rand -base64 "$length" | tr -dc 'a-zA-Z0-9' | head -c "$length" + else + cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c "$length" + fi + fi +} + +# 选择菜单 +select_option() { + local prompt="$1" + shift + local options=("$@") + local choice + + echo "$prompt" >&2 + for i in "${!options[@]}"; do + echo " $((i+1)). ${options[$i]}" >&2 + done + + while true; do + echo -n "请输入选项编号: " >&2 + read choice + if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#options[@]}" ]; then + echo "$choice" + return + fi + print_warning "无效选项,请重新输入" + done +} + +# 主函数 +main() { + print_header "Hair Keeper 模板项目快速配置" + + echo "本脚本将帮助您快速配置项目环境变量和开发工具" + echo "" + + # ==================== 环境变量配置 ==================== + print_header "环境变量配置" + + # --- PostgreSQL 配置 --- + print_info "配置 PostgreSQL 数据库..." + echo "" + + POSTGRESQL_USERNAME=$(read_input "PostgreSQL 用户名" "postgres") + POSTGRESQL_PASSWORD=$(read_password "PostgreSQL 密码" "true") + POSTGRESQL_PORT=$(read_input "PostgreSQL 端口" "5432") + + echo "" + db_url_choice=$(select_option "DATABASE_URL 配置方式:" "自动构造 PostgreSQL URL" "手动输入完整 URL") + + if [ "$db_url_choice" = "1" ]; then + POSTGRESQL_HOSTNAME=$(read_input "PostgreSQL 主机名" "postgresql") + POSTGRESQL_DBNAME=$(read_input "PostgreSQL 数据库名" "postgres") + POSTGRESQL_SCHEMA=$(read_input "PostgreSQL 模式名" "public") + + if [ -n "$POSTGRESQL_PASSWORD" ]; then + DATABASE_URL="postgresql://${POSTGRESQL_USERNAME}:${POSTGRESQL_PASSWORD}@${POSTGRESQL_HOSTNAME}:${POSTGRESQL_PORT}/${POSTGRESQL_DBNAME}?schema=${POSTGRESQL_SCHEMA}" + else + DATABASE_URL="postgresql://${POSTGRESQL_USERNAME}@${POSTGRESQL_HOSTNAME}:${POSTGRESQL_PORT}/${POSTGRESQL_DBNAME}?schema=${POSTGRESQL_SCHEMA}" + fi + else + DATABASE_URL=$(read_input "DATABASE_URL" "") + fi + + echo "" + print_success "PostgreSQL 配置完成" + echo "" + + # --- Redis 配置 --- + print_info "配置 Redis..." + echo "" + + REDIS_HOST=$(read_input "Redis 主机名" "redis") + REDIS_PORT=$(read_input "Redis 端口" "6379") + REDIS_PASSWORD=$(read_password "Redis 密码" "true") + + echo "" + print_success "Redis 配置完成" + echo "" + + # --- MinIO 配置 --- + print_info "配置 MinIO 对象存储..." + echo "" + + MINIO_ENDPOINT=$(read_input "MinIO 端点地址" "minio") + MINIO_API_PORT=$(read_input "MinIO API 端口" "9000") + MINIO_CONSOLE_PORT=$(read_input "MinIO 控制台端口" "9001") + MINIO_USE_SSL=$(read_input "MinIO 是否使用 SSL (true/false)" "false") + MINIO_ROOT_USER=$(read_input "MinIO 用户名" "admin") + MINIO_ROOT_PASSWORD=$(read_password "MinIO 密码" "true") + MINIO_SERVER_URL=$(read_input "MinIO 服务器 URL (用于生成公开访问链接)" "" "true") + MINIO_BUCKET=$(read_input "MinIO 存储桶名称" "app-files") + + echo "" + print_success "MinIO 配置完成" + echo "" + + # --- 应用配置 --- + print_info "配置应用参数..." + echo "" + + SUPER_ADMIN_PASSWORD=$(generate_password 16) + USER_DEFAULT_PASSWORD=$(generate_password 16) + NEXTAUTH_SECRET=$(generate_password 32) + + print_info "已自动生成以下密码:" + echo " SUPER_ADMIN_PASSWORD: $SUPER_ADMIN_PASSWORD" + echo " USER_DEFAULT_PASSWORD: $USER_DEFAULT_PASSWORD" + echo " NEXTAUTH_SECRET: $NEXTAUTH_SECRET" + echo "" + + DB_PARALLEL_LIMIT=$(read_input "数据库批次操作默认并发数" "32") + + echo "" + print_success "应用参数配置完成" + echo "" + + # --- AI API 配置 --- + print_info "配置 AI API..." + echo "" + + PKUAI_API_KEY=$(read_password "PKU AI API Key" "true") + PKUAI_API_BASE=$(read_input "PKU AI API Base URL" "https://chat.noc.pku.edu.cn/") + + echo "" + print_success "AI API 配置完成" + echo "" + + # --- 开发环境配置 --- + print_info "配置开发环境参数..." + echo "" + + DEV_PORT=$(read_input "开发服务器端口" "3000") + DEV_TERMINAL_PORT=$(read_input "开发终端默认端口" "7681") + DEV_TERMINAL_URL=$(read_input "开发终端 URL" "" "true") + DEV_TERMINAL=$(read_input "开发终端 tmux session 名称" "nextdev") + + echo "" + print_success "开发环境配置完成" + echo "" + + # ==================== 写入配置文件 ==================== + print_header "写入配置文件" + + # 写入 .env 文件 + print_info "写入 .env 文件..." + cat > .env << EOF +# ==================== 容器相关 ==================== +POSTGRESQL_USERNAME=${POSTGRESQL_USERNAME} +POSTGRESQL_PASSWORD=${POSTGRESQL_PASSWORD} +POSTGRESQL_PORT=${POSTGRESQL_PORT} +DATABASE_URL="${DATABASE_URL}" + +REDIS_HOST=${REDIS_HOST} +REDIS_PORT=${REDIS_PORT} +REDIS_PASSWORD=${REDIS_PASSWORD} + +MINIO_ENDPOINT=${MINIO_ENDPOINT} +MINIO_API_PORT=${MINIO_API_PORT} +MINIO_CONSOLE_PORT=${MINIO_CONSOLE_PORT} +MINIO_USE_SSL=${MINIO_USE_SSL} +MINIO_ROOT_USER=${MINIO_ROOT_USER} +MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} +MINIO_SERVER_URL=${MINIO_SERVER_URL} +MINIO_BUCKET=${MINIO_BUCKET} + +# ==================== 应用相关 ==================== +SUPER_ADMIN_PASSWORD=${SUPER_ADMIN_PASSWORD} +USER_DEFAULT_PASSWORD=${USER_DEFAULT_PASSWORD} +## 数据库批次操作默认并发数 +DB_PARALLEL_LIMIT=${DB_PARALLEL_LIMIT} + +# ==================== NextAuth.js Configuration ==================== +NEXTAUTH_SECRET=${NEXTAUTH_SECRET} + +# ==================== AI API ==================== +PKUAI_API_KEY=${PKUAI_API_KEY} +PKUAI_API_BASE=${PKUAI_API_BASE} +EOF + print_success ".env 文件已创建" + + # 写入 .env.development 文件 + print_info "写入 .env.development 文件..." + cat > .env.development << EOF +# 仅在开发环境加载 +PORT=${DEV_PORT} +NEXT_PUBLIC_DEV_TERMINAL_DEFAULT_PORT=${DEV_TERMINAL_PORT} +NEXT_PUBLIC_DEV_TERMINAL_URL=${DEV_TERMINAL_URL} +DEV_TERMINAL=${DEV_TERMINAL} +EOF + print_success ".env.development 文件已创建" + + # 写入 .env.production 文件 + print_info "写入 .env.production 文件..." + cat > .env.production << EOF +# 仅在生产环境加载 +EOF + print_success ".env.production 文件已创建" + + # ==================== Claude Code 编程代理配置 ==================== + print_header "Claude Code 编程代理配置" + + # 重命名 CLAUDE.md + if [ -f "CLAUDE.md" ]; then + print_info "重命名 CLAUDE.md 为 TEMPLATE_README.md..." + mv CLAUDE.md TEMPLATE_README.md + print_success "CLAUDE.md 已重命名为 TEMPLATE_README.md" + fi + + # 创建新的 CLAUDE.md + print_info "创建新的 CLAUDE.md..." + cat > CLAUDE.md << 'EOF' +# CLAUDE.md + +本文件为 AI 代理(如 Claude)提供在本代码库中工作的指导说明。 + +## 项目说明 + +本项目基于 Hair Keeper 模板构建(详见 @TEMPLATE_README.md),目前尚未实现业务功能 +EOF + print_success "新的 CLAUDE.md 已创建" + + # 清空 README.md + print_info "清空 README.md..." + > README.md + print_success "README.md 已清空" + + # 创建 Claude Code Router 配置 + print_info "配置 Claude Code Router..." + mkdir -p ~/.claude-code-router + + cat > ~/.claude-code-router/config.json << EOF +{ + "LOG": false, + "LOG_LEVEL": "debug", + "CLAUDE_PATH": "", + "HOST": "127.0.0.1", + "PORT": 3456, + "APIKEY": "", + "API_TIMEOUT_MS": "600000", + "PROXY_URL": "", + "transformers": [], + "Providers": [ + { + "name": "pku-anthropic", + "api_base_url": "${PKUAI_API_BASE}api/anthropic/v1/messages", + "api_key": "${PKUAI_API_KEY}", + "models": [ + "claude-sonnet-4-5-20250929", + "claude-opus-4-5-20251101", + "claude-haiku-4-5-20251001" + ], + "transformer": { + "use": [ + "Anthropic" + ] + } + } + ], + "StatusLine": { + "enabled": false, + "currentStyle": "default", + "default": { + "modules": [] + }, + "powerline": { + "modules": [] + } + }, + "Router": { + "default": "pku-anthropic,claude-opus-4-5-20251101", + "background": "pku-anthropic,claude-haiku-4-5-20251001", + "think": "pku-anthropic,claude-opus-4-5-20251101", + "longContext": "pku-anthropic,claude-opus-4-5-20251101", + "longContextThreshold": 80000, + "webSearch": "", + "image": "pku-anthropic,claude-opus-4-5-20251101" + }, + "CUSTOM_ROUTER_PATH": "" +} +EOF + print_success "Claude Code Router 配置已写入 ~/.claude-code-router/config.json" + + # ==================== Git 版本控制初始化 ==================== + print_header "Git 版本控制初始化" + + # 删除模板项目的 git 仓库 + if [ -d ".git" ]; then + print_info "删除模板项目的 git 仓库..." + rm -rf .git + print_success "已删除 .git 目录" + fi + + # 获取用户的 git 配置信息 + print_info "配置 Git 用户信息..." + echo "" + + GIT_USER_EMAIL=$(read_input "Git 用户邮箱" "") + GIT_USER_NAME=$(read_input "Git 用户名" "") + + echo "" + git init + git config init.defaultBranch main + print_info "设置 Git 用户配置..." + git config user.email "$GIT_USER_EMAIL" + git config user.name "$GIT_USER_NAME" + print_success "Git 用户配置已设置" + + # 初始化新的 git 仓库 + print_info "初始化 Git 仓库..." + git add . + git commit -m "init" + git branch -M main + print_success "Git 仓库已初始化并完成首次提交 (主分支: main)" + + # 配置远程仓库 + echo "" + print_info "配置远程仓库 (格式: https://用户名:密码@gitea.example.com/用户名/仓库名.git)" + GIT_REMOTE_URL=$(read_input "远程仓库链接" "" "true") + + if [ -n "$GIT_REMOTE_URL" ]; then + git remote add origin "$GIT_REMOTE_URL" + print_success "远程仓库已配置: $GIT_REMOTE_URL" + else + print_info "跳过远程仓库配置" + fi + + # ==================== 完成提示 ==================== + print_header "配置完成!" + + echo -e "${GREEN}所有配置已完成!${NC}" + echo "" + echo "Claude Code 编程代理使用说明:" + echo -e " ${CYAN}ccr code${NC} - 打开 Claude Code 编程代理,可通过对话完成编程任务" + echo -e " ${CYAN}ccr ui${NC} - 配置 Claude Code 编程代理对接的大模型" + echo -e " ${CYAN}ccr code --resume${NC} - 查看历史对话" + echo "" + echo "接下来您可以进行以下操作:" + echo -e " ${CYAN}1. pnpm install${NC} - 安装依赖包" + echo -e " ${CYAN}2. pnpm prisma migrate dev${NC} - 迁移数据库并初始化 Prisma 客户端" + echo -e " ${CYAN}3. pnpm run db:seed${NC} - 初始化数据库数据" + echo -e " ${CYAN}4. pnpm run dev${NC} - 运行开发服务器" + echo -e " ${CYAN}5. ccr code${NC} - 打开编程代理后,先输入 ${YELLOW}/init <我的项目的主要功能是...>${NC} 初始化" + echo " 然后再开始使用 AI 完成编程任务" + echo "" + print_success "祝您开发愉快!" +} + +# 运行主函数 +main diff --git a/simple_deploy.sh b/simple_deploy.sh new file mode 100644 index 0000000..a253ff0 --- /dev/null +++ b/simple_deploy.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# 此脚本用来一键部署到生产服务器 +# 需事先配置好ssh免密登录,目标服务器需要安装好node、pnpm、tsx、pm2来运行容器 +set -e + +# 配置 +REMOTE_USER="" +REMOTE_HOST="" +REMOTE_PORT="" +PROJECT_NAME="" +REMOTE_DIR="~/$PROJECT_NAME" + +pnpm run lint +echo "🔨 开始构建项目..." +pnpm run build + +echo "📦 准备部署文件..." +# 创建临时目录 +TEMP_DIR=$(mktemp -d) +trap "rm -rf $TEMP_DIR" EXIT + +# 复制必要文件 +cp -r .next $TEMP_DIR/ +cp -r public $TEMP_DIR/ +cp -r prisma $TEMP_DIR/ +cp package.json $TEMP_DIR/ +cp pnpm-lock.yaml $TEMP_DIR/ +cp next.config.ts $TEMP_DIR/ + +echo "🚀 上传文件到服务器..." +ssh ${REMOTE_USER}@${REMOTE_HOST} "mkdir -p ${REMOTE_DIR}" +rsync -avz --delete \ + --exclude=node_modules \ + --exclude=.git \ + $TEMP_DIR/ ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/ + +# 上传环境变量文件 +echo "📝 上传环境变量文件..." +[ -f .env ] && scp .env ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/.env +[ -f .env.production ] && scp .env.production ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/.env.production + +echo "⚙️ 在服务器上配置和启动服务..." +ssh ${REMOTE_USER}@${REMOTE_HOST} << ENDSSH +cd ${REMOTE_DIR} + +# 安装生产依赖(包括prisma用于数据库迁移和pm2用于进程管理) +echo "📥 安装依赖..." +pnpm install --prod + +# 运行数据库迁移 +echo "🗄️ 运行数据库迁移..." +npx prisma migrate deploy +npx prisma generate + +# 使用PM2管理Next.js服务 +echo "🔄 使用PM2重启服务..." +/home/user/.local/share/pnpm/pm2 delete $PROJECT_NAME || true +/home/user/.local/share/pnpm/pm2 start pnpm --name $PROJECT_NAME -- start -p ${REMOTE_PORT} +# 保存当前 PM2 进程列表的快照,使其在系统重启后能自动恢复 +/home/user/.local/share/pnpm/pm2 save + +echo "✅ 部署完成!服务运行在端口 ${REMOTE_PORT}" +echo "📊 查看服务状态: pm2 status" +echo "📝 查看日志: pm2 logs $PROJECT_NAME" +echo "❌ 关闭服务: pm2 delete $PROJECT_NAME" +ENDSSH + +echo "🎉 部署成功!" +echo "访问地址: http://${REMOTE_HOST}:${REMOTE_PORT}" \ No newline at end of file diff --git a/src/app/(main)/dev/panel/dev-tools.tsx b/src/app/(main)/dev/panel/dev-tools.tsx index 6dd040c..39d4698 100644 --- a/src/app/(main)/dev/panel/dev-tools.tsx +++ b/src/app/(main)/dev/panel/dev-tools.tsx @@ -126,8 +126,7 @@ export function DevTools() { variant="outline" size="sm" onClick={() => { - const port = process.env.NEXT_PUBLIC_DEV_TERMINAL_DEFAULT_PORT || '7681' - window.open(`http://localhost:${port}`, '_blank') + window.open(process.env.NEXT_PUBLIC_DEV_TERMINAL_URL || `http://localhost:${process.env.NEXT_PUBLIC_DEV_TERMINAL_DEFAULT_PORT || '7681'}`, '_blank') }} disabled={!terminalLoaded} className="gap-1.5" @@ -194,7 +193,7 @@ export function DevTools() {
{terminalLoaded ? (