开源地址
https://gitee.com/rainterm/designer-admin
https://github.com/rainterm/designer-admin
后台管理系统脚手架
简介
一套面向企业后台的模块化单体脚手架:后端 Solon + MyBatis-Plus,前端 Vue 3 + Naive UI + Vite,内置登录、RBAC、数据权限、SSO、OAuth2、文件、消息、定时任务与系统设置等基础能力,克隆后即可开始业务开发。
设计上坚持两条原则:
- 简洁、可复用:用最少的代码完成灵活的功能——相同逻辑不重复写,收敛为公共能力(统一响应、分页、异常、日志、缓存、限流、文件与消息),业务模块只写差异部分;可变项不写死在代码里,能靠配置(
app.yml、数据库配置、注解参数)调整的就不改代码。 - 易扩展、可维护:模块之间只依赖对方
*-api,框架层通过 SPI 反向解耦;新增业务模块复制模板即可,不需要改脚手架核心。文件存储、消息通道、消息队列同样面向接口扩展,新增实现只需注册为组件、按type自动装配,调用方无需改动。
为什么选择 Solon
- 启动快、资源占用低:内核轻量,没有 Servlet 容器嵌套,
java -jar直接运行,适合单机与容器化部署。 - 包体积小:依赖精简,最终产出一个可执行的 fat jar;部署时只需 JRE,不需要额外容器。
- 配置简单:一个
app.yml管完端口、数据源、Redis、Sa-Token、Quartz、日志等,环境变量可直接覆盖。 - 插件化生态:MyBatis-Plus、Sa-Token、Quartz、Knife4j 等都有 Solon 插件,能力齐全且装配简单。
- 贴合现代 JDK:项目基于 JDK 25,已开启虚拟线程,I/O 旁路任务不再占用平台线程。
需要新增业务模块时,复制
designer-module-demo(后端)与designer-ui/admin-web/src/modules/demo(前端),按 开发规范 改包名即可。
界面预览










