1. 后台管理系统为什么值得反复造轮子后台管理系统这个东西做过的人都知道它属于那种“看起来简单、做起来琐碎、做好很难”的典型项目。几乎每个开发者职业生涯里都会碰到至少三五个后台系统有的是公司内部用的运营平台有的是给客户交付的管理端还有的是自己接私活时顺手搭的架子。我这些年经手的后台项目少说也有十几个从最早的JSPServlet一路写到后来的前后端分离踩过的坑基本能写一本小册子。这次要聊的这套方案核心是SpringBoot MyBatis-Plus的组合项目代号叫“蓝鲸云”。名字听着挺唬人但本质上它就是一套后台管理系统的脚手架解决的问题非常明确让开发者不用从零开始搭权限、菜单、用户、日志这些通用模块直接拿过来改业务逻辑就行。适合谁看如果你正在做第一个像样的后台项目或者手里有一套老系统想重构但不知道从哪下手再或者你接了个外包需要快速交付一个管理端那这套东西的思路和细节对你都有参考价值。我先说结论SpringBoot MyBatis-Plus 这套组合在后台管理系统这个场景下目前依然是性价比最高的选择之一没有之一。为什么这么说后面会从选型逻辑、核心模块拆解、实操步骤、踩坑记录几个维度展开讲。文章会比较长因为后台系统涉及的东西确实多我尽量把每个关键点都讲透让你看完能直接动手。2. 技术选型背后的取舍逻辑2.1 为什么是SpringBoot而不是别的后台管理系统的技术选型本质上是在“开发效率”和“可控性”之间找平衡。SpringBoot 最大的价值在于自动配置和起步依赖它把原来SSM时代那一堆XML配置全部收敛掉了。我记得早年搭一个Spring MVC项目光web.xml、applicationContext.xml、spring-mvc.xml这几个文件就要写小半天还得反复检查包扫描路径对不对。现在用SpringBoot一个SpringBootApplication注解加一个application.yml五分钟就能跑起来一个带内嵌Tomcat的服务。但这里有个细节很多人忽略SpringBoot的自动配置是有顺序和条件的。比如你同时引入了多个数据源相关的starter它可能会因为条件注解的匹配顺序导致你预期的配置没生效。我在实际项目里遇到过引入MyBatis-Plus后默认的DataSourceAutoConfiguration和自定义数据源冲突的情况最后是通过Primary注解加排除特定自动配置类解决的。这个坑后面会详细说。另一个选SpringBoot的理由是生态完整。后台系统需要的东西——安全框架、缓存、消息队列、定时任务、接口文档——SpringBoot都有对应的starter版本兼容性由官方维护省去了大量找依赖、对版本的时间。对于后台管理系统这种“功能模块多但技术深度不一定深”的场景生态完整比性能极致更重要。2.2 MyBatis-Plus解决了什么痛点原生MyBatis的问题在于CRUD代码重复度太高。一个用户表光是增删改查加条件分页Mapper XML就要写上百行而且每个实体类都要来一遍。MyBatis-Plus的核心价值就是把这些通用操作全部封装成BaseMapper和IService你只需要继承就能用。但MyBatis-Plus真正好用的地方不只是省代码而是它提供了一套条件构造器QueryWrapper、LambdaQueryWrapper。以前写动态SQL要在XML里堆if test...标签现在用LambdaQueryWrapper可以链式调用代码可读性提升非常明显。举个例子一个带多条件筛选的用户列表查询LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(username), User::getUsername, username) .eq(status ! null, User::getStatus, status) .between(startTime ! null endTime ! null, User::getCreateTime, startTime, endTime) .orderByDesc(User::getCreateTime);这种写法比XML里的条件判断清爽太多而且Lambda引用字段名重构时不会因为改字段名而漏改SQL字符串。不过MyBatis-Plus也有它的边界。复杂多表关联查询、存储过程调用、高度定制化的SQL这些场景下MyBatis-Plus的优势就不明显了甚至因为它的封装反而增加了理解成本。我的经验是单表操作和简单关联用MyBatis-Plus复杂查询老老实实写XML两者混用完全没问题不要为了“统一”而强行用条件构造器拼复杂SQL那样只会让代码更难维护。2.3 蓝鲸云这套架子的模块划分一个合格的后台管理系统不管业务是什么底层总逃不开这几个模块认证授权、用户管理、角色管理、菜单权限、操作日志、数据字典、定时任务、文件上传。蓝鲸云这套方案基本覆盖了这些通用能力它的模块划分逻辑是这样的模块核心职责技术要点认证模块登录、登出、Token管理JWT 拦截器权限模块角色、菜单、按钮级权限RBAC模型 注解鉴权用户模块用户CRUD、密码加密BCrypt 逻辑删除日志模块操作日志、登录日志AOP切面 异步写入系统模块字典、配置、公告缓存 通用CRUD监控模块在线用户、服务状态SpringBoot Actuator这个划分的好处是职责清晰、依赖单向。认证模块不依赖业务模块权限模块只依赖用户和菜单业务模块通过注解获取当前用户信息。这样后期加业务功能时不会因为改一个地方而牵动全身。3. 核心模块的细节拆解与实操要点3.1 认证授权JWT方案的正确打开方式后台系统的认证方案主流就两种Session Cookie和JWT Token。蓝鲸云选的是JWT原因很直接前后端分离场景下前端可能是Web、可能是App、可能是小程序Session方案在跨域和分布式部署时会很麻烦而JWT是无状态的服务端不用存Session水平扩展没有压力。但JWT用不好会出大问题。我见过太多项目把JWT当成万能药结果踩了一堆坑。这里说几个关键点第一Token里不要放敏感信息。JWT的Payload是Base64编码的不是加密的任何人拿到Token都能解出里面的内容。所以密码、手机号、身份证号这些绝对不能放进去。一般只放userId、username、角色标识就够了。第二Token过期时间要合理。设太短用户频繁掉线设太长安全性差。我的经验是访问Token设2小时刷新Token设7天。访问Token过期后用刷新Token换新的刷新Token也过期了才要求重新登录。这样既保证了安全性又不会让用户频繁登录。第三Token注销问题。JWT是无状态的服务端没法主动让一个Token失效。解决方案是维护一个黑名单用户登出时把Token加入黑名单存Redis过期时间设为Token剩余有效期每次请求校验时先查黑名单。这个逻辑不复杂但很多项目漏掉了导致用户登出后旧Token还能用。拦截器的实现大概是这样public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BusinessException(未登录); } // 校验Token有效性 Claims claims JwtUtil.parseToken(token); // 检查黑名单 if (redisTemplate.hasKey(token:blacklist: token)) { throw new BusinessException(登录已失效); } // 存入ThreadLocal供后续使用 UserContext.set(claims.get(userId, Long.class)); return true; } }注意ThreadLocal用完一定要在afterCompletion里remove否则线程池复用时会串数据这个问题在并发场景下非常隐蔽排查起来很痛苦。3.2 权限模型RBAC不是万能但够用后台系统的权限99%的场景用RBAC基于角色的访问控制就够了。核心就三张表用户表、角色表、菜单表外加两张关联表。用户关联角色角色关联菜单菜单里区分目录、菜单、按钮三种类型。这里有个设计细节值得说菜单表用树形结构存储。常见方案有两种一种是加parentId字段做递归查询另一种是用路径枚举比如1/2/3表示层级。我推荐parentId 递归查询因为结构清晰增删改查都简单。递归查询在数据量不大菜单一般就几百条时性能完全没问题如果担心性能可以在启动时加载到内存缓存。按钮级权限的实现蓝鲸云用的是自定义注解 AOP。在Controller方法上标注RequiresPermission(user:add)切面里判断当前用户是否拥有这个权限标识。这样权限控制粒度可以细到每个接口。RequiresPermission(user:add) PostMapping(/user) public Result addUser(RequestBody UserDTO userDTO) { // 业务逻辑 }权限标识的命名建议用模块:操作的格式比如user:add、user:delete、role:assign。这样一眼就能看出这个权限是干什么的后期维护方便。3.3 数据层MyBatis-Plus的实战配置MyBatis-Plus的配置有几个地方必须调默认配置直接上生产会出问题。逻辑删除配置。后台系统的数据一般不做物理删除而是标记删除。MyBatis-Plus支持逻辑删除配置如下mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0配置后所有查询自动带上deleted 0条件删除操作自动变成UPDATE ... SET deleted 1。这个功能非常实用但要注意逻辑删除后唯一索引会冲突。比如用户名唯一索引删除了一个用户后再注册同名用户会报错。解决方案是把唯一索引改成(username, deleted)联合唯一或者删除时把username改成一个带时间戳的值。自动填充配置。创建时间、更新时间、创建人、更新人这些字段每次手动set太麻烦。MyBatis-Plus的MetaObjectHandler可以自动填充Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, createBy, Long.class, UserContext.getUserId()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); this.strictUpdateFill(metaObject, updateBy, Long.class, UserContext.getUserId()); } }分页插件配置。MyBatis-Plus的分页需要手动配置拦截器不配的话分页不生效Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }实操心得分页插件一定要指定数据库类型不指定的话它会自动探测多数据源场景下可能探测错误导致分页SQL拼错。3.4 操作日志AOP切面的落地细节操作日志是后台系统的刚需出了问题要能追溯到谁在什么时候做了什么。实现方式用AOP切面最优雅在需要记录日志的方法上加注解切面里异步写入数据库。OperationLog(module 用户管理, type 新增) PostMapping(/user) public Result addUser(RequestBody UserDTO userDTO) { ... }切面里要记录的信息包括操作人、操作模块、操作类型、请求参数、返回结果、IP地址、耗时、操作时间。这里有几个坑请求参数序列化问题。HttpServletRequest的InputStream只能读一次如果在切面里读了Controller里就读不到了。解决方案是用ContentCachingRequestWrapper包装请求或者干脆不读body只记录URL和查询参数。我一般选择后者因为body里可能有密码等敏感信息记录URL和参数已经够追溯了。异步写入的线程安全问题。日志写入用Async异步执行但异步线程里拿不到ThreadLocal里的用户信息ThreadLocal不跨线程。解决方案是在切面里先把用户信息取出来作为参数传给异步方法。日志表数据量问题。操作日志增长很快一个中等规模系统一天几千条很正常。建议按月分表或者定期归档否则半年后日志表几百万条查询会变慢。4. 从零搭建的完整实操流程4.1 环境准备与项目初始化先说环境。JDK建议用JDK 17SpringBoot 3.x要求最低JDK 17而且JDK 17的ZGC和密封类等特性对后台系统也有帮助。如果公司还在用JDK 8那SpringBoot版本要降到2.7.x这个要提前确认好不然后面改起来很麻烦。数据库用MySQL 8.0字符集用utf8mb4排序规则用utf8mb4_general_ci。这里强调一下一定要用utf8mb4而不是utf8因为MySQL的utf8是阉割版存不了emoji和一些特殊字符后台系统里用户输入昵称带emoji是很常见的。项目初始化用Spring Initializr依赖勾选这几个Spring WebMySQL DriverLombokSpring Boot DevTools开发期用Spring Boot Actuator监控用MyBatis-Plus的依赖需要手动加因为Initializr里没有dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency版本选择上MyBatis-Plus 3.5.x对应SpringBoot 2.x和3.x都有适配具体看官方文档的兼容性说明。我用的3.5.5在SpringBoot 3.2上跑没问题。4.2 数据库表结构设计后台系统的表结构设计有几个通用原则所有表加create_time、update_time、create_by、update_by、deleted字段这是基础规范。核心表结构如下-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码, nickname VARCHAR(50) COMMENT 昵称, avatar VARCHAR(255) COMMENT 头像, email VARCHAR(100) COMMENT 邮箱, phone VARCHAR(20) COMMENT 手机号, status TINYINT DEFAULT 1 COMMENT 状态 1正常 0禁用, create_time DATETIME, update_time DATETIME, create_by BIGINT, update_by BIGINT, deleted TINYINT DEFAULT 0, UNIQUE KEY uk_username (username, deleted) ) COMMENT 用户表; -- 角色表 CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_name VARCHAR(50) NOT NULL COMMENT 角色名称, role_key VARCHAR(50) NOT NULL COMMENT 角色标识, sort INT DEFAULT 0 COMMENT 排序, status TINYINT DEFAULT 1, create_time DATETIME, update_time DATETIME, deleted TINYINT DEFAULT 0 ) COMMENT 角色表; -- 菜单表 CREATE TABLE sys_menu ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT DEFAULT 0 COMMENT 父菜单ID, menu_name VARCHAR(50) NOT NULL COMMENT 菜单名称, menu_type TINYINT NOT NULL COMMENT 类型 1目录 2菜单 3按钮, path VARCHAR(200) COMMENT 路由地址, component VARCHAR(200) COMMENT 组件路径, perms VARCHAR(100) COMMENT 权限标识, icon VARCHAR(50) COMMENT 图标, sort INT DEFAULT 0, status TINYINT DEFAULT 1, create_time DATETIME, update_time DATETIME, deleted TINYINT DEFAULT 0 ) COMMENT 菜单表; -- 用户角色关联表 CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) ) COMMENT 用户角色关联表; -- 角色菜单关联表 CREATE TABLE sys_role_menu ( role_id BIGINT NOT NULL, menu_id BIGINT NOT NULL, PRIMARY KEY (role_id, menu_id) ) COMMENT 角色菜单关联表;关联表用联合主键而不是自增ID这样天然防止重复关联也省了一个索引。菜单表的menu_type字段区分目录、菜单、按钮前端根据这个字段决定渲染成什么。4.3 统一响应与全局异常处理后台系统的接口返回格式必须统一否则前端处理起来很痛苦。定义一个Result类Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }全局异常处理用RestControllerAdvice把业务异常、参数校验异常、系统异常分开处理RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public ResultVoid handleBusinessException(BusinessException e) { return Result.error(e.getMessage()); } ExceptionHandler(MethodArgumentNotValidException.class) public ResultVoid handleValidException(MethodArgumentNotValidException e) { String message e.getBindingResult().getFieldError().getDefaultMessage(); return Result.error(message); } ExceptionHandler(Exception.class) public ResultVoid handleException(Exception e) { log.error(系统异常, e); return Result.error(系统繁忙请稍后重试); } }注意全局异常处理里不要把原始异常信息直接返回给前端尤其是SQL异常会暴露表结构信息。统一返回“系统繁忙”即可详细信息记到日志里。4.4 代码生成器的使用MyBatis-Plus提供了代码生成器可以根据表结构自动生成Entity、Mapper、Service、Controller。这个功能在项目初期能省大量时间但生成后一定要手动调整不能直接用。生成器的配置大概是这样FastAutoGenerator.create(url, username, password) .globalConfig(builder - { builder.author(yourname) .outputDir(System.getProperty(user.dir) /src/main/java) .disableOpenDir(); }) .packageConfig(builder - { builder.parent(com.example.admin) .entity(entity) .mapper(mapper) .service(service) .controller(controller); }) .strategyConfig(builder - { builder.addInclude(sys_user, sys_role, sys_menu) .entityBuilder() .enableLombok() .logicDeleteColumnName(deleted) .addTableFills(new Column(create_time, FieldFill.INSERT)) .addTableFills(new Column(update_time, FieldFill.INSERT_UPDATE)); }) .execute();生成出来的代码Controller层需要加上权限注解、参数校验注解Service层需要补充业务逻辑不能直接暴露给前端。我一般把生成的代码当作“半成品”手动完善后才用。5. 常见问题与排查技巧实录5.1 启动报错排查速查表后台系统搭建过程中启动报错是最常见的。我整理了一份速查表覆盖了80%的启动问题报错信息原因解决方案Failed to configure a DataSource没配数据源或配置错误检查application.yml的spring.datasource配置Invalid bound statementMapper XML路径不对或方法名不匹配检查mapper-locations配置和XML的namespaceTable xxx doesnt exist实体类表名映射错误加TableName注解或检查驼峰转换配置Circular dependencyBean循环依赖用Lazy打破循环或重构依赖关系Port 8080 already in use端口被占用改端口或kill占用进程其中Invalid bound statement这个报错特别常见尤其是用MyBatis-Plus的时候。原因通常是Mapper接口和XML没对应上或者XML文件没被扫描到。检查三个地方mybatis-plus.mapper-locations配置的路径对不对、XML的namespace是不是完整的Mapper接口全限定名、方法名和id是不是一致。5.2 分页查询的坑MyBatis-Plus的分页有个经典坑分页插件没配置时分页参数不生效返回的是全部数据。这个坑很隐蔽因为不报错只是数据不对。排查方法是看SQL日志里有没有LIMIT语句没有的话就是分页插件没生效。另一个坑是分页总数不对。当查询涉及多表关联时MyBatis-Plus自动生成的count语句可能不对需要手动指定count语句PageUserVO page new Page(pageNum, pageSize); page.setCountId(selectUserCount); // 指定自定义count查询还有一个性能问题深度分页。LIMIT 100000, 10这种查询MySQL会扫描前100010行然后丢弃前100000行非常慢。解决方案是用游标分页记录上一页最后一条的ID下一页从这ID之后查或者限制最大页数。5.3 权限注解不生效的排查RequiresPermission注解不生效通常是这几个原因第一AOP没生效。检查切面类有没有加Aspect和Component检查启动类有没有EnableAspectJAutoProxy。第二方法内部调用。AOP是基于代理的同一个类里方法A调用方法B方法B上的注解不会生效。解决方案是把方法B移到另一个类或者注入自己的代理对象。第三权限标识不匹配。用户拥有的权限标识和注解里写的不一致。排查方法是把当前用户的权限列表打出来和注解里的值对比。实操心得权限问题排查时我习惯在切面里加一行日志打印“当前用户权限列表”和“需要的权限”一眼就能看出是权限没配还是注解没生效。5.4 跨域问题的正确处理前后端分离项目跨域是绕不开的。SpringBoot处理跨域有三种方式方式一CrossOrigin注解。加在Controller类或方法上适合少量接口。方式二WebMvcConfigurer配置。全局配置适合大部分场景Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }方式三过滤器配置。最灵活可以动态判断Origin。这里有个坑allowCredentials(true)和allowedOrigins(*)不能同时用。SpringBoot 2.4之后如果allowCredentials为trueallowedOrigins必须指定具体域名不能用星号。解决方案是用allowedOriginPatterns(*)代替allowedOrigins(*)。6. 部署与性能优化的实战经验6.1 打包部署的注意事项后台系统打包用Maven的spring-boot-maven-plugin打成可执行jar。打包时注意排除devtools依赖否则生产环境会有安全问题dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-devtools/artifactId optionaltrue/optional /dependencyoptional设为true依赖不会传递打包时也不会包含。启动脚本建议加上JVM参数java -jar admin.jar \ -Xms512m -Xmx1024m \ -XX:UseG1GC \ -XX:MaxGCPauseMillis200 \ -Dspring.profiles.activeprod后台系统一般内存不用太大1G堆内存足够支撑中等规模使用。GC用G1停顿时间可控。6.2 数据库连接池配置SpringBoot默认用HikariCP性能很好但默认配置偏保守。生产环境建议调整spring: datasource: hikari: minimum-idle: 10 maximum-pool-size: 50 idle-timeout: 300000 connection-timeout: 30000 max-lifetime: 1800000maximum-pool-size不是越大越好一般按CPU核数 * 2 磁盘数估算。50对于大多数后台系统足够了。max-lifetime要小于数据库的wait_timeout否则会出现连接被数据库断开但连接池不知道的情况。6.3 缓存的使用策略后台系统里适合缓存的数据菜单权限、数据字典、系统配置。这些数据读多写少缓存后能显著减少数据库压力。缓存用Rediskey的设计要有规范比如admin:menu:user:{userId}、admin:dict:{dictType}。更新数据时先更新数据库再删除缓存不要更新缓存避免并发下的数据不一致。public ListMenuVO getUserMenus(Long userId) { String key admin:menu:user: userId; ListMenuVO menus (ListMenuVO) redisTemplate.opsForValue().get(key); if (menus ! null) { return menus; } menus menuMapper.selectMenusByUserId(userId); redisTemplate.opsForValue().set(key, menus, 30, TimeUnit.MINUTES); return menus; }注意缓存一定要设过期时间哪怕你觉得数据不会变。没有过期时间的缓存是定时炸弹一旦数据出问题你连回滚的机会都没有。6.4 接口性能优化后台系统的接口性能问题大部分出在N1查询上。比如查询用户列表每个用户要查一次角色100个用户就是101次查询。解决方案是用批量查询 内存组装// 先查用户列表 ListUser users userMapper.selectList(wrapper); // 批量查角色 ListLong userIds users.stream().map(User::getId).collect(Collectors.toList()); MapLong, ListRole roleMap roleMapper.selectRolesByUserIds(userIds) .stream().collect(Collectors.groupingBy(Role::getUserId)); // 组装 users.forEach(user - user.setRoles(roleMap.getOrDefault(user.getId(), Collections.emptyList())));这样从101次查询降到2次性能提升非常明显。另外列表查询不要SELECT *只查需要的字段。后台系统的表字段往往很多全查出来浪费带宽和内存。用MyBatis-Plus的select()方法指定字段wrapper.select(User::getId, User::getUsername, User::getNickname, User::getStatus);7. 这套架子的扩展方向蓝鲸云这套基础架子搭好后往哪个方向扩展取决于具体业务。我分享几个常见的扩展点。多数据源支持。如果业务需要读写分离或者对接多个数据库可以用dynamic-datasource-spring-boot-starter通过DS注解切换数据源。配置时注意主从切换的时机写操作走主库读操作走从库但主从同步有延迟刚写完立刻读可能读不到这种场景要强制走主库。工作流集成。后台系统经常需要审批流程可以集成Flowable或Activiti。集成时注意工作流引擎的表和业务表要分开不要混在一起否则后期维护很痛苦。消息通知。站内信、邮件、短信这些通知功能建议抽象成一个通知服务用策略模式支持多种渠道。发送逻辑异步化避免阻塞主流程。数据权限。基础RBAC控制的是“能不能访问某个菜单”但更细粒度的需求是“只能看自己部门的数据”。这个需要在SQL层面加数据权限过滤实现方式可以用MyBatis的拦截器在SQL里自动拼接数据权限条件。操作日志的审计增强。基础的操作日志记录了“谁做了什么”但审计场景可能需要记录“改之前是什么、改之后是什么”。这个需要在切面里对比修改前后的数据实现起来复杂一些但对排查问题非常有帮助。8. 一些踩坑后的个人体会这套架子我从头搭过不止一次每次都会遇到一些新问题也每次都会优化一些细节。说几个印象比较深的。关于密码加密。早期项目我用MD5加盐后来发现MD5计算太快容易被暴力破解。现在统一用BCrypt它内置了盐和计算强度安全性高很多。BCrypt的强度参数默认是10可以调到12但登录接口的响应时间会明显增加需要权衡。关于Token刷新。一开始我设计的方案是访问Token过期后前端用刷新Token换新的访问Token。但实际用下来发现如果用户正在操作时Token过期请求会失败体验不好。后来改成滑动过期每次请求如果Token剩余有效期小于某个阈值就自动续期。这样用户只要在活跃就不会掉线。关于菜单缓存。菜单数据我一开始没缓存每次登录都查数据库。后来发现菜单查询涉及多表关联每次要几十毫秒用户多了之后数据库压力明显。加上Redis缓存后登录响应时间从200ms降到50ms以内。但缓存也带来了新问题管理员改了菜单权限后用户要重新登录才能生效。解决方案是改菜单时主动清除相关用户的缓存或者用消息通知前端刷新。关于日志表。操作日志表我一开始没做归档跑了三个月后表里有两百多万条数据查询变得很慢。后来加了定时任务每月1号把上上个月的数据归档到历史表主表只保留最近两个月的数据。归档用INSERT INTO ... SELECT ...加DELETE注意要分批删一次删太多会锁表。关于接口文档。早期我用Swagger后来发现Swagger的注解侵入性太强代码里到处是ApiOperation。现在用Knife4j它是Swagger的增强版界面更好看而且支持离线文档导出。注解还是那些注解但至少文档看起来舒服多了。最后说一个心态上的体会后台管理系统不要追求一步到位。我见过很多项目一开始就想把权限、工作流、报表、消息全部做进去结果做了半年还没上线。正确的做法是先跑通核心流程——登录、用户管理、角色管理、菜单管理这四个模块能用了就先上线然后根据实际需求迭代。后台系统的价值在于支撑业务而不是技术炫技。