SSM+MySQL人事系统设计与权限事务分页排错实践 简介企业人事信息管理系统毕业设计论文面向计算机相关专业需要完成课程设计、毕业设计或论文选题的学生也适合希望参考SSM项目文档结构与写法的开发者。文档围绕用户基础数据、薪资、福利、培训、任务管理及公告发布等模块展开结合Spring依赖注入、SpringMVC请求处理、MyBatis持久层操作与MySQL数据库说明系统分析、技术选型、功能设计以及权限控制、数据备份等安全考量并保留摘要、目录、相关技术、系统分析等论文常见章节。资源包共1个doc文件约1.19MB以论文正文为主体并呈现B/S结构、MySQL特性与可行性分析等内容便于读者对照人事管理系统的模块划分、数据库选型与整体实现思路。已有84人学习可用于选题参考、论文框架对照、功能梳理或答辩准备时查阅尤其适合初次撰写管理信息系统类论文的读者。1. 拿到一套人事系统先看的不是表多不多很多人第一次接触企业人事信息管理系统第一反应是数表用户表、薪资表、福利表、培训表、公告表、留言表……表越多越觉得复杂越不敢下手。真正跑起来之后才会发现容易出问题的地方从来不是表的数量而是三件事没处理好谁能改这条数据、这条数据当前处于什么状态、这条数据是什么时候被写进来的。一套基于 SSM 框架、跑在 B/S 结构上、数据落在 MySQL 里的企业人事信息管理系统真正的价值不在于把增删改查界面堆满而在于把上面三件事收敛成一条可追踪的链路——请求从浏览器发出穿过 SpringMVC 的 DispatcherServlet、Controller、Service、MyBatis 的 Mapper最终落到某一张表上每一环都能被拦截、被记录、被复现。下面顺着这条链路往下拆看这些看起来很“论文”的设计到底在解决什么。2. SSM 的四道门一次人事查询请求到底穿过了什么2.1 B/S 结构把状态推给了谁浏览器在这个系统里只做两件事把用户填的表单收集成一个 HTTP 请求把服务器返回的内容渲染成页面。所有判断——这条培训记录属于哪个员工、这条薪资还能不能改、当前账号是不是管理员——全部留在服务器端处理。论文里那句“前端部分只用处理少量的事务逻辑”说的就是这个分工。这种分工带来的好处很直接员工分散在不同办公地点客户端装不装软件无所谓升级只改服务器第二天所有人打开浏览器就是新版本。代价也很直接HTTP 是无状态协议每个请求对服务器来说都是陌生面孔服务器必须自己想办法“记住”上一次登录的是谁。An 这套记忆机制就是 Session 加 Cookie 加拦截器后面 4.2 会专门拆。一次请求在容器里大致走这么一条路Tomcat 收到连接按 URL 找到对应的 Web 应用上下文DispatcherServlet 拿请求路径去 HandlerMapping 里查找到匹配的 Controller 方法执行拦截器链的preHandle这一步是权限判断的主要落点HandlerAdapter 把请求参数绑定成方法入参调用 ControllerController 调 ServiceService 调 Mapper 接口Mapper 是接口没有实现类真正的执行由 SqlSession 完成最后由 JDBC 打到 MySQL返回结果按配置决定走视图解析还是直接序列化成 JSON。这条链路上每一层的职责是固定的Controller 只负责收参数、验参数、选视图不做业务判断Service 承载业务规则和事务边界Mapper 只描述 SQL。新人最容易犯的错是把业务判断写进 Controller结果换一个入口比如加一个导出接口就得把同一段逻辑再抄一遍。2.2 Spring 容器与 SqlSession谁持有谁谁负责关Spring 的 IoC 容器在启动时扫描注解把 Controller、Service 实例化成 Bean按类型或名称注入依赖。写Autowired private FuliService fuliService;的时候容器负责把实现类塞进来代码里不需要new。这一层解决的耦合问题不用多说真正值得拆的是 Mapper 这一环。Mapper 是一个接口没有实现类凭什么能被调用靠的是 MapperScannerConfigurer 在启动时扫描指定包下的接口为每个接口生成一个 JDK 动态代理对象注册成 Bean。调用fuliMapper.selectFuliPage(...)时实际进入的是代理的invoke方法代理从 SqlSessionTemplate 拿到当前线程绑定的 SqlSession再交给 Executor 去拼 SQL、设置参数、执行、映射结果集。SqlSessionTemplate 本身是线程安全的它对 SqlSession 做了一层包装真正的 SqlSession 不是线程安全的所以由它来管开关。事务这一段是理解 SSM 的关键。DataSourceTransactionManager在进入被Transactional标注的方法时从数据源借一个 Connection关掉自动提交把它绑到当前线程的 ThreadLocal 上。同一个线程里后续所有 Mapper 调用都会复用这个 Connection方法正常返回就 commit抛异常就 rollback。这就是为什么“发一次薪资要同时写薪资记录和发放流水”这种操作必须放在同一个 Service 方法里——跨方法、跨线程都拿不到同一个连接事务就断在没有明显提示的地方。配一段实际会写到的查询和事务代码!-- FuliMapper.xml按部门分页查福利只取列表页需要的列 -- select idselectFuliPage resultTypecom.demo.entity.Fuli SELECT f.id, f.fuli_name, f.bumen_types, d.index_name AS bumenName, f.create_time FROM fuli f LEFT JOIN dic d ON d.code_index f.bumen_types AND d.dic_code bumen where if testbumenTypes ! nullAND f.bumen_types #{bumenTypes}/if if testkeyword ! null and keyword ! AND f.fuli_name LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY f.id DESC LIMIT #{offset}, #{pageSize} /select这段 XML 里有三个机制点值得说明。#{}是预编译占位符最终走PreparedStatement.setXxx参数不会进入 SQL 文本天然挡掉注入如果换成${}就是字符串拼接用户输入什么就拼进什么等于把 SQL 语句的控制权交出去。where标签会自动处理第一个多余的AND并且在整个条件都为空时不生成WHERE关键字省掉手写WHERE 11的土办法。LIMIT #{offset}, #{pageSize}里两个参数分别是偏移量和条数页码从 1 开始时偏移量要减一次再乘这个换算写错就是“第一页数据永远重复出现”的经典来源。Service public class FuliServiceImpl implements FuliService { Autowired private FuliMapper fuliMapper; // 只读事务告诉数据库这次不写部分场景下能拿到一致性快照减少锁竞争 Transactional(readOnly true) public PageResultFuli page(FuliQuery q, int pageNo, int pageSize) { int offset (pageNo - 1) * pageSize; // 页码换算成偏移量pageNo 从 1 开始 long total fuliMapper.countByCondition(q); // count 与 list 必须共用同一套 where 条件 ListFuli rows total 0 ? Collections.emptyList() : fuliMapper.selectFuliPage(q, offset, pageSize); return new PageResult(total, rows); } }countByCondition和selectFuliPage共用同一个查询条件对象这是分页正确性的前提。很多项目里 count 语句是单独手写的加了新筛选条件只改了 list 忘了改 count前端翻到最后一页就会出现“总数 100 条但每页 10 条只翻到第 5 页就没数据”的诡异现象。逻辑里对total 0做短路也是为了避免在空结果上多打一次数据库。还有一个坑在Transactional本身它靠代理生效同一个类里方法 A 直接调方法 BB 上的注解不会起作用因为这次调用没经过代理对象。人事系统里“批量导入员工”常常在 Service 内部调自己的单条插入方法如果单条方法上写了事务批量的没写中途失败就是一堆残缺数据。2.3 MyBatis 取代 Hibernate 的真实原因论文对这件事只给了一句“更灵活性能也比 Hibernate 更稳定”往下拆其实有三个具体理由。第一是 SQL 的可控性。人事系统的高频查询大多带多个可选条件按部门、按类型、按时间区间、按姓名模糊还经常要做“按部门统计人数”“按月统计发放金额”这类聚合。MyBatis 允许把 SQL 原样写出来索引怎么走、join 顺序如何开发者心里有数。Hibernate 的 HQL 在生成复杂聚合语句时很难精确控制调优要靠日志反推中间隔了一层黑匣子。第二是大字段的处理。看表结构会发现cultivate_content、gonggao_content、liuyan_content这些字段存的都是整段文本。列表页只需要标题和时间不需要正文。MyBatis 的 select 可以精确指定列正文不参与本次查询全字段自动映射的框架则容易把整行数据都拉进内存几十条记录看不出来几千条就会让列表页明显变慢。第三是学习曲线与可调试性。SQL 写在 XML 里出了问题直接看日志里的完整语句拿去过一遍 EXPLAIN 就知道瓶颈在哪。代价同样明显而且是必须承认的SQL 没有编译期检查字段改名之后要全局搜索 XML拼错一个列名只有运行到那一行才报错。这也是第 5 章要专门列一条排查方法的原因。3. 表结构为什么人事系统的表长成「字典 业务表」这个样子3.1 E-R 图里被裁掉的实体才是设计的关键把系统 E-R 图摊开看管理员是一个中心往下挂着用户、培训、薪资、福利、公告、任务、留言这些实体关系线几乎都是“管理”。这种画法看起来平均实际信息量在那些没有画出来的部分为什么薪资和福利是两张表而不是一张为什么培训表上单独挂了一个yonghu_id。培训表挂yonghu_id是因为培训记录在业务上是“某个员工的培训”一条记录归属一个人。薪资表通常是“某次发放对应某个人某个月”福利表则是“某个部门享受某项福利”所以福利表上放的是bumen_types而不是用户 id。这三张表的归属维度不同硬合成一张“员工业务表”字段就会大量为空查询时还得靠类型标志位区分索引也用不上。外键约束的处理是另一个取舍点。E-R 图上画了实体关系但真正建表时业务表之间往往不加物理外键只在应用层做校验。原因很实际外键会在插入和删除时加额外的锁检查删除一个部门之前必须先确认没有任何业务表引用它这在人事系统的日常操作里非常碍事。代价是脏数据只能靠代码保证所以会在删除部门前先查一次该部门下是否还有员工这段代码必须写在 Service 层而不是 Controller 层否则换个入口就绕过去了。3.2 字典表把枚举从代码搬进数据库的代价字典表的结构值得单独拆dic_code、dic_name、code_index、index_name、super_id。这套设计的核心是把“类型”从 Java 里的枚举搬到数据库的行里。公告类型、留言类型、部门类型、培训类型在业务表里都存一个整型码值展示的时候去字典表换名字。CREATE TABLE dic ( id INT NOT NULL AUTO_INCREMENT, dic_code VARCHAR(32) NOT NULL COMMENT 分组标识如 bumen/gonggao/liuyan, dic_name VARCHAR(64) DEFAULT NULL COMMENT 分组中文名, code_index INT NOT NULL COMMENT 码值业务表存的就是它, index_name VARCHAR(64) DEFAULT NULL COMMENT 码值对应的展示名, super_id INT DEFAULT 0 COMMENT 父级码值 id0 表示顶级支持两级分类, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_dic (dic_code, code_index) -- 同一分组内码值不允许重复 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;唯一索引uk_dic是这张表的关键约束它保证“同一个分组下码值唯一”否则同一个code_index出现两行不同的index_name界面展示就会在不同页面之间跳变查起来非常费劲。这么设计的好处是运营动作不需要发版公司新设一个“技能培训”类型往字典表插一行就能在页面上选到不用改代码、不用重新打包部署。super_id支持两级分类部门下面可以挂子部门公告类型下面可以挂子类型一次结构设计覆盖了多个维度的需求。代价有三个。一是每次渲染列表都要多做一次关联查询数据量大的时候需要把整个字典表加载进缓存缓存失效策略又变成新的问题。二是码值和含义的对应关系只存在于数据库里代码里if (type 3)这种判断一旦写下去读代码的人必须去查数据库才知道 3 是什么所以规范做法是在 Java 侧定义常量类做映射。三是删除字典项没有约束保护——如果某个码值已经被业务表引用直接删掉字典行会让历史数据的类型变成一串孤零零的数字界面上显示空白。所以字典项只能停用不能删除这是设计之初就该定下来的规则。3.3 create_time 与 insert_time两个时间字段不是冗余培训表里有on_time、down_time、create_time公告表和留言表里有insert_time和create_time。新手很容易觉得这是重复建设随手删掉一个后面补数据的时候就出问题了。create_time是系统时间记录这条行在数据库里被写进去的那一刻通常由程序填或数据库默认值填。insert_time是业务时间指公告实际发布的时间、留言实际提交的时间。两者在正常流程里几乎一致但在两种场景下会分叉一是补录历史数据把上个月的公告录进系统create_time是今天insert_time应该是上个月那一天二是预发布公告定好明天早上九点生效create_time是录入时间insert_time是生效时间。如果只保留一个字段列表按时间倒序排列时补录的历史公告会全部挤在最前面用户看到的就是一堆“上个月的公告排在本周公告之上”的怪现象。培训表的on_time/down_time更明显它们描述的是一个区间和“这行数据什么时候被写进来”完全是两回事用create_time排序培训计划毫无意义。字段类型也值得提一句。on_time、down_time用Date只存日期create_time用DATETIME存到秒这是有意区分培训周期按天算多出来的时分秒没有业务含义反而会让“同一天开始的培训”在排序时忽前忽后。这种细节在论文里通常不会写明但它是判断一份表设计是否真的想过业务的标准之一。4. 登录与权限一次密码校验背后藏着几层判断4.1 账号和密码分开提示是体验还是漏洞登录流程图里画得很清楚先判断账号是否存在不存在就提示“账号错误”账号存在再判断密码不匹配提示“密码错误”。这个设计在体验上是加分的用户不用猜是自己记错了用户名还是记错了密码。从安全角度看它把系统的用户名单暴露了出去攻击者拿一个账号名去试返回“账号错误”说明这个账号不存在返回“密码错误”说明账号真实存在剩下的事情就是慢慢撞密码。这种做法在公网系统上属于明确不该出现的模式正确做法是统一提示“账号或密码错误”并且在连续失败若干次之后锁定一段时间。但这套系统是企业内部使用的人事管理系统账号由管理员统一开通员工名单本身在内部就是公开信息账号枚举带来的风险远小于“员工打电话问 IT 自己用户名是不是拼错了”的沟通成本。所以这里的选择是可以理解的前提是必须配合其他措施登录失败次数计数、验证码、密码强度校验。论文里提到“程序在实现基本要求功能时也为数据信息面临的安全问题提供了一些实用的解决方案”落到代码上通常就是这几项。校验逻辑本身也有个容易忽略的点密码不应该明文存。规范做法是存哈希值登录时把用户输入哈希一遍再比对。如果为了“管理员能帮员工找回密码”而存明文或者可逆加密一旦数据库被拿走所有员工的密码就全泄了而且很多人会在多个系统复用同一个密码。人事系统里这条尤其敏感因为员工信息本身就包含身份证号、手机号这类内容。// 登录校验先取用户再比对哈希两步都不做“账号是否存在”的分支返回 public LoginResult login(String username, String rawPassword) { User user userMapper.selectByUsername(username); // 账号不存在时也走一次哈希计算让两条分支耗时接近避免通过响应时间反推账号是否存在 String hash user null ? passwordEncoder.encode(rawPassword) : user.getPassword(); if (user null || !passwordEncoder.matches(rawPassword, hash)) { loginFailCounter.increment(username); // 失败计数超过阈值锁定 return LoginResult.fail(账号或密码错误); } loginFailCounter.clear(username); return LoginResult.ok(user.getId(), user.getRoleType()); }代码里那句“账号不存在时也走一次哈希”是防止时序攻击的常见做法。如果账号不存在直接返回整个请求可能只要几毫秒账号存在时要算一次哈希可能要几十毫秒。攻击者不需要看提示文字只看响应时间就能判断账号是否存在。对内网系统来说这属于过度设计但它说明了一个原则安全措施要在接口层面统一而不是靠提示语遮掩。4.2 Session 与拦截器无状态协议怎么记住「你是谁」登录成功之后服务器把用户 id、角色、昵称写进 HttpSession容器会自动下发一个 JSESSIONID 的 Cookie 给浏览器后续请求带上这个 Cookie容器就能找回对应的 Session 对象。这是 B/S 结构下最省事的会话方案缺点是 Session 存在单机内存里做集群需要额外配置共享存储。拦截器是这套机制的执行者。它挂在 DispatcherServlet 之后、Controller 之前能在业务代码执行前统一做判断public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { // false不存在时不新建 Session避免给未登录的请求凭空造一堆会话对象 HttpSession session req.getSession(false); Object userId (session null) ? null : session.getAttribute(userId); if (userId null) { resp.sendRedirect(req.getContextPath() /login); return false; // 返回 false 直接中断Controller 不会被执行 } return true; } }注册的时候要配白名单把登录页、登录提交接口、静态资源排除掉否则会出现“访问登录页被重定向到登录页”的死循环Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) // 默认全拦 .excludePathPatterns( // 再逐个放行 /login, /doLogin, /static/**, /css/**, /js/**, /img/**); } }addPathPatterns(/**)加excludePathPatterns的顺序不能反这是“默认拒绝”的思路新模式默认是安全的需要显式声明才能被访问。反过来写“只拦需要登录的路径”新增一个页面忘了加进去就是一个未授权入口。还有一个实际的坑Session 默认超时通常是 30 分钟用户在录入一份很长的培训内容时超时了点保存直接跳回登录页前面写的内容全丢。处理方式有两种一是提交前用异步请求探一下会话是否还有效失效时给提示而不是直接跳转二是把表单内容暂存到本地登录后恢复。第一种更简单也更能让用户接受。4.3 越权防护到底该压在哪一层权限这件事最容易做半套。把左侧菜单按角色隐藏掉看起来不同角色看到的入口不一样实际上用户只要手动敲一个 URL照样能打开那个页面。菜单隐藏是交互设计不是权限控制。真正的控制点有两层。第一层在拦截器判断当前会话的角色有没有访问这个路径的资格。这一层拦的是“功能级”越权比如普通员工不该进薪资管理页面。第二层在 Service判断这条数据是不是属于当前用户。这一层拦的是“数据级”越权比如员工 A 修改留言请求里带的留言 id 是员工 B 的功能上他有留言权限但这条数据不该让他改。// 数据归属校验必须落在 ServiceController 只负责把当前登录用户传进来 Transactional public void updateLiuyan(Integer liuyanId, Integer currentUserId, String content) { Liuyan old liuyanMapper.selectById(liuyanId); if (old null) { throw new BizException(留言不存在); } // 核心一句不是自己的留言一律拒绝与角色无关管理员走另一个入口 if (!Objects.equals(old.getYonghuId(), currentUserId)) { throw new BizException(无权修改他人的留言); } liuyanMapper.updateContent(liuyanId, content); }这段代码里把校验放在 Service 而不是 Controller是因为同一个业务动作可能有多个入口页面提交、批量操作、将来的开放接口。校验写在 Controller新增一个入口就得记得再写一遍漏一次就是一个漏洞。写在 Service所有入口都会经过。还有一种更隐蔽的越权删除操作只传 id不校验归属也不校验存在性。前端把 id 传到页面上用户改一下数字就能删掉别人的数据。所以删除接口的入参应该是最小集合且服务端必须重新查一次做校验不能相信前端传来的任何状态字段。5. 避坑与排查人事系统最容易翻车的几个位置5.1 数据层的三个高频故障现象一明明输入的是中文存进数据库变成一串问号。原因基本锁定在字符集链路的某一环断了数据库的库或表不是utf8mb4或者 JDBC 连接串没指定characterEncodingutf8或者 Tomcat 的请求体编码没设三者只要有一个环节按 ISO-8859-1 解释中文就会在那一环被替换掉。排查办法是从末端往前查先在数据库里SELECT看是不是已经坏了如果库里是好的、页面显示是坏的问题在响应编码如果库里就是问号问题在写入链路。解决方式是库表统一utf8mb4utf8存不下 emoji 和部分生僻字员工姓名里真的有连接串带上useUnicodetruecharacterEncodingutf8并在web.xml里配CharacterEncodingFilter且forceEncodingtrue。现象二新增一条培训记录报 400日志里说字符串不能转成 Date。前端传的是2024-05-01这样的字符串后端实体里的字段是Date类型SpringMVC 默认的转换器不认识这个格式。原因是没有告诉框架用哪种格式解析。解决方式有两种一是在字段上加DateTimeFormat(pattern yyyy-MM-dd)二是在全局配置里注册一个统一格式的转换器。顺带提一个更隐蔽的问题即使解析成功页面上显示的时间也可能差 8 小时那是 MySQL 连接串没配serverTimezoneAsia/Shanghai驱动按 UTC 解释了这个时间。这两个问题经常一起出现排查时先看日志有没有转换异常再看时区配置。现象三分页控件显示总数 200但翻到第 12 页就没数据了。这是 count 查询和 list 查询条件不一致的典型症状。count 用的是简化版的 wherelist 用的是完整条件加 joinjoin 一旦是一对多就会把行数放大两种口径下的总数和实际条数对不上。排查方式是打开 MyBatis 的 SQL 日志把 count 和 list 两条语句都打出来对比 where 子句。解决办法是把条件抽成一个sql片段两条语句都include它从结构上保证一致而不是靠人肉同步。5.2 交互与部署上的两个隐性坑现象四删除按钮点快了两下同一批数据删了两条或者报“记录不存在”。原因有两层。一是删除请求走了 GET浏览器会预取、会缓存双击就是两次独立请求二是按钮没有防重复提交第一次请求还没返回第二次又发出去了。更深一层的问题是物理删除删掉就没了误删只能从备份恢复对人事数据来说代价太大。解决方式是三步删除改成 POST 并加二次确认弹窗提交后立刻禁用按钮直到响应返回把物理删除改成逻辑删除加一个is_deleted标志位列表查询统一带is_deleted 0相当于给自己留了后悔药。逻辑删除的代价是所有查询都要记得加这个条件最好在 Mapper 层统一处理不要靠每个开发者自觉。现象五本地跑得好好的部署到服务器上页面样式全丢、接口 404。先看路径。本地开发时应用通常跑在根路径/部署到容器里可能变成/hrms所有绝对路径/css/main.css就都指向了错误位置。解决方式是用req.getContextPath()或者页面里的${pageContext.request.contextPath}拼前缀不要写死。再看大小写Linux 文件系统区分大小写Windows 不区分本地UserList.jsp引用成userlist.jsp能跑服务器上直接 404。最后看静态资源有没有被拦截器拦住——如果/static/**忘了加进白名单浏览器请求 css 会被重定向到登录页返回的是一段 HTML样式自然不生效浏览器控制台里会看到“资源被解释为样式表但传输的 MIME 类型是 text/html”这类提示。6. 设计取舍一套「够用」的架构难在哪把 SSM MySQL 这套组合放到今天的技术环境里看确实不算新。但拿它做一套企业内部的人事系统很多取舍反而是想清楚之后的结果而不是能力不足的妥协。字典表就是这个思路的典型。用 Java 枚举存类型编译期就能查错重构也安全但每加一个类型都得改代码、重新打包、重新部署。用字典表运营动作变成一条 insert代价是类型含义漂移到了数据库里代码可读性下降还要额外管缓存。判断标准其实很朴素这个类型的变更频率是否高到值得为它牺牲类型安全。公告类型、部门类型这类会随组织调整而变化的东西放进数据库是合理的订单状态这种和业务逻辑强绑定的东西放进枚举才是对的。物理删除和逻辑删除也是一样。逻辑删除保住了数据代价是每条查询都得带过滤条件遗忘一次就会把已删除的记录重新显示出来。人事系统的薪资、培训记录往往要留档追溯所以逻辑删除值得而像临时导入的中间数据物理删除反而更干净。账号密码分开提示这个选择更能说明问题同一个设计在内网系统里是体验优化在公网系统里是安全漏洞。判断一个设计好不好脱离部署环境和使用人群是没有意义的。一份毕业设计论文里写“为数据信息面临的安全问题提供了一些实用的解决方案”值不值得信就看它有没有把这些前提讲清楚。验证一套这样的系统是否真的能 work我的习惯是走三条路径用不同角色分别登录手动拼几个不该有权限的 URL看能不能进去造一批跨部门的数据试试能不能改到别人的记录把数据库连接池配到很小同时点十几个查询看会不会有连接泄漏。这三条走完架构上的问题基本就暴露了。从那以后我每次接手一份别人写的毕设代码都强制先把登录、越权、分页这三条链路跑一遍再去看功能列表有多长。功能多不代表结构好链路通才说明设计是真的想过。希望帮到你。本文还有配套的精品资源点击获取