内置能力
| 能力 | 说明 |
|---|---|
| 登录 | Sa-Token 会话、滑块验证码、失败锁定、登录限流 |
| RBAC | 用户 - 角色 - 菜单三级权限,按钮级权限标识 |
| 数据权限 | 角色维度行级数据范围,MyBatis 拦截器自动改写 SQL |
| SSO | 一次性票据跨应用免二次登录 |
| OAuth2 | Client Credentials + Scope 的开放接口 |
| 文件管理 | 本地 / S3 可插拔存储,上传校验、图片压缩、元数据落库、私有下载 |
| 消息管理 | 站内信、邮件通道配置与发送记录 |
| 消息推送 | SSE 实时站内信,多实例经消息总线广播 |
| 定时任务 | Quartz JDBC 集群调度,动态 cron、执行日志 |
| 系统设置 | 系统名称、Logo、默认布局与主题色 |
登录
- 基于 Sa-Token,token 放在
Authorization请求头(本项目约定不带Bearer前缀),会话存 Redis,可多实例共享。 - 登录失败达到阈值后触发 AJ-Captcha 滑块验证码;验证码凭据一次性、AES 加密传输,失败计数与锁定状态存 Redis。
- 锁定维度:账号 + IP 组合锁定,另按 IP 累计失败拦截「一个密码试很多账号」的密码喷洒;账号维度只用于决定是否出验证码。
- IP 不可伪造:锁定用的客户端 IP 统一由
TrustedProxyHostUtil解析——对端 socket IP 不在可信代理网段时直接采用 socket IP 并忽略所有转发头;只有来自可信代理(默认回环 + Docker 网桥,可用designer.client-ip.trusted-proxies收窄)的请求,才沿X-Forwarded-For从右往左取第一个不可信地址。客户端伪造的转发头既不被采信,也无法换出一个新的锁定桶;取不到来源时该请求不参与 IP 锁定,避免所有无 IP 请求共用一个兜底键而被刷到阈值、误锁他人。 - 登录接口额外加
@RateLimit(默认 10 次/分钟 + 100 次/24 小时,按 IP),与验证码、账号锁定形成三层防护。 - 支持在线用户查看、单个 / 批量踢下线;登录日志记录账号、IP、浏览器、系统与结果。
RBAC
- 用户 - 角色 - 菜单三级模型:
sys_user/sys_role/sys_menu,关联表为sys_user_role、sys_role_menu。 - 菜单分目录 / 菜单 / 按钮三类;按钮即权限标识(如
system:user:add),后端@SaCheckPermission,前端v-perm指令。 - 登录后通过
/admin-api/system/auth/routers拉取菜单树,前端据此动态注册路由与侧栏;隐藏页(visible=0)注册路由但不进菜单。 - 组织能力包含部门树、岗位(
sys_post+sys_user_post),角色可分配菜单权限与数据权限。 - 超级管理员拥有全部权限,并受保护逻辑约束,避免被误删或降权。
数据权限
- 配置在角色上:
sys_role.data_scope支持「全部 / 自定义部门 / 本部门 / 本部门及以下 / 仅本人」五档;自定义部门存sys_role_dept。多角色取并集,超管不受限。 - 过滤由 MyBatis 拦截器自动完成(
AutoDataScopeHandler+LazyHandlerDataPermissionInterceptor),SELECT / UPDATE / DELETE 以及连表、子查询、分页 count 都会被改写,业务代码不写dept_id IN (...)。 - 约定优于配置:表有
dept_id按部门过滤;没有dept_id但有create_by时,通过「创建人反查用户部门」表达部门范围。create_by由AutoFillHandler插入时自动填充,业务表不加字段也能享受数据权限。 - 权限快照存 Redis(
perm:user:{userId}),写角色 / 菜单 / 用户角色关系后在事务提交后主动失效;TTL 仅作兜底。 - 写入侧的越权校验由
DataPermissionGuard#canSeeDept承担(INSERT没有 WHERE,拦截器管不到);确需绕开时使用DataPermissionContext.ignore(...)或@InterceptorIgnore(dataPermission = "true"),并明确理由。 - 两个选型边界:反查部门按「创建人当前部门」解释;需要「数据归属创建时部门」的业务(合同、凭证等)必须给表加
dept_id。
SSO
- 跨应用免二次登录:已登录后台签发一次性票据,客户端拿票据换回同一登录态。
- 票据存 Redis,默认 60 秒过期(
designer.sso.ticket-ttl可配),用后即删。 - 后台维护 SSO 客户端白名单(标识、允许 Origin、打开地址、接入路径,
sys_sso_client);带redirect登录时只校验 Origin,不校验具体路径。 - 换票接口:
POST /admin-api/system/auth/sso/exchange;签发接口:POST /admin-api/system/auth/sso/ticket。 - 可运行示例见 designer-ui/sso-demo,完整流程见 docs/SSO.md。
OAuth2
- 面向第三方系统的开放接口,采用 Client Credentials 模式。
- 令牌端点:
POST /open-api/oauth2/token,支持client_id/client_secret表单参数或 Basic Auth;返回 RFC 6749 JSON(不包统一Result)。 /open-api/**按路径匹配 Scope(sys_oauth_scope)校验access_token;缺少授权 Scope 返回 403。- 后台可管理 OAuth2 客户端、重置密钥、维护开放 Scope(
sys_oauth_client/sys_oauth_client_scope)。 - 后端 8080 不直接对外,第三方统一经 Nginx 的
/open-api/前缀访问。
文件管理
-
存储方式可插拔:内置本地磁盘与 S3 兼容(MinIO、OSS、COS 等)实现,实现类通过
type()/configFields()自描述,前端据此动态渲染配置表单。 -
上传元数据落
sys_file,存储配置落sys_storage_config,后台可切换默认存储、配置公开 / 私有访问。 -
上传校验:扩展名白名单推导 Content-Type(不采信客户端声明)、大小限制、路径穿越双重校验。
-
图片压缩:开关在系统管理 → 存储配置 → 编辑 →「图片压缩」,按配置在服务端落盘前压缩,所有走
FileStorageService#store的入口统一生效(不必在每个上传点单独处理)。格式 行为 JPEG 有损重编码,质量参数生效(10–100) PNG 无损重编码,质量参数无效(固定最高 deflate 级别) GIF / WebP / AVIF / SVG / ICO 跳过:动图重编码只剩第一帧,其余 JDK ImageIO 不支持编码 压缩后体积更大或压缩失败时降级保留原文件,不阻断上传。
-
图片缓存:文件名含 UUID、内容不会被原地覆盖,公开文件
Cache-Control: public, max-age=31536000, immutable,私有文件private, no-store。代理下载接口与 S3 直链(上传时写入对象元数据)共用StorageCachePolicy,避免两条链路缓存策略分叉。 -
下载:私有文件需登录(
/admin-api/system/file/download/**),公开文件免登录(/app-api/system/file/download/**);私有下载用 Path 限定的 HttpOnly Cookie,避免放开全局 Cookie 鉴权。 -
响应增加
X-Content-Type-Options: nosniff与 CSP sandbox,前端用AttachmentUploader复用上传、回显与删除。
消息管理
- 消息通道可插拔:内置站内信与邮件(SMTP)通道,新增短信、钉钉、微信、飞书只需实现
MessageChannelProvider并注册为 Solon 组件。 - 通道配置存
sys_message_config,发送记录存sys_message_record;后台支持新增 / 编辑 / 测试发送。 - 配置字段由 Provider 的
configFields()声明,前端动态渲染,不需要为每种通道单独写页面。 - SMTP 密码、存储 AccessKey / SecretKey 等敏感配置可用
VAULT_PASSWORD做主密钥 AES 加密后落库(ENC(...)),未配置时保持明文兼容历史数据。
消息推送
- 站内信通过 SSE 实时推送:
/admin-api/system/notice/sse,支持未读数、全部已读、消息中心入口。 - 单实例内存维护连接;多实例通过
NoticeSseHub订阅消息总线主题,发送方只publish一次,消息能到达浏览器实际连接的那台节点。 - SSE 连接有定时心跳,避免中间代理因空闲断开;前端
store/notice.js负责建连、重连与红点状态。 - SSE 端点的 token 通过专用 Filter(
SseQueryTokenFilter)从 query 提升为请求头,只匹配该路径,不放开全局 query / Cookie 鉴权。
定时任务
- 基于 Quartz JDBC JobStore + 集群模式:调度状态落
QRTZ_*表,多实例靠数据库行锁协调,同一任务不会被重复执行。 sys_job保存业务定义(任务名、cron、调用目标、启停、并发策略),QRTZ_*保存运行时状态;应用启动时由SysJobServiceImpl对账。- 支持 cron 表达式、调用目标(bean.method)、并发 / 禁止并发两种执行策略,执行结果写
sys_job_log。 - 任务基类
AbstractQuartzJob统一处理执行前后钩子、耗时统计与异常日志;日志异步落库,不阻塞任务线程。 - 调度器线程池默认 10 个线程,可在
app.yml的org.quartz.threadPool.threadCount调整。
系统设置
- 单行配置表
sys_setting(固定id=1),维护系统名称、登录页副标题、Logo(含圆角)、默认布局与默认主题色。 - 公开接口
GET /admin-api/system/setting/public免登录,登录页在拿到 token 前即可展示系统名称与 Logo。 - 前端浏览器标题、侧栏 / 顶栏名称、favicon 都跟随系统设置;未自定义偏好时使用系统默认布局与主题色。
- 布局与主题色按维度分别判断:用户改过布局不会覆盖系统默认主题色,反之亦然。
设计
技术栈选择
| 层 | 选型 | 版本 | 选择理由 |
|---|---|---|---|
| 运行时 | JDK | 25 | 使用虚拟线程、record 等现代特性 |
| 后端框架 | Solon | 4.1.0 | 轻量、启动快、配置简单、插件化 |
| ORM | MyBatis-Plus | 3.5.17 | 单表 CRUD 省样板代码,逻辑删除 / 自动填充开箱即用 |
| 多表查询 | MyBatis-Plus-Join | 1.5.9 | 连表避免手写 XML,仍保留 SQL 可控性 |
| 鉴权 | Sa-Token | 1.46.0 | 登录、权限、会话、踢人一套解决,Redis 插件成熟 |
| 密码哈希 | BCrypt(at.favre.lib) | 0.10.2 | Sa-Token 1.41 起不再维护加密模块,独立实现更稳 |
| JSON | snack4 | Solon 4.x 内置 | 与 Solon 渲染链一致,少一个依赖 |
| 定时任务 | Quartz | 2.5.0 | JDBC JobStore + 集群,满足多实例调度 |
| 消息总线 | 自研 MessageBus SPI |
— | 默认 Redis Pub/Sub,可替换 RabbitMQ 等 |
| 导入导出 | EasyExcel | 4.0.3 | 流式读写,内存占用可控 |
| 数据库 | MySQL | 8.0.13+ | 通用、运维成本低;表结构依赖 8.0.13 起支持的函数索引 |
| 缓存 / 会话 | Redis | 7 | 会话、权限快照、限流、验证码、Pub/Sub 共用 |
| 前端框架 | Vue | 3.5 | 组合式 API,模块边界清晰 |
| UI 组件库 | Naive UI | 2.45 | 主题变量完善,适合做统一外观 |
| 构建 | Vite | 8 | 冷启动与 HMR 快;生产构建 minify 并预生成 .gz |
| 状态管理 | Pinia | 4 | 轻量、TypeScript 友好 |
| 路由 | Vue Router | 5.3 | 动态路由与守卫能力完整 |
| 图表 | ECharts | 5.6 | 仪表盘与统计图 |
开发规范
完整约定见 docs/开发规范.md。编码风格以工具可校验的规则为准:
- 后端遵循《阿里巴巴 Java 开发手册》:根
pom.xml集成 PMD + P3C 规则集(命名、常量、注释、OOP、并发、异常、集合、流程控制、ORM),执行mvn pmd:pmd生成扫描报告。 - 前端使用 ESLint:扁平配置(
eslint.config.js),基于@eslint/js+eslint-plugin-vue的flat/essential,pnpm run lint检查、pnpm run lint:fix自动修复;只查正确性,不强制格式(不接 Prettier)。
部署脚本
deploy/ 下提供一套“本机打包 + Docker 运行”的脚本,数据库与应用分开部署:
| 脚本 | 作用 |
|---|---|
install-env.sh |
检查 / 安装 Docker、JDK 25、Maven、Node.js(--check 仅检查) |
deploy-db.sh |
启动 MySQL + Redis(不自动建表) |
deploy-app.sh |
本地 Maven + 前端打包,构建运行镜像并启动 Server / Web |
deploy-minio.sh |
可选,启动本地 MinIO(S3 兼容存储) |
stop.sh / restart.sh / logs.sh |
停止、重启、查看日志 |
生产环境建议:
- 后端容器只绑定
127.0.0.1:8080,所有流量经 Nginx 转发,避免绕过代理伪造 IP。 - Nginx 覆写(而不是追加)
X-Forwarded-For,丢弃客户端伪造链。 - 后端与应用容器均非 root 运行,日志与上传目录使用数据卷。
- 通过
.env注入数据库、Redis、JVM 参数、VAULT_PASSWORD与接口文档开关。
文档说明
| 文档 | 说明 |
|---|---|
| docs/开发规范.md | 前后端编码约定、模块依赖方向、新增业务模块步骤 |
| docs/SSO.md | 单点登录流程与三步接入指引 |
| deploy/ | Docker 部署脚本与 Nginx / Dockerfile 配置(用法见下方「生产部署」) |
| designer-module-demo | 业务模块模板源码(复制后改包名即可开启新模块) |
| designer-ui/sso-demo/README.md | SSO 客户端示例工程 |
后台设计考虑
分层结构
designer-admin/
├── designer-common/ # 通用契约:Result / PageQuery / BaseEntity / 异常 / 工具 / 注解
├── designer-framework/ # 技术能力与 SPI:鉴权、限流、验证码、日志、任务、存储、消息、MyBatis
├── designer-module-system/
│ ├── system-api/ # 跨模块契约:*Api 及 Api 自用类型
│ └── system-service/ # RBAC、认证、文件、消息、任务、监控等实现
├── designer-module-demo/ # 业务模块模板(demo-api / demo-service)
├── designer-server/ # 唯一启动组装:DesignerApp + app.yml + 胖包
├── designer-ui/ # 前端:admin-web、sso-demo、shared/captcha
├── sql/init/mysql/ # 按模块组织的初始化 SQL
├── deploy/ # Docker 部署脚本与配置
└── docs/ # 开发规范、SSO 等文档
一次请求的典型路径:
Controller(参数校验 / 权限注解 / 限流)
→ Service 接口 + Impl(事务 / 缓存 / 业务规则)
→ Mapper(MyBatis-Plus / MPJ,数据权限拦截器自动改写)
→ MySQL
跨模块调用只走 *Api:*-service 在 api 包实现 *ApiImpl,由它完成 Api 类型与内部 entity / DTO / VO 的转换。这样模块可以独立演进,也避免把内部模型暴露出去。
设计模式
| 模式 | 落地位置 | 作用 |
|---|---|---|
| 策略 + 注册表 | FileStorageProvider / MessageChannelProvider + Registry |
存储、消息通道按 type 装配,新增实现不改核心流程 |
| 工厂 / 选择器 | MessageBusManager |
按 designer.mq.type 选定消息总线实现,配错启动即失败 |
| 模板方法 | AbstractQuartzJob |
固定“执行前 → 执行 → 执行后记日志”骨架,子类只写任务体 |
| 拦截器 / 责任链 | GlobalExceptionFilter、OperLogInterceptor、RateLimitInterceptor、MyBatis 数据权限拦截器 |
横切能力与业务解耦,按顺序生效 |
| 依赖倒置 | PermissionLoader、OperLogWriter、MessageRecordWriter、JobLogWriter、MessageConfigSource、StorageConfigSource、InboxSink |
framework 定义接口,system-service 提供实现,框架不反向依赖业务 |
| 门面 | FileStorageService、MessageService |
对外隐藏配置解析、Provider 选择、记录落库等细节 |
| 建造者 | StoredFile.builder() |
文件元数据字段多,构建过程更清晰 |
参数验证
- Controller 类标注
@Valid,请求体参数用@Body @Validated,配合@NotBlank、@Length、@Email等约束注解。 GlobalExceptionFilter捕获ValidatorException,统一返回{code: 400, msg: "参数校验失败:..."},不把堆栈暴露给调用方。- 分页参数由
PageQuery统一处理:页码小于 1 归一到 1,pageSize最大 500,避免恶意大分页。 - 文件上传按扩展名白名单推导类型,校验大小与路径;不采信客户端声明的
Content-Type。 - 注意:Solon 的参数校验由 Filter 执行,没有写约束的字段等于没有校验,不能指望
@Valid自动兜底。
日志记录
- 使用 Solon 内置 Logback 集成,
logback-solon.xml定义三路输出:CONSOLE:控制台;FILE:全量文件(INFO 及以上);ERROR_FILE:仅 ERROR,默认文件名加-error后缀。
- 文件按「天 + 单文件大小」滚动,默认单文件 10 MB、保留 7 天、总量上限 1 GB,均可通过
solon.logging.appender.*调整。 - 操作日志:
@OperLog由OperLogInterceptor环绕,记录标题、业务类型、方法、URL、IP、操作人、参数、结果、耗时与异常;写入异步化,失败不影响主流程。 - 登录日志(
sys_login_log)与任务日志(sys_job_log)独立落库,便于审计与排障。 - 未知异常只在服务端记录详细堆栈,响应统一为“系统繁忙,请稍后重试”,避免泄露 SQL、类名、路径等内部信息。
事务管理
- 写操作在 Service 实现层使用 Solon
@Transaction,事务边界保持在数据库写操作附近,避免长事务。 - 日志、消息记录等旁路写入通过
RunUtil.asyncAndTry异步执行,不占用事务时间。 - 权限快照失效挂在
TranUtils.listen(...).afterCommit():事务内解析受影响用户,提交后再删缓存;回滚时不删,保证缓存与数据库一致。 - 注意 Solon 的
@Transaction、@Cache、@RateLimit都依赖代理,同一个 bean 内部this.xxx()自调用不会生效;需要横切能力时把方法放到另一个 bean,或从外部入口调用。
缓存管理
- 声明式缓存使用
@Cache/@CacheRemove,由RedisCacheService落到 Redis,多实例共享;Redis 异常时 fail-open(读当未命中、写只记日志),不回源失败放大成接口 500。 - 只缓存「读多写少 + 与调用者无关」的公共数据:字典、下拉选项、启用的 Scope / 白名单、系统设置公开信息等;分页与关键字查询不加缓存。
- 键名与 tag 统一收敛在
SystemCacheConsts;key = "...:#{dictType}"依赖-parameters编译,参数改名会导致缓存键求值异常。 - 权限快照单独用 Redis 管理(
perm:user:{userId}),TTL 默认 600 秒;正常失效靠写侧主动删键,TTL 只覆盖直连改库等绕过应用的变更。 - 缓存键统一加前缀(
cache:/perm:/rate:等),便于排查与批量清理。
线程池管理
solon.threads.virtual.enabled: true:Solon 的并行 / 调度执行器使用虚拟线程,日志落库、消息发送、SSE 分发等 I/O 旁路任务不再占用平台线程。- 旁路任务统一用
RunUtil.asyncAndTry(...),异常被吞掉并记录,不影响主流程;SSE 心跳用RunUtil.scheduleAtFixedRate。 - Quartz 使用独立线程池(
org.quartz.threadPool.threadCount,默认 10),与 Web 请求线程隔离。 - 数据库连接池使用 HikariCP(
maximumPoolSize: 10,minimumIdle: 2),连接超时、空闲回收、最大生命周期均在app.yml配置。 - 当前没有为业务单独拆 CPU 密集型线程池;如果新增批量计算、报表导出等重任务,建议单独定义执行器并加队列 / 拒绝策略与监控,避免挤占 Web 与调度线程。
接口限流
- 声明式注解
@RateLimit可标在 Controller 类或方法上,支持多条规则(如“30 秒 1 次 + 每天 20 次”),规则按声明顺序校验。 - 维度支持
IP/USER/GLOBAL/HEADER;未登录的USER维度自动退化为 IP,避免所有匿名请求共用一个桶。 - IP 维度防伪造:计数主体由
ClientIpUtil统一归一化(与落库的登录 / 操作日志同一个值),底层取自TrustedProxyHostUtil的可信解析——不可信对端只认 socket IP,可信代理才允许携带转发头。因此伪造X-Forwarded-For/X-Real-IP换不来新的限流桶;Nginx 侧同时覆写(而非追加)转发头,形成应用与代理两层防线。 - 底层是 Redis 固定窗口计数(Lua 原子操作),多实例阈值不会被放大成「实例数 × 阈值」;限流键包含
count x window,改阈值等于换新桶。 - 拦截器
index = -1,位于校验、缓存、事务之前,被限的请求不会白开事务、白查缓存。 - Redis 未装配或维度取不到值时 fail-open 并打日志:限流是保护手段,不应把业务接口变成 500。
- Nginx 侧对验证码等入口另配
limit_req,超限返回 429,与应用层限流语义一致。
消息队列
MessageBusSPI 提供publish/subscribe,订阅返回可关闭的Subscription;默认实现为 Redis Pub/Sub,另有进程内local实现用于单机调试。MessageBusManager按designer.mq.type选择实现,启动时校验配置;配置写错直接失败并列出可用实现,避免静默退化成单机。- Redis Pub/Sub 不持久化、不补发,适合站内信这类实时广播;需要可靠投递时新增实现(如 RabbitMQ / Kafka)并标注
@Component即可接入。 - 当前主要用途是 SSE 跨实例广播:每个实例订阅同一主题,发送方只发布一次,消息到达浏览器实际连接的节点。
- 多套环境共用 Redis 时必须使用不同主题名(Pub/Sub 不区分 database index),否则消息会串环境。
升级 SQL 脚本管理
- 初始化脚本:
sql/init/mysql/按模块组织,00-run-init.sh按_base → system → demo顺序导入;新增业务模块时增加同名目录并把模块名加入脚本的MODULES。 - 初始化脚本是破坏性的(
DROP TABLE重建 + 插入种子数据),只用于首次初始化或本地重置,禁止对有数据的生产库重复执行。 - 版本要求:
schema.sql用函数索引(IF(deleted = 0, 0, NULL))在逻辑删除下维持唯一约束,依赖 MySQL 8.0.13+;更低版本或 MariaDB 需改为虚拟生成列 + 普通唯一索引。 - 增量升级:约定放在
sql/upgrade/mysql/,按V{版本}__{说明}.sql命名,版本单调递增、只追加不修改历史脚本;发布时按版本顺序执行并登记执行结果。 - 当前仓库尚未内置 Flyway / Liquibase 自动迁移,增量脚本需人工评审与执行;后续接入时建议把现有
schema.sql作为 baseline,再把升级脚本纳入 CI 校验。 - 无论是否使用迁移工具,生产变更都要求:先备份、再执行、可回滚,并在发布记录中登记版本号。
接口文档
- 使用 OpenAPI2 + Knife4j,启动后访问
http://localhost:8080/doc.html。 DocConfig按包扫描分为三组:管理端接口、前台接口、开放接口;只有同时标注@Api与@ApiOperation的接口才会出现在文档中。- 统一声明
Result响应体与 200 / 400 / 401 / 403 / 500 状态码说明,减少每个接口重复描述。 - 生产环境建议通过
KNIFE4J_ENABLE=false关闭文档;如需保留,至少设置KNIFE4J_PRODUCTION=true,并限制为内网访问。
前端设计考虑
分层结构
前端按「壳层 → 业务模块 → 公共能力」三层组织,依赖只能自上而下,业务模块之间互不引用:
designer-ui/admin-web/src/
├── layout/ # 布局壳:Sidebar / Navbar / TagsView / MessageInbox / PreferenceSettings
├── views/ # 壳层页面:login / dashboard / profile / error
├── router/ # 常量路由 + 动态路由注册(按菜单树解析 modules/*/views)
├── store/ # Pinia:user(token / 权限 / 路由)、app(偏好)、setting(系统设置)、notice(站内信)
├── api/ # 壳层接口:request 实例、auth、captcha、monitor
├── modules/ # 业务模块,按模块隔离
│ ├── system/ # 系统管理:api/ + views/<域>/index.vue
│ └── demo/ # 示例模块:api/ + views/
├── components/ # 全局通用组件(PageTable / SearchBar / Dialog …)
├── composables/ # 可复用逻辑 useXxx()
├── directives/ # 全局指令(v-perm)
├── theme/ · styles/ # 主题色派生、Naive UI overrides、全局 CSS 变量
├── constants/ · utils/ # 常量与工具函数
└── App.vue · main.js · settings.js
各层职责:
| 层 | 目录 | 职责 | 约束 |
|---|---|---|---|
| 壳层 | layout/、views/、router/、store/、api/ |
登录页、布局、导航、页签、动态路由、全局状态与请求封装 | 只承载框架级能力,不放业务逻辑 |
| 业务层 | modules/<module>/ |
该模块的 api/ 与 views/,必要时自建 components/、utils/ |
只引用 @/api、@/components、@/composables 等公共层,不引用其他模块 |
| 公共层 | components/、composables/、utils/、constants/、theme/、styles/ |
与业务无关的组件、组合式函数、工具、常量、主题 | 不得反向依赖 modules/ 或 views/ |
一次请求与渲染的典型路径:
views / modules 页面
→ usePagination / useFormDialog(状态与分页)
→ modules/<module>/api(按模块组织的接口函数)
→ @/api/request(axios 实例:token、统一响应、401 / 429)
→ /admin-api(Nginx / Vite 代理)
关键约定:
- 业务代码只进
modules/<module>/,不往views//api/壳层塞业务页面,保证壳层可随脚手架统一升级。 - 模块之间不互相 import:需要跨模块数据时走各自
api/调后端接口,或提升为components//composables/公共能力。 - 路由不需要手写:菜单
component填{module}/{path},router/index.js用import.meta.glob('../modules/*/views/**/*.vue')解析,新增页面只加目录与菜单记录。 - 新增业务模块:复制
modules/demo→modules/foo,在菜单里填foo/index即可,不需要改路由或壳层代码。
公共布局
- 布局壳在
src/layout/:侧栏、顶栏、页签(TagsView)、消息中心、偏好设置统一收口,业务页面只关心内容区。 - 内置 7 种布局:垂直、双列菜单、水平、侧边导航、混合垂直、混合双列、内容全屏,可在偏好设置中实时切换并持久化。
- 动态路由:常量路由写在
router/index.js,登录后按后端菜单树注册;菜单component字符串按{module}/{path}解析到src/modules/{module}/views/{path}.vue。 - 权限指令
v-perm="'system:user:add'"无权限时直接移除元素;路由守卫负责登录态、用户信息与菜单加载。 - 请求统一走
@/api/request:自动带Authorization,统一处理{code,msg,data}、401 跳登录、429 限流提示与 FormData 边界。
公共组件
| 组件 | 作用 |
|---|---|
SearchBar |
搜索区容器,自动绑定查询 / 重置 / 展开,操作按钮对齐 |
PageTable |
基于 n-data-table,自动绑定分页、勾选、加载态、空状态与横向滚动 |
PaginationBar |
统一分页条,页码 / 每页条数双向绑定 |
Dialog |
统一弹框,支持尺寸、全屏、拖拽、页脚按钮与提交 loading |
DetailDrawer |
详情抽屉,适合只读信息展示 |
AttachmentUploader |
附件上传、回显、删除,复用后端文件接口 |
TableToolbar / TableEmpty |
表格工具栏与统一空状态 |
UserPickerDialog / IconPicker / LayoutPicker / ThemeColorPicker |
选人、图标、布局、主题色等业务通用选择器 |
配套组合式函数:
usePagination(fetcher):查询条件、分页、加载态、勾选、排序、重置一次搞定,返回值可直接传给SearchBar与PageTable。useFormDialog():新增 / 编辑弹框状态机,表单校验通过后自动关闭。useBatchDelete()/useConfirmAction()/useActionLock():批量删除、二次确认、按钮防重复点击。useDict():字典数据加载与缓存;useClientSort():前端排序。
一个典型 CRUD 页面由 SearchBar + PageTable + Dialog + usePagination + useFormDialog 拼装,业务代码只保留字段、接口与校验规则。
外观和布局方便修改
- 主题色由
src/theme/index.js从单一主色派生完整色板(hover / pressed / light / suppl),同时生成 Naive UIthemeOverrides与 CSS 变量。 - 全局样式统一使用
--color-primary、--color-surface、--color-border、--radius等变量,业务页面不写死颜色与圆角。 - 偏好设置支持主题色、布局、页签、侧栏等切换并持久化;未自定义时回退到「系统设置」下发的默认布局与默认主题色。
- 品牌信息(系统名称、Logo、Logo 圆角、favicon)由系统设置驱动,换品牌不需要改代码。
- 新增外观维度时,优先在
theme/与styles/index.css增加变量,再由组件消费,避免逐页调样式。
构建压缩
生产产物在构建期完成压缩,运行时 nginx 只负责挑合适的文件下发,不在请求路径上现压。
| 环节 | 做法 | 说明 |
|---|---|---|
| JS / CSS 内容压缩 | vite.config.js 显式 build.minify: 'oxc'、build.cssMinify: true |
去掉换行与多余空格;写死配置避免被改成 false 导致体积翻倍 |
index.html |
plugins/compressAssets.js 的 minifyHtml |
Vite 默认不压 HTML;插件去注释、折叠空白,并单独压缩内联脚本 |
| gzip 预压缩 | 同插件的 compressAssets,对 dist/** 生成同名 .gz(gzip -9) |
只压文本类扩展名;小于 1KB 或压完反而变大的跳过 |
| 下发 | nginx gzip_static on + gzip on(见 deploy/nginx/default.conf) |
优先下发构建好的 .gz,找不到再回落现压;gzip_vary on 让代理按 Accept-Encoding 分缓存 |
compressAssets在closeBundle执行,等public/也复制进dist/之后再扫,一次构建覆盖全部静态资源。.gz是构建产物,不要手动创建或提交(dist/已在.gitignore);本地vite preview也会命中预压缩文件,无需额外配置。
快速开始(本地开发)
0. 环境要求
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 25+ | 后端编译与运行(maven.compiler.release=25) |
| Maven | 3.9+ | 后端构建 |
| Node.js | 18+(推荐 22) | 前端构建 |
| pnpm | 10.x | 前端包管理(packageManager: pnpm@10.33.0) |
| MySQL | 8.0.13+ | 数据库 |
| Redis | 7 | 会话、缓存、限流、验证码、消息总线(必需) |
MySQL 最低 8.0.13:唯一约束用函数索引
UNIQUE KEY (业务键, (IF(deleted = 0, 0, NULL)))实现「逻辑删除后可重建同名记录」(deleted = 1时表达式为NULL,不占用唯一值),该语法 8.0.13 才引入,低版本会报语法错误,MariaDB 不支持。
一键检查 / 安装环境:
bash
cd deploy
./install-env.sh --check # 仅检查
./install-env.sh # 按需安装(Linux 建议 sudo)
1. 准备 MySQL 与 Redis
连接信息在 designer-server/src/main/resources/app.yml,支持环境变量覆盖:
- MySQL:
DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME - Redis:
REDIS_HOST/REDIS_PORT/REDIS_PASSWORD/REDIS_DB
本地也可以用 Docker 只起数据库:
bash
cd deploy
cp .env.example .env
./deploy-db.sh # MySQL + Redis,不自动建表
2. 初始化数据库
脚本为破坏性初始化(
DROP TABLE重建 + 插入种子数据),仅首次或需要重置时执行,勿对有数据的库重复运行。
bash
# 交互式:按提示输入连接信息,回车使用默认值
bash sql/init/mysql/00-run-init.sh
# 免交互(CI / 脚本)
DB_HOST=127.0.0.1 DB_PORT=3306 DB_USER=root DB_PASSWORD=root DB_NAME=designer_admin \
bash sql/init/mysql/00-run-init.sh
默认数据库名为 designer_admin,可在交互中或通过 DB_NAME 自定义。
3. 启动后端
在 IDE 中运行 com.designer.admin.DesignerApp(模块 designer-server),或命令行:
bash
# 仓库根目录
mvn -pl designer-server -am -DskipTests clean package
java -jar designer-server/target/designer-server.jar
启动后:
- 管理端 API:
http://localhost:8080/admin-api - 健康检查:
GET http://localhost:8080/admin-api/system/health - 接口文档:
http://localhost:8080/doc.html - 示例接口:
GET http://localhost:8080/admin-api/demo/ping(需登录)
4. 启动前端
bash
# 需要 pnpm:corepack enable pnpm 或 npm i -g pnpm
cd designer-ui/admin-web
pnpm install
pnpm run dev # http://localhost:5173
pnpm run build # 产出 dist/
开发环境已配置 Vite 代理,/admin-api、/app-api、/open-api 均转发到 http://localhost:8080。
默认账号
| 账号 | 密码 | 角色 |
|---|---|---|
| admin | admin123 | 超级管理员(全部权限) |
| guest | guest123 | 普通用户(只读示例) |
以上为种子数据中的演示账号,部署到生产环境前必须修改密码或删除。
生产部署
数据库与应用分开部署:本机用 JDK / Maven / Node 打包,再构建运行镜像;MySQL、Redis 与应用容器分离。
部署流程
bash
cd deploy
# 1. 检查并按需安装环境(Docker / JDK 25 / Maven / Node)
./install-env.sh
# 2. 配置环境变量
cp .env.example .env
vim .env
# 3. 启动 MySQL + Redis(不自动建表)
./deploy-db.sh
# 4. 首次 / 重置时初始化数据库
(cd .. && bash sql/init/mysql/00-run-init.sh)
# 5. 本地打包 + 构建镜像 + 启动 Server / Web
./deploy-app.sh
可选:./deploy-minio.sh 启动本地 MinIO(S3 兼容存储,API 9000 / Console 9001)。
使用外部数据库时,只需配置 .env 中的 DB_* / REDIS_* 后执行 ./deploy-app.sh;建表仍通过 sql/init/mysql/00-run-init.sh(支持交互输入,也可用环境变量免交互)。
常用命令
| 命令 | 说明 |
|---|---|
./install-env.sh |
检查并安装环境(--check 仅检查) |
./deploy-db.sh |
启动 MySQL、Redis(不自动建表) |
./deploy-minio.sh |
启动本地 MinIO(可选) |
./deploy-app.sh |
本地打包并部署应用 |
./deploy-app.sh --skip-build |
跳过打包,复用 deploy/build 已有产物 |
./restart.sh app / ./stop.sh app |
重启 / 停止应用 |
./logs.sh server |
查看后端容器标准输出日志 |
访问入口
| 入口 | 默认端口 | 说明 |
|---|---|---|
| 管理后台 | APP_PORT=80 |
浏览器访问 http://<服务器IP> |
| 后端 API | SERVER_PORT=8080 |
仅绑定 127.0.0.1,不直接对外,统一经 Nginx |
| 接口文档 | SERVER_PORT |
生产环境建议 KNIFE4J_ENABLE=false 关闭 |
日志
- 文件日志:容器内
/app/logs/designer-server.log(全量)与/app/logs/designer-server-error.log(仅 ERROR),挂载在server_logs卷;按「天 + 单文件 10 MB」滚动,默认保留 7 天、总量上限 1 GB。 - 容器 stdout:
./logs.sh server看到的是 Docker 收集的标准输出,由 compose 的logging.options限制大小(max-size=50m/max-file=5)。 - 调整策略:改
solon.logging.appender.file.*(或error.*单独覆盖),或用环境变量LOG_PATH更换日志路径。
发布与运维
- 日常发版只执行
./deploy-app.sh,不必动数据库;--skip-build可复用已有打包产物。 - 数据库表需手动初始化;增量升级 SQL 按版本执行并登记,禁止重跑破坏性的初始化脚本。
- 上传文件在
server_uploads卷,升级 / 迁移时需与数据库一起备份。 - 修改
.env中的默认密码、VAULT_PASSWORD,并为 MySQL / Redis 配置强密码;本机库时DB_PASSWORD与MYSQL_ROOT_PASSWORD保持一致。 - 多实例部署时:Sa-Token 会话、权限快照、限流、验证码、消息总线共用 Redis;Quartz 共用同一套
QRTZ_*表;各实例org.quartz.scheduler.instanceId必须不同。 - 反向代理不在默认可信网段(回环 +
172.16.0.0/12)内时,必须通过designer.client-ip.trusted-proxies补上,否则后端会把代理自身地址当成客户端 IP,导致登录锁定与 IP 限流全部挤在同一个桶里;该网段也不宜过宽,否则网段内地址可以互相伪装。 - 健康检查:
GET /admin-api/system/health;Nginx 侧/health已转发到该接口。


评论