--- URL: "https://elysia-admin.top/" LLMS_URL: "https://elysia-admin.top/\\index.md" title: "Elysia Admin - 基于 ElysiaJS + Vue3 的全栈后台管理系统" description: "Elysia Admin 是一套基于 Bun + ElysiaJS + DrizzleORM + Vue3 的高性能全栈后台管理系统,开箱即用,内置 RBAC 权限、JWT 认证、定时任务、队列等企业级功能。" --- --- title: Elysia Admin - 基于 ElysiaJS + Vue3 的全栈后台管理系统 description: Elysia Admin 是一套基于 Bun + ElysiaJS + DrizzleORM + Vue3 的高性能全栈后台管理系统,开箱即用,内置 RBAC 权限、JWT 认证、定时任务、队列等企业级功能。 --- --- URL: "https://elysia-admin.top/architecture/commands.html" LLMS_URL: "https://elysia-admin.top/\\architecture\\commands.md" title: "内置命令 - Elysia Admin 架构" description: "详细说明 Elysia Admin 项目内置的所有命令,包括开发启动、生产构建、数据库操作及 Docker 管理命令的使用方法。" --- --- title: 内置命令 - Elysia Admin 架构 description: 详细说明 Elysia Admin 项目内置的所有命令,包括开发启动、生产构建、数据库操作及 Docker 管理命令的使用方法。 --- # 内置命令 项目脚本定义在 `server/package.json`(后端)和 `admin/package.json`(前端)。下文 **未特别说明时,均在 `server/` 目录执行**。 ## 命令速查 ### 后端 server/ | 命令 | 作用 | |------|------| | `bun run dev` | 开发:HTTP 主进程(watch + 新模块自动重载) | | `bun run dev:plain` | 开发:仅原生 `bun --watch`(无新模块目录监听) | | `bun run dev:workers` | 开发:构建 Processor 后启动 Worker | | `bun run dev:all` | 开发:主进程 + Worker 同时启动 | | `bun run dev:debug` | 调试:inspect 6499,供 IDE Attach | | `bun run dev:debug:browser` | 调试:推荐,配合 debug.bun.sh | | `bun run dev:debug:watch` | 调试 + 热重载(易卡顿,慎用) | | `bun run build` | 生产构建(主进程 + Worker + Processor) | | `bun run build:processors` | 仅重建队列 Processor | | `bun run start` | 生产:直接启动 `dist/index.js` | | `bun run test` | 单元测试 | | `bun run test:watch` | 测试 watch 模式 | | `bun run test:coverage` | 测试 + 覆盖率 | | `bun run create:module` | 脚手架:后端 CRUD 模块 | | `bun run create:page` | 脚手架:前端 CRUD 页面 | | `bun run db:push` | Schema 推送到数据库 | | `bun run db:pull` | 从数据库拉取 Schema | | `bun run gen:crypto-keys` | 生成 RSA 密钥对并写入 yaml 的 `crypto` 段 | | `bun run docker:build` | 构建 Docker 镜像(含 dist 全量产物) | | `bun run docker:run` | 运行容器 | | `bun run docker:stop` / `docker:rm` / `docker:logs` | 容器管理 | ### 前端 admin/ | 命令 | 作用 | |------|------| | `pnpm dev` | Vite 开发服务器(默认 3006) | | `pnpm build` | 生产构建 → `admin/dist/` | | `pnpm serve` | 预览构建结果 | ## 开发启动 ### dev 只启动 HTTP 主进程。入口为 `script/dev-watch-modules.ts`:内部仍以 `bun --watch src/index.ts` 跑服务,并额外轮询 `src/modules` 目录集合。新增或删除模块目录且写盘稳定约 5 秒后,会 touch 已在模块图中的 `src/modules/index.ts`,触发 `bun --watch` 自行重载,从而无需手动重启即可访问新接口。已有模块的 `route.ts` / `handle.ts` / `dto.ts` 改动仍走原生热重载。日常写接口用这个即可。 ```bash cd server bun run dev ``` ### dev:plain 逃生口:行为与原先直接 `bun --watch src/index.ts` 一致,不监听新模块目录。排查守护脚本问题时可用。 ```bash bun run dev:plain ``` ### dev:workers 先执行 `build:processors`,再启动 Worker 进程消费队列、跑定时任务。只用接口、不碰队列时可以不开。 ```bash bun run dev:workers ``` ### dev:all 主进程和 Worker 一起启,日志带 `[server]` / `[workers]` 前缀,`Ctrl+C` 同时退出。全栈联调、测队列时推荐。 ```bash bun run dev:all ``` 使用队列或定时任务时,至少保证 Worker 在跑。详见 [队列](/guide/queue)。 > `package.json` 中开发类脚本已内置 `NODE_ENV=development`、构建类脚本已内置 `NODE_ENV=production`,一般无需在命令行前手动指定环境变量。 ## 调试 日常开发用 `bun run dev`;需要断点再用下面命令。 | 场景 | 命令 | |------|------| | 正常开发 | `bun run dev` | | 浏览器断点(推荐) | `bun run dev:debug:browser` + [debug.bun.sh](https://debug.bun.sh) | | Cursor / VS Code Attach | `bun run dev:debug`,F5 连 6499 端口 | ```mermaid flowchart LR A[bun run dev] --> N[日常开发] B[dev:debug:browser] --> D[debug.bun.sh 断点] C[dev:debug + F5] --> I[IDE Attach 较慢] ``` ### dev:debug `--inspect=6499` 启动,不含 watch。配合 VS Code 任务 `Server: dev:debug` 或 launch **Debug Server (IDE, 较慢)**。 ```bash cd server bun run dev:debug ``` ### dev:debug:browser Bun 官方推荐的调试方式,比 IDE Attach 流畅。启动后把终端里的 `https://debug.bun.sh/#localhost:6499/...` 复制到浏览器,在 Sources 里下断点。 ```bash bun run dev:debug:browser ``` 也可通过 **Terminal → Run Task → Server: dev:debug (browser, 推荐)** 启动。 ### dev:debug:watch 同时开 inspect 和 watch,调试时容易卡顿或反复重连,仅在「边改代码边调试」时用。 **使用方式:** ```bash [bun] cd server bun run dev:debug:watch ``` 服务已手动启动时,可用 **Attach Server (6499)** 配置连接。 ## 构建 ### build 生产环境完整构建:主进程、Worker、所有 Processor、静态资源、`production.yaml`、`ecosystem.config.cjs`。 ```bash cd server bun run build ``` 产物目录 `dist/`: ``` dist/ ├── index.js # HTTP 主进程 ├── workers.js # Worker 进程 ├── ecosystem.config.cjs # PM2 配置 ├── production.yaml # 生产配置(部署前请修改) ├── public/ # 前端静态资源(若已复制 admin 构建产物) ├── dist/cjs/ # BullMQ 沙箱 bootstrap └── processors/ ├── system-cron.js ├── flow-buffer.js └── trade-order.js ``` 部署说明见 [部署](/guide/deploy)。 ### build:processors 只重建 `processors/*.js` 和 `dist/cjs/`。改了 `processor.ts` 或 `worker-sandbox/*.ts` 后执行,再重启 Worker。 ```bash bun run build:processors ``` ## 测试 测试在 `server/test/`,`test/preload.ts` 会把配置指向 `development.yaml`(`bun test` 默认 `NODE_ENV=test`,否则会找不存在的 `test.yaml`)。 当前覆盖:纯函数、校验、树结构、时间、`CreateQueryBuilder` 等;**不含** handle 层、真实 DB/Redis、网络请求。 ```bash cd server bun run test bun run test:watch bun run test:coverage ``` ## 模块脚手架 在 **`server/`** 执行。依赖 `server/database/schema/` 里**已存在**的 Drizzle 表;字典、菜单、handoff SQL 仍需人工或 AI 补充。完整流程见 [AI 开发指南](/guide/ai-guide)。 ### create:module 生成 `server/src/modules/{group}-{name}/` 下的 `route.ts`、`dto.ts`、`handle.ts`。 ```bash cd server bun run create:module business-goods --tag 商品管理 ``` | 参数 | 说明 | |------|------| | `--tag` | 模块中文名(API 文档分组) | | `--schema business_goods` | 指定 schema 文件名,默认由 slug 推导 | | `--dry-run` | 仅预览,不写文件 | ### create:page 生成前端 CRUD 五件套到 `admin/`(types、api、views)。脚本仍在 server 侧运行。 ```bash bun run create:page business goods --tag 商品管理 ``` 参数与 `create:module` 相同。`business goods` 对应路由目录 `admin/src/views/business/goods/`。 ### 推荐流程 ```bash cd server bun run create:module business-goods --tag 商品管理 bun run create:page business goods --tag 商品管理 # 再用 AI 补业务校验、字典、菜单与 server/database/sql/business-goods-init.sql ``` ## 数据库 **仅建议在开发环境使用。** ```bash # 将本地 schema 推送到 PostgreSQL bun run db:push # 从数据库拉取结构到本地 schema bun run db:pull ``` 表结构说明见 [数据库设计](./database)。业务查询写法见 [数据库操作](/guide/database-operation)。 ## 请求加解密密钥 在 **`server/`** 执行。脚本生成 RSA-2048 密钥对(私钥 PKCS8、公钥 SPKI),写入 `server/src/config/` 下对应环境的 yaml,只替换 `crypto` 段的 `privateKey` / `publicKey`。`crypto.enabled` 保持原值,需要启用时再手动改成 `true`。协议与前后端开关见 [请求加解密](/guide/crypto)。 ### gen:crypto-keys ```bash cd server bun run gen:crypto-keys bun run gen:crypto-keys production bun run gen:crypto-keys production --force ``` | 参数 | 说明 | |------|------| | (省略) | 写入 `development.yaml` | | `production` | 写入 `production.yaml` | | `--force` | 已有密钥时强制覆盖 | 目标文件里必须已经有 `crypto:` 段,否则脚本直接退出。`privateKey` 已有内容且未加 `--force` 时也会拒绝写入:覆盖后,用旧私钥解不开的报文无法再恢复。私钥只留在服务端 yaml 中,不要提交到仓库。生产环境建议先生成密钥,确认 `enabled` 后再打开加密。 ## Docker 在 `server/` 目录,构建并运行含 HTTP + Worker 的完整 dist 镜像(单容器双进程)。 ```bash bun run docker:build # 构建镜像 hnq1/elysia-admin:latest bun run docker:run # 启动容器,映射 3000 bun run docker:stop # 停止 bun run docker:rm # 删除容器 bun run docker:logs # 查看日志 ``` Compose 多服务示例见 [部署](/guide/deploy)。 ## 工作区任务 打开 `elysia-admin.code-workspace` 后,**Terminal → Run Task** 常用项: | 任务 | 说明 | |------|------| | `Dev: Full Stack` | 前后端一起启 | | `Server: dev` | 仅后端 | | `Admin: dev` | 仅前端 | | `Server: dev:debug (browser, 推荐)` | 浏览器调试 | | `Server: dev:debug` | IDE 调试 | --- URL: "https://elysia-admin.top/architecture/database.html" LLMS_URL: "https://elysia-admin.top/\\architecture\\database.md" title: "数据库设计 - Elysia Admin 架构" description: "详细介绍 Elysia Admin 的数据库设计,包括系统管理、监控管理、业务管理各模块的表结构、字段说明、关联关系及索引设计最佳实践。" --- --- title: 数据库设计 - Elysia Admin 架构 description: 详细介绍 Elysia Admin 的数据库设计,包括系统管理、监控管理、业务管理各模块的表结构、字段说明、关联关系及索引设计最佳实践。 --- # 数据库设计 本文介绍 Elysia Admin 的 PostgreSQL 表结构设计。Schema 源码在 `server/database/schema/`,以 Drizzle 定义为准;本文便于快速查阅表职责、字段含义和表间关系。 日常 CRUD 与查询写法见 [数据库操作](/guide/database-operation);新建业务表规范可参考 `.ai/AI_SCHEMA_GUIDE.md`。 ## 技术栈 | 组件 | 选型 | |------|------| | 数据库 | PostgreSQL >= 16 | | ORM | [Drizzle ORM](https://orm.drizzle.team/) | | 数据库驱动 | [postgres](https://github.com/porsager/postgres)(轻量级 PG 驱动) | | 缓存 | Redis >= 6([Ioredis](https://ioredis.org/),会话、缓存与队列共用) | | 迁移 | Drizzle Kit(`bun run db:push` / `db:pull`,见下方常用命令) | ## 设计原则 - **BaseSchema**:业务表统一继承基础审计字段(创建/更新人、时间、软删、备注) - **软删除**:`delFlag = true` 标记删除,查询时默认过滤 - **主键**:系统用户用 UUID;其余多数表用 `bigserial` 自增 - **外键**:Drizzle schema 中声明 `references`,保证引用完整性 - **命名**:字段驼峰(代码侧)对应数据库下划线列名 - **状态**:启用/禁用常用 `boolean` 的 `status`;业务流程状态常用 `varchar` + 字典 ## BaseSchema 所有业务表通过 `...BaseSchema` 展开以下字段(定义见 `server/database/base-schema.ts`): | 字段 | 类型 | 说明 | 默认 | |------|------|------|------| | createTime | timestamp | 创建时间 | now() | | createBy | varchar(36) | 创建人 ID(UUID 字符串) | null | | updateTime | timestamp | 更新时间 | null | | updateBy | varchar(36) | 更新人 ID | null | | delFlag | boolean | 软删除标志 | false | | remark | varchar(255) | 备注 | null | 下文各表字段表**不含** BaseSchema 重复列,实际表结构均包含上述字段。 ## 模块总览 ```mermaid flowchart TB subgraph 系统 system U[system_user] R[system_role] M[system_menu] D[system_dict] N[system_notice] end subgraph 监控 monitor J[monitor_job] end subgraph 业务 business BM[business_merchant] BO[business_orders] BP[business_payments] end U --> R R --> M BO --> BM BO --> BP ``` | 模块 | 表数量 | 主要职责 | |------|--------|----------| | 系统管理 | 17 | RBAC、字典、日志、存储、IP 黑名单、通知公告、站点部署 | | 监控管理 | 1 | 定时任务配置 | | 业务管理 | 5 | 商户、订单、支付、退款 | ## 系统管理 ### system_user 用户表 存储后台账号。主键 `userId` 为 UUID。 | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | userId | uuid | PK | 用户 ID | | username | varchar(64) | NOT NULL,部分唯一索引 `WHERE del_flag = false` | 登录名。已软删行不占用用户名 | | password | varchar(255) | NOT NULL | 密码(加密) | | nickname | varchar(64) | | 昵称 | | email | varchar(64) | | 邮箱 | | phone | varchar(11) | | 手机 | | sex | varchar(1) | | 0 未知 / 1 男 / 2 女 | | avatar | varchar(255) | | 头像 URL | | deptId | bigint | FK | 部门 ID | | status | boolean | | 启用 / 禁用 | 关联:多对一 `system_dept`;多对多 `system_role`(经 `system_user_role`)。 用户名唯一性由部分唯一索引 `system_user_username_unique`(`WHERE del_flag = false`)保证。从列级 `UNIQUE` 升级时,先去掉旧约束,再执行 `bun run db:push` 或手工建该索引。 ### system_role 角色表 | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | roleId | bigserial | PK | 角色 ID | | roleName | varchar(64) | NOT NULL | 角色名称 | | roleCode | varchar(64) | NOT NULL | 角色编码 | | sort | integer | | 排序 | | status | boolean | | 状态 | 关联:多对多用户、菜单(经关联表)。 ### system_user_role 用户角色关联 | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | userId | uuid | FK | 用户 ID | | roleId | bigint | FK | 角色 ID | ### system_menu 菜单表 树形菜单与前端路由配置,支持外链、iframe、KeepAlive、固定标签等。 | 字段 | 类型 | 说明 | |------|------|------| | menuId | bigserial PK | 菜单 ID | | path / name / component | varchar | 路由路径、名称、组件 | | title / icon | varchar | 标题、图标 | | showBadge / showTextBadge | | 徽章 | | isHide / isHideTab | boolean | 隐藏菜单 / 标签 | | link / isIframe / isFullPage | | 外链、内嵌、全屏 | | keepAlive / fixedTab / activePath | | 缓存、固定标签、激活路径 | | sort / status / parentId | | 排序、状态、父级 | ### system_menu_btn 菜单按钮 | 字段 | 类型 | 说明 | |------|------|------| | btnId | bigserial PK | 按钮 ID | | menuId | bigint FK | 所属菜单 | | title | varchar | 按钮名 | | permission | varchar | 权限标识,如 `system:user:create` | | sort / status | | 排序、状态 | ### system_role_menu 角色菜单关联 | 字段 | 类型 | 说明 | |------|------|------| | roleId | bigint FK | 角色 | | menuId | bigint FK | 菜单 | | menuBtnId | bigint FK | 按钮(可选) | ### RBAC 关系图 用户通过角色获得菜单与按钮权限: ```mermaid erDiagram system_user ||--o{ system_user_role : has system_role ||--o{ system_user_role : has system_role ||--o{ system_role_menu : grants system_menu ||--o{ system_role_menu : on system_menu ||--o{ system_menu_btn : contains system_user }o--|| system_dept : belongs_to ``` ### system_dept 部门表 树形组织架构,`parentId` 自关联。 | 字段 | 类型 | 说明 | |------|------|------| | deptId | bigserial PK | 部门 ID | | deptName | varchar(64) | 部门名称 | | parentId | bigint | 父部门 | | sort / status | | 排序、状态 | ### system_api 接口表 记录 API 路径与方法,用于权限与文档。 | 字段 | 类型 | 说明 | |------|------|------| | apiId | bigserial PK | API ID | | apiName | varchar(64) | 名称 | | apiPath | varchar(255) | 路径 | | apiMethod | varchar(10) | GET / POST 等 | | status | boolean | 状态 | ### system_dict_type / system_dict_data 字典 类型表定义 `dictType`,数据表存 label/value,前端 `useDictStore` 读取。 **system_dict_type** | 字段 | 说明 | |------|------| | dictId | PK | | dictName / dictType | 名称、类型编码(唯一) | | status | 状态 | **system_dict_data** | 字段 | 说明 | |------|------| | dictCode | PK | | dictType | 关联类型 | | dictLabel / dictValue | 显示文本、实际值 | | dictSort | 排序 | | tagType / customClass | 标签样式 | | status | 状态 | ### system_login_log 登录日志 | 字段 | 说明 | |------|------| | logId | PK | | loginType / loginName | 登录方式、用户名 | | clientType / clientPlatform | 客户端 | | ipaddr / loginLocation | IP、地理位置 | | userAgent / os | UA、系统 | | message / status | 提示、成功与否 | ### system_oper_log 操作日志 中间件 `AddOperLog` 写入,路由 `meta.isLog: true` 时记录。 | 字段 | 说明 | |------|------| | operId | PK | | title / action | 模块、操作名 | | requestMethod | HTTP 方法 | | userId / operName | 操作人 | | operUrl / operIp / operLocation | URL、IP、地点 | | operParam / jsonResult | 请求参数、响应摘要 | | costTime | 耗时(ms) | | status | 是否成功 | ### system_ip_black IP 黑名单 | 字段 | 说明 | |------|------| | ipBlackId | PK | | ipAddress | IP(唯一) | | status | 是否生效 | ### system_storage 存储配置 对象存储(OSS / COS / S3 / RustFS 等)连接信息,详见 [文件存储](/guide/storage)。 | 字段 | 说明 | |------|------| | storageId | PK | | name | 配置名称(唯一) | | region / endpoint / bucket | 区域、端点、桶 | | accessKey / secretKey | 密钥 | | status | 是否启用(同时仅一条可为 true) | ### system_notice 通知公告 后台「系统管理 → 通知公告」维护的系统公告,内容存 HTML(富文本编辑器)。 | 字段 | 说明 | |------|------| | noticeId | PK | | title | 公告标题 | | noticeType | 通知类型,字典 `system_notice_type` | | content | 公告正文(HTML) | | status | 发布状态(启用 / 停用) | | sort | 排序 | Handoff SQL:`server/database/sql/system-notice-init.sql`(菜单与按钮权限)。 ### system_deploy_site 站点 后台「站点部署」保存的 Nginx 站点。未删除行的域名唯一(部分唯一索引 `uk_system_deploy_site_server_name`)。用法见 [站点部署](/guide/site-deploy)。 | 字段 | 说明 | |------|------| | siteId | PK | | name | 站点名 | | serverName | 域名 | | wwwRoot / webRoot / certDir | 网站根目录、HTTP-01 校验目录、证书目录 | | email | 续签联系邮箱 | | sslAutoRenew / reloadNginx | 自动续签、续签后是否 `nginx -s reload` | | useBaota / httpRedirect | 是否按宝塔目录生成配置、HTTP 是否跳 HTTPS | | locations | 反代与静态路径(jsonb) | | certExpireAt / lastRenewAt / lastRenewMsg | 证书到期时间、最近续签时间与结果 | ### system_deploy_cert 证书记录 每次申请或续签写一条记录。同一站点仅一条 `isCurrent = true` 且未删除的记录(部分唯一索引 `uk_system_deploy_cert_current`)。 | 字段 | 说明 | |------|------| | certId | PK | | siteId | 所属站点 | | issuer / serialNumber / fingerprint | 颁发者、序列号、SHA-256 指纹 | | notBefore / notAfter | 生效与到期时间 | | success / isCurrent | 是否成功、是否当前证书 | | renewSource | `manual` 立即续签,或 `cron` 定时任务 | | message | 结果说明 | ## 监控管理 ### monitor_job 定时任务 与 `system-cron-queue`、后台「定时任务」页配合。`jobName` 须与 `worker-sandbox` 注册名一致。 | 字段 | 类型 | 说明 | |------|------|------| | jobId | bigserial PK | 任务 ID | | jobName | varchar(64) UNIQUE | 任务名称 | | jobCron | varchar(64) | Cron 表达式 | | jobArgs | varchar(256) | JSON 数组参数 | | status | boolean | 是否启用 | 详见 [定时任务](/guide/cron)。 ## 业务管理 支付示例模块:商户 → 订单 → 支付流水 → 退款。 ```mermaid erDiagram business_merchant ||--o{ business_merchant_configs : has business_merchant ||--o{ business_orders : receives business_orders ||--o{ business_payments : pays business_orders ||--o{ business_refund : refunds business_payments ||--o{ business_refund : from ``` ### business_merchant 商户表 | 字段 | 类型 | 说明 | |------|------|------| | id | bigserial PK | 商户 ID | | name | varchar(100) | 商户名称 | | status | boolean | 状态 | ### business_merchant_configs 商户支付配置 一条记录对应一个商户的一种支付渠道,供 `Pay(channel, platform)` 使用。 | 字段 | 类型 | 说明 | |------|------|------| | id | bigserial PK | 配置 ID | | merchantId | bigint FK | 商户 | | channel | varchar(20) | alipay / wechat / paypal | | appId / mchId | varchar | 应用 ID、微信商户号 | | privateKey / publicKey | text | 密钥 PEM | | config | jsonb | notifyUrl、apiV3Key 等扩展 | | status | boolean | 是否启用 | 详见 [支付集成](/guide/payment)。 ### business_orders 订单表 | 字段 | 类型 | 说明 | |------|------|------| | id | bigserial PK | 订单 ID | | orderNo | varchar(64) UNIQUE | 订单号 | | userId | varchar(64) | 下单用户(UUID 字符串) | | merchantId | bigint FK | 商户 | | title / description | varchar | 标题、描述 | | amount | decimal(10,2) | 金额 | | currency | varchar(10) | 默认 CNY | | status | varchar(20) | 字典 `system_orders_status` | | expireTime | timestamp | 过期时间 | | extra | jsonb | 商品列表等扩展 | 订单状态(字典 `system_orders_status`): | 值 | 含义 | |----|------| | 0 | 待支付 | | 1 | 已支付 | | 2 | 已取消 | | 3 | 已过期 | | 4 | 已退款 | ### business_payments 支付流水 | 字段 | 类型 | 说明 | |------|------|------| | id | bigserial PK | 支付 ID | | orderId | bigint FK | 订单 ID | | orderNo | varchar(64) UNIQUE FK | 订单号 | | merchantConfigId | bigint FK | 商户配置 | | paymentNo | varchar(64) UNIQUE | 本地支付单号 | | platform | varchar(20) | 终端:app / h5 / mini / pc | | paymentMethod | varchar(20) | 渠道:alipay / wechat / paypal | | amount | decimal(10,2) | 实付金额 | | status | varchar(20) | 字典 `system_pay_status` | | thirdTradeNo | varchar(100) | 第三方交易号 | | extra | jsonb | 回调原始数据等 | 支付状态(`system_pay_status`):0 待支付、1 成功、2 失败、3 已关闭。 ### business_refund 退款表 | 字段 | 类型 | 说明 | |------|------|------| | id | bigserial PK | 退款 ID | | orderId | bigint FK | 订单 | | paymentId | bigint FK | 支付流水 | | refundNo | varchar(64) UNIQUE | 退款单号 | | amount | decimal(10,2) | 退款金额 | | status | varchar(20) | 字典 `system_refund_status` | | thirdRefundNo | varchar(100) | 第三方退款号 | | extra | jsonb | 扩展 | 退款状态:0 退款中、1 成功、2 失败。 ## 索引建议 常用查询字段建议加索引(列名为数据库下划线形式): ```sql -- 用户 CREATE INDEX idx_user_username ON system_user(username); CREATE INDEX idx_user_dept ON system_user(dept_id); CREATE INDEX idx_user_status ON system_user(status); -- 菜单 CREATE INDEX idx_menu_parent ON system_menu(parent_id); CREATE INDEX idx_menu_status ON system_menu(status); -- 订单 CREATE INDEX idx_order_no ON business_orders(order_no); CREATE INDEX idx_order_user ON business_orders(user_id); CREATE INDEX idx_order_merchant ON business_orders(merchant_id); CREATE INDEX idx_order_status ON business_orders(status); CREATE INDEX idx_order_create_time ON business_orders(create_time); -- 支付 CREATE INDEX idx_payment_order ON business_payments(order_id); CREATE INDEX idx_payment_no ON business_payments(payment_no); CREATE INDEX idx_payment_third_trade ON business_payments(third_trade_no); -- 日志 CREATE INDEX idx_login_log_time ON system_login_log(create_time); CREATE INDEX idx_oper_log_time ON system_oper_log(create_time); CREATE INDEX idx_oper_log_user ON system_oper_log(user_id); ``` Drizzle `db:push` 不会自动创建上述索引,可按需在 schema 或迁移 SQL 中补充。 ## 常用命令 在 `server/` 目录执行: ```bash # 将 schema 推送到数据库(开发常用) bun run db:push # 从数据库拉取结构到本地 schema bun run db:pull ``` 使用 Drizzle Kit 迁移工作流时: ```bash bun drizzle-kit generate bun drizzle-kit migrate ``` 初始化 SQL 脚本在 `server/database/sql/`,新模块菜单权限可合并进 `{模块}-init.sql` 手动执行。 ## 性能建议 - 列表查询走 `FindPage` + `CreateQueryBuilder`,避免一次拉全表 - 热点字典、配置用 Redis `WithCache`,见 [缓存](/guide/cache) - 日志表定期归档或清理 - 联表查询注意 N+1,优先 `FindPageWithJoin` 或批量查询 - 连接池大小按并发调优,勿过大占满数据库连接 ## 相关文档 - [数据库操作](/guide/database-operation) - [缓存](/guide/cache) - [支付集成](/guide/payment) - [定时任务](/guide/cron) --- URL: "https://elysia-admin.top/architecture/structure.html" LLMS_URL: "https://elysia-admin.top/\\architecture\\structure.md" title: "项目结构 - Elysia Admin 架构" description: "详细说明 Elysia Admin 的整体目录结构,包括前端 Vue3 模块划分和后端 ElysiaJS 分层架构设计,帮助开发者快速了解项目组织方式。" --- --- title: 项目结构 - Elysia Admin 架构 description: 详细说明 Elysia Admin 的整体目录结构,包括前端 Vue3 模块划分和后端 ElysiaJS 分层架构设计,帮助开发者快速了解项目组织方式。 --- # 项目结构 这里将详细说明 `Elysia Admin` 的整体结构,包括前端和后端的目录组织、文件功能及模块划分,能够帮助开发人员快速了解项目架构。 ## 整体结构 ``` elysia-admin/ ├── admin/ # Vue 3 前端 ├── server/ # Elysia + Bun 后端 ├── .ai/ # AI 开发指南(模块 SOP、代码示例等) ├── .cursor/ # Cursor 规则与 Skill ├── .claude/ # Claude 规则 ├── .codex/ # Codex 配置 ├── .kiro/ # Kiro Steering ├── .qoder/ # Qoder 规则 ├── .trae/ # Trae 规则 ├── .vscode/ # VS Code 工作区配置 ├── AGENTS.md # 项目级 AI 指引 ├── elysia-admin.code-workspace ├── LICENSE ├── README.md └── .gitignore ``` ## 前端结构 前端采用 `Vue 3` 框架构建,遵循模块化、组件化的设计原则。 ``` ├── src │ ├── api # API 接口相关代码 │ │ ├── auth.ts # 认证相关 API 接口定义(登录、注册、用户信息等) │ │ └── system-manage.ts # 系统管理相关 API 接口定义(菜单、用户、角色管理等) │ ├── App.vue # Vue 根组件,定义应用全局结构和入口 │ ├── assets # 静态资源目录 │ │ ├── images # 图片资源目录 │ │ ├── styles # 全局样式文件 │ │ │ ├── core # 核心样式(系统级样式) │ │ │ ├── custom # 自定义样式(业务级样式) │ │ │ └── index.scss # 样式入口文件 │ │ └── svg # SVG 相关资源 │ │ └── loading.ts # 加载动画 SVG 定义 │ ├── components # 组件目录 │ │ ├── business # 业务组件(业务相关自定义组件) │ │ │ └── comment-widget # 评论组件 │ │ └── core # 核心组件(系统级通用组件库) │ │ ├── banners # 横幅组件 │ │ ├── base # 基础组件 │ │ ├── cards # 卡片组件 │ │ ├── charts # 图表组件 │ │ ├── forms # 表单组件 │ │ ├── layouts # 布局组件 │ │ ├── media # 媒体组件 │ │ ├── others # 其他组件 │ │ ├── tables # 表格组件 │ │ ├── text-effect # 文本特效组件 │ │ ├── theme # 主题相关组件 │ │ ├── views # 视图组件 │ │ └── widget # 小部件组件 │ ├── config # 项目配置目录 │ │ ├── assets # 静态资源配置 │ │ │ └── images.ts # 图片资源路径配置 │ │ ├── modules # 模块化配置 │ │ │ ├── component.ts # 组件配置 │ │ │ ├── fastEnter.ts # 快捷入口配置 │ │ │ ├── festival.ts # 节日/活动配置 │ │ │ └── headerBar.ts # 顶部栏配置 │ │ ├── index.ts # 配置入口文件 │ │ └── setting.ts # 系统设置配置 │ ├── directives # Vue 自定义指令 │ │ ├── business # 业务指令 │ │ │ ├── highlight.ts # 高亮指令 │ │ │ └── ripple.ts # 波纹效果指令 │ │ ├── core # 核心指令 │ │ │ ├── auth.ts # 认证指令 │ │ │ └── roles.ts # 角色权限指令 │ │ └── index.ts # 指令入口文件 │ ├── enums # 枚举定义 │ │ ├── appEnum.ts # 应用级枚举(主题类型、语言类型等) │ │ └── formEnum.ts # 表单相关枚举(表单状态、验证规则等) │ ├── env.d.ts # TypeScript 环境声明文件 │ ├── hooks # Vue 3 Composable 函数(可复用逻辑) │ │ ├── core # 核心 Hooks │ │ │ ├── useAppMode.ts # 应用模式相关逻辑 │ │ │ ├── useAuth.ts # 认证相关逻辑 │ │ │ ├── useCeremony.ts # 节日/仪式相关逻辑 │ │ │ ├── useChart.ts # 图表相关逻辑 │ │ │ ├── useCommon.ts # 通用逻辑 │ │ │ ├── useFastEnter.ts # 快捷入口逻辑 │ │ │ ├── useHeaderBar.ts # 顶部栏逻辑 │ │ │ ├── useLayoutHeight.ts # 布局高度计算逻辑 │ │ │ ├── useTable.ts # 表格逻辑 │ │ │ ├── useTableColumns.ts # 表格列配置逻辑 │ │ │ ├── useTableHeight.ts # 表格高度计算逻辑 │ │ │ └── useTheme.ts # 主题切换逻辑 │ │ └── index.ts # Hooks 入口文件 │ ├── locales # 国际化(i18n)资源 │ │ ├── index.ts # 国际化入口文件 │ │ └── langs # 多语言文件 │ │ ├── en.json # 英文语言包 │ │ └── zh.json # 中文语言包 │ ├── main.ts # 项目主入口文件 │ ├── mock # Mock 数据目录 │ │ ├── json # JSON 格式 Mock 数据 │ │ │ └── chinaMap.json # 中国地图数据 │ │ ├── temp # 临时 Mock 数据 │ │ │ ├── articleList.ts # 文章列表数据 │ │ │ ├── commentDetail.ts # 评论详情数据 │ │ │ ├── commentList.ts # 评论列表数据 │ │ │ └── formData.ts # 表单数据 │ │ └── upgrade # 更新日志数据 │ │ └── changeLog.ts # 变更日志数据 │ ├── plugins # 插件配置 │ │ ├── echarts.ts # ECharts 图表库配置 │ │ └── index.ts # 插件入口文件 │ ├── router # Vue Router 路由相关代码 │ │ ├── core # 路由核心功能 │ │ │ ├── ComponentLoader.ts # 组件加载器 │ │ │ ├── IframeRouteManager.ts # Iframe 路由管理器 │ │ │ ├── MenuProcessor.ts # 菜单处理器 │ │ │ ├── RouteRegistry.ts # 路由注册器 │ │ │ ├── RouteTransformer.ts # 路由转换器 │ │ │ ├── RouteValidator.ts # 路由验证器 │ │ │ └── index.ts # 核心功能入口 │ │ ├── guards # 路由守卫 │ │ │ ├── afterEach.ts # 全局后置守卫 │ │ │ └── beforeEach.ts # 全局前置守卫 │ │ ├── modules # 路由模块定义 │ │ │ ├── article.ts # 文章模块路由 │ │ │ ├── dashboard.ts # 仪表盘路由 │ │ │ ├── examples.ts # 示例页面路由 │ │ │ ├── exception.ts # 异常页面路由 │ │ │ ├── help.ts # 帮助页面路由 │ │ │ ├── index.ts # 路由模块入口 │ │ │ ├── result.ts # 结果页面路由 │ │ │ ├── safeguard.ts # 安全防护路由 │ │ │ ├── system.ts # 系统管理路由 │ │ │ ├── template.ts # 模板页面路由 │ │ │ └── widgets.ts # 小组件路由 │ │ ├── routes # 路由配置 │ │ │ ├── asyncRoutes.ts # 异步路由(动态路由) │ │ │ └── staticRoutes.ts # 静态路由(固定路由) │ │ ├── index.ts # 路由主入口 │ │ └── routesAlias.ts # 路由别名定义 │ ├── store # Pinia 状态管理 │ │ ├── modules # 状态管理模块 │ │ │ ├── menu.ts # 菜单状态管理 │ │ │ ├── setting.ts # 设置状态管理 │ │ │ ├── table.ts # 表格状态管理 │ │ │ ├── user.ts # 用户状态管理 │ │ │ └── worktab.ts # 工作标签页状态管理 │ │ └── index.ts # Pinia 入口文件 │ ├── types # TypeScript 类型定义 │ │ ├── api # API 相关类型 │ │ │ └── api.d.ts # API 接口类型定义 │ │ ├── common # 通用类型定义 │ │ │ ├── index.ts # 通用类型入口 │ │ │ └── response.ts # 响应类型定义 │ │ ├── component # 组件相关类型 │ │ │ ├── chart.ts # 图表组件类型 │ │ │ └── index.ts # 组件类型入口 │ │ ├── config # 配置相关类型 │ │ │ └── index.ts # 配置类型定义 │ │ ├── import # 自动导入类型声明 │ │ │ ├── auto-imports.d.ts # 自动导入的函数类型 │ │ │ └── components.d.ts # 自动导入的组件类型 │ │ ├── router # 路由相关类型 │ │ │ └── index.ts # 路由类型定义 │ │ ├── store # 状态管理相关类型 │ │ │ └── index.ts # Store 类型定义 │ │ └── index.ts # 类型定义总入口 │ ├── utils # 工具函数目录 │ │ ├── constants # 常量定义 │ │ │ ├── index.ts # 常量入口 │ │ │ └── links.ts # 链接常量 │ │ ├── form # 表单相关工具 │ │ │ ├── index.ts # 表单工具入口 │ │ │ ├── responsive.ts # 响应式表单工具 │ │ │ └── validator.ts # 表单验证工具 │ │ ├── http # HTTP 请求工具 │ │ │ ├── error.ts # 错误处理 │ │ │ ├── index.ts # HTTP 工具入口 │ │ │ └── status.ts # 状态码处理 │ │ ├── navigation # 导航相关工具 │ │ │ ├── index.ts # 导航工具入口 │ │ │ ├── jump.ts # 页面跳转工具 │ │ │ ├── route.ts # 路由工具 │ │ │ └── worktab.ts # 工作标签页工具 │ │ ├── storage # 存储相关工具 │ │ │ ├── index.ts # 存储工具入口 │ │ │ ├── storage-config.ts # 存储配置 │ │ │ ├── storage-key-manager.ts # 存储键管理 │ │ │ └── storage.ts # 存储工具实现 │ │ ├── sys # 系统相关工具 │ │ │ ├── console.ts # 控制台工具 │ │ │ ├── error-handle.ts # 错误处理 │ │ │ ├── index.ts # 系统工具入口 │ │ │ ├── mittBus.ts # 事件总线 │ │ │ └── upgrade.ts # 升级相关工具 │ │ ├── table # 表格相关工具 │ │ │ ├── tableCache.ts # 表格缓存 │ │ │ ├── tableConfig.ts # 表格配置 │ │ │ └── tableUtils.ts # 表格工具函数 │ │ ├── ui # UI 相关工具 │ │ │ ├── animation.ts # 动画工具 │ │ │ ├── colors.ts # 颜色工具 │ │ │ ├── emojo.ts # 表情工具 │ │ │ ├── index.ts # UI 工具入口 │ │ │ ├── loading.ts # 加载动画工具 │ │ │ └── tabs.ts # 标签页工具 │ │ ├── index.ts # 工具函数总入口 │ │ └── router.ts # 路由工具函数 │ └── views # 页面组件目录(含 views/dev/sql 开发 SQL 控制台,仅开发环境使用) ├── tsconfig.json # TypeScript 配置文件 ├── .env # 环境变量配置文件 ├── .env.development # 开发环境变量配置文件 ├── .env.production # 生产环境变量配置文件 ├── .gitattributes # Git 属性文件 ├── .gitignore # Git 忽略文件 ├── .prettierignore # Prettier 忽略文件 ├── .prettierrc # Prettier 配置文件 ├── .stylelintignore # Stylelint 忽略文件 ├── .stylelintrc # Stylelint 配置文件 ├── commitlint.config.cjs # Commitlint 配置文件 ├── eslint.config.mjs # ESLint 配置文件 ├── index.html # HTML 入口文件 ├── package.json # 项目依赖配置文件 ├── pnpm-lock.yaml # pnpm 锁定文件 └── vite.config.ts # Vite 配置文件 ``` ## 后端结构 后端采用 `Elysia.js` 框架构建,遵循分层架构设计。 ``` ├── database # 数据库定义 │ ├── schema # 业务表 Schema │ ├── sql # 初始化 SQL、模块 handoff SQL │ └── base-schema.ts # 公共字段定义 ├── script # 构建 / 注册表 / 种子脚本 ├── test # 单元测试 │ ├── core │ ├── shared │ └── types ├── public # 静态资源 ├── src │ ├── config # 环境配置 │ ├── constants # 枚举与字典常量 │ ├── core # 核心基础设施 │ │ └── database # PG / Redis / Repository / 事务 │ ├── infrastructure # 外部集成 │ │ ├── clients # 第三方客户端 │ │ │ └── payment # 支付客户端 │ │ │ └── providers # alipay / paypal / wechat / crypto │ │ ├── cron # 进程内定时任务 │ │ ├── queue # BullMQ 队列 │ │ │ ├── config │ │ │ ├── core │ │ │ ├── runtime │ │ │ └── queues # flow-buffer / system-cron / trade-order │ │ └── storage # 对象存储 │ │ └── providers # cos / kodo / oss / rustfs │ ├── middleware # 中间件 │ │ └── guards # 认证 / 权限 / IP 黑名单 / 限流 │ ├── modules # 业务模块 │ │ ├── auth # 认证 │ │ ├── business-merchant # 商户管理 │ │ ├── business-orders # 订单管理 │ │ ├── business-payments # 支付管理 │ │ ├── business-refund # 退款管理 │ │ ├── crypto # 加解密公钥(请求/响应加密开关) │ │ ├── dev-sql # 开发 SQL 控制台(仅 development,生产不注册) │ │ ├── monitor-cache # 缓存监控 │ │ ├── monitor-job # 定时任务监控 │ │ ├── monitor-online # 在线用户监控 │ │ ├── system-api # API 接口管理 │ │ ├── system-dept # 部门管理 │ │ ├── system-deploy # 站点部署(Nginx 配置与证书) │ │ ├── system-dict # 字典管理 │ │ ├── system-ip-black # IP 黑名单 │ │ ├── system-login-log # 登录日志 │ │ ├── system-menu # 菜单管理 │ │ ├── system-notice # 通知公告 │ │ ├── system-oper-log # 操作日志 │ │ ├── system-role # 角色管理 │ │ ├── system-storage # 存储配置 │ │ └── system-user # 用户管理 │ ├── worker-sandbox # 队列沙箱任务注册 │ ├── shared # 共享工具函数 │ ├── types # 类型定义 │ ├── app.ts # 应用入口 │ └── index.ts # 总入口 ├── Dockerfile ├── drizzle.config.ts ├── package.json ├── bun.lock ├── tsconfig.json └── README.md ``` --- URL: "https://elysia-admin.top/ecosystem/business-packages.html" LLMS_URL: "https://elysia-admin.top/\\ecosystem\\business-packages.md" title: "业务包 - Elysia Admin" description: "社区贡献的可复用业务模块集合,快速查找并复用常见业务实现,减少重复造轮子。" layout: false sidebar: false navbar: false --- --- title: 业务包 - Elysia Admin description: 社区贡献的可复用业务模块集合,快速查找并复用常见业务实现,减少重复造轮子。 layout: false sidebar: false navbar: false --- --- URL: "https://elysia-admin.top/ecosystem/related-links.html" LLMS_URL: "https://elysia-admin.top/\\ecosystem\\related-links.md" title: "相关链接 - Elysia Admin" description: "Elysia Admin 开发与维护涉及的核心技术栈参考链接,包括 ElysiaJS、Bun、DrizzleORM、BullMQ、PostgreSQL、Redis、Docker 等官方文档。" --- --- title: 相关链接 - Elysia Admin description: Elysia Admin 开发与维护涉及的核心技术栈参考链接,包括 ElysiaJS、Bun、DrizzleORM、BullMQ、PostgreSQL、Redis、Docker 等官方文档。 --- # 相关链接 本章为 `Elysia Admin` 开发与维护过程中涉及的核心技术栈、工具及规范提供权威参考。 ## 官方文档 权威的框架与运行时手册,涵盖核心 API 与最佳实践。 1. **Art Design Pro Documentation** - URL: [https://www.artd.pro/docs/zh/](https://www.artd.pro/docs/zh/) - 摘要: 基于 Ant Design Vue 的中后台前端解决方案,提供丰富的组件与页面模板。 - 访问日期: 2026-04-09 2. **Bun Official Documentation** - URL: [https://bun.sh/docs](https://bun.sh/docs) - 摘要: Bun 运行时官方文档,包含高性能 HTTP 服务、包管理及测试运行器的详细说明。 - 访问日期: 2026-04-09 3. **ElysiaJS Official Documentation** - URL: [https://elysiajs.com/](https://elysiajs.com/) - 摘要: ElysiaJS 框架文档,专注于 TypeScript 深度优化与极高性能的 Web 路由实现。 - 访问日期: 2026-04-09 4. **BullMQ Documentation** - URL: [https://docs.bullmq.io/](https://docs.bullmq.io/) - 摘要: BullMQ 是一个基于 Redis 构建,专为 Node.js 和 Python 设计的高性能、分布式消息队列与任务批处理库。 - 访问日期: 2026-04-15 ## 数据库与缓存 数据持久化与性能优化的技术支柱。 1. **Drizzle ORM Documentation** - URL: [https://orm.drizzle.team/docs/overview](https://orm.drizzle.team/docs/overview) - 摘要: 轻量级、类型安全的 TypeScript ORM,支持 PostgreSQL 与 Drizzle Kit 迁移工具。 - 访问日期: 2026-04-09 2. **PostgreSQL Official Manual** - URL: [https://www.postgresql.org/docs/current/index.html](https://www.postgresql.org/docs/current/index.html) - 摘要: 全球领先的开源关系型数据库官方手册,提供详尽的 SQL 参考与配置指南。 - 访问日期: 2026-04-09 3. **Redis Documentation** - URL: [https://redis.io/docs/latest/](https://redis.io/docs/latest/) - 摘要: 高性能内存数据库,本项目用于缓存加速、分布式锁及限流机制。 - 访问日期: 2026-04-09 ## 存储与工具 提升开发效率与系统可用性的辅助设施。 1. **Docker Documentation** - URL: [https://docs.docker.com/](https://docs.docker.com/) - 摘要: 容器化部署官方文档,涵盖镜像构建、多容器编排及生产环境配置。 - 访问日期: 2026-04-09 2. **RustFS Repository** - URL: [https://github.com/Maurice-Yu/RustFS](https://github.com/Maurice-Yu/RustFS) - 摘要: 兼容 S3 协议的高性能开源对象存储,项目默认推荐的私有部署存储方案。 - 访问日期: 2026-04-09 ## 开发规范与社区 保持代码质量与获取最新动态的渠道。 1. **Bun Discord Community** - URL: [https://bun.sh/discord](https://bun.sh/discord) - 摘要: Bun 官方活跃社区,实时获取运行时更新及技术解答。 - 访问日期: 2026-04-09 2. **ElysiaJS GitHub Discussions** - URL: [https://github.com/elysiajs/elysia/discussions](https://github.com/elysiajs/elysia/discussions) - 摘要: Elysia 官方论坛,涵盖功能提案、疑难排查及生态插件讨论。 - 访问日期: 2026-04-09 3. **TypeScript Handbook** - URL: [https://www.typescriptlang.org/docs/handbook/intro.html](https://www.typescriptlang.org/docs/handbook/intro.html) - 摘要: 现代 TypeScript 语法与类型系统全手册,项目类型安全的基础参考。 - 访问日期: 2026-04-09 ## 扩展阅读 深入底层原理与行业最佳实践。 1. **RESTful API Design Best Practices** - URL: [https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design](https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design) - 摘要: 微软 Azure 团队总结的 API 设计最佳实践,确保接口简洁且易于维护。 - 访问日期: 2026-04-09 2. **S3-compatible Storage Compatibility List** - URL: [https://github.com/gaul/s3proxy/wiki/S3-Compatibility-List](https://github.com/gaul/s3proxy/wiki/S3-Compatibility-List) - 摘要: 各类 S3 兼容存储方案的深度对比,辅助进行存储服务选型。 - 访问日期: 2026-04-09 --- URL: "https://elysia-admin.top/guide/ai-guide.html" LLMS_URL: "https://elysia-admin.top/\\guide\\ai-guide.md" title: "AI 开发指南" description: "介绍 Cursor、Qoder、Trae、Kiro、Codex、Claude Code 的项目规则与 `.ai/` 规范文档,配合提示词让 AI 按 Elysia Admin 风格生成前后端模块代码。" --- --- title: AI 开发指南 description: 介绍 Cursor、Qoder、Trae、Kiro、Codex、Claude Code 的项目规则与 `.ai/` 规范文档,配合提示词让 AI 按 Elysia Admin 风格生成前后端模块代码。 --- # AI 开发指南 在本仓库里用 AI 做业务模块,你不需要每次粘贴长篇模板。打开项目后,IDE 会自动加载规则;你只需发一条合适的提示词,AI 就会按 Elysia Admin 的目录结构和代码风格来生成。 ## 工作原理 项目根目录下有多套 Agent 规则(Cursor、Qoder、Trae、Kiro、Codex、Claude Code),语义一致。AI 对话时会读取这些规则,以及 `.ai/` 里的工作流与代码模板。 ```mermaid flowchart LR A[你发提示词] --> B[IDE 加载规则] B --> C[AI 读 .ai/ 规范] C --> D[生成代码 + SQL 文件] D --> E[你手动执行 handoff SQL / 可选 db:push] ``` 提示词里带上触发语,更容易匹配项目 Skill: - `按 module dev workflow` 或 `走完整 SOP` - 脚手架相关:`先用脚手架` / `脚手架已生成` - 需要菜单时:`含菜单权限和 handoff SQL` - 多模块 / 完整项目:`拆分任务` / `分阶段执行` / `大任务拆解` / `完整项目规划` ## 我该从哪开始 | 场景 | 做法 | |------|------| | Schema 已建好,要标准 CRUD | 先跑脚手架,再用 AI 做增量(见下节) | | 从零生成整个模块 | 用「一句话」提示词,AI 会建议是否先跑脚手架 | | 字段多、只改前端或后端 | 用「结构化」提示词,分块写清需求 | | 多个业务模块 / 子系统 | 先出 `feature-tasks` 任务包,你选阶段后再写代码(见「大任务分阶段」) | | 用 VS Code 本地开发 | 打开 `elysia-admin.code-workspace`,任务 `Dev: Full Stack` 启前后端 | | 用 ChatGPT / Claude 网页版 | 见文末「网页版 AI」 | | 查规范文件 | 见「规范文档」 | ## 大任务分阶段 当需求一次点名**多个业务模块**或完整子系统时,不要直接开写代码。先让 AI 按 `.ai/AI_PHASED_TASKS.md` 拆成可交接的任务包,你选定某一阶段(或说「继续 / 下一个」)后再实现。 **何时用:** 多模块 / 子系统 / 你说了 `拆分任务`、`分阶段执行`、`大任务拆解`、`完整项目规划` 等。 **何时不用:** 单个业务模块、单点修 bug、局部增强 —— 仍走 Skill / 脚手架 / `AI_MODULE_WORKFLOW`。 任务包写在**当前 IDE** 的配置目录下(不要落到仓库根): | IDE | 目录 | |-----|------| | Cursor | `.cursor/feature-tasks/{slug}/` | | Claude Code | `.claude/feature-tasks/{slug}/` | | Codex | `.codex/feature-tasks/{slug}/` | | Qoder | `.qoder/feature-tasks/{slug}/` | | Trae | `.trae/feature-tasks/{slug}/` | | Kiro | `.kiro/feature-tasks/{slug}/` | 每个特性一个 `kebab-case` slug,目录内含 `CHECKLIST.md`(总清单)与 `01-*.md`、`02-*.md`…(分阶段详情)。同一功能只写在一个 IDE 目录,不要跨工具双写。 ## 脚手架 + AI 单表标准 CRUD、且 Drizzle Schema 已存在时,推荐先跑脚手架,再让 AI 补业务逻辑和排版。Agent 细则见 `.ai/AI_MODULE_SCAFFOLD.md`。 在 `server/` 目录执行: ```bash cd server bun run create:module business-goods --tag 商品管理 bun run create:page business goods --tag 商品管理 ``` 可选参数:`--schema business_goods` 指定表名,`--dry-run` 仅预览不写文件。完整说明见 [内置命令 - 模块脚手架](/architecture/commands#模块脚手架命令)。 脚手架会生成后端 `route.ts` / `dto.ts` / `handle.ts` 骨架,以及前端 types、api、views。接下来把下面「脚手架之后」的提示词发给 AI,让它补校验、表格排版、字典和 handoff SQL。 最后由你在本机**手动执行** `server/database/sql/business-goods-init.sql`,并在后台把菜单 component 配成 `/business/goods/index`。 若 AI 修改了 Drizzle Schema,它可能会询问是否代跑 `bun run db:push`(在 `server/` 目录)。同意后偏好会写入 `.ai/dev-preferences.local.md`(不提交 Git),后续默认可代跑。 ![输出截图](/guide/4.png) ## 提示词 示例模块均为 `business-goods`。复制后把模块名、字段、父菜单换成你的业务。 ### 脚手架之后 适合 Schema 已有、标准 CRUD。先跑脚手架,AI 只补增量,省 token。 ``` business-goods 已执行 create:module 和 create:page,请按 AI_MODULE_SCAFFOLD 增量规范: 1. handle 补价格/库存等业务校验; 2. 表格和弹窗按 AI_PAGE_QUALITY 排版,status 接字典; 3. 输出 server/database/sql/business-goods-init.sql(菜单+按钮权限)。 不要重写 route/dto/api 骨架。 ``` ### 一句话 日常最省事。AI 会在计划里补默认值;无表时会先问你,不会擅自改 drizzle。 ``` 按 module dev workflow 做商品管理(business-goods):名称、价格、库存、备注;全栈 CRUD,要菜单权限、字典和 server/database/sql/business-goods-init.sql。 若 Schema 已有,请先建议运行 create:module + create:page。 ``` ### 两句补充 需要指定父菜单或字段类型时用。减少 AI 猜测。 ``` business-goods 全栈模块,字段 name/price/stock/remark,status 用 boolean。 表若没有先给 schema 方案;菜单挂「业务管理」(/business)下;输出 handoff SQL。 ``` ### 结构化 字段很多、或只要前端 / 只要后端时用。 **仅后端** ``` 按 module dev workflow,仅后端:商品管理模块 business-goods - 字段:name、price、stock、remark、status(boolean) - 标准 CRUD(创建、列表、详情、更新、软删除) - 登录认证 + 操作日志;无需定时任务 - 暂不需要前端和 handoff SQL(或:需要 SQL 补菜单权限) ``` **仅前端** ``` 按 module dev workflow,仅前端:business/goods 商品管理页 - types/api/business-goods.d.ts、api/business/goods.ts - index.vue + goods-search.vue + goods-dialog.vue - 遵循 AI_PAGE_QUALITY:art-full-height、useDictStore,勿硬编码业务枚举 - 权限与后端 business:goods:* 一致 ``` **全栈(字段写全)** ``` 按 module dev workflow,全栈商品管理 business-goods: 【后端】server/src/modules/business-goods/ - 表:goodsId、name、price、stock、status(boolean)、remark;主表 + BaseSchema - 标准 CRUD;meta.permission:business:goods:create|query|update|delete 【前端】admin/src/ - types/api/business-goods.d.ts、api/business/goods.ts - views/business/goods/(index + goods-search + goods-dialog) 【运维】 - 父菜单:/business(业务管理) - 字典:若无 `goods_status` 则在 SQL 中补(`dict_type` 短且语义化,优先 2–3 段;status 用 boolean 可省略字典) - 生成 server/database/sql/business-goods-init.sql(菜单/按钮/角色,MCP 查库优先) ``` ### 常见变体 | 需求 | 在提示词里加上 | |------|----------------| | 只要代码,不要 SQL | `暂不需要菜单和 handoff SQL` | | 只补菜单权限 | `补 business-goods 菜单权限 SQL,查库后写入 handoff 文件` | | 表已存在 | `schema 已有 business_goods,不要改 drizzle` | | 允许 AI 代跑 db:push | `允许 AI 在 server/ 执行 db:push`(或同意后会写入本地偏好文件) | | 要定时任务 | `需要 task.ts`(见下节说明) | 定时 / 队列任务在 `task.ts` 定义函数,于 `server/src/worker-sandbox/` 注册。详见 [定时任务](./cron)、[队列](./queue)。 ## AI 会交付什么 发完提示词,AI 通常按下面顺序产出。无表时会先提案 Schema,**等你同意**再改 drizzle。 | 产出 | 说明 | |------|------| | 计划 | 5 行内说明 + 待改文件清单 | | Schema | 查 `server/database/schema/`;无表则提案,**须你同意**后再改 drizzle | | `db:push` | Schema 变更后,AI 可能询问是否代跑(见 `.ai/dev-preferences.local.example.md`) | | 代码 | 后端四文件;全栈含前端 types / api / views | | 字典 | 对齐 `system_dict_*` 或写进 handoff SQL | | SQL 文件 | `server/database/sql/{模块}-init.sql`(**仅生成文件,须你手动执行**) | | 前端 | `art-full-height`、`useDictStore`、弹窗排版 | | 说明 | MCP 查库摘要,或「未连 Postgres MCP」 | AI **不会**擅自 `git add` / `commit`,也**不会**用脚本、psql、MCP 写入等方式代跑 handoff SQL。提交与 SQL 执行均由你自己完成。 配置 Postgres MCP 后,菜单和字典 SQL 更准确。查运行时数据(dict、menu、权限)**优先 MCP 只读**;**不要**让 AI 读 `server/database/sql/pg.sql`(全库备份,可能与当前库不一致)。配置与纪律见上方 **MCP 入口** 或 [MCP 服务](./mcp),Agent 细则见 `.ai/AI_MCP_SETUP.md`。 ## 规范文档 `.ai/` 目录存放 AI 工作流与代码模板。工作流文档为英文(便于 Agent 理解);本页与提示词为中文,给人阅读。 | 文件 | 用途 | |------|------| | `AI_MODULE_WORKFLOW.md` | 主流程:Schema → 脚手架 → 代码 → SQL | | `AI_MODULE_SCAFFOLD.md` | `create:module` + `create:page` 细则 | | `AI_PHASED_TASKS.md` | 多模块 / 完整项目分阶段任务包 | | `AI_CODE_EXAMPLES.md` | 代码模板**索引**(按任务再打开对应详版) | | `AI_CODE_EXAMPLES_BACKEND.md` | 后端 `dto` / `handle` / `route` 模板 | | `AI_CODE_EXAMPLES_FRONTEND.md` | 前端 types / api / views 模板 | | `AI_PAGE_QUALITY.md` | 列表、搜索、字典、排版 | | `AI_UI_LAYOUT.md` | 弹窗、抽屉表单布局 | | `AI_HANDOFF_SQL.md` | 合并 SQL、菜单查库 | | `AI_SCHEMA_GUIDE.md` | 建表规范、`db:push` 流程 | | `AI_MCP_SETUP.md` | Postgres、Chrome DevTools MCP | | `AI_CONTEXT_CAPSULE.md` | 一页速查 | | `dev-preferences.local.example.md` | 本地偏好示例(如允许 AI 代跑 `db:push`) | Handoff SQL 输出路径:`server/database/sql/{模块名}-init.sql`(**开发者手动执行**) Cursor Skill:`.cursor/skills/elysia-module-dev/SKILL.md` Qoder Skill:`.qoder/skills/elysia-module-dev/SKILL.md` ## IDE 规则 多处 Agent 规则与 `.cursor/rules/` 语义一致。改架构或读文件纪律时,请同步 Cursor、Qoder、Trae、Kiro、Codex、Claude Code(各目录 README 有说明)。 ### Cursor 打开仓库即可,无需额外配置。 | 文件 | 何时生效 | 作用 | |------|----------|------| | `general.mdc` | 始终 | 架构、依赖方向、读文件纪律 | | `backend.mdc` | 编辑 `server/src/**` | 后端模块、repository、`db:push` | | `frontend.mdc` | 编辑 `admin/src/**` | 前端页面、useTable | Skill:`.cursor/skills/elysia-module-dev/SKILL.md` ### Qoder 与 Cursor 语义对齐,规则在 `.qoder/` 目录。 | 文件 | 对应 Cursor | 作用 | |------|-------------|------| | `.qoder/rules/general.md` | `general.mdc` | 始终生效 | | `.qoder/rules/backend.md` | `backend.mdc` | 编辑 `server/src/**` | | `.qoder/rules/frontend.md` | `frontend.mdc` | 编辑 `admin/src/**` | | `.qoder/skills/elysia-module-dev/SKILL.md` | Cursor Skill | 全栈模块开发 Skill | 说明见 `.qoder/README.md`。根目录 `AGENTS.md` 也会被 Qoder 识别,但 `.qoder/rules/` 优先级更高。 ### Trae | 文件 | 对应 Cursor | |------|-------------| | `.trae/rules/general.md` | `general.mdc` | | `.trae/rules/backend.md` | `backend.mdc` | | `.trae/rules/frontend.md` | `frontend.mdc` | ### Kiro | 文件 | 作用 | |------|------| | `product.md` / `structure.md` / `tech.md` | 始终:产品、目录、技术栈 | | `frontend-vue.md` / `backend-elysia.md` | 编辑前后端时加载 | | `ai-ops-supplement.md` | 工作流、MCP、SQL、排版 | 通过 `#[[file:.ai/...]]` 引用大文档,避免 steering 里重复贴模板。 ### Codex | 文件 | 对应 Cursor | 作用 | |------|-------------|------| | `AGENTS.md`(仓库根) | `general.mdc` | 全局架构、模块触发语 | | `server/AGENTS.md` | `backend.mdc` | 在 `server/` 下工作时叠加 | | `admin/AGENTS.md` | `frontend.mdc` | 在 `admin/` 下工作时叠加 | Codex 从 Git 根向当前目录逐级合并 `AGENTS.md`。验证:`codex --print-instructions`。说明见 `.codex/README.md`。 ### Claude Code | 文件 | 对应 Cursor | 作用 | |------|-------------|------| | `.claude/CLAUDE.md` | — | 项目入口、命令、`.ai/` 索引 | | `.claude/rules/general.md` | `general.mdc` | 始终生效 | | `.claude/rules/backend.md` | `backend.mdc` | 编辑 `server/src/**` 时懒加载 | | `.claude/rules/frontend.md` | `frontend.mdc` | 编辑 `admin/src/**` 时懒加载 | 说明见 `.claude/README.md`。个人偏好可写 `CLAUDE.local.md`(不提交)。 ### VS Code 编辑器 | 文件 | 作用 | |------|------| | `.vscode/settings.json` | 格式化、ESLint、Prettier | | `.vscode/extensions.json` | 推荐 Volar、ESLint、Prettier、Bun | | `.vscode/tasks.json` | `Server: dev`、`Admin: dev`、`Dev: Full Stack` | | `.vscode/launch.json` | Bun 调试 `server/src/index.ts` | | `elysia-admin.code-workspace` | 多根工作区(admin / server / docs) | ## 网页版 AI 未在本仓库 IDE 中打开项目、无法自动加载规则时,适用 ChatGPT、Claude 网页版等。 先把 `.ai/README.md` 贴进对话,再按需贴 `AI_MODULE_WORKFLOW.md`、`AI_CODE_EXAMPLES.md`。然后贴下面短前缀和你的需求,**不要**再贴 100+ 行内联代码。 ``` # elysia-admin 约束摘要 - 前后端分离:Vue3 + Elysia/Bun;模块 server/src/modules/{group}-{name}/(dto/handle/route/task) - 代码模板以 AI_CODE_EXAMPLES.md 为准;handle 不要包 try/catch,业务错误用 BaseResultData.fail(4xx, msg) - 前端:useTable + ArtSearchBar + ArtForm;art-full-height;业务枚举用 useDictStore - 全栈:查 schema → 代码 → server/database/sql/{module}-init.sql;改表须先征得同意 - handoff SQL 仅生成文件,开发者手动执行;禁止 AI 用脚本/psql/MCP 代跑 - 查 dict/menu 优先 Postgres MCP;禁止读 server/database/sql/pg.sql(备份,可能过期) - Schema 变更后 AI 可询问是否代跑 db:push;同意写入 .ai/dev-preferences.local.md - 按 AI_MODULE_WORKFLOW 的交付格式输出 ``` 示例需求: ``` 按 module dev workflow,创建 business-goods 全栈:goodsId、name、price、stock、status、remark; 含菜单权限与 server/database/sql/business-goods-init.sql。 ``` 无法读仓库时,至少粘贴 **`AI_CODE_EXAMPLES.md` 全文** 后再发需求。 ## 你需要手动做的 - 在本机**手动执行** **`server/database/sql/{模块}-init.sql`**(AI 只生成文件,不会代跑;Postgres MCP 对 AI 一般只读) - Schema 变更后,若未授权 AI 代跑,请在 `server/` 自行执行 **`bun run db:push`** - 确认 AI 计划里的默认值是否符合业务(尤其「一句话」提示词) - Git 提交由**你自己**完成 ### 数据库相关纪律 | 事项 | 说明 | |------|------| | Handoff SQL | 只能你手动在本地/测试库执行;AI 禁止写临时代码、psql、MCP 写入来代跑 | | 查 dict / menu | 优先 Postgres MCP 只读;**勿**用 `server/database/sql/pg.sql`(备份快照,可能与现库不一致) | | `db:push` 偏好 | 首次 AI 会询问;同意后写入 `.ai/dev-preferences.local.md`(gitignore),后续可默认代跑 | AI 读文件有纪律:新建模块只读一个参考(后端 `system-api/`、前端 `system/user/`);改现有文件只读该文件;不读 `node_modules/`、`dist/`、`server/database/sql/pg.sql`。跑过脚手架后,只读生成文件和当前 Schema 即可。 列表导出优先用 `ArtExcelExport`,不必单独为导出造后端(除非业务需要)。 --- URL: "https://elysia-admin.top/guide/cache.html" LLMS_URL: "https://elysia-admin.top/\\guide\\cache.md" title: "缓存操作 - Elysia Admin 指南" description: "介绍 Elysia Admin 中封装的 Redis 缓存操作方法,包括基础的 Set/Get/Del,以及防缓存击穿的 WithCache、CacheInsert、CacheUpdate 等组合操作。" --- --- title: 缓存操作 - Elysia Admin 指南 description: 介绍 Elysia Admin 中封装的 Redis 缓存操作方法,包括基础的 Set/Get/Del,以及防缓存击穿的 WithCache、CacheInsert、CacheUpdate 等组合操作。 --- # 缓存操作 项目通过 Redis 做数据缓存,减少重复查库。基础读写走 `@/core/database/redis`,带防击穿和列表增量维护的组合操作走 `@/core/cache`。 缓存键建议用 `CacheEnum`(`@/constants/enum`)统一管理,避免硬编码字符串散落各处。字典、部门树等模块已按此方式使用。 ```mermaid flowchart LR H[handle.ts] --> R[redis Set/Get/Del] H --> C[WithCache 等组合函数] C --> R R --> Redis[(Redis)] ``` ## 基础 API 从 `@/core/database/redis` 导入。值会自动 JSON 序列化 / 反序列化。 | 函数 | 用途 | |------|------| | `Set(key, value, expire?)` | 写入缓存,`expire` 为秒;不传则保留已有 TTL | | `SetMulti(items)` | 批量写入,`items` 为 `{ key, value, expire? }[]` | | `Get(key)` | 读取,不存在返回 `null` | | `Del(key \| keys)` | 删除单个或批量 | | `Keys(pattern)` | 按前缀或通配符查找键 | ```ts import { Set, Get, Del } from '@/core/database/redis'; await Set('user:1', { name: 'John', age: 30 }, 60); const user = await Get('user:1'); await Del(['user:1', 'user:2']); ``` ```ts import { SetMulti } from '@/core/database/redis'; await SetMulti([ { key: 'user:1', value: { name: 'John' }, expire: 60 }, { key: 'user:2', value: { name: 'Jane' }, expire: 60 }, ]); ``` 业务数据变更后记得主动 `Del` 相关键。例如字典模块在增删改后会清除 `CacheEnum.DICT_TYPE` 和对应的 `DICT_DATA` 键,避免读到过期数据。 ## WithCache `WithCache` 封装「先读缓存,未命中再查库并回写」的逻辑,并用分布式锁防止热点键过期时大量请求同时穿透到数据库。 ```mermaid flowchart TD A[读 Redis] -->|命中| Z[返回缓存] A -->|未命中| B[尝试获取锁] B -->|成功| C[双重检查缓存] C --> D[执行 dbQueryFn] D --> E[写入 Redis] E --> Z B -->|失败| F[短暂等待重试] F -->|读到缓存| Z F -->|仍无缓存| G[直接查库] G --> Z ``` ```ts [handle.ts] import { WithCache } from '@/core/cache'; import { CacheEnum } from '@/constants/enum'; import { CreateQueryBuilder, FindAll } from '@/core/database/repository'; import { systemDictTypeSchema } from '@database/schema/system_dict'; const data = await WithCache( CacheEnum.DICT_TYPE, async () => { const where = CreateQueryBuilder(systemDictTypeSchema).eq('delFlag', false).build(); return await FindAll(systemDictTypeSchema, where); }, 300, // 过期秒数,默认取 config.app.baseCacheTime ); ``` | 参数 | 说明 | 默认 | |------|------|------| | `cacheKey` | Redis 键 | — | | `dbQueryFn` | 缓存未命中时执行的查库函数 | — | | `expire` | 过期时间(秒) | `config.app.baseCacheTime` | | `lockTtl` | 分布式锁 TTL(秒) | 10 | | `retryTimes` | 抢锁失败重试次数 | 3 | | `retryDelay` | 重试间隔(毫秒) | 100 | 获锁方回写缓存后,其他等待的请求会通过双重检查或重试读到新值;重试超时则直接查库兜底。 ## 列表缓存维护 `CacheInsert`、`CacheUpdate`、`CacheDelete` 用于在**不整键失效**的前提下,增量维护已缓存的**数组**数据。适合列表类缓存;结构复杂或一致性要求高时,直接 `Del` 后让 `WithCache` 重建更稳妥。 从 `@/core/cache` 导入: | 函数 | 用途 | |------|------| | `CacheInsert(cacheKey, data)` | 向数组末尾追加一项;缓存不存在则新建 `[data]` | | `CacheUpdate(cacheKey, key, data)` | 按 `key` 字段匹配并合并更新;无匹配项则追加 | | `CacheDelete(cacheKey, key, values)` | 按 `key` 字段值数组过滤删除;删空则移除整个键 | ```ts import { CacheInsert, CacheUpdate, CacheDelete } from '@/core/cache'; // 缓存结构: [{ id: 1, name: 'A' }, { id: 2, name: 'B' }] await CacheInsert('users:list', { id: 3, name: 'New User' }); await CacheUpdate('users:list', 'id', { id: 1, name: 'Updated', age: 25 }); await CacheDelete('users:list', 'id', [1, 2]); ``` 三个函数均返回 `boolean` 表示是否操作成功,内部异常会记日志并返回 `false`。 ## 使用建议 - 键名走 `CacheEnum`,新增业务缓存时在 `constants/enum` 里补常量 - 写操作后主动失效相关键,不要依赖 TTL 兜底业务一致性 - 高频读、低频写的数据适合 `WithCache`;写多读少的场景慎用列表增量维护 - 后台「系统监控 → 缓存管理」可查看 `CacheEnum` 中注册的缓存键(见 `monitor-cache` 模块) --- URL: "https://elysia-admin.top/guide/cron.html" LLMS_URL: "https://elysia-admin.top/\\guide\\cron.md" title: "定时任务 - Elysia Admin 指南" description: "详细介绍 Elysia Admin 中基于 BullMQ Sandboxed Processors 的定时任务创建、注册、构建及后台管理配置流程,支持多实例高可用部署。" --- --- title: 定时任务 - Elysia Admin 指南 description: 详细介绍 Elysia Admin 中基于 BullMQ Sandboxed Processors 的定时任务创建、注册、构建及后台管理配置流程,支持多实例高可用部署。 --- # 定时任务 定时任务用于按 Cron 表达式周期执行业务逻辑,例如清理过期数据、同步外部状态。系统基于 **BullMQ Sandboxed Processors** 实现:每个任务在独立子进程中运行,不会阻塞主进程的 HTTP 事件循环。 整体链路如下。后台配置的任务名、Cron 和参数会写入队列调度;Worker 消费后,经 `worker-sandbox` 注册表找到 `task.ts` 里的函数并执行。 ```mermaid flowchart TD subgraph 代码侧 T[task.ts 定义函数] --> SB[worker-sandbox 注册] SB --> P[processor 子进程] end subgraph 运行侧 UI[后台定时任务] --> Q[BullMQ 队列] Q --> W[Worker 消费] W --> P P --> Fn[执行业务函数] end ``` 与 [队列](./queue) 里的异步任务共用同一套 Redis 与 Worker 模型,区别只是触发来源:定时任务由 Cron 调度,异步任务由业务代码 `addJob` 投递。 ## 任务函数 在业务模块的 `task.ts` 中定义并导出任务函数。参数顺序和类型需与后台配置的「任务参数」一致。 ```ts [task.ts] // server/src/modules/monitor-job/task.ts import { logger } from '@/shared/logger'; export function jobDemo(name: string, age?: number, isActive?: boolean) { logger.info(`jobDemo 执行 - 姓名: ${name}, 年龄: ${age ?? '未提供'}, 状态: ${isActive ? '激活' : '未激活'}`); } ``` `task.ts` 里写纯函数即可,复杂业务可调用同模块的 `handle.ts`,避免在 processor 里重复逻辑。 ## 沙箱注册 任务函数写好后,必须在 `worker-sandbox` 里注册,Worker 子进程才能按名称找到它。 **不要**在 `processor.ts` 里直接 `import '@/modules/...'`,以保持 infrastructure 与 modules 的依赖边界。 ```ts [system-cron-tasks.ts] // server/src/worker-sandbox/system-cron-tasks.ts import { jobDemo } from '@/modules/monitor-job/task'; import type { TaskFn } from '@/infrastructure/queue/core/processor-utils'; export function registerSystemCronSandboxTasks( register: (name: string, fn: TaskFn) => void, ): void { // 第一个参数必须与后台「任务名称」完全一致 register('测试任务', jobDemo); } ``` `processor.ts` 只负责调用注册函数,不关心具体业务: ```ts [processor.ts] // server/src/infrastructure/queue/queues/system-cron/processor.ts import type { SandboxedJob } from 'bullmq'; import { registerSystemCronSandboxTasks } from '@/worker-sandbox/system-cron-tasks'; import { createTaskRegistry, parseArgs } from '../../core/processor-utils'; const { register, get } = createTaskRegistry(); registerSystemCronSandboxTasks(register); export default async function processor(job: SandboxedJob) { const { taskName, jobArgs } = job.data; const taskFn = get(taskName); if (!taskFn) throw new Error(`[SystemCron] 未找到任务: ${taskName}`); await taskFn(...parseArgs(jobArgs)); return { success: true, taskName }; } ``` Processor 运行在独立子进程中,可以正常使用数据库、Redis 等工具,但无法访问主进程的内存单例。子进程会自行初始化所需连接。 ## 构建与启动 修改 `worker-sandbox/*.ts` 或 `processor.ts` 后,需要重新构建 processor 才能生效。 ```bash cd server bun run build:processors ``` 开发环境单独启动 Worker: ```bash bun run dev:workers ``` 前后端和 Worker 一起启动时,可用 `bun run dev:all`。 ## 后台配置 代码注册完成后,在后台添加调度配置。路径:**系统监控 → 定时任务**。 ![添加任务](/guide/1.png) | 字段 | 说明 | |------|------| | 任务名称 | 与 `register('测试任务', ...)` 的第一个参数**完全一致**,全局唯一 | | Cron 表达式 | 执行频率,可用 [在线工具](https://tool.lu/crontab/) 校验 | | 任务参数 | JSON 数组,如 `["张三", 25, true]`,顺序和类型对应函数参数 | 提交后把任务状态设为「开启」,系统会按 Cron 表达式自动触发。 ## 部署注意 至少需要一个 Worker 进程在运行,否则任务只会积压在 Redis 中,不会被消费。生产环境通常用 PM2 单独管理 Worker,与 HTTP 服务分开部署。 多实例部署时,BullMQ 通过 Redis 保证同一触发时刻只有一个 Worker 消费任务,一般不需要额外的分布式锁。Worker 崩溃后由进程管理器重启;未完成的任务数据持久化在 Redis 中,重启后可恢复。 --- URL: "https://elysia-admin.top/guide/crypto.html" LLMS_URL: "https://elysia-admin.top/\\guide\\crypto.md" title: "请求加解密 - Elysia Admin 指南" description: "介绍 Elysia Admin 的请求与响应加解密:RSA-OAEP 包裹 AES-256-GCM、前后端开关、密钥生成,以及明文兼容与日志还原。" --- --- title: 请求加解密 - Elysia Admin 指南 description: 介绍 Elysia Admin 的请求与响应加解密:RSA-OAEP 包裹 AES-256-GCM、前后端开关、密钥生成,以及明文兼容与日志还原。 --- # 请求加解密 接口可以在传输层之外再包一层应用层加密:每次请求用随机 AES-256-GCM 密钥加密 JSON,再用服务端 RSA 公钥把这把密钥交给后端。浏览器抓包时看到的是密文;没有加密头的客户端仍按明文访问。 实现位于 `server/src/middleware/crypto.ts`、`server/src/shared/crypto-envelope.ts` 与 `admin/src/utils/crypto.ts`。`crypto.enabled` 为 false 时中间件不注册。 ```mermaid flowchart LR FE[前端生成 AES 密钥] --> RSA[RSA-OAEP 加密密钥] RSA --> H[请求头 X-Encrypt-Key] FE --> AES[AES-GCM 加密 JSON] AES --> Body["请求体 data"] H --> BE[后端私钥解出 AES 密钥] Body --> BE BE --> Handle[校验与 handle 看到明文] Handle --> Out["响应 encrypted + data"] ``` ## 开关 两边都打开才会加密。任一关闭时,管理端自动退回明文。 | 位置 | 配置 | 作用 | |------|------|------| | 后端 `development.yaml` / `production.yaml` | `crypto.enabled` | 为 `true` 才注册中间件,并允许下发公钥 | | 后端同一段 | `crypto.privateKey` / `crypto.publicKey` | PKCS8 私钥与 SPKI 公钥(PEM) | | 前端 `admin/.env*` | `VITE_API_ENCRYPT` | 为 `true` 才去拉公钥并加密 | 开发配置里 `crypto.enabled` 默认为 true,生产配置默认为 false。公钥接口是 `GET /api/crypto/public-key`(`ipRateLimit: 60:30`,无需登录),返回 `{ enabled, publicKey }`。前端只拉取一次;`enabled` 为 false、没有公钥或请求失败时,后续请求保持明文。 ## 生成密钥 在 `server/` 下执行。脚本只替换 `crypto` 段的 `privateKey` / `publicKey`,不改当前的 `enabled`。 ```bash bun run gen:crypto-keys bun run gen:crypto-keys production bun run gen:crypto-keys production --force ``` 已有密钥时不加 `--force` 会拒绝覆盖,避免旧密钥加密的报文无法再解。私钥只放在服务端 yaml 中,不要提交到仓库。PEM 也可以手工写成 YAML 块,或把换行写成 `\n`。 生产环境开启前:先生成密钥,再把 `crypto.enabled` 改为 `true`,并把前端 `VITE_API_ENCRYPT` 设为 `true` 后重新构建。配置结构见 [部署](/guide/deploy) 的部署前准备。 ## 协议 算法为 RSA-OAEP(SHA-256)+ AES-256-GCM。IV 为 12 字节,和密文拼在一起再做 Base64。 1. 前端用 Web Crypto 生成本次 AES 密钥,公钥加密后放入请求头 `X-Encrypt-Key`。 2. JSON 请求体改为 `{ "data": "" }`。GET 等没有 JSON 体的请求只带请求头,响应仍可加密。 3. 后端在 `onParse` 里解密,下游 DTO 校验和 `handle` 拿到的是明文对象。 4. JSON 对象响应加密为 `{ "encrypted": true, "data": "" }`,并设置响应头 `x-encrypted: 1`。 5. 解密失败返回 HTTP 400,文案为「请求解密失败」。 文件上传(`FormData` 或非 JSON 体)和 `blob` / `arraybuffer` / `stream` 响应不加密。刷新 Token 的独立请求走同一套信封。 ## 明文与日志 没有 `X-Encrypt-Key` 的请求原样进入后续中间件,第三方调用和本地调试可以继续发 JSON。 响应加密发生在 `mapResponse`。随后的 `onAfterResponse` 会把上下文里的 `response` 还原成加密前的业务对象,操作日志、请求日志和限流记录仍能读取 `response.code` 等字段。守卫链的位置见 [中间件](/guide/middleware)。 --- URL: "https://elysia-admin.top/guide/database-operation.html" LLMS_URL: "https://elysia-admin.top/\\guide\\database-operation.md" title: "数据库操作 - Elysia Admin 指南" description: "介绍 Elysia Admin 封装的 PostgreSQL 数据库操作方法,包括增删改查、条件构造器、联表查询、分页查询及事务管理的完整使用指南。" --- --- title: 数据库操作 - Elysia Admin 指南 description: 介绍 Elysia Admin 封装的 PostgreSQL 数据库操作方法,包括增删改查、条件构造器、联表查询、分页查询及事务管理的完整使用指南。 --- # 数据库操作 业务模块的 `handle.ts` 通过 `@/core/database/repository` 访问 PostgreSQL,底层基于 Drizzle ORM。你一般不需要手写 SQL,用封装好的增删改查和 `CreateQueryBuilder` 即可覆盖大部分场景。 下文示例统一使用 `AppContext`(`import type { AppContext } from '@/types/app-context'`),与中间件注入的 `ctx.body`、`ctx.query`、`ctx.params`、`ctx.user` 一致。接口入参校验见 [参数验证](./parameter-validation)。 ```mermaid flowchart TD H[handle.ts] --> QB[CreateQueryBuilder] H --> R[repository 函数] QB --> R R --> PG[Drizzle / PostgreSQL] ``` ## API 速查 所有函数均从 `@/core/database/repository` 导入。 | 函数 | 用途 | |------|------| | `InsertOne` | 插入,不返回记录 | | `InsertOneAndRes` | 插入,返回新记录 | | `InsertMany` | 批量插入 | | `FindOneByKey` | 按主键查单条 | | `FindAll` | 查全部(不分页) | | `FindPage` | 分页查询 | | `FindAllWithJoin` | 联表查全部 | | `FindPageWithJoin` | 联表分页 | | `UpdateByKey` | 按主键更新,不返回 | | `UpdateByKeyAndRes` | 按主键更新,返回记录 | | `SoftDeleteByKeys` | 软删除(`delFlag = true`) | | `HardDelete` | 按 where 条件硬删除 | | `HardDeleteByKeys` | 按主键数组硬删除 | | `CreateQueryBuilder` | 链式构造 where 条件 | 传入 `ctx` 时,封装会自动填充 `createBy` / `updateBy`(表里有对应字段时)。用 `customData` 时把 `ctx` 设为 `null`。 ## 插入 ```ts [handle.ts] import type { AppContext } from '@/types/app-context'; import { InsertOneAndRes } from '@/core/database/repository'; import { BaseResultData } from '@/core/result'; import { businessMerchantSchema } from '@database/schema/business_merchant'; export async function create(ctx: AppContext) { const res = await InsertOneAndRes(businessMerchantSchema, ctx); return BaseResultData.ok(res); } ``` 只需写入、不关心返回值时用 `InsertOne`。批量导入用 `InsertMany(schema, ctx, dataArray)`。 ## 查询条件 ### QueryBuilder 方法 | 方法 | 说明 | 示例 | |------|------|------| | `eq` | 等于 | `eq('status', true)` | | `ne` | 不等于 | `ne('type', 'x')` | | `in` / `notIn` | 在 / 不在数组中 | `in('id', [1, 2, 3])` | | `like` / `ilike` | 模糊匹配(区分 / 不区分大小写) | `like('name', keyword)` | | `notLike` / `notIlike` | 不包含 | `notLike('name', 'test')` | | `leftLike` / `rightLike` | 左 / 右模糊 | `rightLike('code', 'A')` | | `gt` / `gte` / `lt` / `lte` | 比较 | `gte('amount', 0)` | | `between` / `notBetween` | 区间 | `between('age', 18, 60)` | | `dateRange` | 日期范围 | `dateRange('createTime', start, end)` | | `isNull` / `isNotNull` | 空值判断 | `isNull('deletedAt')` | | `or` / `not` | 组合条件 | `or()` / `not()` | | `join` | 联表 | 见下文 | | `custom` | 自定义 SQL 片段 | `custom(sql)` | 值为 `undefined`、`null` 或空字符串时,对应条件会自动跳过,不必手动判断。 ### 构造 where ```ts const where = CreateQueryBuilder(businessMerchantSchema) .eq('delFlag', false) .eq('status', status) .dateRange('createTime', startTime, endTime) .build(); ``` 等价于: ```sql WHERE del_flag = false AND status = $status AND create_time BETWEEN $start AND $end ``` ## 查询 ### 按主键 ```ts [handle.ts] import { FindOneByKey } from '@/core/database/repository'; export async function findOne(ctx: AppContext) { const res = await FindOneByKey(businessMerchantSchema, 'id', ctx.params.id); return BaseResultData.ok(res); } ``` ### 列表与分页 不分页: ```ts const where = CreateQueryBuilder(systemDictTypeSchema).eq('delFlag', false).build(); const list = await FindAll(systemDictTypeSchema, where); ``` 分页(`pageNum`、`pageSize` 等通常来自 `ctx.query`,与 `BaseListQueryDto` 对齐): ```ts [handle.ts] import { CreateQueryBuilder, FindPage } from '@/core/database/repository'; export async function findList(ctx: AppContext) { const { pageNum = 1, pageSize = 10, orderByColumn = 'createTime', sortRule = 'desc', startTime, endTime, status } = ctx.query; const where = CreateQueryBuilder(businessMerchantSchema) .eq('delFlag', false) .eq('status', status) .dateRange('createTime', startTime, endTime) .build(); const res = await FindPage(businessMerchantSchema, where, { pageNum, pageSize, orderByColumn, sortRule }); return BaseResultData.ok(res); } ``` 返回的 `res` 结构为 `{ list, total }`。 ### 联表查询 通过 `.join()` 把关联表数据挂到主表字段上,再用 `FindAllWithJoin` 或 `FindPageWithJoin` 查询。 ```ts [handle.ts] import { eq } from 'drizzle-orm'; import { CreateQueryBuilder, FindAllWithJoin } from '@/core/database/repository'; import { ListToTree } from '@/shared/tree'; import { systemMenuSchema } from '@database/schema/system_menu'; import { systemMenuBtnSchema } from '@database/schema/system_menu_btn'; export async function findTree(ctx: AppContext) { const builder = CreateQueryBuilder(systemMenuSchema) .eq('delFlag', false) .join({ joinSchema: systemMenuBtnSchema, fieldName: 'authList', foreignKey: 'menuId', primaryKey: 'menuId', defaultValue: [], where: eq(systemMenuBtnSchema.delFlag, false), multiple: true, }); const data = await FindAllWithJoin(systemMenuSchema, builder); const tree = ListToTree(data, { idKey: 'menuId', parentKey: 'parentId', childrenKey: 'children', rootValue: 0, sortKey: 'sort', }); return BaseResultData.ok(tree); } ``` 联表分页与上面类似,把 `FindAllWithJoin` 换成 `FindPageWithJoin`,并传入分页参数。 `join` 常用字段: | 字段 | 说明 | |------|------| | `joinSchema` | 关联表 schema | | `fieldName` | 挂到主表记录上的属性名 | | `foreignKey` / `primaryKey` | 关联键 | | `multiple` | `true` 为一对多,`false` 为一对一 | | `where` | 关联表的额外过滤 | | `defaultValue` | 无关联数据时的默认值 | ## 更新 ```ts [handle.ts] import { UpdateByKeyAndRes } from '@/core/database/repository'; import { systemDeptSchema } from '@database/schema/system_dept'; export async function update(ctx: AppContext) { const res = await UpdateByKeyAndRes(systemDeptSchema, 'deptId', ctx); return BaseResultData.ok(res); } ``` 传入 `ctx` 时,`ctx.body` 中的主键用于定位记录,其余字段写入更新;`updateTime` 和 `updateBy` 会自动处理。日期字符串经 `ParseDateFields` 转为 `Date`(见 [参数验证](./parameter-validation.md))。 ## 删除 业务表默认用软删除,关联表、中间表常用硬删除。 **软删除** — 将 `delFlag` 标为 `true`,`ctx.params.ids` 为主键列表(路由通常为 `/:ids`): ```ts [handle.ts] import { SoftDeleteByKeys } from '@/core/database/repository'; export async function remove(ctx: AppContext) { await SoftDeleteByKeys(systemApiSchema, 'apiId', ctx); return BaseResultData.ok(); } ``` **硬删除** — 物理移除记录: ```ts import { eq } from 'drizzle-orm'; import { HardDelete, HardDeleteByKeys } from '@/core/database/repository'; // 按条件 await HardDelete(systemRoleMenuSchema, eq(systemRoleMenuSchema.roleId, roleId)); // 按主键数组 await HardDeleteByKeys(systemRoleMenuSchema, 'roleId', [1, 2, 3]); ``` ## 事务 多表写入需要原子性时,用 `@/core/database/transaction`。事务内抛出错误会自动回滚。 ### RunTransaction 最常用的写法。回调参数 `tx` 与 Drizzle 事务对象一致,可直接 `insert` / `update` / `select`。 ```ts import { RunTransaction } from '@/core/database/transaction'; import { systemUserSchema } from '@database/schema/system_user'; import { systemRoleSchema } from '@database/schema/system_role'; const result = await RunTransaction(async (tx) => { const user = await tx.insert(systemUserSchema).values({ userName: 'admin', nickName: 'Administrator', }).returning(); const role = await tx.insert(systemRoleSchema).values({ roleName: 'admin', roleKey: 'admin', roleSort: 1, }).returning(); return { user: user[0], role: role[0] }; }); ``` ### 隔离级别与只读 第二个参数可传配置: ```ts // 隔离级别:read uncommitted | read committed | repeatable read | serializable await RunTransaction(async (tx) => { /* ... */ }, { isolationLevel: 'serializable' }); // 只读事务 await RunTransaction(async (tx) => { return await tx.select().from(systemUserSchema); }, { accessMode: 'read only' }); ``` ### 构建器与钩子 需要链式配置隔离级别、只读模式或生命周期钩子时,用 `CreateTransaction`: ```ts import { CreateTransaction } from '@/core/database/transaction'; const result = await CreateTransaction<{ userId: number }>() .isolation('serializable') .readOnly() .onBegin(() => console.log('事务开始')) .onCommit(() => console.log('提交成功')) .onRollback((err) => console.error('已回滚', err)) .execute(async (tx) => { const user = await tx.insert(systemUserSchema).values({ userName: 'builder' }).returning(); return { userId: user[0].userId }; }) .run(); ``` `RunTransaction` 也支持 `onBegin`、`onCommit`、`onRollback`、`onError` 钩子,适合提交后清缓存、回滚时记日志等场景。 ### 批量与嵌套 `CreateBatchTransaction` 可串行或并行跑多个**独立**事务: ```ts import { CreateBatchTransaction } from '@/core/database/transaction'; const results = await CreateBatchTransaction() .add('创建用户', async (tx) => tx.insert(systemUserSchema).values({ userName: 'u1' }).returning()) .add('创建角色', async (tx) => tx.insert(systemRoleSchema).values({ roleName: 'r1' }).returning()) .runAll(); // 串行 // .runAllParallel() // 并行,互不影响 ``` 可复用函数用 `WithTransaction`,传入已有 `tx` 时加入同一事务,否则自动开新事务: ```ts import { WithTransaction, TransactionContext } from '@/core/database/transaction'; import pg from '@/core/database/pg'; async function createUser(name: string, tx?: TransactionContext) { return WithTransaction(tx ?? pg, async (t) => { return t.insert(systemUserSchema).values({ userName: name }).returning(); }); } async function createRole(name: string, tx?: TransactionContext) { return WithTransaction(tx ?? pg, async (t) => { return t.insert(systemRoleSchema).values({ roleName: name }).returning(); }); } // 共享同一事务 await RunTransaction(async (tx) => { await createUser('john', tx); await createRole('admin', tx); }); // 各自独立事务 await createUser('jane'); ``` ## 参考 - [Drizzle ORM 事务](https://orm.drizzle.org.cn/docs/transactions) - [PostgreSQL 事务](https://www.runoob.com/postgresql/postgresql-transaction.html) --- URL: "https://elysia-admin.top/guide/deploy.html" LLMS_URL: "https://elysia-admin.top/\\guide\\deploy.md" title: "部署 - Elysia Admin 指南" description: "介绍 Elysia Admin 的多种生产环境部署方案,包括 Docker 容器化部署、PM2 进程管理部署及宝塔面板部署,附安全建议与故障排查指南。" --- --- title: 部署 - Elysia Admin 指南 description: 介绍 Elysia Admin 的多种生产环境部署方案,包括 Docker 容器化部署、PM2 进程管理部署及宝塔面板部署,附安全建议与故障排查指南。 --- # 部署 生产环境需要 PostgreSQL、Redis,以及(若使用队列/定时任务)独立运行的 Worker 进程。本文介绍 Docker 与 PM2 两种常见方式,以及宝塔面板上的操作要点。 ::: tip 站点部署 后台还可以维护本机 Nginx 站点:生成配置、用 Let's Encrypt 申请和续签证书。见 [站点部署](/guide/site-deploy)。 ::: ```mermaid flowchart LR subgraph 服务器 PM2[PM2] --> Main[index.js HTTP] PM2 --> Worker[workers.js 队列] end Main --> PG[(PostgreSQL)] Main --> Redis[(Redis)] Worker --> PG Worker --> Redis ``` ## 部署前准备 无论哪种方式,上线前请先改好配置。 **后端** — 编辑 `server/src/config/production.yaml`: | 配置项 | 说明 | |--------|------| | `app.port` / `app.prefix` | 服务端口与 API 前缀(默认 `/api`) | | `app.corsOrigin` | 前端访问域名,生产环境务必改成你的域名 | | `database.*` | PostgreSQL 连接信息 | | `redis.*` | Redis 连接信息 | | `jwt.*.secret` | 生产环境请换成随机强密钥 | | `crypto.enabled` / `crypto.privateKey` / `crypto.publicKey` | 请求加解密。生产默认关闭。开启前在 `server/` 执行 `bun run gen:crypto-keys production` 写入密钥,详见 [请求加解密](/guide/crypto) | **前端(前后端同机部署时)** 在 `admin/` 配置 `.env.production` 的 `VITE_API_BASE_URL`,构建后把产物放进 `server/public/`: ```bash cd admin pnpm install pnpm build # 将 admin/dist/ 内文件复制到 server/public/ ``` 然后执行 `server` 的 `bun run build`,`public/` 会一并打进 `dist/public/`。 **前端(前后端分离时)** `admin/dist/` 部署到 Nginx / CDN,只需保证 `VITE_API_BASE_URL` 指向后端地址,`production.yaml` 的 `corsOrigin` 包含前端域名。 **依赖服务** - PostgreSQL >= 16,库表已初始化(开发环境 `bun db:push` 或执行项目 SQL) - Redis >= 6,队列与缓存依赖它 ### 生产启动顺序与双栈探测 生产环境(`NODE_ENV=production`)主进程启动顺序为: 1. **双栈出网探测**(`shared/dual-stack-probe`):探测 IPv4 / IPv6 可达性,并据此设置 DNS `resultOrder` 2. 并行连接 PostgreSQL(`EnsurePg`)与 Redis(`ConnectRedis`) 3. 初始化队列 4. 创建应用并开始监听 若探测判定双栈半残或完全不可达,进程会**失败退出**,避免连库/连 Redis 卡在错误协议栈上。内网或特殊网络若探测误判,可设置环境变量 `SKIP_NETWORK_PROBE=1` 跳过探测并强制按 IPv4 策略启动。进程关闭时会一并关闭队列连接。 ## Docker 部署 适合已有容器编排经验的团队。镜像在 `server/Dockerfile`:build 阶段在容器内执行 `bun run build`,运行阶段为 `debian:bookworm-slim` + Bun 二进制(非 `--compile` 单文件),**单容器内同时启动 HTTP 与 Worker**(`WORKDIR` 为 `/app/dist`)。 **环境要求:** Docker;可选 Docker Compose。 进入 `server/` 目录: ```bash cd server ``` 构建镜像: ```bash bun run docker:build # 等价于 docker build -t hnq1/elysia-admin:latest . ``` 运行容器(需挂载生产配置,建议加 `--init` 改善信号转发): ```bash bun run docker:run # 或手动指定配置卷 docker run -d \ --name elysia-admin \ --init \ -p 3000:3000 \ -e NODE_ENV=production \ -e CONFIG_PATH=/app/dist/production.yaml \ -v /path/to/production.yaml:/app/dist/production.yaml \ hnq1/elysia-admin:latest ``` ### Docker Compose 本地或单机一键拉起应用 + PostgreSQL + Redis 时,可在 `server/` 下创建 `docker-compose.yml`: ```yaml version: '3.8' services: app: image: hnq1/elysia-admin:latest container_name: elysia-admin restart: unless-stopped init: true ports: - "3000:3000" environment: - NODE_ENV=production - CONFIG_PATH=/app/dist/production.yaml volumes: - ./production.yaml:/app/dist/production.yaml depends_on: - postgres - redis postgres: image: postgres:16-alpine container_name: elysia-admin-db restart: unless-stopped environment: - POSTGRES_DB=elysia_admin - POSTGRES_USER=admin - POSTGRES_PASSWORD=your_password volumes: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432" redis: image: redis:7-alpine container_name: elysia-admin-redis restart: unless-stopped ports: - "6379:6379" volumes: - redis_data:/data volumes: postgres_data: redis_data: ``` 启动: ```bash docker-compose up -d ``` 常用命令: ```bash # 查看日志 bun run docker:logs docker logs -f elysia-admin # 停止 / 删除 bun run docker:stop bun run docker:rm # 重启 docker restart elysia-admin ``` ## PM2 部署 推荐大多数 VPS 场景。项目构建后会生成 **主进程 + Worker 进程** 两份入口,PM2 同时托管,HTTP 与队列互不影响。 **环境要求:** Node.js >= 22、Bun、PM2;服务器已装好 PostgreSQL 与 Redis。 ```bash npm i -g pm2 bun ``` ### 本地构建 在开发机或 CI 上进入 `server/` 执行生产构建: ```bash cd server bun run build ``` 构建完成后 `dist/` 目录结构如下: ``` dist/ ├── public/ # 前端静态资源(若已复制 admin 构建产物) ├── ecosystem.config.cjs # PM2 配置(appId 来自 production.yaml) ├── index.js # 主进程:HTTP + 静态资源 ├── workers.js # Worker:队列消费 + 定时任务 ├── dist/cjs/ # BullMQ 沙箱 bootstrap(Worker 依赖,不可缺少) ├── processors/ # 各队列 Processor │ ├── system-cron.js │ ├── flow-buffer.js │ └── trade-order.js └── production.yaml # 生产配置(部署前请按服务器环境修改) ``` ### 上传与启动 将 `dist/` **里面的所有文件**上传到服务器目录(例如 `/www/wwwroot/elysia-admin`),不是上传 `dist` 文件夹本身。服务器上目录应与上图一致。 上传后编辑服务器上的 `production.yaml`,填入真实的数据库、Redis、域名等配置。 ```bash cd /www/wwwroot/elysia-admin pm2 start ecosystem.config.cjs ``` PM2 会启动两个进程(名称来自 `production.yaml` 的 `app.id`,默认类似 `Elysia-Admin`): | 进程 | 入口 | 职责 | |------|------|------| | `{appId}` | `index.js` | HTTP API、静态页面、Bull Board | | `{appId}-workers` | `workers.js` | 队列消费、定时任务 | 两个进程都要在跑,否则任务会积压在 Redis。详见 [队列](./queue)。 ### PM2 常用命令 ```bash pm2 status pm2 logs Elysia-Admin pm2 logs Elysia-Admin-workers pm2 restart all pm2 restart Elysia-Admin pm2 restart Elysia-Admin-workers pm2 stop all pm2 delete all pm2 save # 保存进程列表 pm2 startup # 开机自启 pm2 monit # 资源监控 ``` 进程名以你 `production.yaml` 里 `app.id` 为准,上表仅为示例。 ## 宝塔面板 适合习惯图形界面的运维。推荐宝塔 >= 11.0.0。 从 [宝塔官网](https://www.bt.cn/new/download.html) 安装面板后: **安装运行环境** 在软件商店安装 PostgreSQL(>= 16)、Redis(>= 6)、Nginx(反向代理用)。 **安装 Node 与 Bun** 进入「网站 → Node 项目」,安装 Node.js(推荐 v22+)。终端执行: ```bash npm i -g bun pm2 ``` **上传构建产物** 与 PM2 部署相同:本地 `bun run build` 后,把 `dist/` 内文件上传到例如 `/www/wwwroot/elysia-admin`,修改 `production.yaml`。 **数据库连接** ![pgSQL数据库配置](/guide/6.jpg) **添加 Node 项目** 在「Node 项目」中新建项目,指向 PM2 配置文件: ![项目配置](/guide/3.jpg) 也可在 SSH 终端直接: ```bash cd /www/wwwroot/elysia-admin pm2 start ecosystem.config.cjs ``` Nginx 反代时,把域名指到 `app.port`,静态资源可由后端 `public/` 直接提供,或单独配置 `admin` 构建产物。 ## 部署后检查 | 检查项 | 做法 | |--------|------| | 站点可访问 | 浏览器打开你的域名,能加载登录页 | | 双进程正常 | `pm2 status` 显示 main + workers 均为 online | | 日志无报错 | `pm2 logs` 或 `docker logs` 查看启动错误 | | 数据库 / Redis | 登录、字典等接口正常;缓存监控有数据 | | API | 调几个关键接口(登录、列表)确认 200 | | 队列 | Admin → 系统监控 → 队列监控,确认 Worker 在消费 | ## 安全建议 - 修改默认端口,不要直接把 3000 暴露公网;对外只开 80 / 443 - 配置防火墙,数据库和 Redis 不要对公网开放 - Nginx 配置 HTTPS,强制跳转 - `production.yaml` 中 JWT secret、数据库密码使用强随机值 - 定期备份 PostgreSQL 与重要文件 - 管理后台、Bull Board 建议限制 IP 或加额外鉴权 ## 故障排查 | 现象 | 排查方向 | |------|----------| | 端口无法访问 | `netstat -tunlp \| grep 3000` 看是否监听;检查防火墙 | | 启动即退出 | `pm2 logs` 看报错;常见为 `production.yaml` 数据库连不上,或生产双栈探测失败(见上文「生产启动顺序与双栈探测」;可临时 `SKIP_NETWORK_PROBE=1`) | | 登录 401 / CORS | 检查 `corsOrigin` 是否包含前端域名 | | 定时 / 队列不执行 | Worker 进程是否 online;`dist/cjs/`、`processors/` 是否完整上传 | | Processor 报错 | 是否忘记在本地重新 `bun run build` 后再上传 | | 静态页 404 | `public/` 是否包含前端构建产物 | 仍无法解决可查看 [常见问题](/other/faq) 或在 Gitee 提交 Issue。 --- URL: "https://elysia-admin.top/guide/first-api.html" LLMS_URL: "https://elysia-admin.top/\\guide\\first-api.md" title: "第一个接口 - Elysia Admin 指南" description: "学习如何在 Elysia Admin 中创建第一个 API 接口,包括模块创建、路由定义、DTO 参数校验、处理函数实现及完整代码整合流程。" --- --- title: 第一个接口 - Elysia Admin 指南 description: 学习如何在 Elysia Admin 中创建第一个 API 接口,包括模块创建、路由定义、DTO 参数校验、处理函数实现及完整代码整合流程。 --- # 第一个接口 本章将详细介绍如何在 `Elysia Admin` 项目中创建第一个 API 接口,包括接口定义、参数校验、响应处理等完整流程;下文按「模块目录 → 路由 → DTO → 处理函数 → 整合」的顺序编写,最后一节会用示意图归纳各文件的协作关系。 ## 创建业务模块 首先,在 `/server/src/modules/` 目录下创建业务模块目录,例如: `/server/src/modules/business-goods/`。 **命名规范:** - `business` 作为业务模块的前缀,用于区分不同类型的模块 - `goods` 作为业务模块的具体名称,描述模块的业务领域 ## 定义接口路由 在 `/server/src/modules/business-goods/` 目录下创建 `route.ts` 文件,用于定义接口路由配置。 ```ts [route.ts] import type { IRouteModule } from "@/types/route"; const BusinessGoodsModule: IRouteModule = { // 业务模块分类名称(用于 API 文档分组) tags: '商品管理', // 接口列表 routes: [ { url: '/business/goods', // 接口路径 method: 'post', // 接口 HTTP 方法 summary: '创建商品', // 接口描述(将显示在 API 文档中) handle: () => {}, // 接口处理函数(暂时为空实现) // 接口元数据配置 meta: { isAuth: true, // 是否需要登录认证 isLog: true, // 是否记录操作日志 ipRateLimit: '60:2', // IP 限流配置(60秒内最多2次请求) permission: 'business:goods:create', // 权限配置 }, } ], }; export default BusinessGoodsModule; ``` 保存文件后,接口会自动注册到后端服务中。在开发环境下,API 文档也会实时更新。 ## 创建数据传输对象 (DTO) `DTO`(Data Transfer Object)用于校验前端传递的参数是否符合要求,以及确保后端响应的数据类型正确。若参数或响应数据不符合定义,系统会自动返回错误响应。 在 `/server/src/modules/business-goods/` 目录下创建 `dto.ts` 文件: ```ts [dto.ts] import { t } from 'elysia'; import { BaseResultDto } from '@/types/dto'; /** * 创建商品请求参数校验 */ export const CreateDto = { body: t.Object({ name: t.String({ description: '商品名称', error: '商品名称格式错误', minLength: 5 }), price: t.Number({ description: '商品价格', error: '商品价格格式错误', min: 0 }), stock: t.Number({ description: '商品库存', error: '商品库存格式错误', min: 0 }), remark: t.Optional(t.String({ description: '商品备注' })), }), }; ``` ## 实现接口处理函数 在 `/server/src/modules/business-goods/` 目录下创建 `handle.ts` 文件,用于实现接口的业务逻辑: ```ts [handle.ts] import type { AppContext } from '@/types/app-context'; import { BaseResultData } from '@/core/result'; /** * 创建商品接口处理函数 * @param ctx 请求上下文(含 user、routeInfo 等) * @returns 标准响应格式 */ export async function create(ctx: AppContext) { const data = ctx.body; // 这里可以实现具体的业务逻辑,如数据库保存操作 return BaseResultData.ok(); } ``` ## 整合代码 将创建的文件进行整合,更新 `route.ts` 文件以引用 DTO 和处理函数: ::: code-group ```ts [route.ts] import type { IRouteModule } from "@/types/route"; import { CreateDto } from "./dto"; import { create } from "./handle"; const BusinessGoodsModule: IRouteModule = { // 业务模块分类名称 tags: '商品管理', // 接口列表 routes: [ { url: '/business/goods', // 接口路径 method: 'post', // 接口方法 summary: '创建商品', // 接口描述 dto: CreateDto, // 接口参数校验 handle: create, // 接口处理函数 // 接口元数据 meta: { isAuth: true, // 是否登录后才能访问 isLog: true, // 是否记录日志 ipRateLimit: '60:2', // IP 限流配置(60s 内最多 2 次请求) permission: 'business:goods:create', // 权限配置 }, } ], }; export default BusinessGoodsModule; ``` ```ts [dto.ts] import { t } from 'elysia'; import { BaseResultDto } from '@/types/dto'; export const CreateDto = { body: t.Object({ name: t.String({ description: '商品名称', error: '商品名称格式错误', minLength: 5 }), price: t.Number({ description: '商品价格', error: '商品价格格式错误', min: 0 }), stock: t.Number({ description: '商品库存', error: '商品库存格式错误', min: 0 }), remark: t.Optional(t.String({ description: '商品备注' })), }), }; ``` ```ts [handle.ts] import type { AppContext } from '@/types/app-context'; import { BaseResultData } from '@/core/result'; export async function create(ctx: AppContext) { const data = ctx.body; // 保存操作 return BaseResultData.ok(); } ``` ::: ## 模块文件协作 完成以上五步后,`route.ts`、`dto.ts`、`handle.ts` 三个文件已在同一个模块目录中各司其职。约定在 `server/src/modules/` 下按领域建目录(如 `business-goods`);`route.ts` 会被启动流程扫描并自动注册到应用(开发环境逐模块加载,失败会 warn 并跳过;生产环境使用预生成的 `route-registry.generated.ts`),因此不必手写「把路由挂到 app」的样板代码。下面的示意图归纳了「谁参与注册、谁负责校验与业务」的协作关系: ```mermaid flowchart LR M[modules/business-xxx/] --> R[route.ts
路由与 meta] M --> D[dto.ts
入参/出参校验] M --> H[handle.ts 等
业务逻辑] R --> Auto[启动扫描注册] ``` 通过以上步骤,我们成功创建了一个具备以下特性的商品创建接口: - **模块化设计**:代码结构清晰,职责分明 - **登录认证**:通过 `isAuth: true` 确保只有登录用户可以访问 - **参数校验**:使用 DTO 验证请求参数的合法性 - **操作日志**:通过 `isLog: true` 记录接口调用情况 - **接口限流**:通过 `ipRateLimit` 防止恶意请求 - **权限控制**:通过 `permission` 配置实现细粒度的权限管理 ## 按环境注册模块(stages) `IRouteModule` 支持可选字段 `stages`,用于控制模块在哪些环境注册与打进生产包: | 写法 | 行为 | |------|------| | 省略 `stages` | `development` + `production` 两端都注册 / 打包 | | `stages: ['development']` | 仅开发环境加载(如内置 `dev-sql` SQL 控制台) | | `stages: ['production']` | 仅生产环境 | | `stages: []` | 永不加载 | 必须使用**字面量数组**(如 `stages: ['development']`),供 `script/generate-registry.ts` 用正则解析;不要写成变量或计算值。生产构建只收录含 `production` 的模块;运行时 `LoadRouteModules` 也会按规范化后的 `NODE_ENV`(`production` vs 其余 → `development`)过滤。 示例(仅开发环境可用的模块): ```ts [route.ts] import type { IRouteModule } from '@/types/route' const DevOnlyModule: IRouteModule = { tags: '开发工具', stages: ['development'], routes: [ { url: '/dev/example', method: 'get', summary: '仅开发环境', handle: () => ({ ok: true }), meta: { isAuth: true }, }, ], } export default DevOnlyModule ``` --- URL: "https://elysia-admin.top/guide/mcp.html" LLMS_URL: "https://elysia-admin.top/\\guide\\mcp.md" title: "MCP 服务" description: "Elysia Admin 官方 MCP(elysia-admin-mcp)源码活索引与 PostgreSQL MCP(pg-mcp-server)数据库只读查询的配置、工具清单与使用纪律。" --- --- title: MCP 服务 description: Elysia Admin 官方 MCP(elysia-admin-mcp)源码活索引与 PostgreSQL MCP(pg-mcp-server)数据库只读查询的配置、工具清单与使用纪律。 --- # MCP 服务 用 AI 做业务模块时,除了项目自带的 rules 与 `.ai/` 规范,还可以接入两个 MCP 服务,分别提供**源码事实**与**运行时事实**: | MCP | 包名 | 职责 | |-----|------|------| | 官方 MCP | `elysia-admin-mcp` | 组件 props、Hook 签名、后端方法、Drizzle 表结构、模块权限码 | | 数据库 MCP | `pg-mcp-server` | 只读查询 dict / menu / 权限等运行时数据 | 两者互补,不重复:官方 MCP 不连数据库;数据库 MCP 不扫源码。规范与流程仍由 `.ai/`、`AGENTS.md` 负责。 ```mermaid flowchart LR subgraph sourceFacts [源码事实] A[elysia-admin-mcp] A --> A1[组件 / Hook / 工具] A --> A2[后端方法 / 表结构] A --> A3[模块权限码] end subgraph runtimeFacts [运行时事实] B[pg-mcp-server] B --> B1[system_dict_*] B --> B2[system_menu] B --> B3[system_role_menu] end C[AI 生成代码与 SQL] --> A C --> B ``` ## 一、官方 MCP(elysia-admin-mcp) [Elysia Admin MCP](https://www.npmjs.com/package/elysia-admin-mcp) 是项目的**源码活索引** MCP 服务器。它实时扫描前端与后端源码(默认 `admin/` 与 `server/`,可通过环境变量自定义),把组件 props、后端方法签名、Drizzle 表结构等**必须与代码同步的事实**变成可查询接口。 ### 定位 - **镜头而非仓库**:只索引源码事实,约定/流程/文档的家仍在 `.ai/` 与 `AGENTS.md` - **只读安全**:不连业务数据库、不执行 SQL、不写文件 - **落盘缓存**:源码未变时秒级启动(mtime 指纹失效) - **中文检索**:内置高频后端方法中文别名,搜「分页」「事务」「软删」直接命中 ### 工具清单 分 3 组,共 14 个工具。每个工具返回结果均带 `path`,便于 AI 跳转核实。 #### A. 统一检索 | 工具 | 用途 | |------|------| | `search_asset` | 全局模糊搜索组件/Hook/工具/后端方法/表/模块,不确定「有没有现成的 X」时的第一站 | #### B. 前端源码索引 | 工具 | 用途 | |------|------| | `list_components` | 列出 Art 组件清单(可按分类过滤) | | `get_component` | 获取组件 props/emits/slots + 最小用法示例 | | `get_hook` | 获取 `useTable`/`useAuth` 等 Hook 的参数与返回值 | | `get_util` | 获取 `request`/`validator` 等工具导出成员 | | `list_api_types` | 列出 `Api.*` 类型命名空间 | | `get_api_type` | 获取某命名空间的 ListItem/List/SearchParams 类型 | #### C. 后端源码索引 | 工具 | 用途 | |------|------| | `list_backend_methods` | 按 core/shared/infrastructure 列出方法(重点方法优先) | | `get_backend_method` | 获取 `FindPage`/`WithTransaction` 等方法签名 | | `get_infra_capability` | 获取 payment/storage/queue/cron/mail 入口用法 | | `list_modules` | 列出模块及权限码数量 | | `get_module` | 获取模块路由、权限码与文件构成 | | `list_schemas` | 列出 Drizzle 表名 | | `get_schema` | 获取表字段、类型、主键与软删标记 | ### 客户端接入 所有客户端均需设置环境变量 `ELYSIA_ADMIN_ROOT` 指向你的 Elysia Admin 项目根目录。若前端/后端目录名非 `admin/` / `server/`,可额外设置 `ELYSIA_ADMIN_DIR` / `ELYSIA_SERVER_DIR`。 #### Cursor `.cursor/mcp.json`: ```json { "mcpServers": { "elysia-admin": { "command": "npx", "args": ["-y", "elysia-admin-mcp"], "env": { "ELYSIA_ADMIN_ROOT": "D:/path/to/elysia-admin", "ELYSIA_ADMIN_DIR": "", "ELYSIA_SERVER_DIR": "" } } } } ``` > `ELYSIA_ADMIN_DIR` / `ELYSIA_SERVER_DIR` 留空即使用默认 `admin/` / `server/`。填相对名(如 `"webapp"`)会拼到 `ELYSIA_ADMIN_ROOT` 下;填绝对路径则直接使用(可在 root 之外)。 > 本地开发时可将 `command` 改为 `node`、`args` 指向本仓库 `dist/index.js`。 #### Claude Code ```bash claude mcp add elysia-admin -- npx -y elysia-admin-mcp # 并在环境或配置中设置 ELYSIA_ADMIN_ROOT(可选 ELYSIA_ADMIN_DIR / ELYSIA_SERVER_DIR) ``` #### Codex `~/.codex/config.toml`: ```toml [mcp_servers.elysia-admin] command = "npx" args = ["-y", "elysia-admin-mcp"] [mcp_servers.elysia-admin.env] ELYSIA_ADMIN_ROOT = "D:/path/to/elysia-admin" # ELYSIA_ADMIN_DIR = "webapp" # 可选,相对 root # ELYSIA_SERVER_DIR = "D:/other/repo/backend" # 可选,绝对路径 ``` #### Qoder / Kiro / Trae 在各自的 MCP 配置中添加 stdio 服务,`command` / `args` / `env` 字段与 Cursor 一致。 ### 环境变量 | 变量 | 说明 | |------|------| | `ELYSIA_ADMIN_ROOT` | Elysia Admin 项目根目录。未设置时从 cwd 向上自动查找(需含 `admin/` 与 `server/`);若两个自定义目录均为绝对路径,则 root 回退为 cwd | | `ELYSIA_ADMIN_DIR` | 前端代码目录(可选)。为空 → `{root}/admin`;相对名 → `{root}/{name}`;绝对路径 → 直接使用 | | `ELYSIA_SERVER_DIR` | 后端代码目录(可选)。为空 → `{root}/server`;相对名 → `{root}/{name}`;绝对路径 → 直接使用 | | `ELYSIA_MCP_NO_CACHE` | 设为 `1` 强制重建索引,忽略缓存(等价于 `--no-cache`) | ## 二、数据库 MCP(pg-mcp-server) [pg-mcp-server](https://github.com/ericzakariasson/pg-mcp-server) 是 PostgreSQL 的 MCP 服务器,让 AI 通过受控接口**只读**查询数据库,用于核对 dict、menu、权限等运行时数据。 ### Cursor 配置 ```json { "mcpServers": { "postgres": { "command": "npx", "args": ["--yes", "pg-mcp-server", "--transport", "stdio"], "env": { "DATABASE_URL": "postgresql://postgres:postgres@localhost:5432/postgres" } } } } ``` > 请将 `DATABASE_URL` 替换为你本机或测试环境的 PostgreSQL 连接串。 ### 配置项 | 变量 | 说明 | |------|------| | `DATABASE_URL` | PostgreSQL 连接串(必填) | | `DANGEROUSLY_ALLOW_WRITE_OPS` | 启用写入(默认 `false`,**建议保持只读**) | | `DEBUG` | 启用调试日志(默认 `false`) | | `PG_SSL_ROOT_CERT` | 可选 TLS CA 证书路径(如云数据库) | ### 工具与资源 **工具** | 工具 | 用途 | |------|------| | `query` | 执行 SQL 查询,例如 `{ "sql": "SELECT * FROM system_menu LIMIT 10" }` | **资源** | 资源 URI | 用途 | |----------|------| | `postgres://tables` | 列出所有表 | | `postgres://table/{schema}/{table}` | 获取表结构与示例数据 | ### 只读纪律 与项目 `.ai/AI_MCP_SETUP.md` 对齐: | 事项 | 说明 | |------|------| | 查 dict / menu / 权限 | **优先** Postgres MCP 只读查询 | | 禁止读备份 | **不要**让 AI 读 `server/database/sql/pg.sql`(全库备份,可能与现库不一致) | | 禁止 MCP 代跑 SQL | handoff SQL(`server/database/sql/{模块}-init.sql`)仍由**开发者手动执行** | | 禁止 MCP 写入 | 不要用 MCP 做 DDL、迁移或业务写入;`DANGEROUSLY_ALLOW_WRITE_OPS` 保持 `false` | | Schema 真相源 | Drizzle schema 文件仍是表结构权威,MCP 用于核对运行时数据 | **只读用途示例:** - 核对表与列是否与 schema 文件一致 - 查询 `system_dict_type` / `system_dict_data` 获取业务字典 - 查询 `system_menu` 获取父菜单 `menu_id`、`MAX(sort)`、重复 `permission` - 查询 `system_role` 用于 `system_role_menu` 插入 **MCP 不可用时**,AI 应声明「Postgres MCP 未连接」,并在 handoff SQL 中优先用 `SELECT ... INTO` / CTE 子查询,仍输出完整 SQL 文件。 ## 三、推荐使用顺序 ```mermaid flowchart TD S1[1. elysia-admin-mcp 查源码事实] --> S2[2. pg-mcp-server 只读查运行时数据] S2 --> S3[3. 生成后端 / 前端代码] S3 --> S4[4. 输出 handoff SQL 文件] S4 --> S5[5. 开发者手动执行 SQL] ``` 1. **源码事实**(`elysia-admin-mcp`)—— 查组件、Hook、后端方法、表结构、权限码 2. **运行时事实**(`pg-mcp-server` 只读)—— 查 dict、menu、角色权限行 3. **生成代码** —— 后端 `handle.ts` → 前端 views 4. **输出 SQL** —— `server/database/sql/{模块}-init.sql`(仅生成文件) 5. **手动执行** —— 开发者在本机执行 handoff SQL ## 相关 - [AI 开发指南](./ai-guide) —— 提示词、脚手架与 AI 交付物 - [数据库操作](./database-operation) —— repository 与 QueryBuilder 用法 - 仓库内 `.ai/AI_MCP_SETUP.md` —— Agent 侧 MCP 纪律(英文) --- URL: "https://elysia-admin.top/guide/middleware.html" LLMS_URL: "https://elysia-admin.top/\\guide\\middleware.md" title: "中间件 - Elysia Admin 指南" description: "介绍 Elysia Admin 中间件的使用方式,包括请求加解密、请求预处理(IP黑名单、认证、限流、权限)和响应后处理(操作日志、响应时间统计)的实现方法。" --- --- title: 中间件 - Elysia Admin 指南 description: 介绍 Elysia Admin 中间件的使用方式,包括请求加解密、请求预处理(IP黑名单、认证、限流、权限)和响应后处理(操作日志、响应时间统计)的实现方法。 --- # 中间件 中间件在请求生命周期的不同阶段注入通用逻辑:进入业务 `handle` 之前按需解密、做鉴权与限流,响应发出之前按需加密,响应发出之后写操作日志。代码集中在 `server/src/middleware/`。`app.ts` 里先挂 `CryptoMiddleware`,再挂 `GlobalMiddleware` 和 `GlobalResponseMiddleware`。 路由上的 `meta` 字段(`isAuth`、`permission`、`ipRateLimit` 等)决定哪些守卫会生效,配置方式见 [第一个接口](./first-api)。加解密开关与报文格式见 [请求加解密](./crypto)。 ```mermaid flowchart TD Req[HTTP 请求] --> C[CryptoMiddleware 按需解密] C --> R[onRequest 记录 startTime] R --> B[onBeforeHandle 守卫链] B --> H[handle.ts 业务逻辑] H --> Enc[CryptoMiddleware 按需加密响应] Enc --> A[onAfterResponse 还原明文并写日志] B -->|守卫返回错误| Err[直接响应,跳过 handle] ``` 任一守卫返回错误响应时,后续 `handle` 不会执行。排查「接口没进业务逻辑」时,可先确认认证、权限或限流是否在中途拦截。 ## 请求加解密 `CryptoMiddleware` 在 `crypto.enabled` 为 true 时注册,挂在全局守卫之前。 1. `onRequest`:请求头带 `X-Encrypt-Key` 时,用 RSA 私钥解出本次 AES 密钥。 2. `onParse`:JSON 体为 `{ data }` 时解密成对象,下游校验与 `handle` 看到的是明文。 3. `mapResponse`:本次请求有 AES 密钥且响应是 JSON 对象时,加密成 `{ encrypted: true, data }`。 4. `onAfterResponse`:把上下文里的 `response` 还原成加密前的业务对象,后面的操作日志才能读到 `response.code`。 没有加密头的请求不经过解密。协议、密钥生成和前端开关见 [请求加解密](./crypto)。 ## 请求预处理 `onBeforeHandle` 里按顺序执行守卫。守卫返回 `BaseResultData.fail(...)` 时,`executeGuard` 会抛出并中断请求。 ```ts [middleware/index.ts] import { Elysia } from 'elysia'; import config from '@/config'; import { AnalysisRoute } from './analysis'; import { AuthGuard } from './guards/auth'; import { PermissionGuard } from './guards/permission'; import { IpBlackGuard } from './guards/ipblack'; import { ApiGuard } from './guards/api'; import { IpRateLimitGuard } from './guards/ipratelimit'; const { guard } = config; export function GlobalMiddleware(app: Elysia) { app.onBeforeHandle(async (ctx) => { if (guard.ipBlacklist) await executeGuard(IpBlackGuard, ctx); if (guard.apiSwitch) await executeGuard(ApiGuard, ctx); await executeGuard(AnalysisRoute, ctx); await executeGuard(AuthGuard, ctx); await executeGuard(IpRateLimitGuard, ctx); await executeGuard(PermissionGuard, ctx); }); } ``` | 守卫 | 作用 | 触发条件 | |------|------|----------| | `IpBlackGuard` | IP 黑名单拦截 | `config.guard.ipBlacklist` 开启 | | `ApiGuard` | API 熔断开关 | `config.guard.apiSwitch` 开启 | | `AnalysisRoute` | 解析当前路由,注入 `routeInfo`、`routeKey` | 始终执行 | | `AuthGuard` | JWT 认证,注入 `ctx.user` | 路由 `meta.isAuth: true` | | `IpRateLimitGuard` | IP 限流 | 路由配置了 `meta.ipRateLimit` | | `PermissionGuard` | 按钮权限校验 | 路由配置了 `meta.permission` | 自定义守卫放在 `middleware/guards/` 下,按同样方式 `executeGuard` 接入即可。 ## 响应后处理 `onRequest` 记录请求开始时间,`onAfterResponse` 在响应完成后执行副作用,不阻塞返回。 ```ts [middleware/index.ts] import { logger } from '@/shared/logger'; import { AddOperLog } from '@/modules/system-oper-log/handle'; import { IpRateLimitRecord } from './guards/ipratelimit'; export function GlobalResponseMiddleware(app: Elysia) { app.onRequest((ctx) => { ctx.startTime = Date.now(); }); app.onAfterResponse(async (ctx) => { process.env.NODE_ENV !== 'production' && logger.logRequest(ctx); await AddOperLog(ctx); await IpRateLimitRecord(ctx); }); } ``` | 钩子 | 时机 | 典型用途 | |------|------|----------| | `onRequest` | 请求刚进入 | 记录 `startTime` | | `onAfterResponse` | 响应已发出 | 操作日志、限流计数、开发环境请求日志 | 路由 `meta.isLog: true` 时,`AddOperLog` 会把本次请求写入操作日志表。 ## 在 handle 里读上下文 `handle.ts` 使用 `AppContext` 类型(`@/types/app-context`),直接访问中间件注入的字段,无需 `(ctx as any)`。 ```ts [handle.ts] import type { AppContext } from '@/types/app-context'; import { GetClientIp } from '@/shared/ip'; import { BaseResultData } from '@/core/result'; export async function handleRequest(ctx: AppContext) { const startTime = ctx.startTime; // onRequest const user = ctx.user; // AuthGuard const routeInfo = ctx.routeInfo; // AnalysisRoute const routeKey = ctx.routeKey; const ip = ctx.ip ?? GetClientIp(ctx); // IpBlackGuard 等 // 业务逻辑... return BaseResultData.ok(); } ``` 常用注入字段: | 字段 | 来源 | 说明 | |------|------|------| | `routeInfo` | `AnalysisRoute` | 当前路由的 `tags`、`summary`、`meta` | | `routeKey` | `AnalysisRoute` | 路由唯一键,如 `GET:/api/system/user/list` | | `user` | `AuthGuard` | 当前登录用户(含 `permissions`) | | `startTime` | `onRequest` | 请求开始时间戳 | | `ip` | IP 相关守卫 | 客户端 IP | ## 错误处理 全局异常由 `middleware/error-handler.ts` 的 `ConfigureErrorHandler` 统一捕获,在 `app.ts` 中与上述中间件一并注册。业务层用 `BaseResultData.fail(code, msg)` 返回错误即可,不必在 `handle` 里包 `try/catch`。 --- URL: "https://elysia-admin.top/guide/parameter-validation.html" LLMS_URL: "https://elysia-admin.top/\\guide\\parameter-validation.md" title: "参数验证 - Elysia Admin 指南" description: "介绍 Elysia Admin 中的接口参数校验方案,包括 BaseResultDto、BaseListQueryDto、CrudDto 生成器及常用工具函数的使用方法。" --- --- title: 参数验证 - Elysia Admin 指南 description: 介绍 Elysia Admin 中的接口参数校验方案,包括 BaseResultDto、BaseListQueryDto、CrudDto 生成器及常用工具函数的使用方法。 --- # 参数验证 每个业务模块的 `dto.ts` 负责两件事:校验前端传来的 `body` / `query` / `params`,以及声明接口成功时的响应结构。校验不通过时,Elysia 会自动返回错误,不会进入 `handle.ts`。 `query`、`params`、`body` 的基础写法基于 Elysia 内置的 TypeBox,这里不展开。需要时可直接查阅官方文档: - [Elysia 参数校验](https://elysiajs.com/essential/validation.html) - [Elysia TypeBox](https://elysiajs.com/validation/typebox.html) 下文介绍项目封装的 DTO 工具,以及标准 CRUD 模块里最常见的写法。 ## 在模块里怎么用 `dto.ts` 定义校验规则,`route.ts` 通过 `dto` 字段引用;服务启动时注册到 Elysia。请求到达后,先按规则校验参数,通过才进入 `handle.ts`。 ```mermaid flowchart TD subgraph 启动时 D[dto.ts 定义 ListDto] --> R[route.ts 绑定 dto] R --> Reg[注册到 Elysia] end subgraph 请求时 Req[HTTP 请求] --> Valid{参数校验} Valid -->|通过| H[handle.ts] Valid -->|不通过| Err[自动返回错误] end Reg -.-> Valid ``` `dto.ts` 导出规则,`route.ts` 只负责绑定: ```ts [dto.ts] import { t } from 'elysia'; import { CrudDto } from '@/types/dto'; import { InsertBusinessGoods, SelectBusinessGoods } from '@database/schema/business_goods'; export const CreateDto = CrudDto.create( InsertBusinessGoods, SelectBusinessGoods, ['name', 'price'] ); export const ListDto = CrudDto.list(SelectBusinessGoods, { name: t.Optional(t.String({ description: '商品名称' })), }); ``` ```ts [route.ts] import { CreateDto, ListDto } from './dto'; import { create, findList } from './handle'; const routes = [ { url: '/business/goods', method: 'post', dto: CreateDto, handle: create, /* meta... */ }, { url: '/business/goods/list', method: 'get', dto: ListDto, handle: findList, /* meta... */ }, ]; ``` 更完整的模块搭建流程见 [第一个接口](./first-api)。 ## 响应格式 项目接口统一返回 `{ code, msg, data }`。`BaseResultDto` 和 `BaseResultListDto` 用来在 OpenAPI 文档里声明这个结构,并约束 `data` 的类型。 **单条数据** ```json { "code": 200, "msg": "success", "data": { "goodsId": 1, "name": "示例商品", "price": 99 } } ``` **分页列表** ```json { "code": 200, "msg": "success", "data": { "list": [ { "goodsId": 1, "name": "示例商品" } ], "total": 100 } } ``` `handle.ts` 里用 `BaseResultData.ok(data)` 返回,格式会自动对齐 DTO 声明。 ## BaseResultDto 声明「成功时 `data` 是某个对象」的响应。常与 `body` / `query` 写在同一个导出对象里。 ```ts [dto.ts] import { t } from 'elysia'; import { BaseResultDto } from '@/types/dto'; export const DetailDto = { ...BaseResultDto(t.Object({ goodsId: t.Number(), name: t.String(), price: t.Number(), })), }; ``` `CrudDto.create` / `update` / `findOne` 内部已自动带上 `BaseResultDto`,一般不需要手写。 ## BaseResultListDto 声明分页列表响应,`data.list` 为数组,`data.total` 为总数。 ```ts [dto.ts] import { t } from 'elysia'; import { BaseResultListDto } from '@/types/dto'; export const CustomListDto = { query: t.Object({ pageNum: t.Number({ default: 1 }), pageSize: t.Number({ default: 10 }), keyword: t.Optional(t.String()), }), ...BaseResultListDto(t.Object({ goodsId: t.Number(), name: t.String(), })), }; ``` `CrudDto.list` 已内置 `BaseResultListDto`,标准列表接口优先用它。 ## BaseListQueryDto 列表查询的 `query` 基座,自带分页、排序、时间范围字段,再叠加业务筛选条件。 内置字段: | 字段 | 说明 | 默认 | |------|------|------| | `pageNum` | 页码 | 1 | | `pageSize` | 每页条数 | 10 | | `orderByColumn` | 排序字段 | 可选 | | `sortRule` | 排序规则 | 可选 | | `startTime` | 开始时间 | 可选 | | `endTime` | 结束时间 | 可选 | ```ts [dto.ts] import { t } from 'elysia'; import { BaseListQueryDto } from '@/types/dto'; export const SearchDto = { query: BaseListQueryDto({ name: t.Optional(t.String({ description: '商品名称' })), status: t.Optional(t.Boolean({ description: '状态' })), }), }; ``` `handle.ts` 里可直接解构 `ctx.query`,分页字段已经过校验。 ## CrudDto 基于 Drizzle 的 `InsertXxx` / `SelectXxx` schema 快速生成标准 CRUD 的 DTO。新建模块时脚手架会生成骨架,你通常只需调整必填字段和额外查询项。 | 方法 | 用途 | 生成内容 | |------|------|----------| | `create` | 新增 | `body` + 单条响应 | | `update` | 更新 | `body`(主键必填,其余可选)+ 单条响应 | | `list` | 分页列表 | `query`(含分页)+ 列表响应 | | `findAll` | 全量查询 | `query` + 数组响应 | | `findOne` | 单条详情 | 单条响应 | ### create 指定哪些字段必填,其余字段从 `insertSchema` 自动变为可选。 ```ts [dto.ts] import { CrudDto } from '@/types/dto'; import { InsertBusinessGoods, SelectBusinessGoods } from '@database/schema/business_goods'; export const CreateDto = CrudDto.create( InsertBusinessGoods, SelectBusinessGoods, ['name', 'price', 'stock'] ); ``` ### update 主键必填,其他字段可选。日期字段 `createTime` / `updateTime` 接受 ISO 字符串。 ```ts [dto.ts] export const UpdateDto = CrudDto.update(SelectBusinessGoods, 'goodsId'); // 需要额外字段时传入第四个参数 export const UpdateDto = CrudDto.update(SelectBusinessGoods, 'goodsId', { tags: t.Optional(t.Array(t.String())), }); ``` ### list 分页查询,在 `BaseListQueryDto` 上叠加筛选字段。 ```ts [dto.ts] export const ListDto = CrudDto.list(SelectBusinessGoods, { name: t.Optional(t.String({ description: '商品名称' })), status: t.Optional(t.Boolean({ description: '状态' })), }); ``` 敏感字段(如密码)可在传入前用 `t.Omit` 去掉: ```ts [dto.ts] export const ListDto = CrudDto.list( t.Omit(SelectSystemUser, ['password']), { username: t.Optional(t.String()) } ); ``` ### findAll 不分页,返回全部匹配数据。`query` 只包含你传入的筛选字段。 ```ts [dto.ts] export const FindAllDto = CrudDto.findAll(SelectBusinessGoods, { status: t.Optional(t.Boolean({ description: '状态' })), }); ``` ### findOne 详情接口,只声明响应结构。路径参数 `id` 由路由 `url` 定义,不在 DTO 里重复写。 ```ts [dto.ts] export const FindOneDto = CrudDto.findOne(SelectBusinessGoods); ``` ![Swagger / API 截图](/guide/5.png) ## 工具函数 ### CreateUpdateDto `CrudDto.update` 内部使用。需要手写更新 DTO、又不想逐个字段标 `Optional` 时,可以直接用它:主键必填,其余字段可选,日期字段按字符串处理。 ```ts [dto.ts] import { CreateUpdateDto } from '@/types/dto'; import { SelectBusinessGoods } from '@database/schema/business_goods'; export const CustomUpdateDto = { body: CreateUpdateDto(SelectBusinessGoods, 'goodsId'), }; ``` ### ParseDateFields 前端传来的 `createTime` / `updateTime` 是字符串,写入数据库前转成 `Date`。 ```ts [handle.ts] import { ParseDateFields } from '@/types/dto'; export async function update(ctx: AppContext) { const data = ParseDateFields(ctx.body); // data.createTime / data.updateTime 已是 Date 对象 } ``` ## 非标准接口 不是所有接口都走 `CrudDto`。登录、改密码、自定义业务动作等,直接在 `dto.ts` 手写 `body` / `query` 即可。 ```ts [dto.ts] export const ChangePasswordDto = { body: t.Object({ oldPassword: t.String({ description: '旧密码' }), newPassword: t.String({ description: '新密码' }), }), }; ``` ```ts [route.ts] { url: '/system/user/password', method: 'put', dto: ChangePasswordDto, handle: updatePassword, meta: { /* ... */ } } ``` 校验规则和响应声明保持在 `dto.ts`,`route.ts` 只做绑定——这和标准 CRUD 的做法一致。 --- URL: "https://elysia-admin.top/guide/payment.html" LLMS_URL: "https://elysia-admin.top/\\guide\\payment.md" title: "支付集成 - Elysia Admin 指南" description: "介绍 Elysia Admin 中支付宝、微信支付、PayPal 的统一支付适配层,包括 Pay 路由、商户配置、下单查询退款与异步回调,以及与商户配置表、业务支付模块的配合方式。" --- --- title: 支付集成 - Elysia Admin 指南 description: 介绍 Elysia Admin 中支付宝、微信支付、PayPal 的统一支付适配层,包括 Pay 路由、商户配置、下单查询退款与异步回调,以及与商户配置表、业务支付模块的配合方式。 --- # 支付集成 对接支付宝、微信、PayPal 时,各家的 API、签名、回调格式都不一样。项目在 `server/src/infrastructure/clients/payment/` 封装了统一入口 `Pay(channel, platform)`,用「渠道 + 终端」路由到具体实现,对外暴露同一套 `create` / `query` / `refund` / `notify` / `notifySuccess` 方法。 类型定义在 `server/src/types/pay.ts`。业务模块 `business-payments` 已从 `business_merchant_configs` 读商户配置并调用 `Pay`,可作为参考实现。 ```mermaid flowchart LR Biz[handle.ts] --> Pay["Pay(channel, platform)"] Pay --> Reg[registry 解析] Reg --> AP[alipay/*] Reg --> WC[wechat/*] Reg --> PP[paypal/*] ``` ## 渠道与终端 `channel` 为支付渠道,`platform` 为终端类型。只有下表中打勾的组合才能调用,否则会抛出「不支持的支付组合」。 | channel | app | h5 | mini | pc | |---------|:---:|:--:|:----:|:--:| | alipay | ✅ | ✅ | ✅ | ✅ | | wechat | ✅ | ✅ | ✅ | ✅ | | paypal | ❌ | ✅ | ❌ | ✅ | TypeScript 类型:`PaymentChannel`、`PaymentPlatform`(`@/types/pay`)。 ## 快速入门 ```ts import { Pay } from '@/infrastructure/clients/payment'; import type { MerchantConfig } from '@/types/pay'; const client = Pay('alipay', 'mini'); ``` | 方法 | 作用 | |------|------| | `create` | 发起支付,返回本地 `paymentNo` 与前端调起参数 `payload` | | `query` | 主动查询支付状态 | | `refund` | 发起退款 | | `notify` | 解析异步回调(验签、解密) | | `notifySuccess` | 返回各平台要求的成功应答字符串 | 各方法第一个参数均为 `MerchantConfig`,第二个为业务入参(`PaymentCreateParams`、`QueryParams` 等)。 ## 商户配置 `MerchantConfig` 定义在 `@/types/pay`。私钥与密钥只存服务端,不要下发前端。 | 字段 | 说明 | |------|------| | `appId` | 支付宝/微信 AppID,或 PayPal Client ID | | `mchId` | 微信商户号 | | `privateKey` | 商户私钥 PEM,或 PayPal Client Secret | | `publicKey` | 支付宝公钥 / 微信平台证书公钥(回调验签) | | `config` | 扩展 JSON,各渠道专用字段见下文 | 多商户场景:从 `business_merchant_configs` 按 `merchantId`、`channel` 查出记录,把 `app_id`、`mch_id`、`private_key`、`public_key`、`config` 映射为 `MerchantConfig` 传入 `Pay`。 **支付宝私钥**:支持 PKCS#8(`BEGIN PRIVATE KEY`)与 PKCS#1(`BEGIN RSA PRIVATE KEY`);纯 Base64 无 PEM 头时会按 PKCS#1 自动补全。 **微信验签**:除顶层 `publicKey` 外,可在 `config.platformCerts` 按证书序列号维护多把平台公钥,对应回调头 `Wechatpay-Serial`,便于证书轮换。 ### 支付宝 config `buildAlipayRequest` 会读取 `notifyUrl`、`returnUrl`,至少配置异步通知地址。 ```json { "notifyUrl": "https://your.domain/api/business/payments/notify", "returnUrl": "https://your.domain/h5/pay/result" } ``` - `notifyUrl`:支付结果异步通知,需外网 HTTPS 可访问 - `returnUrl`:同步跳转页(PC/H5 支付完成后回跳);纯 App API 可为空 若业务 `create` 入参里也传了 `notifyUrl` / `returnUrl`,请与库中配置保持一致,避免网关公共参数与下单入参矛盾。 ### 微信 config V3 接口**必须**有 `serialNo` 和 `apiV3Key`,缺省会直接报错。 ```json { "serialNo": "7132D72A000000XXXXXXXXXXXXXXXXXX", "apiV3Key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "refundNotifyUrl": "https://your.domain/api/business/payments/refund-notify", "platformCerts": { "序列号A": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----", "序列号B": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" } } ``` - `serialNo`:商户 API 证书序列号,用于请求头 `Authorization` - `apiV3Key`:32 字节 APIv3 密钥,解密回调 `resource` 密文 - `refundNotifyUrl`:可选,退款结果通知 - `platformCerts`:可选,按序列号选公钥验签;未命中时用顶层 `publicKey` 下单 body 核心字段(`appid`、`mchid`、`out_trade_no`、`notify_url`、`amount`)不会被 `extra` 覆盖;`extra` 仅用于补充字段(如小程序 `openid`)。 ### PayPal config REST 用 `appId` + `privateKey`(Client ID / Secret)。回调验签依赖 Webhook ID。 ```json { "webhookId": "1AB23C45DE6789012" } ``` 注意: - `webhookId` 缺省将无法完成回调验签 - 当前 `providers/paypal/base.ts` 默认 API 根地址为**沙箱** `api-m.sandbox.paypal.com`;生产环境需改为 `https://api-m.paypal.com`,并使用 Live 密钥 - `PaymentCreateParams.notifyUrl` **不会**传给 PayPal 下单;支付结果靠 PayPal 后台 Webhook + 本项目 `notify` + `webhookId` ## 发起支付 create ```ts const result = await Pay('alipay', 'mini').create(merchantConfig, { orderNo: 'ORDER_20240101_001', title: '商品名称', description: '商品描述', // 可选 amount: '99.00', // 字符串,单位元;微信会安全转为分 currency: 'CNY', // 可选;PayPal 常用 USD notifyUrl: 'https://your.domain/pay/notify', returnUrl: 'https://your.domain/pay/return', // 可选 extra: {}, }); ``` 返回值 `PaymentCreateResult`: | 字段 | 说明 | |------|------| | `paymentNo` | 本地支付单号,建议写入支付流水表 | | `thirdTradeNo` | 部分渠道下单阶段即返回第三方单号 | | `payload` | 交给前端调起支付,形态随渠道/终端变化 | `paymentNo` 可选传入:业务已有流水号时带上;否则服务端生成。支付宝各端、微信各端、PayPal H5/PC 均支持。 ### extra 常见必填 | 渠道 / 终端 | 字段 | 说明 | |-------------|------|------| | alipay + mini | `extra.buyerId` | 买家支付宝用户标识 | | wechat + mini | `extra.openid` | 用户 openid,缺省报错 | | wechat + h5 | `extra.clientIp` | 用户出口 IP,建议填写 | | paypal + h5 | `extra.cancelUrl` | 取消支付跳转;不传则用 `returnUrl` | ### payload 形态摘要 | 终端 | 常见 payload | |------|----------------| | 支付宝 App | 原生 SDK 订单串 | | 支付宝 H5 / PC | `{ payUrl }` | | 支付宝小程序 | `{ tradeNo }` → `my.tradePay` | | 微信 App / 小程序 | App 调起参数 / `wx.requestPayment` 参数 | | 微信 H5 | `{ h5Url }` | | 微信 PC | `{ codeUrl }` 扫码 | | PayPal H5 | `{ approveUrl, orderId }` | | PayPal PC | `{ orderId }` 供 JS SDK | ## 查询 query ```ts const result = await Pay('wechat', 'mini').query(merchantConfig, { paymentNo: 'LOCAL_PAYMENT_NO', thirdTradeNo: '4200001...', // 依渠道含义不同 }); ``` `result.status`:`'pending' | 'success' | 'failed' | 'closed'`。成功时关注 `paidAt`、`thirdTradeNo`。 | 渠道 | `thirdTradeNo` | |------|----------------| | 支付宝 / 微信 | 可选,有助于精确查询 | | PayPal | **必填**,须为 PayPal Order ID(`payload.orderId`),不能仅用本地 `paymentNo` | ## 退款 refund ```ts const result = await Pay('alipay', 'app').refund(merchantConfig, { orderNo: 'ORDER_20240101_001', paymentNo: 'LOCAL_PAYMENT_NO', thirdTradeNo: '2024...', refundNo: 'REFUND_20240101_001', amount: '10.00', totalAmount: '99.00', reason: '用户申请退款', extra: { // PayPal:captureId 必填;currency 建议与捕获币种一致 }, }); ``` - 微信:金额字符串解析为元再转分,避免浮点误差 - PayPal:`extra.captureId` 必填;`extra.currency` 建议填写,缺省 USD ## 异步回调 支付网关 POST 到你配置的 `notifyUrl`。路由里读取**原始 body** 和**请求头**,交给 `notify` 验签解析;业务校验金额与幂等后更新订单,用 `notifySuccess()` 返回值作为 HTTP 响应体。 微信特别注意:必须用原始 body 字符串,不要用已 `JSON.parse` 再 `stringify` 的对象代替。 ```ts // 推荐:从 request 读原始 body(适配所有渠道) const rawBody = await request.text(); const headers: Record = {}; request.headers.forEach((v, k) => { headers[k] = v; }); const client = Pay('wechat', 'mini'); try { const result = await client.notify(merchantConfig, { rawBody, headers }); if (result.status === 'success') { // 更新订单、支付流水(注意幂等与金额校验) } return new Response(client.notifySuccess(), { status: 200 }); } catch { return new Response('fail', { status: 400 }); } ``` 成功响应格式(`notifySuccess()`): | 渠道 | 返回值 | |------|--------| | alipay | `success` | | wechat | `{"code":"SUCCESS","message":"成功"}` | | paypal | 空字符串 `''`(HTTP 2xx 即可) | 验签失败应返回非 2xx,以便渠道按策略重试。 ```mermaid sequenceDiagram participant GW as 支付网关 participant API as 业务路由 participant Pay as Pay(...).notify GW->>API: POST 原始 body + headers API->>Pay: notify(merchantConfig, ...) Pay->>Pay: 验签 / 解密 Pay-->>API: NotifyResult API->>GW: 200 + notifySuccess() ``` ### PayPal 回调补充 - 验签依赖 `paypal-*` 请求头,适配层对大小写不敏感 - 支付成功类事件以 `PAYMENT.CAPTURE.*` 为主;`thirdTradeNo` 一般为 Capture ID - 需要 Order ID 时看 `extra.paypalOrderId` - `orderNo` 来自 `custom_id` / `invoice_id`;`paymentNo` 与 `reference_id` 对齐 ## 完整示例:支付宝小程序 ```ts import { Pay } from '@/infrastructure/clients/payment'; import type { MerchantConfig } from '@/types/pay'; const merchantConfig: MerchantConfig = { appId: '2021000000000000', privateKey: '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----', publicKey: '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----', config: { notifyUrl: 'https://your.domain/pay/notify/alipay', returnUrl: 'https://your.domain/pay/return', }, }; const { paymentNo, payload } = await Pay('alipay', 'mini').create(merchantConfig, { orderNo: order.orderNo, title: order.title, amount: order.amount, notifyUrl: 'https://your.domain/pay/notify/alipay', extra: { buyerId: user.alipayUid }, }); // 1. 将 paymentNo 写入 business_payments 流水表 // 2. 将 payload.tradeNo 返回小程序,调用 my.tradePay({ tradeNO: payload.tradeNo }) // 回调路由(原始 body): // const result = await Pay('alipay', 'mini').notify(merchantConfig, { rawBody, headers }); // return Pay('alipay', 'mini').notifySuccess(); ``` 业务侧完整流程(订单校验、商户配置、事务写流水)见 `server/src/modules/business-payments/handle.ts` 的 `payOrder` 与 `payOrderNotify`。 ## 扩展新渠道 在 `server/src/infrastructure/clients/payment/providers/` 下新增目录,实现 `IPaymentProvider`(`@/types/pay`)。 在 `payment/index.ts` 的 `registry` 注册 `channel -> platform -> provider` 映射。 在 `server/src/types/pay.ts` 扩展 `PaymentChannel` 联合类型。 完成后即可 `Pay('新渠道', '某平台')` 获得统一调用方式。 ## 常见问题 - **不支持的支付组合**:对照渠道终端表,PayPal 不支持 app/mini - **微信 API 报错缺 serialNo / apiV3Key**:检查 `config` JSON 是否完整 - **回调验签失败**:微信用原始 body;支付宝公钥是否配对;PayPal `webhookId` 是否与后台一致 - **PayPal 沙箱/生产混用**:沙箱密钥不能打生产网关 - **金额不一致**:支付金额以数据库订单为准,不要用前端传来的金额(见 `business-payments` 注释) --- URL: "https://elysia-admin.top/guide/queue.html" LLMS_URL: "https://elysia-admin.top/\\guide\\queue.md" title: "队列 - Elysia Admin 指南" description: "详细介绍 Elysia Admin 基于 BullMQ 的队列系统,包括任务投递、延迟任务、优先级、幂等投递、自定义队列、重试策略、限流控制及多实例高可用部署。" --- --- title: 队列 - Elysia Admin 指南 description: 详细介绍 Elysia Admin 基于 BullMQ 的队列系统,包括任务投递、延迟任务、优先级、幂等投递、自定义队列、重试策略、限流控制及多实例高可用部署。 --- # 队列 有些业务不适合在 HTTP 请求里同步做完,比如发邮件、订单超时取消、批量导出。这类任务可以丢进队列,由后台 Worker 异步执行,接口快速返回,用户不用干等。 项目基于 **BullMQ Sandboxed Processors** 实现:主进程负责接收请求并投递任务;Redis 存队列;独立 Worker 进程取任务后,在**子进程**里跑 Processor,避免阻塞 HTTP 事件循环。 定时任务也走同一套设施(`system-cron-queue`),区别只是触发方式。沙箱注册与后台配置见 [定时任务](./cron)。 ## 架构概览 先把进程边界搞清楚,后面自定义队列时才知道文件该写在哪。 ```mermaid flowchart TD subgraph 主进程 server/src/index.ts HTTP[HTTP 请求] --> AddJob[queueManager.addJob] AddJob --> Redis[(Redis)] end subgraph Worker 进程 runtime/worker.ts Redis --> Worker[Worker 消费] Worker --> Spawn[spawn 子进程] Spawn --> Proc[dist/processors/*.js] end subgraph 子进程内 Proc --> Sandbox[worker-sandbox 任务注册] Sandbox --> Biz[业务函数 / handle] end Biz --> DB[(数据库 / Redis)] ``` 三层隔离: | 层级 | 说明 | |------|------| | 主进程 vs Worker | Worker 挂了,网站照常响应 | | Worker vs Processor 子进程 | 某个任务崩溃,不影响同队列其他任务 | | 子进程互相隔离 | 耗时计算不会卡住 Worker 事件循环 | 关键目录: | 路径 | 作用 | |------|------| | `infrastructure/queue/queues/*/queue.ts` | 注册队列(主进程加载) | | `infrastructure/queue/queues/*/worker.ts` | 注册 Worker(Worker 进程加载) | | `infrastructure/queue/queues/*/processor.ts` | 子进程入口,构建到 `dist/processors/` | | `worker-sandbox/*-tasks.ts` | 业务函数注册,可 import `modules` | | `runtime/app.ts` / `runtime/worker.ts` | 汇总 import 各队列 | ## 启动服务 本地开发至少要有一个 Worker 在跑,否则任务只会堆在 Redis 里没人消费。 ```bash cd server # 推荐:HTTP + Worker 一起启 bun run dev:all # 或分开启(调试 Worker 时常用) bun run dev # 仅主进程 bun run dev:workers # 仅 Worker(内部会先执行 build:processors) ``` 改动了 `processor.ts` 或 `worker-sandbox/*.ts` 后,必须重新构建 Processor,否则子进程跑的仍是旧代码: ```bash bun run build:processors # 然后重启 Worker(dev:workers 或 PM2) ``` ## 投递任务 所有投递通过 `queueManager` 完成,在 `handle.ts`、定时逻辑等主进程代码里均可调用。 ```ts import { queueManager } from '@/infrastructure/queue'; ``` 项目内置三个队列,可以先对照理解再自定义: | 队列名 | 用途 | Processor 模式 | |--------|------|----------------| | `system-cron-queue` | 定时任务 | `taskName` + `jobArgs` | | `flow-buffer-queue` | 延迟/缓冲类业务 | 按 `job.name` 分发 | | `trade-order-queue` | 订单示例(演示用) | 直接读 `job.data` | ### 基本投递 最常用的写法,把业务数据放进 `job.data`,Worker 在子进程里读取。 ```ts await queueManager.addJob('trade-order-queue', { orderId: 'ORD-001', action: 'pay', orderData: { amount: 100 }, }); ``` `trade-order` 的 Processor 会根据 `action` 分支处理(`create` / `pay` / `cancel` / `refund`),可在 `queues/trade-order/processor.ts` 查看示例。 ### 延迟任务 `delay` 单位为毫秒。适合「下单后 30 分钟未支付自动取消」这类场景。 ```ts // 30 分钟后执行 await queueManager.addJob('trade-order-queue', { orderId: 'ORD-001', action: 'cancel', }, { delay: 30 * 60 * 1000, }); ``` 项目里订单模块的真实写法(`flow-buffer-queue`,按 `job.name` 分发): ```ts // server/src/modules/business-orders/handle.ts(节选) const queue = queueManager.getQueue('flow-buffer-queue')!; await queue.add('订单超时处理', { orderNo }, { delay: timeout }); ``` 这里的 `'订单超时处理'` 必须与 `worker-sandbox/flow-buffer-tasks.ts` 里 `register('订单超时处理', ...)` 的名称一致。 ### 优先级 数字**越小**优先级**越高**,`1` 为最高。多任务积压时,高优先级先被消费。 ```ts await queueManager.addJob('flow-buffer-queue', data, { priority: 1, }); ``` ### 幂等投递 传入 `jobId` 后,相同 ID 的任务不会重复入队。适合防止用户重复点击、接口重试导致重复扣款。 ```ts const orderId = 'ORD-001'; await queueManager.addJob('trade-order-queue', data, { jobId: `pay-${orderId}`, }); ``` ### 批量投递 一次塞多条任务,比循环 `addJob` 更高效。 ```ts await queueManager.addBulkJobs('flow-buffer-queue', [ { data: { action: 'write', payload: { userId: 1 } } }, { data: { action: 'write', payload: { userId: 2 } } }, { data: { action: 'write', payload: { userId: 3 } } }, ]); ``` ### 控制保留数量 避免 Redis 里堆积过多历史任务记录。 ```ts await queueManager.addJob('system-cron-queue', data, { removeOnComplete: 100, // 完成后只留最近 100 条 removeOnFail: 200, // 失败后只留最近 200 条 }); ``` ## 定时调度 除了后台「定时任务」页面配置外,也可以在代码里用 `schedule` 注册 Cron。规则存在 Redis 中,服务重启后自动恢复,不用每次启动重新注册。 ```ts import { queueManager, schedule, removeSchedule } from '@/infrastructure/queue'; const queue = queueManager.getQueue('system-cron-queue')!; // 注册:每天凌晨 2 点 await schedule(queue, 'daily-cleanup', { cron: '0 2 * * *', data: { taskName: 'cleanup', // 对应 worker-sandbox 注册名 jobArgs: JSON.stringify([30]), // 传给任务函数的参数数组 }, }); // 移除 await removeSchedule(queue, 'daily-cleanup', '0 2 * * *'); ``` 常用 Cron 表达式: | 表达式 | 说明 | |--------|------| | `* * * * *` | 每分钟 | | `*/5 * * * *` | 每 5 分钟 | | `0 * * * *` | 每小时整点 | | `0 2 * * *` | 每天凌晨 2 点 | | `0 2 * * 1` | 每周一凌晨 2 点 | | `0 2 1 * *` | 每月 1 日凌晨 2 点 | 建议用 [Cron 在线工具](https://tool.lu/crontab/) 校验。`taskName` 与 `worker-sandbox` 注册、后台表单「任务名称」须完全一致,详见 [定时任务](./cron)。 ## 自定义队列 下面以新增 `email-notify-queue`(邮件通知)为例,从零走一遍。自定义队列本质是**新增一组文件 + 改三处注册 + 构建**。 整体 checklist: ``` queues/email-notify/queue.ts ← 注册队列 queues/email-notify/processor.ts ← 子进程逻辑 queues/email-notify/worker.ts ← 注册 Worker worker-sandbox/email-notify-tasks.ts ← 业务函数(可 import modules) runtime/app.ts ← import queue runtime/worker.ts ← import worker ``` ### 注册队列 主进程启动时加载,用于 `addJob` 投递。 ```ts // server/src/infrastructure/queue/queues/email-notify/queue.ts import { queueManager } from '../../core'; export default queueManager.registerQueue({ name: 'email-notify-queue', description: '邮件通知队列', }); ``` ### 沙箱任务 **不要**在 `processor.ts` 里直接 `import '@/modules/...'`。业务函数写在 `worker-sandbox`,保持 infrastructure 与 modules 的依赖边界(与定时任务相同)。 ```ts // server/src/worker-sandbox/email-notify-tasks.ts import type { TaskFn } from '@/infrastructure/queue/core/processor-utils'; async function sendEmail(to: string, subject: string, body: string) { console.log(`发送邮件到 ${to}: ${subject}`); // 实际项目可 import 模块 handle:import { sendMail } from '@/modules/xxx/handle'; } export function registerEmailNotifySandboxTasks( register: (name: string, fn: TaskFn) => void, ): void { register('sendEmail', sendEmail as TaskFn); } ``` ### Processor 构建后输出到 `dist/processors/email-notify.js`,Worker 以沙箱方式 spawn 执行。 ```ts // server/src/infrastructure/queue/queues/email-notify/processor.ts import type { SandboxedJob } from 'bullmq'; import { registerEmailNotifySandboxTasks } from '@/worker-sandbox/email-notify-tasks'; import { createTaskRegistry, parseArgs } from '../../core/processor-utils'; const { register, get } = createTaskRegistry(); registerEmailNotifySandboxTasks(register); export default async function processor(job: SandboxedJob) { const { taskName, jobArgs } = job.data; const taskFn = get(taskName); if (!taskFn) throw new Error(`未找到任务: ${taskName}`); await taskFn(...parseArgs(jobArgs)); return { success: true, taskName }; } ``` 子进程可以正常连数据库、Redis,但**拿不到**主进程内存里的单例(如已建立的连接对象),会在子进程内重新初始化。 ### Worker 指定并发数、限流等运行参数。 ```ts // server/src/infrastructure/queue/queues/email-notify/worker.ts import path from 'path'; import { queueManager, RateLimitPresets } from '../../core'; const processorFile = path.resolve(process.cwd(), 'dist/processors/email-notify.js'); queueManager.registerWorker({ queueName: 'email-notify-queue', processor: processorFile, options: { concurrency: 5, // 同时处理 5 个任务 ...RateLimitPresets.low, // 每秒最多 5 个(见下文「限流」) }, }); ``` ### 挂载与构建 `runtime/app.ts` 增加: ```ts import '../queues/email-notify/queue'; ``` `runtime/worker.ts` 增加: ```ts import '../queues/email-notify/worker'; ``` 新增 `queues/{name}/processor.ts` 后,`discoverProcessorEntries()` 会在 `bun run build` / `bun run build:processors` 时自动纳入构建,无需改构建脚本。 构建并投递测试任务: ```bash bun run build:processors bun run dev:workers ``` ```ts await queueManager.addJob('email-notify-queue', { taskName: 'sendEmail', jobArgs: JSON.stringify(['user@example.com', '验证码', '您的验证码是 123456']), }); ``` 到 Bull Board 面板确认任务从 waiting → completed。 ### 任务数据格式 两种 Processor 模式**不要混用**,新手最容易在这里踩坑: **模式 A:`taskName` + `jobArgs`** 用于 `system-cron-queue` 和上文 `email-notify` 自定义队列。 ```ts // 投递 await queueManager.addJob('email-notify-queue', { taskName: 'sendEmail', jobArgs: JSON.stringify(['a@b.com', '标题', '正文']), }); // Processor 内 const { taskName, jobArgs } = job.data; const taskFn = get(taskName); await taskFn(...parseArgs(jobArgs)); ``` **模式 B:按 `job.name` 分发** 用于 `flow-buffer-queue`。投递时第一个参数是任务名,`data` 整包传给函数。 ```ts // 投递(注意用 queue.add,第一个参数是 job.name) const queue = queueManager.getQueue('flow-buffer-queue')!; await queue.add('订单超时处理', { orderNo: 'xxx' }, { delay: 60000 }); // Processor 内(flow-buffer/processor.ts) const taskFn = get(job.name); await taskFn(job.data); ``` | 队列 | 投递方式 | Processor 取任务 | |------|----------|------------------| | `system-cron-queue` | `addJob` + `taskName` | `job.data.taskName` | | 自定义(email-notify 示例) | 同上 | 同上 | | `flow-buffer-queue` | `queue.add(name, data)` | `job.name` | | `trade-order-queue` | `addJob` + 业务字段 | 直接读 `job.data` | ## 重试策略 任务失败时 BullMQ 会自动重试。投递时通过选项控制策略,也可用项目预设。 ```ts import { RetryPresets, buildRetry } from '@/infrastructure/queue'; // 预设:指数退避,最多 5 次 await queueManager.addJob('trade-order-queue', data, { ...RetryPresets.aggressive, }); ``` | 预设 | 策略 | 次数 | 初始延迟 | |------|------|------|----------| | `RetryPresets.none` | 不重试 | 1 | — | | `RetryPresets.standard` | 固定间隔 | 3 | 2s | | `RetryPresets.aggressive` | 指数退避 | 5 | 1s | 自定义: ```ts // 固定间隔:失败后等 5 秒重试,最多 3 次 await queueManager.addJob('email-notify-queue', data, { ...buildRetry({ attempts: 3, strategy: 'fixed', delay: 5000 }), }); // 指数退避:1s → 2s → 4s → 8s,最多 4 次 await queueManager.addJob('trade-order-queue', data, { ...buildRetry({ attempts: 4, strategy: 'exponential', delay: 1000 }), }); ``` 对外部 API(发邮件、发短信)建议配合 `RateLimitPresets.low`,避免重试风暴把第三方打挂。 ## 限流控制 限流在 **Worker 注册时**配置,控制单位时间内最多处理多少个任务,与投递时的 `addJob` 无关。 ```ts import { RateLimitPresets, buildRateLimit } from '@/infrastructure/queue'; queueManager.registerWorker({ queueName: 'email-notify-queue', processor: processorFile, options: { concurrency: 5, // 同时并发处理的任务数 ...RateLimitPresets.low, // 每秒最多 5 个 }, }); ``` | 预设 | 每秒上限 | 适用场景 | |------|----------|----------| | `RateLimitPresets.low` | 5 | 邮件、短信等外部 API | | `RateLimitPresets.medium` | 20 | 普通业务处理 | | `RateLimitPresets.high` | 100 | 高并发写入缓冲 | 自定义: ```ts options: { concurrency: 10, ...buildRateLimit({ max: 50, duration: 2000 }), // 2 秒内最多 50 个 } ``` `concurrency` 是「同时跑几个」,`limiter` 是「单位时间最多跑几个」,两者同时生效。发邮件队列通常 `concurrency` 略高、`limiter` 偏低。 ## Processor 开发 ### 日志与进度 在 Bull Board 里可查看任务日志和进度条,调试时很有用。 ```ts export default async function processor(job: SandboxedJob) { await job.log('开始处理...'); await job.updateProgress(30); // 业务逻辑... await job.updateProgress(100); await job.log('处理完成'); return { success: true }; } ``` ### 子进程里投递新任务 子进程**不能**用主进程的 `queueManager` 单例,需要自行创建 `Queue`: ```ts import { Queue } from 'bullmq'; import Redis from 'ioredis'; import { getQueueEnvConfig } from '../../config/env'; const cfg = getQueueEnvConfig(); const conn = new Redis({ host: cfg.redis.host, port: cfg.redis.port, maxRetriesPerRequest: null, }); // 注意队列名带 appId 前缀 const notifyQueue = new Queue(`${cfg.appId}-email-notify-queue`, { connection: conn }); await notifyQueue.add('sendEmail', { taskName: 'sendEmail', jobArgs: JSON.stringify(['user@example.com', '通知', '处理完成']), }); ``` ### 修改后记得构建 ```bash bun run build:processors ``` ## 监控面板 登录 Admin 后台,进入 **系统监控 → 队列监控**(路径 `/monitor/bullmq`)。 面板支持: - 查看各队列 waiting / active / completed / failed / delayed 统计 - 按状态浏览任务、查看详情与运行日志 - 延迟任务倒计时与立即执行(promote) - 单任务 / 批量重试失败任务、删除、清空 completed/failed - 可选 3 秒 / 5 秒自动刷新 任务一直停在 waiting,优先检查 Worker 是否在跑(`bun run dev:workers` 或 PM2 里的 worker 进程)。 ## 部署与高可用 - BullMQ 通过 Redis 协调,同一任务只会被一个 Worker 消费,多实例部署一般不需要额外分布式锁 - 建议 HTTP 与 Worker 分开用 PM2 管理;Worker 崩溃重启不影响 API - 任务数据在 Redis 中持久化,重启后未完成的任务会继续被消费 ## 常见问题 - **任务不执行**:Worker 没启动,或改了 Processor 没 `build:processors` - **未找到任务**:`taskName` / `job.name` 与 `worker-sandbox` 注册名不一致 - **两种数据格式混用**:`flow-buffer` 用 `job.name`,cron 和自定义队列用 `taskName`,见上文对照表 - **子进程报错连不上 DB**:子进程会自行初始化连接,检查环境变量与 Redis/Postgres 是否可达 ## 参考 - [BullMQ 官方文档](https://docs.bullmq.io/) - [Sandboxed Processors](https://docs.bullmq.io/guide/workers/sandboxed-processors) - [Repeatable Jobs](https://docs.bullmq.io/guide/jobs/repeatable) - [Rate Limiting](https://docs.bullmq.io/guide/rate-limiting) - [Retrying Jobs](https://docs.bullmq.io/guide/retrying-failing-jobs) - [Cron 表达式工具](https://tool.lu/crontab/) - [ioredis](https://github.com/redis/ioredis) --- URL: "https://elysia-admin.top/guide/site-deploy.html" LLMS_URL: "https://elysia-admin.top/\\guide\\site-deploy.md" title: "站点部署 - Elysia Admin 指南" description: "介绍后台站点部署:维护域名与反代路径、预览 Nginx 配置、用 Let's Encrypt HTTP-01 申请与续签证书。" --- --- title: 站点部署 - Elysia Admin 指南 description: 介绍后台站点部署:维护域名与反代路径、预览 Nginx 配置、用 Let's Encrypt HTTP-01 申请与续签证书。 --- # 站点部署 后台「站点部署」用来维护本机 Nginx 站点:保存域名与反代路径,预览配置文本,并用 Let's Encrypt 申请证书。把 Elysia Admin 自身用 Docker 或 PM2 跑起来,见 [部署](/guide/deploy)。 代码在 `server/src/modules/system-deploy/`。站点表 `system_deploy_site`、证书记录表 `system_deploy_cert`,字段见 [数据库设计](/architecture/database)。 ```mermaid flowchart TD Form[保存站点] --> Preview[预览 Nginx 配置] Preview --> Panel[自行写入面板或 vhost] Form --> Renew[立即续签或定时续签] Renew --> ACME[HTTP-01 校验] ACME --> Pem[写入 fullchain.pem 与 privkey.pem] Pem --> Reload[可选 nginx -s reload] ``` ## 前置条件 证书申请和 Nginx 重载都在**运行后端的那台机器**上执行。 - 本机可执行 `openssl`(生成账户密钥与证书请求)和 `nginx`(仅在开启自动重载时调用 `nginx -s reload`) - 域名的 80 端口能访问到该机,Let's Encrypt 用 HTTP-01 读取 `/.well-known/acme-challenge/` - 进程对网站校验目录、证书目录可写 - 定时续签依赖 Worker。任务名必须是「站点证书续签」,种子数据中的 Cron 为 `15 3 * * *`(每天 03:15)。库里没有这条任务时,在「定时任务」里按该名称新增并启用。详见 [定时任务](/guide/cron) ACME 账户密钥写在进程工作目录的 `storage/certs/account.pem`,账户 URL 写在同目录的 `account.url`。 ## 站点字段 未删除站点的域名唯一。站点名只允许字母、数字、下划线和连字符,且以字母或数字开头(宝塔路径会拼进文件名)。 | 字段 | 说明 | |------|------| | 站点名 `name` | 用于宝塔 include、日志文件名 | | 域名 `serverName` | Nginx `server_name` | | 网站根目录 `wwwRoot` | 静态站点(管理端等)默认 `root` | | 校验目录 `webRoot` | HTTP-01 文件根目录,默认进程工作目录 | | 证书目录 `certDir` | 默认 `storage/certs/<域名>`,其下为 `fullchain.pem`、`privkey.pem` | | 邮箱 | 开启自动续签时必填,作为 ACME 账户联系邮箱 | | 自动续签 `sslAutoRenew` | 交给定时任务扫描 | | 自动重载 `reloadNginx` | 续签成功后执行 `nginx -s reload`;关闭时需要自己到面板重载 | | 宝塔目录 `useBaota` | 默认开启。配置中带宝塔 `vhost` include 与 `/www/wwwlogs/` 日志路径 | | HTTP 跳 HTTPS `httpRedirect` | 仅出现在含证书的那份配置里;`/.well-known/` 不跳转 | | 反代 `locations` | 路径列表,见下一节 | 校验文件的绝对路径是 `/.well-known/acme-challenge`。预览接口会把 `challengeDir`、`certDir` 一并返回,详情和列表卡片也会带上解析后的绝对路径。 ## 反代路径 未传 `locations` 时使用四条预设;传了数组则按数组保存,可以是空数组。 | kind | 默认 | 行为 | |------|------|------| | `home` | `/` → `127.0.0.1:4002` | 反代 | | `admin` | `/admin`,静态目录取 `wwwRoot` | `try_files` 回退到 `index.html` | | `api` | `/api` → `127.0.0.1:4003` | 反代 | | `rustfs` | `/zqwz/` → `127.0.0.1:9000`,上传限制 `50m` | 反代,`Host` 使用 `$host` | | `custom` | 自填路径 | `proxy` 反代到端口,或 `static` 指向目录 | 路径以 `/` 开头,不能含 `..` 和换行等特殊字符,同一站点内不能重复。更长的路径写在更前面,避免被短前缀截走。RustFS 只反代到本机,不要把 9000 端口暴露到公网。 若当前启用的存储配置名称是 `RustFS`,站点表单通过 `GET /api/system/storage/rustfs-bucket` 读取桶名,用作默认反代路径。存储配置见 [文件存储](/guide/storage)。 ## 预览配置 `POST /system/deploy/preview/:id` 只返回文本,不写 Nginx 文件。一次给出两份: - `httpOnly`:只监听 80,申请证书前先让校验目录可访问 - `ssl`:额外监听 `443 ssl` 与 `443 quic`,引用证书目录里的 `fullchain.pem`、`privkey.pem` 把对应文本放到面板或 vhost 后,由你自己重载 Nginx。宝塔开关打开时,配置会 include: - `/www/server/panel/vhost/nginx/extension/<站点名>/*.conf` - `/www/server/panel/vhost/nginx/well-known/<站点名>.conf` - `/www/server/panel/vhost/rewrite/node_<站点名>.conf` 这些文件不存在时,Nginx 检测会失败,需要在面板里先有同名站点,或关掉「宝塔目录」再预览。 ## 申请与续签 签发走 Let's Encrypt 生产目录 `https://acme-v02.api.letsencrypt.org/directory`。每次续签都新建订单,有效期以返回的证书为准。成功后原子替换 `fullchain.pem` 与 `privkey.pem`(失败会留住旧文件),并写入 `system_deploy_cert`。同一站点只有一条 `isCurrent = true` 的未删除记录。 立即续签是 `POST /system/deploy/renew/:id`。定时任务调用同一逻辑,但只处理同时满足这两点的站点:`sslAutoRenew` 为 true,且没有 `certExpireAt` 或距离到期不足 30 天。 手动续签与定时扫描共用 Redis 锁 `deploy-cert-renew`(约 10 分钟)。锁被占用时,手动接口返回「正在续签,请稍后再试」,定时任务直接跳过。续签失败会把原因写到站点的「最近续签结果」和证书记录,不中断其他站点。 开启了自动重载且续签成功时,才会执行 `nginx -s reload`。命令不存在或返回非 0 时,接口仍视为证书已更新,文案里会提示到面板手动重载。 ## 权限 路由都要求登录,权限码如下。 | 权限 | 接口 | |------|------| | `system:deploy:query` | 列表、全部、详情 | | `system:deploy:create` | 创建 | | `system:deploy:update` | 更新 | | `system:deploy:delete` | 软删除 | | `system:deploy:preview` | 生成配置 | | `system:deploy:renew` | 立即续签 | | `system:deploy:cert` | 证书记录 | --- URL: "https://elysia-admin.top/guide/storage.html" LLMS_URL: "https://elysia-admin.top/\\guide\\storage.md" title: "文件存储 - Elysia Admin 指南" description: "介绍 Elysia Admin 的文件存储方案,包括 RustFS 安装、S3 对象存储配置、预签名 URL 直传流程,以及 Web 端和 uni-app 小程序端的上传实现。" --- --- title: 文件存储 - Elysia Admin 指南 description: 介绍 Elysia Admin 的文件存储方案,包括 RustFS 安装、S3 对象存储配置、预签名 URL 直传流程,以及 Web 端和 uni-app 小程序端的上传实现。 --- # 文件存储 项目通过 **预签名 URL** 实现文件直传:客户端先把文件 PUT 到对象存储,后端只负责签发短时 URL,不经手文件流。 支持所有兼容 S3 协议的服务,包括 RustFS(自建)、阿里云 OSS、腾讯云 COS、七牛 Kodo 等。无法采购云服务时,可用 Docker 快速部署 RustFS。 ## 为什么用对象存储 文件存本地磁盘会带来迁移成本高、难横向扩展、占用后端带宽和磁盘 I/O、缺少容灾与生命周期管理等问题。对象存储把这些能力交给专用服务,后端只保存文件 URL。 ## 部署 RustFS RustFS 是兼容 S3 的开源对象存储,Docker 一键启动: ```bash docker run -d --name rustfs_container --user root \ -p 9000:9000 -p 9001:9001 \ -v /mnt/rustfs/data:/data \ -e RUSTFS_ACCESS_KEY=rustfsadmin \ -e RUSTFS_SECRET_KEY=rustfsadmin \ -e RUSTFS_CONSOLE_ENABLE=true \ rustfs/rustfs:latest \ --address :9000 --console-enable \ --access-key rustfsadmin --secret-key rustfsadmin /data ``` | 端口 | 用途 | |------|------| | 9000 | S3 API,上传与下载 | | 9001 | Web 管理控制台 | 默认凭据:`rustfsadmin` / `rustfsadmin`。生产环境务必修改 Access Key 和 Secret Key。 ## 后台配置 登录后台,进入 **系统管理 → 存储配置**,填写 Endpoint、Bucket、Access Key、Secret Key 等信息后启用。 ![存储配置表单](/guide/2.png) 注意: - 系统内置主流服务商模板,可快速填写 - 同一时间只能启用一种存储配置;启用新配置会自动禁用旧的 - 站点部署在填写 RustFS 反代路径时,会请求 `GET /api/system/storage/rustfs-bucket`,读取当前启用且名称为 `RustFS` 的桶名。详见 [站点部署](/guide/site-deploy) | 字段 | 说明 | |------|------| | 名称 | 供应商标识,如 RustFS、OSS、COS | | Region | 服务区域;RustFS 可填 `us-east-1` | | Endpoint | API 访问地址,不含 bucket 前缀(OSS 由代码自动拼接) | | Bucket | 存储桶名称 | | Access Key / Secret Key | 访问密钥 | ### 配置示例 **RustFS** ```json { "name": "RustFS", "region": "us-east-1", "endpoint": "http://192.168.1.100:9000", "bucket": "public", "access_key": "rustfsadmin", "secret_key": "rustfsadmin" } ``` **腾讯云 COS** ```json { "name": "COS", "region": "ap-nanjing", "endpoint": "cos.ap-nanjing.myqcloud.com", "bucket": "my-bucket-1234567890", "access_key": "AKIDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "secret_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } ``` `bucket` 格式为 `<桶名>-`,密钥在「访问管理 → API 密钥管理」获取。 **阿里云 OSS** ```json { "name": "OSS", "region": "cn-hangzhou", "endpoint": "oss-cn-hangzhou.aliyuncs.com", "bucket": "my-bucket", "access_key": "LTAI5txxxxxxxxxxxxxxxxxx", "secret_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } ``` `endpoint` 不含桶名前缀,代码内部会拼成 `.`。 **七牛 Kodo** ```json { "name": "Kodo", "region": "", "endpoint": "https://your-bucket.qiniudn.com", "bucket": "my-kodo-bucket", "access_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "secret_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } ``` `endpoint` 填存储桶访问域名,`region` 可留空。 ## 上传流程 ```mermaid flowchart TD A[选择文件] --> B[GET /api/system/storage/presign] B --> C[返回预签名 URL] C --> D[客户端 PUT 直传对象存储] D --> E[取 URL 去掉查询参数] E --> F[写入业务表单] ``` 后端接口 `GET /api/system/storage/presign?fileName=xxx` 会读取当前启用的存储配置,调用 `StorageService.getPresignedUrl` 签发 URL。前端封装为 `fetchGeneratePresign`(`admin/src/api/system/storage.ts`),`ArtForm` 上传组件已内置这套流程。 直传的优势:不占后端带宽、支持大文件、后端只做轻量签名。 ## Web 端 浏览器用原生 `fetch` 即可完成,与 `ArtForm` 中的实现一致: ```ts import { fetchGeneratePresign } from '@/api/system/storage' const presignUrl = await fetchGeneratePresign({ fileName: file.name }) const response = await fetch(presignUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type || 'application/octet-stream' }, }) // 持久化地址:去掉签名查询参数 const fileUrl = presignUrl.split('?')[0] ``` ## uni-app / 小程序 小程序环境不能直接把 `File` 对象 PUT 出去,需要先用 `getFileSystemManager` 读出 `ArrayBuffer`,再调用后端获取预签名 URL,最后用 `uni.request` 以 `PUT` 上传。下面是一份可直接复制使用的完整示例,`generatePreSignUrl` 需对接你的 presign 接口(与 Web 端 `fetchGeneratePresign` 等价)。 ```ts [ts] /** * 通过文件名称检测文件类型返回对应的 Content-Type */ function getContentTypeByFileName(fileName: string) { const ext = fileName.split('.').pop()?.toLowerCase(); if (!ext || ext === fileName) return 'application/octet-stream'; switch (ext) { case 'jpg': case 'jpeg': return 'image/jpeg' case 'png': return 'image/png' case 'webp': return 'image/webp'; case 'svg': return 'image/svg+xml'; case 'gif': return 'image/gif' case 'mp4': return 'video/mp4' case 'mp3': return 'audio/mpeg'; case 'avi': return 'video/x-msvideo' case 'mov': return 'video/mov' case 'pdf': return 'application/pdf' case 'docx': return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' case 'xlsx': return 'application/vnd.ms-excel' case 'txt': return 'text/plain' case 'csv': return 'text/csv' case 'wav': return 'audio/wav'; case 'ogg': return 'audio/ogg'; case 'html': return 'text/html'; case 'css': return 'text/css'; case 'js': return 'application/javascript'; case 'json': return 'application/json' case 'xml': return 'application/xml' case 'zip': return 'application/zip' case 'rar': return 'application/x-rar-compressed' case '7z': return 'application/x-7z-compressed' case 'tar': return 'application/x-tar' case 'gz': return 'application/x-gzip' default: return 'application/octet-stream' } } /** * 通过 url 获取文件名称 */ function getFileNameByUrl(url: string) { let fileName = url.split('/').pop() || '' const ext = fileName.split('.').pop() fileName = fileName.slice(0, 10) fileName = fileName.replace(/[^\w]/g, '_') return fileName + (ext ? '.' + ext : '') } /** 文件读成arrayBuffer */ const fsm = uni.getFileSystemManager(); async function readFileAsBuffer(url: string): Promise { return new Promise((resolve, reject) => { fsm.readFile({ filePath: url, success(res) { resolve(res.data as ArrayBuffer) }, fail(err) { console.error('文件读取失败:', err) reject(err) }, }) }) } /** S3预签名单文件上传 */ export function runOne(url: string): Promise { return new Promise(async (resolve, reject) => { const fileName = getFileNameByUrl(url) const contentType = getContentTypeByFileName(fileName) const arrayBuffer = await readFileAsBuffer(url) const preSignUrl = await generatePreSignUrl(fileName) const fileUrl = preSignUrl.split('?')[0] uni.request({ url: preSignUrl, method: 'PUT', header: { 'Content-Type': contentType, }, data: arrayBuffer, success(res) { if (res.statusCode === 200) resolve(fileUrl) }, fail(err) { console.error('上传文件失败:', err) reject(err) }, }) }) } /** * 简易版通用S3预签名文件上传 */ export function uploadFileToS3() { /** 图片上传 */ function image(options?: any): Promise { return new Promise((resolve, reject) => { uni.chooseImage({ success: async (result: UniApp.ChooseImageSuccessCallbackResult) => { const paths = result.tempFilePaths const tempFilePaths: string[] = typeof paths === 'string' ? [paths] : [...paths] const fileUrls = await Promise.all(tempFilePaths.map(runOne)) const formate = fileUrls.length > 1 ? fileUrls : fileUrls[0] resolve(formate) }, fail(err: UniApp.GeneralCallbackResult) { console.error("选择图片失败:", err) reject(err) }, ...options, }) }) } /** 视频上传 */ function video(options?: any): Promise { return new Promise((resolve, reject) => { uni.chooseVideo({ success: async (result: UniApp.ChooseVideoSuccess) => { const fileUrl = await runOne(result.tempFilePath) resolve(fileUrl) }, fail(err: UniApp.GeneralCallbackResult) { console.error("选择视频失败:", err) reject(err) }, ...options, }) }) } // #ifdef MP-WEIXIN /** 文件上传 */ function file(options?: any): Promise { return new Promise((resolve, reject) => { wx.chooseMessageFile({ success: async (result) => { const tempFilePaths = result.tempFiles.map(item => item.path) const fileUrls = await Promise.all(tempFilePaths.map(runOne)) const formate = fileUrls?.length > 1 ? fileUrls : fileUrls[0] resolve(formate) }, fail(err) { console.error("选择文件失败:", err) reject(err) }, ...options, }) }) } // #endif return { image, video, // #ifdef MP-WEIXIN file, // #endif } } ``` 使用方式: ```ts const uploader = uploadFileToS3() const imageUrl = await uploader.image() const videoUrl = await uploader.video() // 微信小程序 const fileUrl = await uploader.file() ``` ## 常见问题 - **文件名**:上传前过滤特殊字符,或用 UUID 重命名,避免部分环境兼容问题 - **时效**:预签名 URL 通常 15–60 分钟有效,拿到后尽快上传 - **CORS**:跨域 PUT 失败时,检查对象存储控制台的 CORS 规则 - **Content-Type**:务必与文件类型一致,否则浏览器可能强制下载而非预览 - **微信小程序域名**:若出现 `request:fail url not in domain list`,说明请求域名未加入白名单。登录 [微信公众平台](https://mp.weixin.qq.com/) → **开发管理 → 开发设置 → 服务器域名**,将后端 API 域名(presign 接口)和对象存储 Endpoint 域名(PUT 上传)添加到 **request 合法域名** --- URL: "https://elysia-admin.top/other/faq.html" LLMS_URL: "https://elysia-admin.top/\\other\\faq.md" title: "常见问题 - Elysia Admin" description: "解答 Elysia Admin 使用过程中的常见问题,包括 PM2 集群模式限制、多实例部署方案及 Nginx 负载均衡配置。" --- --- title: 常见问题 - Elysia Admin description: 解答 Elysia Admin 使用过程中的常见问题,包括 PM2 集群模式限制、多实例部署方案及 Nginx 负载均衡配置。 --- # 常见问题 ## 为什么不再支持二进制打包? 从引入 BullMQ Sandboxed Processors 之后,二进制打包就不再可行了。主要原因有两个: - 第一,`config/index.ts` 通过 `readFileSync` 读取 `production.yaml`,二进制文件没有文件系统,读不到外部文件。 - 第二,也是更根本的问题,Sandboxed 模式下 BullMQ 会用 `child_process.spawn` 启动独立子进程来执行每个任务,而二进制包里没有 `bun` 运行时,也没有 `processors/` 目录,子进程根本无法启动。 如果放弃 Sandboxed 模式改回函数模式,二进制是可以打包的,但会失去进程隔离的优势。综合考虑,直接使用 PM2 部署是更合适的方案,稳定性和功能都更完整。 ## 为什么无法使用PM2的cluster(集群模式)? `PM2` 的集群模式并不是自己发明了多进程管理,而是深度依赖 `Node.js` 原生的 `cluster` 模块。当你设置 `exec_mode: 'cluster'` 时,`PM2` 实际上是在调用 `Node.js` 的内部 API 来分叉进程并共享端口(TCP 句柄)。 但是我们的 `Elysia Admin` 项目完全依赖 `Bun` 。 `Bun` 虽然兼容了很多 `Node.js` API,但它并没有完全实现 cluster 模块(或者说实现方式与 Node 不同)。 因此你需要手动多实例: ```js [ecosystem.config.cjs] module.exports = { apps: [ { name: 'Elysia-Admin-1', script: 'bun', args: 'run index.js', interpreter: 'node', exec_mode: 'fork', // 必须 fork env: { PORT: 3001, // 指定不同端口 NODE_ENV: 'production' } }, { name: 'Elysia-Admin-2', script: 'bun', args: 'run index.js', interpreter: 'node', exec_mode: 'fork', env: { PORT: 3002, // 指定不同端口 NODE_ENV: 'production' } }, // 可以继续加 3, 4... ] }; ``` ```txt [nginx] http { upstream elysia_app { server 127.0.0.1:3001; server 127.0.0.1:3002; # 负载均衡策略,默认是轮询 } server { listen 80; location / { proxy_pass http://elysia_app; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } } ``` ## 支付回调收不到通知,是什么原因? 支付网关(支付宝、微信、PayPal)的异步通知是由**网关主动 POST 到你的服务器**,因此回调地址必须满足以下条件: - **外网可访问**:本地开发环境(如 `localhost`)无法接收回调,需使用内网穿透工具(如 [ngrok](https://ngrok.com/))或直接部署到公网服务器。 - **使用 HTTPS**:微信支付和支付宝生产环境均要求回调地址为 HTTPS。 - **及时返回 2xx**:网关在收到 2xx 之前会按策略重试,务必在完成业务处理后用 `notifySuccess()` 的返回值作为响应体,并返回 HTTP 200。 ## 微信支付调用报错:serialNo 缺失 微信支付 V3 接口要求在请求头 `Authorization` 中携带商户 API 证书序列号。若报错提示 `serialNo` 缺失,请检查商户配置 `MerchantConfig.config.serialNo` 是否已正确填写。 ```json { "serialNo": "7132D72A000000XXXXXXXXXXXXXXXXXX", "apiV3Key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } ``` `serialNo` 可在微信商户平台「账户中心 → API 安全 → 管理证书」页面查看;`apiV3Key` 在同页面「APIv3 密钥」处设置。**两者缺一不可,缺省会导致所有微信支付接口调用失败。** ## PayPal 为什么默认连接的是沙箱环境? `providers/paypal/base.ts` 中 API 根地址默认指向 `api-m.sandbox.paypal.com`(沙箱),便于本地开发调试。上线生产环境前,需将地址改为 `https://api-m.paypal.com`,并使用 **Live** 应用的 Client ID / Secret;沙箱与生产密钥**不可混用**,否则会导致鉴权失败。 ## 如何做路由拦截与接口中间件? 路由注册与全局守卫分两处,按需求改对应文件即可: | 需求 | 文件 | 说明 | | :--- | :--- | :--- | | 路由拦截 / 注册策略 | [`server/src/modules/index.ts`](../../server/src/modules/index.ts) | `RegisterRoutes()` 自动加载各模块 `route.ts`,按 `meta.isAuth` 拆分为公共路由与需登录路由;需登录的路由统一走 `app.guard` 校验 `authorization` 请求头。若要调整**哪些路由需要登录、如何分组挂载**,在此修改。 | | 接口中间件(守卫链) | [`server/src/middleware/index.ts`](../../server/src/middleware/index.ts) | `GlobalMiddleware` 在 `onBeforeHandle` 中依次执行:IP 黑名单 → API 熔断 → 路由分析 → 登录认证 → IP 限流 → 权限校验;`GlobalResponseMiddleware` 负责请求日志与操作日志。新增或调整**请求进入业务前**的拦截,在此修改。 | `CreateApp()`([`server/src/app.ts`](../../server/src/app.ts))会先挂载全局中间件,再调用 `RegisterRoutes()` 注册业务路由。单条路由还可通过 `route.ts` 的 `meta`(如 `isAuth`、`permission`、`ipRateLimit`)配合守卫生效。 --- URL: "https://elysia-admin.top/other/social.html" LLMS_URL: "https://elysia-admin.top/\\other\\social.md" title: "找到我们 - Elysia Admin" description: "加入 Elysia Admin 社区,通过微信或 QQ 群与作者及其他开发者交流,获取最新动态与技术支持。" --- --- title: 找到我们 - Elysia Admin description: 加入 Elysia Admin 社区,通过微信或 QQ 群与作者及其他开发者交流,获取最新动态与技术支持。 --- # 找到我们 有问题想聊?遇到 bug 想反馈?或者只是想认识一下同道中人?欢迎加入我们的社区,大家一起搞事情。 ## 微信 有商务合作意向、或者遇到问题想直接跟作者沟通,扫码加微信就行,备注 `Elysia Admin` 方便通过。 作者微信二维码 ## QQ 群 日常技术交流、问题反馈都在群里,欢迎加入。 | 群名 | 群号 | | --- | --- | | elysia-admin 1群 | `527636808` | QQ 交流群二维码 感谢支持本项目的朋友,名单见 [鸣谢](/other/sponsors)。 ## 说在最后 社区氛围很重要,进群之后多交流、多分享,有好的想法或者发现了问题都可以直接说。 项目是开源的,欢迎提 PR 和 Issue,一起把它做得更好。 --- URL: "https://elysia-admin.top/other/sponsors.html" LLMS_URL: "https://elysia-admin.top/\\other\\sponsors.md" title: "感谢支持 - Elysia Admin" description: "感谢自愿打赏与支持 Elysia Admin 开源项目的朋友们,你们的鼓励是项目持续维护的动力。" --- --- title: 感谢支持 - Elysia Admin description: 感谢自愿打赏与支持 Elysia Admin 开源项目的朋友们,你们的鼓励是项目持续维护的动力。 --- # 感谢支持 `Elysia Admin` 是开源项目,维护与文档更新离不开社区朋友的鼓励。以下为自愿打赏支持的朋友,排名不分先后,在此一并致谢。 ::: info 暂不接受陌生人打赏。如需支持项目,请先 [添加作者微信](/other/social#微信) 后再进行打赏。 :::

{{ item.nickname }}

暂无鸣谢名单。

--- URL: "https://elysia-admin.top/start/change-log.html" LLMS_URL: "https://elysia-admin.top/\\start\\change-log.md" title: "更新日志 - Elysia Admin" description: "Elysia Admin 版本更新记录,包含各版本新增功能、问题修复及重要变更说明。" --- --- title: 更新日志 - Elysia Admin description: Elysia Admin 版本更新记录,包含各版本新增功能、问题修复及重要变更说明。 --- # 更新日志 ## v1 ::: timeline v1.5.10(2026-10-22) - 新增了请求/响应加解密:`crypto.enabled` 为 true 时注册 `CryptoMiddleware`。请求头 `X-Encrypt-Key` 用 RSA-OAEP(SHA-256)解出本次 AES-256-GCM 密钥,JSON 体 `{ data: Base64(iv 12 字节 + 密文) }` 解密后再进入校验;响应对称为 `{ encrypted: true, data }`,并带 `x-encrypted: 1`。没有该头的请求仍走明文。加密后的 `onAfterResponse` 会还原明文,操作日志仍能读取 `response.code`。前端仅当 `VITE_API_ENCRYPT=true` 且 `GET /api/crypto/public-key` 返回启用和公钥时才加密,否则自动退回明文;文件上传与二进制响应跳过。密钥用 `bun run gen:crypto-keys` 写入 yaml(生产环境加参数 `production`,覆盖加 `--force`),私钥不要提交到仓库。详见 [请求加解密](/guide/crypto)。 - 修复了软删除后用户名无法再次使用的问题:`system_user.username` 由列级 `UNIQUE` 改为部分唯一索引 `system_user_username_unique WHERE del_flag = false`;`RegisterUser` 只在未删除行中查重。已有库升级时需去掉旧的列级唯一约束,再同步该索引(`bun run db:push` 或手工 DDL)。 - 新增了站点部署:后台维护站点(域名、宝塔目录开关、反代 location:首页 / 管理端 / API / RustFS / 自定义),可预览仅 HTTP 与含证书的两套 Nginx 配置;本机 `openssl` 走 Let's Encrypt HTTP-01 签发 `fullchain.pem` / `privkey.pem`。支持立即续签;定时任务「站点证书续签」处理已开启自动续签、且没有证书或剩余不足 30 天的站点,可选执行 `nginx -s reload`。Redis 锁避免并发续签,未删除站点的域名唯一。另增 `GET /api/system/storage/rustfs-bucket`,供站点表单读取当前启用的 RustFS 桶名。详见 [站点部署](/guide/site-deploy)。 - 修复了富文本标题被样式压平的问题:WangEditor 编辑区补回 h1–h6 字号与字重,加粗改为 700,标题下拉不再用 `!important` 统一字号。通知公告弹窗在 `reset` 之后调用 `clearValidate`,避免选择器误报校验。 ::: ::: timeline v1.5.9(2026-10-06) - 修复了登录等热路径对 Redis 全库 SCAN 的依赖:refresh / 在线用户改为 SET 索引维护,登录、登出、踢人、改密按用户清理,不再 `Keys` 扫库;`Keys` 仅留给缓存监控等低频运维接口。 - 增强了 refresh 索引升级兼容:升级前散落的 refresh key 在该用户首次清理时 SCAN 一次并写入迁移标记,之后走索引,避免反复全库扫描。 - 修复了生产构建临时 stub 残留:`runUnifiedBuild` / `runProcessorBuild` 在 `finally` 中清理 stub 目录,构建失败也会删临时入口,降低目录被占用导致的二次构建异常。 - 优化了开发 SQL 控制台对显式事务脚本的支持:含 `BEGIN`/`COMMIT` 时剥离关键字,事务体走 `pgClient.begin`,其后语句再 `unsafe`,避免连接池禁止 `unsafe` 发送事务关键字。 - 修复了菜单保存漏传全屏页字段:提交时补上 `isFullPage`,全屏页配置编辑后不再丢失。 ::: ::: timeline v1.5.8(2026-09-17) - 新增了开发环境 SQL 控制台:后端 `dev-sql` 模块(`POST /dev/sql/execute`,`stages: ['development']`,需登录);前端 `admin/src/views/dev/sql/` 支持执行任意 SQL、返回列/行与耗时,SELECT 超 1000 行截断;生产构建与注册不会打进该模块。 - 新增了路由模块级 `stages`(`'development' | 'production'`):缺省两端都注册;须字面量数组供 `generateRegistry` 解析;`stages: []` 永不加载;生产包仅收录含 `production` 的模块;脚手架模板与后端示例/规则已同步说明。 - 修复了生产环境双栈出网半残导致启动异常:新增 `dual-stack-probe`,生产启动前探测 IPv4/IPv6,按结果设置 DNS `resultOrder`;半残/不可达则失败退出;`SKIP_NETWORK_PROBE=1` 可跳过并强制 IPv4。 - 优化了启动顺序:先探测(生产)→ 并行 `EnsurePg` / `ConnectRedis` → 再初始化队列 → 再建应用监听;关闭时一并关闭队列,降低 Redis/队列过早连上的问题。 - 修复了 WangEditor 图片/视频上传:改为走对象存储预签名上传,与现有 Storage 上传链路一致。 - 增强了 AI 大任务协作规范:新增 `.ai/AI_PHASED_TASKS.md`(多模块/完整项目分阶段拆解);各 IDE 规则与 Skill 增加触发与目录约定(`.cursor|claude|codex|qoder|trae|kiro/feature-tasks/`)。 ::: ::: timeline v1.5.7(2026-09-08) - 修复了分页排序不稳定的问题:`FindPage` / `FindAll` 在主排序字段外追加主键升序作为 tie-breaker,避免并列值下 `LIMIT/OFFSET` 漏行或重复。 - 优化了字典数据列表筛选:`system-dict` 数据列表对 `dictLabel` / `dictType` 由模糊 `like` 改为精确 `eq`,与类型下拉、标签精确匹配场景一致。 - 优化了用户管理页视觉与信息密度:列表改为头像/首字母 + 昵称/账号/邮箱一体展示,状态改为启停胶囊,时间拆分日期与时刻;新增/编辑弹窗采用自定义标题区与更清晰的提交文案,并去掉入场过渡动画以免干扰操作。 - 新增了前端版本更新检测:生产环境以 `assets` 下带 hash 的 js/css 为指纹,在路由切换与页签切回前台时比对远端 `index.html`;发现新版本弹出「立即刷新 / 稍后」,懒加载 chunk 失效时自动刷新兜底,开发环境自动关闭。 ::: ::: timeline v1.5.6(2026-08-25) - 优化了列表关键字模糊查询:`QueryBuilder` 的 `like` / `ilike` / `notLike` / `notIlike` / `leftLike` / `rightLike` 在拼接 SQL 前统一对字符串关键字执行 `trim()`,避免首尾空格导致查不到结果;纯空格输入视为空并跳过条件,各模块 `handle.ts` 无需改动。 - 增强了 AI 开发规范:英文规则文件(`.ai` / IDE rules)新增或修改正文须用英文(仅本次编辑,不批量汉英重写);新建 `dict_type` 须短且语义化(优先 2–3 段如 `goods_status`),可复用则不新建,纯布尔启停勿建字典;同步 Cursor / Claude / Qoder / Trae / Kiro / `AGENTS.md` 与 `guide/ai-guide.md`。 ::: ::: timeline v1.5.5(2026-08-08) - 优化了开发态热重载:`bun run dev` 改为由 `script/dev-watch-modules.ts` 守护;轮询 `src/modules` 目录集合,新增/删除模块且写盘稳定约 5 秒后 touch 已在模块图中的 `src/modules/index.ts`,让 `bun --watch` 自行重载,无需手动重启即可访问新接口。 - 增强了慢写兜底:新模块目录出现后的 30 秒内若继续补写 `handle.ts` / `dto.ts` 等,会再触发一次重载,避免脚手架或 AI 分批落盘时模块被静默跳过。 - 新增了逃生命令 `bun run dev:plain`,行为与原先直接 `bun --watch src/index.ts` 一致;`dev:all` 继续走 `dev`,自动受益。 - 新增了业务编号生成工具 `core/biz-no.ts`:基于 Redis 日递增序号,格式为前缀 + 日期 + 序号(如 `ORD20260808000123`);订单号 / 支付单号 / 退款单号由 UUID 改为 `GenerateBizNo`,前缀常量 `BizNoPrefix` 放在 `constants/enum.ts`。 - 优化了对象存储上传文件名:预签名 `put` 时由 `StorageService` 统一生成短对象键(日期目录 + 18 位短 UUID + 扩展名,如 `20260808/0197f2c8a1b34d5e9f.png`),不再拼接毫秒时间戳与原始中文文件名;四个 provider 仅负责签名,`get` 仍透传原 key,历史文件可正常访问。 - 修复了前端 `tsconfig.json` 的弃用告警:`moduleResolution` 由已废弃的 `node`(node10)改为 Vite 场景适用的 `bundler`,不再需要 `ignoreDeprecations` 静音;因新解析模式会遵守依赖包 `exports` 字段,同步在 `paths` 中为 `@wangeditor/editor-for-vue` 指向其声明文件,避免退化为隐式 `any`。 - 修复了重置密码接口的前端类型与后端契约不一致:`Api.Auth.ResetPasswordParams.uid` 由 `string` 改为 `number`,与 `auth/dto.ts` 中 `t.Number()` 对齐;`admin` 全量 `vue-tsc --noEmit` 已零错误。 ::: ::: timeline v1.5.4(2026-07-29) - 增强了 AI 开发规范:软删除场景下唯一性校验须覆盖已删记录,新表唯一约束改用部分唯一索引 `WHERE del_flag = false`;表单须前后端双校验;`handle.ts` 导出函数须写 JSDoc;实体下拉走缓存 `/options` 接口,禁止用分页 `/list`;响应 DTO 须声明完整字段以免被 Elysia 剥离;明确 **禁止读写** `server/database/sql/pg.sql`(仅作备份快照)。 - 同步了各 IDE Agent 规则与 Skill(Cursor / Claude / Qoder / Trae / Kiro / Codex `AGENTS.md`),并补齐 `.ai` 模板索引、生成规则与前端硬规则中的对应条目。 - 优化了模块脚手架产出:`create:module` 生成的 `handle.ts` 五个导出函数自动带中文 JSDoc;`create:page` 生成的表格列、搜索/弹窗表单标签、placeholder 与校验 `message` 改用 Schema 行尾注释解析出的中文标签,开箱与后端 `fieldLabels` 对齐。 ::: ::: timeline v1.5.3(2026-07-22) - 增强了生产构建安全:`bun run build` 复制 `production.yaml` 到 `dist` 后,自动为 `jwt.accessToken.secret` / `jwt.refreshToken.secret` 注入 ≥32 字节随机串(`base64url`),源码配置不变;每次重新打包会轮换密钥,旧 token 失效。 - 修复了登录失败可枚举用户名的问题:账号不存在与密码错误统一返回 `400` + 用户名或密码错误,与忘记密码接口的防探测思路一致。 - 修复了改密 / 重置后会话未失效的问题:新增 `InvalidateUserSession`,在 `SetUserPassword`、禁用用户、软删用户时清理 `ONLINE_USER` / refresh / 菜单缓存;登出与在线用户强退复用同一逻辑。 - 增强了 Refresh Token 重放检测:轮换时写入 `refreshUsed` 墓碑;已作废的 refresh 再被使用时调用 `InvalidateUserSession` 整族吊销,并返回 `401`「会话已失效,请重新登录」。 - 优化了密码哈希性能:`BcryptHash` / `BcryptCompare` 由 `hashSync` / `verifySync` 改为异步 `Bun.password.hash` / `verify`,避免并发登录阻塞事件循环。 - 优化了操作日志落库:`onAfterResponse` 中 `AddOperLog` 改为投递专用 BullMQ 队列 `system-oper-log-queue`,由 Worker 异步 `InsertOne`,降低请求尾延迟并减轻与请求共用连接池的争抢。 - 增强了 API 安全响应头:新增 `SecurityHeadersMiddleware`,统一下发 `X-Content-Type-Options` / `X-Frame-Options` / `CSP frame-ancestors` / `Referrer-Policy`;HSTS 由配置项 `app.hsts` 控制(开发关、生产开,需 HTTPS / 前置 TLS)。 - 优化了鉴权 Redis 压力:`AuthGuard` 对 `ONLINE_USER` 增加 2s 进程内 L1;登出/强退/改密与角色权限刷新时清本机缓存(多实例他机最多滞后约 2s)。 - 优化了角色权限变更:按 `roleId` 查受影响用户,只删 `USER_PERM` / 菜单 / L1,不再 `Keys(ONLINE_USER+*)`;鉴权与 `findPerm` 按需加载权限。 - 修复了列表缓存写路径竞态:IP 黑名单改为写库后 `Del(IP_BLACK)`,读继续 `WithCache`;移除无锁的 `CacheInsert` / `CacheUpdate` / `CacheDelete`。 - 增强了列表分页边界:`FindPage` 统一 clamp(`pageNum≥1`,`pageSize` 默认 10、上限 100);`BaseListQueryDto` 同步 `minimum` / `maximum` 校验。 ::: ::: timeline v1.5.2(2026-07-21) - 新增了 AI 后端「跨表数据访问」规范:跨表须调用所属模块 `handle.ts` 导出的 PascalCase 函数,禁止直接引入他模块 schema;路由函数小写驼峰,跨模块导出 PascalCase。 - 优化了生产构建的 chunk 输出:`Bun.build` 的 chunk 命名改为 `chunks/chunk-[hash].[ext]`,打包产物统一落在 `dist/chunks/` 目录,清理时可整文件夹删除,不再散落于 `dist/` 根目录。 - 修复了 chunk 移入子目录后的路径解析:`src/config/index.ts` 与 `src/app.ts` 改用 `getDistRoot()` 定位 dist 根目录,避免 `import.meta.dirname` 指向 `dist/chunks/` 导致 `production.yaml` 与静态资源 `public/` 读取失败。 ::: ::: timeline v1.5.1(2026-07-19) - 重构了服务端日志:自研 `console` Logger 替换为 [pino](https://github.com/pinojs/pino);开发环境用 `pino-pretty` 彩色输出,生产环境按日写入 `dist/logs/YYYYMMDD/`(主进程 `app.log` / `error.log` / `http.log`,Worker / 沙箱 Processor 为 `worker.log` / `worker-error.log`)。 - 新增了配置项 `log.level`、`log.showRequestParams`(`development.yaml` / `production.yaml`):控制日志级别,以及 HTTP 访问日志是否记录 query / body / params。 - 修复了 BullMQ 沙箱 Processor 定时任务日志落到 PM2 stdout 的问题:识别 `dist/processors/*.js` 与 `dist/dist/cjs` 入口,日志目录锚定 `dist/logs`,子进程继承 `APP_ROLE=worker`。 - 优化了日志分流:`warn` / `error` 写入错误文件,`info` 保留在 app/worker;请求校验失败仅记短消息,不再整包 dump 进 `app.log`(详情以 `http.log` 状态码为准)。 - 增强了启动失败可观测性:配置校验失败、未捕获异常与启动失败通过 `appendFatalLog` 同步写入当天 `error.log` / `worker-error.log`;PM2 `ecosystem` 不再采集 stdout/stderr,排查直接看 `dist/logs/YYYYMMDD/`。 - 调整了 TypeScript 配置:移除已弃用的 `baseUrl`,`paths` 改为相对 tsconfig 的显式映射(含 `database/*`、`script/*`)。 - 修复了在线用户列表接口响应校验失败:`monitor-online` 的 `ListDto` 将 `username` / `email` / `phone` / `sex` / `avatar` 改为可选且允许 `null`,与 Redis 中实际在线用户字段一致。 ::: ::: timeline v1.5.0(2026-07-11) - 新增了 [MCP 服务](/guide/mcp) 指南:覆盖官方 `elysia-admin-mcp`(源码活索引,14 个工具)与 `pg-mcp-server`(PostgreSQL 只读查询)的职责边界、Cursor / Claude Code / Codex 接入配置、环境变量与只读纪律;与 `.ai/AI_MCP_SETUP.md` 对齐。 ::: ::: timeline v1.4.14(2026-07-08) - 新增了登录日志与操作日志列表筛选:后端 `system-login-log` / `system-oper-log` 扩展 `ListDto` 与 `QueryBuilder`(用户/IP/模块/操作/请求方式/用户类型/状态及 `createTime` 日期范围);前端拆分 `loginlog-search.vue`、`operlog-search.vue` 搜索栏,字典下拉与日期快捷范围与列表页联动。 - 优化了 `CrudDto.create` 与模块脚手架:`fieldLabels` / `fieldTypes` 为必填字段自动生成中文 `error` 校验文案;`create:module` 模板从 Schema 字段 `comment` 写入 DTO,避免校验失败时前端收到 JSON 而非可读提示。 - 重构了 AI 代码模板与 Token 预算:`.ai/AI_CODE_EXAMPLES.md` 拆为索引 + `AI_CODE_EXAMPLES_BACKEND.md` / `AI_CODE_EXAMPLES_FRONTEND.md`,各 IDE Agent 规则统一「按任务只读对应章节、单轮最多 2–3 份 .ai 文档」等读文件纪律;`dto.ts` 校验须含中文 `error` 写入全局规则。 - 新增了 Qoder IDE 项目配置(`.qoder/rules/`、`elysia-module-dev` Skill、`settings.json`),与 Cursor / Claude / Trae / Kiro / Codex 规则语义对齐;根目录 `AGENTS.md` 与分层 rules 说明见 `.qoder/README.md`。 - 新增了开发者本地偏好示例 `.ai/dev-preferences.local.example.md`:复制为 gitignore 的 `dev-preferences.local.md` 并设置 `db_push: allowed` 后,AI 可在 schema 变更后自动执行 `bun run db:push`。 ::: ::: timeline v1.4.13(2026-07-03) - 重构了 Docker 镜像构建:改为多阶段执行 `bun run build`,运行镜像仅包含完整 `dist/`(`index.js`、`workers.js`、`processors/`、`dist/cjs/`、`production.yaml`、`public/`);单容器同时启动 HTTP 与 Worker,不再使用 `--compile` 单文件 + distroless;新增 `server/.dockerignore` 并收窄 build 阶段 COPY(仅 `script/`、`src/`、`database/schema`、`public/` 等必要文件)。 - 优化了生产构建:`script/build.ts` 合并为单次 `Bun.build`,index / Worker / 全部 Processor 共享 chunk,减少重复打包与构建时间;stub 入口由 `script/build-shared.ts` 临时生成于 `script/.build-entries/`。 - 统一了 Processor 增量构建:`script/build-processors.ts` 与生产 build 共用 `runProcessorBuild`、`copyBullmqCjs`(BullMQ 沙箱 bootstrap 统一输出至 `dist/dist/cjs/`,修正 dev 原先写到 `dist/cjs/` 的路径偏差)。 - 拆分了启动种子数据:`script/seed.prod.ts`(API 同步 + 定时任务恢复)、`script/seed.dev.ts`(空库执行 `pg.sql`);`src/index.ts` 生产路径仅静态加载 prod,dev 动态 import,减小生产 bundle 体积。 - 移除了无效的 `task-registry.generated.ts` 构建:任务恢复仍由 `task-registry.ts` 的 `RegisterAllTasks()` 从数据库加载,不再在 build 时扫描 `modules/*/task.ts`。 - 更新了 [部署指南](/guide/deploy) Docker 章节与 [内置命令](/architecture/commands) Docker 说明:反映单容器双进程、配置挂载路径与 `--init` 建议。 - 更新了 [队列指南](/guide/queue) 自定义队列 checklist:移除向 `build.ts` / `build-processors.ts` 手动追加 processor 列表的过时步骤(已由 `discoverProcessorEntries()` 自动发现)。 ::: ::: timeline v1.4.12(2026-07-01) - 移除了服务端 `zod` 依赖:配置校验改为 Elysia `t`(TypeBox)schema + `Value.Check`,与路由 DTO 校验栈统一;`IConfig` 类型改为 `Static`。 - 优化了生产构建体积:主进程与 Worker、多个 Processor 分别启用 `bun build --minify --splitting` 共享 chunk;相较独立单文件打包,主进程 + Worker 与 Processor 区 JS 总体积显著下降(部署时需连同 `dist/` 根目录及 `dist/processors/` 下的 `chunk-*.js` 一并上传)。 - 新增了 Processor 构建自动发现:`discoverProcessorEntries()` 扫描 `queues/*/processor.ts`,新增沙箱队列无需再改 `build.ts` / `build-processors.ts` 硬编码列表;构建参数抽取至 `script/build-shared.ts`。 - 调整了 Docker 镜像构建:编译参数由 `--minify-whitespace --minify-syntax` 改为完整 `--minify`。 - 简化了本地开发与构建命令:`package.json` 中 `dev` / `dev:debug*` / `dev:workers` 内置 `NODE_ENV=development`,`build` / `build:processors` / `start` 内置 `NODE_ENV=production`;新增 `bun run start` 生产直启 `dist/index.js`,无需再在命令行前手动设环境变量。 ::: ::: timeline v1.4.11(2026-06-23) - 修复了生产环境打包部署后静态资源路径错误:`staticPlugin` 原先使用 `resolve(import.meta.dirname, '../public')`,打包后 `index.js` 位于部署目录(如 `/www/wwwroot/elysia-admin`)时实际解析到上一级 `public/`(如 `/www/wwwroot/public`),与 `build` 复制到 `dist/public` 的布局不一致,启动时报 `ENOENT: no such file or directory`;生产环境改为同目录 `public`,开发环境保持 `../public` 指向 `server/public`。 - 修复了未登录调用登出接口可能误删缓存的问题:`logout` 在无 `userId` 时直接返回成功,避免 `undefined` 拼入 Redis key 前缀。 ::: ::: timeline v1.4.10(2026-06-16) - 新增了系统管理「通知公告」菜单及前后端 CRUD,通知类型复用字典 `system_notice_type`,内容支持富文本编辑; - 新增了仪表盘「分析页」「电商页」示例(`dashboard/analysis`、`dashboard/ecommerce`),含销售趋势、热门商品、访客洞察等演示模块,种子数据补充对应菜单; - 修复了系统用户批量删除对 uuid 主键无效的问题:`SoftDeleteByKeys` 原先对 `ctx.params.ids` 一律 `.map(Number)`,uuid 会变成 `NaN` 导致软删除失效;现按主键列 `dataType` 区分,`number` 仍转数字、`string`(含 uuid)保留原值;`system-user` 的 `remove` 改走 repository 抽象,移除 handle 内直连 `pg` 的写法。 ::: ::: timeline v1.4.9(2026-06-12) - 修复了后台 CRUD 弹窗「编辑后再新增」时表单数据未清理的问题:`initFormData` 使用 `Object.assign(formData, { ...row })` 会把列表行的主键(如 `deptId`、`apiId`)与审计字段挂到 reactive 表单上,切换新增后仍随 create 请求提交导致接口报错。 - 统一改为 `getDefaultFormData()` 打开弹窗时先重置默认值、删除可能残留的审计字段、编辑模式再显式赋值;已修复部门(`dept-dialog`)、接口(`api-dialog`)、角色(`role-edit-dialog`)、IP 黑名单(`blacklist-dialog`)、定时任务(`job-dialog`)、存储配置(`storage-dialog`)共 6 个弹窗;字典、用户、菜单等页面此前已采用相同模式无需改动。 - 修复了系统用户新增时备注未入库的问题:`system-user` 模块 `CreateDto` 未声明 `remark`(及 `avatar`、`status`),Elysia 校验请求体时会丢弃未定义字段;编辑接口基于 `SelectSystemUser` 含 `remark` 故不受影响。已在 `CreateDto` 补全上述字段。 - 修复了 `/api/system/menu/simple` 只勾选子菜单时子菜单丢失的问题:`handleMenuListToTree` 在父节点未授权时会丢弃孤儿节点;现于 `loadUserMenuTree` 自动补全祖先链,未直接授权的父级合成 `isHide`,并增加孤儿挂 root 兜底。 - 修复了只勾选按钮权限时侧边栏误显示所属菜单的问题:`GetRoleMenuIdsAndBtnIds` 原先将按钮行的 `menuId` 一并计入可见菜单;现 `menuIds` 仅取 `menuBtnId` 为 null 的纯菜单授权行,按钮权限仍通过 `menuBtnIds` 与 API 权限字符串正常生效。 - 修复了角色权限弹窗仍展示已禁用菜单与按钮的问题:`/api/system/menu/tree` 原先仅过滤 `delFlag`;新增查询参数 `enabledOnly`,角色权限分配时传 `true` 仅返回 `status=true` 的菜单与按钮,菜单管理页保持展示全部记录。 ::: ::: timeline v1.4.8(2026-06-10) - 新增了模块脚手架:在 `server/` 下执行 `bun run create:module` 生成后端 `route.ts` / `dto.ts` / `handle.ts`,`bun run create:page` 生成后台 types、api、views 标准 CRUD 骨架;依赖已有 Drizzle Schema,支持 `--schema`、`--dry-run`。 - 新增了脚手架脚本与模板(`server/script/create-module.ts`、`create-page.ts`、`script/scaffold/`)及 `server/test/script/scaffold.test.ts` 单测。 - 新增了 `.ai/AI_MODULE_SCAFFOLD.md`,说明脚手架使用场景、命令示例及跑完后 AI 增量职责(业务逻辑、表格/弹窗排版、字典与 handoff SQL)。 - 更新了 AI 模块工作流(`.ai/AI_MODULE_WORKFLOW.md` Step 2.5)、`.ai/README.md`、`.ai/AI_CONTEXT_CAPSULE.md` 与 Cursor Skill(`.cursor/skills/elysia-module-dev/`):标准单表 CRUD 优先脚手架,再让 AI 做增量。 - 同步更新了 Cursor / Codex / Claude Code / Trae / Kiro 的 Agent 规则(`AGENTS.md`、`server/AGENTS.md`、`admin/AGENTS.md` 等),各 IDE 均可识别 `create:module` + `create:page` 推荐流程。 - 更新了 [AI 开发指南](/guide/ai-guide):新增「推荐流程(脚手架 + AI)」与档0 提示词(脚手架已生成后的增量开发)。 - 补充了 [内置命令](/architecture/commands)「模块脚手架命令」章节。 - 优化了 BullMQ Redis 键前缀:Queue / Worker 使用 `app.id` 作为 `prefix`(如 `Elysia-Admin:flow-buffer-queue:meta`),替代默认 `bull:` + 队列名拼 `appId` 的旧形态;多应用共用 Redis 时仍按应用隔离,旧 `bull:*` 键需手动清理。 - 新增了内置队列监控,替代原 Bull Board 独立面板:移除 `@bull-board/api`、`@bull-board/elysia` 依赖及 `bull-board-ui.ts`,监控能力统一纳入 Admin 后台与 RBAC。 - 新增了后端 `monitor-queue` 模块(`server/src/modules/monitor-queue/`):队列列表、按状态分页查任务、任务详情与日志、单/批量重试、删除、清空 completed/failed、暂停/恢复、延迟任务立即执行(promote);权限码 `monitor:queue:query` / `retry` / `delete` / `clean` / `pause` / `promote`。 - 新增了前端队列监控页(`admin/src/views/monitor/bullmq/`):三栏布局(队列列表 / 任务列表 / 任务详情),支持延迟倒计时、运行日志、3 秒 / 5 秒自动刷新;配套 `api/monitor/queue.ts` 与 `types/api/monitor-queue.d.ts`。 - 扩展了 `QueueManager`:注册队列时记录 `description` 元数据,对外提供 `getQueueMeta` / `getAllQueueMeta` 供监控接口使用;`closeAll` 时同步清空内部 Map。 - 更新了种子数据 `server/database/sql/pg.sql`:补充「系统监控 → 队列监控」菜单(`/monitor/bullmq`)及按钮权限;启动横幅移除 Bull Board 地址提示。 - 更新了 [队列指南](/guide/queue) 与 [部署指南](/guide/deploy) 监控章节:入口改为 Admin 后台,说明新面板能力与权限要求。 - 修复了生产环境经 Nginx 反代后登录日志 IP 全为 `127.0.0.1`:`production.yaml` 默认开启 `trustProxy` 并配置 `trustedProxyCidrs`(`127.0.0.1` / `::1`),在受信代理场景下正确解析 `X-Forwarded-For` / `X-Real-IP`(需反代层传递对应头)。 - 为 `server/src/config/schema.ts` 各配置段与字段补充了 JSDoc 注释,说明用途、单位及可被环境变量覆盖的项。 ::: ::: timeline v1.4.7(2026-06-09) - 重构了后端错误处理:HTTP 路由 handler 移除重复的 `try/catch`,未捕获异常统一由全局 `onError` 收口;服务端日志保留完整 `stack`,客户端仍返回通用 500 文案。 - 新增了 `server-error` 工具(`getHttpStatus` / `getPgErrorCode` / `logServerError`),支持业务 `httpStatus`(如 409)、PostgreSQL 唯一约束 `23505` 等在全局层映射。 - 调整了 `BaseResultData.fail`:仅对业务主动返回的字符串 500 打日志,避免与 `onError` 重复记录异常对象。 - 拆分了应用启动配置:`ConfigureErrorHandler` → `middleware/error-handler.ts`,`ConfigureOpenAPI` → `infrastructure/openapi.ts`,`ConfigureBullMQUI` → `infrastructure/queue/bull-board-ui.ts`,`app.ts` 仅保留 `CreateApp` 组装逻辑。 - 更新了 AI 代码模板(`.ai/AI_CODE_EXAMPLES.md`):标准 CRUD handler 不再生成通用 `try/catch`;补充 `useDictStore` 字典字段示例。 - 新增了 AI 模块开发工作流(`.ai/AI_MODULE_WORKFLOW.md` 及 Schema / Handoff SQL / MCP / 页面排版子指南),Handoff SQL 统一输出至 `server/database/sql/{模块名}-init.sql`。 - 新增了 Cursor 项目 Skill(`.cursor/skills/elysia-module-dev/`),9 步 checklist 对齐 Schema → 代码 → 字典 → 前端 → SQL 交付;AI 规范文档(`.ai/`、`.cursor/rules/`)面向 Agent 统一为英文。 - 新增了 `.ai/AI_PAGE_QUALITY.md`(列表页 `art-full-height`、`useDictStore`、弹窗排版等整页质量规范)。 - 同步更新了 Trae / Kiro steering 与 `.cursor/rules/general.mdc`(模块开发触发语、Git 只读、MCP 查库优先)。 - 重写了 [AI 开发指南](/guide/ai-guide.html):三档提示词(一句话 / 两句 / 结构化)、网页版短前缀指引,移除过时内联代码模板。 - 新增了 OpenAI Codex 分层指令(根目录及 `server/`、`admin/` 的 `AGENTS.md`)与 `.codex/README.md`。 - 新增了 Claude Code 配置(`.claude/CLAUDE.md`、`.claude/rules/`),规则语义与 Cursor / Trae 对齐。 - 新增了 VS Code / Cursor 开发配置(`.vscode/`:settings、extensions、tasks、launch)及多根工作区 `elysia-admin.code-workspace`。 - 新增了后端调试命令:`dev:debug`(IDE Attach 6499)、`dev:debug:browser`(推荐,配合 [debug.bun.sh](https://debug.bun.sh))、`dev:debug:watch`(调试 + 热重载,默认不推荐)。 - 新增了 `server/script/dev-debug.ts` 调试启动脚本,终端提示浏览器断点流程。 - 调整了 `.vscode/launch.json`:改用内置 Node 调试器 Attach 6499,替代原 `type: bun`(Cursor / Trae 下扩展调试适配器常不可用);`.vscode/tasks.json` 补充 `Server: dev:debug` 相关任务。 - 补充了 [内置命令](/architecture/commands.html) 文档:独立「调试命令」「构建命令」章节,说明命令与编辑器 Tasks / Launch 配套用法。 ::: ::: timeline v1.4.6(2026-06-08) - 新增了启动期 Zod strict 校验 `*.yaml` 配置,缺字段、类型错误或未知 key 时 fail-fast 并输出可读错误。 - 修复了 PostgreSQL 连接池 `connect_timeout` 配置未生效(原误读 `connection_timeout`)。 - 修复了 PM2 生产部署下队列 Worker 沙箱 processor 子进程找不到 `production.yaml` 的问题(`CONFIG_PATH` 误解析为 `dist/dist/` 路径)。 - 修复了用户管理删除用户失败的问题。 - 修复了用户弹窗「编辑 → 新增」切换时表单未完整重置、异步详情回填错乱的问题(Gitee IJRSXJ)。 - 修复了登录后菜单/权限数据为空时路由初始化失败,500 页无法返回登录页的问题;菜单校验失败时自动登出并提示,异常页支持「重新登录」兜底。 ::: ::: timeline v1.4.5(2026-06-01) - 修复了字典类型弹窗在「编辑 → 新增」切换时表单未完整重置,偶发提交 400 的问题。 - 更新了字典类型编码修改逻辑:同步级联更新下属字典数据的 `dictType`,并清理类型/数据相关 Redis 缓存;前端同步刷新右侧字典值列表与当前选中类型。 - 新增了 `sha256WithRsa`(`RSA-SHA256`)供 v3 请求签名与 JSAPI 调起签名使用,与 OpenSSL 算法名对齐。 - 优化了 `buildWechatOrderBody`,仅白名单透传 `attach`、`goods_tag`、`time_expire` 等文档允许的可选字段,避免将 `openid`、`clientIp` 等由各渠道自行处理的字段误写入通用下单 body。 - 更新了微信支付回调验签,统一使用 `RSA-SHA256`(`crypto.createVerify`),与微信 v3 平台证书验签规范一致。 - 接入了 Bun 内置 `bun test`(`test` / `test:watch` / `test:coverage`),新增 `server/test/` 纯函数与 `QueryBuilder` 单元测试基线。 - 补充了架构文档测试命令说明([内置命令 - 测试命令](/architecture/commands.html#测试命令))。 ::: ::: timeline v1.4.4(2026-05-21) - 修复了在环境中同时存在 Node.js v24 时,`@bull-board/elysia` 顶层静态 import 触发 CJS `require()` 加载含 Top-Level Await 的 ESM 模块而报错的问题:将 `@bull-board/api`、`@bull-board/elysia` 及 `queues` 的 import 由顶层静态引入改为 `configureBullMQUI` 函数内部的动态 `await import()`。 - 文档:补充了存储配置指南中各服务商(RustFS、COS、OSS、Kodo)的后端配置字段示例。 ::: ::: timeline v1.4.3(2026-05-20) - 将系统用户主键 `userId` 由自增数字改为 UUID 字符串,避免管理端与小程序等多套用户表在 Redis 在线会话、令牌等场景下 ID 冲突;同步调整 `createBy`/`updateBy` 及关联表、前后端类型定义。 - 修复了在线用户强退无效的问题:强退接口不再将 UUID 转为数字,并同步清除刷新令牌与菜单缓存。 - 优化了在线监控页:强退当前登录账号后自动执行前端登出。 ::: ::: timeline v1.4.2(2026-05-18) - 修复了菜单删除或禁用后左侧栏仍显示的问题:用户菜单查询增加 `delFlag`、`status` 过滤,菜单变更后自动清除 Redis 菜单缓存。 - 优化了菜单管理页:保存或删除后自动刷新侧边栏动态路由(后端权限模式)。 ::: ::: timeline v1.4.1(2026-05-16) - 规范了 HTTP 路由注册方式,并收紧路由、接口处理函数的类型定义,便于 IDE 提示与排查。 - 优化了路由自动加载:某个业务模块加载失败时会在日志中明确提示,不再静默跳过。 - 优化了队列 Worker 与业务模块的耦合方式:沙箱任务统一登记,基础设施层不再直接引用业务代码。 - 新增了请求上下文类型 `AppContext`,业务接口可 typed 访问当前用户、路由权限等信息,减少 `(ctx as any)` 写法。 - 生产构建时预生成的路由列表带类型信息,TypeScript 检查更准确。 - 更新了后端开发规范与工程配置,与当前目录划分(业务类型 / 声明文件 / 沙箱任务)保持一致。 - 调整了定时任务与 Redis 分布式锁的代码归属,与「工具层不依赖核心层」的架构约定一致。 ::: ::: timeline v1.4.0(2026-05-15) - 优化了订单管理前后端功能。 - 新增了支付调试能力。 - 优化了服务端启动日志的版本提示。 - 更新了项目文档与 AI 开发指引。 > 自 2026-05-15 起,版本号按功能域重新对齐。原文档中的 v1.2.2 对应 v1.3.2,当前最新版为 v1.4.2。 ::: ::: timeline v1.3.2(2026-05-13) - 新增了服务端预编译能力。 - 新增了公开注册 `allowPublicRegister` 与 `ALLOW_PUBLIC_REGISTER` 开关,生产配置默认关闭。 - 修复了注册假成功(未向上抛出异常)与重置密码操作顺序不一致的问题。 - 优化了 IP 获取逻辑,仅在 `trustProxy` 受信代理场景下解析 `X-Forwarded-For`。 - 增加了 IP 归属地查询超时与降级处理,失败不再影响登录流程。 - IP 路由限流改为 Redis 原子计数,消除高并发下的「先读后写」竞态。 - 修复了 `BaseResultData.fail` 在 500 状态码下错误日志拼接问题。 - 优化了 `WithCache`,现在可正确缓存 `0`、`false`、空数组等合法结果。 - 统一了 CORS/静态资源配置。 - 规范了 `SIGINT`/`SIGTERM` 服务关闭顺序,移除重复的信号处理。 - 为 `IConfig` 补充了针对 `app.listen` 返回值类型的 TypeScript 修复。 ::: ::: timeline v1.3.1(2026-05-12) - 新增了退款接口。 - 新增了 AI 开发规范文件。 - 修复了 `WithCache` 缓存命中判断缺陷(空数组和对象类型缓存永远未命中问题)。 - 修复了权限守卫读取路径错误,细粒度接口权限校验现已正确生效。 - 修复了 `GetMenuPermissionByRoleIds` 和 `GetRoleMenuIdsAndBtnIds` 在角色无按钮权限时触发全表扫描,导致权限数据返回异常的严重 Bug。 - 优化了 `FindPage` 分页查询,使用窗口函数 `COUNT(*) OVER()` 将常规分页由 2 次 DB 查询合并为 1 次。 - 优化了认证守卫,去除了 JWT 重复解析(移除冗余的 `ParseToken` 调用)。 - 优化了 IP 黑名单缓存,SQL 层直接过滤 `status=true` 数据,减少缓存体积。 - 修复了角色权限变更时 `forEach(async)` 并发无序问题,改为串行处理确保稳定性。 ::: ::: timeline v1.3.0(2026-05-10) - 新增了支付适配层与路由分发。 - 新增了支付宝 PC / H5 / App / 小程序各端接入,并修复了各端支付与回调问题。 - 新增了微信支付、PayPal 多渠道接入。 - 新增了 PC 端支付完成处理与异步通知回调校验优化。 - 同步更新了支付相关文档。 ::: ::: timeline v1.2.0(2026-04-18) - 修复了配置角色权限后菜单不更新等问题。 - 移除了 `build:binary` 命令,具体原因可在 [常见问题](/other/faq.md) 中查看。 - 优化了创建订单接口。 - 修复了生产环境 Worker 处理器路径错误。 ::: ::: timeline v1.1.0(2026-04-15) - 新增了 `BullMQ`,添加了队列功能。 - 基于 `BullMQ` 重写了定时任务功能。 ::: ::: timeline v1.0.3(2026-04-10) - 扩充了项目文档体系(快速开始、架构、中间件等章节)。 - 调整了 `server` 中的部分接口目录。 ::: ::: timeline v1.0.2(2026-04-01) - 新增了订单模块初版接口。 ::: ::: timeline v1.0.1(2026-03-24) - 新增了业务模块。 - 规范化了部分代码结构。 - 修复了数据库迁移交互不显示的问题。 ::: ::: timeline v1.0.0(2026-03-04) - 整个系统前后端全部可用。 - 完成了认证、字典、菜单/角色/部门、API 管理、种子数据等核心模块。 - 新增了四重守卫与全局中间件、登录日志、操作日志。 - 新增了存储/文件上传、定时任务(Redis 分布式锁)。 - 新增了缓存模块、接口限流、登录次数限制。 - 启用了 YAML 配置,优化了 Drizzle ORM 配置。 - 修复了 PM2 在线部署等问题。 ::: ## v0 ::: timeline v0.0.0(2025-12-12) - 创建项目,第一次提交。 > [CSDN文章链接:](https://blog.csdn.net/weixin_63443072/article/details/158653317?spm=1001.2014.3001.5501) > > [掘金文章链接:](https://juejin.cn/post/7612957140249362482) ::: --- URL: "https://elysia-admin.top/start/introduction.html" LLMS_URL: "https://elysia-admin.top/\\start\\introduction.md" title: "介绍 - Elysia Admin" description: "了解 Elysia Admin 是什么、核心特色、技术选型及内置功能,快速掌握这套基于 ElysiaJS + Vue3 的全栈后台管理解决方案。" --- --- title: 介绍 - Elysia Admin description: 了解 Elysia Admin 是什么、核心特色、技术选型及内置功能,快速掌握这套基于 ElysiaJS + Vue3 的全栈后台管理解决方案。 --- # 介绍

Elysia Admin

Elysia Admin

一个基于 [ElysiaJS](https://elysiajs.com/) + [Art Design Pro](https://www.artd.pro/docs/zh/) 的现代化全栈后台管理系统。
Node.js Version Bun Version PostgreSQL Redis License
## Elysia Admin 是什么? `Elysia Admin` (elysia-admin)是一套基于 `ElysiaJS` 后端与 `Vue 3` 前端的全栈解决方案。我们致力于解决国内 `Node.js` 开发者在构建后台系统时的痛点,提供一套开箱即用的高性能、高颜值管理模板。 本项目的核心特色在于其独特的后端架构:我们彻底告别了繁琐且难以维护的链式调用,创新性地引入了类似 `Vue Router` 的配置化写法。这种模块化的路由配置方式,不仅让代码逻辑更加清晰直观,更完美契合了国内开发者的思维习惯,让你能更专注于业务逻辑,充分释放 `Bun` 运行时的强大性能潜力。 ![start](/start/1.png) ## 什么时候使用? 如果你符合以下任何一种情况,`Elysia Admin` 将是你的最佳选择: - 全栈TypeScript拥趸:追求端到端的类型安全,希望前后端共享类型定义,消除类型不一致带来的 Bug。 - 资源受限环境:部署服务器配置有限,或追求极致性价比,需要一款内存占用极低(~80MB)的高性能框架。 - 拒绝链式调用:厌倦了 `Elysia` 传统的链式 API 写法,渴望像 `Vue Router` 一样通过配置式路由来管理接口,让代码结构清晰、易于维护。 - 追求开发效率:希望降低学习成本,拒绝重复造轮子,需要一套开箱即用、规范化的全栈脚手架。 ## 在线体验 - 官网地址: - 演示地址: - 账号:`admin` - 密码:`123456` (建议仅用于体验,勿修改核心数据) - 代码下载: - gitee仓库: - github仓库: **提示**:演示环境为公共测试服,请勿进行破坏性操作。 ## 主要特征 本项目不仅是一个脚手架,更是一套经过优化的企业级解决方案: - 前端基于 `Art Design Pro` 构建,继承了其所有现代化特性。提供精美、响应式的界面设计,开箱即用的高级组件。 - 创新实现后端 **接口与定时任务的自动注册机制**。无需繁琐的手动引入,系统自动扫描并加载模块,极大降低了项目配置的复杂度,让开发更专注于业务逻辑。 - 从数据库到前端全程 `TypeScript` 覆盖。后端集成 `DrizzleORM`,利用其强大的类型推导能力,实现数据库操作与 API 响应的端到端类型安全,拒绝 `any`。 - 区别于传统的链式调用,采用类似 `Vue Router` 的 **配置式路由架构**。将接口路径、方法、权限与处理函数解耦,结构清晰,逻辑分明,完美契合国内开发者的工程化思维。 ## 技术选型 本项目严格遵循现代化全栈开发标准,采用高性能、类型安全的技术栈构建。 ### 单仓目录与运行时 仓库内与日常开发最相关的是 **admin**(Vue 管理端)、**server**(Elysia 接口服务)以及文档 **docs**; 线上典型路径是浏览器访问 **admin**,通过 **HTTP** 调用 **server**,由后者访问 **PostgreSQL** 做持久化、访问 **Redis** 做会话、缓存与队列等。 下面示意图把「界面 → API → 数据」与下一小节「环境依赖」里的 PostgreSQL、Redis、Bun 等条目对齐;本地开发则多为「前端 dev server + 后端监听端口」,与图中所述依赖方向一致。 ```mermaid flowchart TB subgraph Client["前端 admin"] V[Vue 3 + Vite] end subgraph Server["后端 server"] E[ElysiaJS on Bun] end subgraph Data["数据与中间件"] PG[(PostgreSQL)] RD[(Redis)] end V <-->|HTTP API| E E --> PG E --> RD ``` 若单独启动队列 **Worker** 消费任务,通常仍通过同一 **Redis** 与 HTTP 服务协作,详见 [队列指南](/guide/queue)。 ### 环境依赖 **必须** : - [`Node.JS >= 22.0.0`](https://nodejs.org/) - [`Bun >= 1.2.21`](https://bun.com/) - [`Redis >= 6`](https://redis.io/) - [`PostgreSQL >= 16`](https://www.postgresql.org/) **可选** : - [`Nvm`](https://github.com/nvm-sh/nvm) - [`Pnpm`](https://pnpm.io/) - [`RustFS`](https://rustfs.com.cn/) - [`Docker`](https://www.docker.com/) - [`PM2`](https://pm2.keymetrics.io/) ### 前端技术 - 框架核心:[`Vue 3`](https://vuejs.org/) + [`TypeScript`](https://www.typescriptlang.org/) (端到端类型安全) - 构建工具:[`Vite`](https://vitejs.dev/) (极速热更新) - 状态管理:[`Pinia`](https://pinia.vuejs.org/) (Vue 官方推荐) - 路由系统:[`Vue Router`](https://router.vuejs.org/zh/) - UI 组件库:[`Element Plus`](https://element-plus.org/zh-CN/) + [`Tailwind CSS`](https://tailwindcss.com/) (原子化 CSS 辅助) - 样式处理:[`Scss`](https://scss-lang.com/) (增强样式可维护性) ### 后端依赖 - 核心框架:[`ElysiaJS`](https://elysiajs.com/) (Bun 生态高性能框架) - 数据持久层:[`Drizzle ORM`](https://orm.drizzle.team/) (类型推断优先,比传统 ORM 更轻量) - 数据库驱动:[`Postgres`](https://github.com/porsager/postgres) (轻量级 PG 驱动) + [`Ioredis`](https://ioredis.org/) - 工具库: - [`Jose`](https://github.com/panva/jose):JWT/OIDC 标准加密处理。 - [`Croner`](https://github.com/hexagon/croner):轻量级定时任务调度。 - [`nodemailer`](https://nodemailer.com/):邮件发送服务。 - [`BullMQ`](https://bullmq.io/):任务队列处理。 ## 内置功能 ### 认证与权限 - 全面认证:支持用户注册、登录、忘记密码及密码修改。采用双 Token (Access/Refresh) 认证机制,确保持续会话的安全性。 - 用户管理:完善的用户 CRUD 功能,支持查看用户详情、分配角色及所属部门。 - 角色管理:标准的 RBAC 模型,支持自定义角色名称、描述及细粒度的权限分配。 - 菜单管理:前后端联动的动态菜单系统。支持配置名称、路径、图标及权限标识,实现菜单与权限的实时同步。 - 部门管理:可视化的组织架构管理,支持多层级部门结构与详细描述。 - 字典管理:系统级数据字典维护,支持名称、类型、值的动态配置,极大提升业务开发的灵活性。 - 通知公告:系统管理下的公告维护,通知类型复用数据字典,内容支持富文本编辑。 - 接口熔断:支持动态修改接口状态。无需重启服务即可实现接口的灰度发布或紧急下线,运维响应更从容。 - 防爆破与黑名单:内置 IP 黑名单机制。登录接口集成防爆破逻辑,多次验证失败自动拉黑 IP,有效防御恶意攻击。 - 在线用户监控:实时查看当前在线用户列表(含用户名、IP、状态),支持管理员强制下线指定用户。 - 日志审计:完善的日志系统,包含登录日志与操作日志。支持在服务端灵活配置记录级别,满足合规审计需求。 - 存储配置 (S3):支持配置各类 S3 兼容存储(如 RustFS、阿里云 OSS、腾讯云 COS 等)。采用后端生成签名 URL、前端直传模式,完全不占用应用服务器带宽。 - 缓存数据管理:可视化的存储列表查看器。支持在线查看、调试及修改系统缓存数据,极大便利了开发调试与问题排查。 - 请求加解密:可选的 RSA-OAEP + AES-GCM 信封。前后端同时开启后,JSON 接口以密文传输;未带加密头的请求仍走明文。详见 [请求加解密](/guide/crypto)。 ### 系统与工具 - 定时任务调度:内置可视化任务管理面板。支持 Cron 表达式配置,可动态启动或暂停任务,轻松处理周期性业务。 - 自动化接口文档:后端自动生成交互式 API 文档。开发调试无需借助第三方工具,浏览器即可查看与测试接口。 - 站点与证书:后台维护域名和反代路径,预览 Nginx 配置,并通过 Let's Encrypt 申请、续签证书。详见 [站点部署](/guide/site-deploy)。 ### 支付模块 - 多渠道支付接入:统一适配层 `Pay(channel, platform)` 支持支付宝、微信支付、PayPal,按渠道 + 终端自动路由到对应实现。 - 商户配置管理:支持多商户、多渠道配置,密钥仅存储在服务端,不下发前端。 - 下单 / 查询 / 退款:统一接口签名,各渠道差异由适配层屏蔽,业务侧只需调用 `create`、`query`、`refund`。 - 异步回调验签:内置各渠道验签与解密逻辑,业务层只需处理 `NotifyResult`,无需关心原始报文格式。 ## 源码分支 | 名称 | 描述 | 地址 | 进度 | | --- | --- | --- | --- | | `master` | 主分支,包含最新版本的代码 | [https://gitee.com/nian-qian/elysia-admin/tree/master](https://gitee.com/nian-qian/elysia-admin/tree/master) | `已完成` | --- URL: "https://elysia-admin.top/start/quick-start.html" LLMS_URL: "https://elysia-admin.top/\\start\\quick-start.md" title: "快速开始 - Elysia Admin" description: "Elysia Admin 快速启动指南,包含前置依赖安装、源码下载、数据库创建、项目配置及运行步骤,帮助你快速搭建开发环境。" --- --- title: 快速开始 - Elysia Admin description: Elysia Admin 快速启动指南,包含前置依赖安装、源码下载、数据库创建、项目配置及运行步骤,帮助你快速搭建开发环境。 --- # 快速开始 本章将提供 `Elysia Admin` 项目的快速启动指南,帮助您快速搭建开发环境并运行项目。 ## 前置依赖 > **重要:** 请确保已安装以下环境或软件,否则可能导致项目依赖安装失败或启动异常。 - [`Node.js >= 22.0.0`](https://nodejs.org/) - [`Bun >= 1.2.21`](https://bun.com/) - [`Redis >= 6`](https://redis.io/) - [`PostgreSQL >= 16`](https://www.postgresql.org/) ## 下载源码 ::: code-group ```bash [gitee] git clone https://gitee.com/nian-qian/elysia-admin.git ``` ```bash [github] git clone https://github.com/meishanlaoyao/elysia-admin.git ``` ::: ## 启动项目 本项目前端使用 `pnpm` 管理依赖,后端使用 `bun` 运行项目。 ### 安装依赖 ::: code-group ```bash [bun] # 安装后端依赖 bun install ``` ```bash [pnpm] # 安装前端依赖 pnpm install ``` ::: ### 创建数据库 首先需要在 PostgreSQL 中创建一个数据库,以下使用 `Navicat Premium 17` 工具演示数据库创建过程: ![创建数据库](/start/2.png) > **注意:** 数据库创建完成后即可关闭。在 `开发环境` 下,启动后端服务时会自动初始化数据库;在 `生产环境` 下不会自动初始化数据库,以防止误操作线上已有的数据库。 ### 配置项目 #### 后端配置 后端配置文件位于 `/server/src/config/` 目录下,请根据实际部署环境修改对应配置文件: - `development.yaml`:开发环境配置 - `production.yaml`:生产环境配置 #### 前端配置 前端配置文件位于 `/admin/` 目录下,请根据实际部署环境修改对应配置文件: - `env.development`:开发环境配置 - `env.production`:生产环境配置 ### 运行项目 ::: code-group ```bash [bun] # 运行后端项目 bun dev ``` ```bash [pnpm] # 运行前端项目 pnpm dev ``` ::: 后端启动成功后,控制台会输出以下信息: ```bash [terminal] ============================================================ 🚀 Elysia-Admin 启动成功 ============================================================ 服务地址: http://localhost:3000/api API文档: http://localhost:3000/api/openapi OpenAPI JSON: http://localhost:3000/api/openapi/json 启动时间: 2026/4/6 11:17:38 运行环境: development Bun版本: 1.3.8 进程ID: 25360 ============================================================ ``` 前端项目启动后会自动打开浏览器,默认访问地址为:。 ### 问题排查 1. 若在启动过程中遇到问题,可通过顶部搜索框搜索相关解决方案。 2. 若问题仍未解决,请访问代码仓库平台创建 issue,我们会及时处理。