☰
基于SpringBoot+JWT的高校教师电子名片系统设计与实现
2026/10/13 13:51:07 网站建设 项目流程

1. 项目整体设计思路拆解

1.1 这个系统到底在解决什么问题

先聊点实际的。高校里老师的名片信息,向来是个"看着简单、管起来麻烦"的东西。你打开任何一个高校的官网,教师简介页五花八门——有的挂在学院首页,有的躺在教务处系统里,有的干脆就是一张早年扫描的Word截图。更别提老师们评上职称、发了新论文、换了研究方向之后,这些信息往往几个月都更新不了一次。

我做这个高校教师电子名片系统之前,先花了两天去摸了一下真实需求。走访了几位一线教师和院系教务员,反馈高度集中:第一,老师需要一个自己能维护的信息展示页,而不是每次更新都得找管理员改代码;第二,名片信息要有分类,不能眉毛胡子一把抓,基本信息、研究方向、论文著作、授课情况得分开;第三,浏览端要方便,别人想看某位老师的联系方式,两步之内必须找到。这三个痛点,直接决定了这个系统的主角不是"名片展示",而是"名片管理"。

所以这个项目的定位很明确:一个基于SpringBoot的管理后台 + 一个对外展示的电子名片前台,教师自己登录后台维护资料,访客在前台按院系、按姓名检索查看。整体权限模型只有三种角色——管理员、教师、访客,没有花哨的RBAC多层嵌套,核心逻辑干净利落。

1.2 为什么选SpringBoot做底座

这个项目选型的时候,我也纠结过是不是要用更轻的Node.js或者更"现代"的框架,但综合评估下来,SpringBoot仍是这类教务类系统的最优解。理由不复杂。

第一,稳定性和生态成熟度没得挑。高校系统最忌讳的就是花里胡哨的框架三天两头出兼容问题,SpringBoot的自动配置机制能让一个Web项目在几分钟内跑起来,而且后续接入权限框架、持久层框架、文件存储都有非常成熟的标准姿势。对于经常需要横向扩展功能的教务系统来说,这一点省心太多。

第二是团队协作的熟悉度。这类项目不是一个人写完就扔,后面很可能有同学接手、有导师验收、有维护人员跟进的。SpringBoot + MyBatis Plus这套组合在国内高校的项目里普及率极高,随便拉一个学过Java Web的人都能快速上手,代码的可读性和交接成本远远优于某些小众框架。

第三,SpringBoot的约定优于配置极大降低了踩坑概率。它默认内嵌Tomcat,打成一个jar包就能跑,不需要单独部署Web服务器,这对部署环境有限的高校机房来说非常友好。实测下来,把一个打包好的jar扔到服务器上,只要装了JDK 8及以上,java -jar一行命令就能启动,省掉了配置Tomcat的种种烦心事。

1.3 功能模块的边界划分

功能模块我在原型阶段就改了三次,最后敲定的结构是这样的:

  • 前台展示端:教师名片列表(支持按院系筛选、关键词搜索)、教师详情页(名片完整信息)、院系导航。
  • 教师端:登录后可编辑个人基础信息、维护研究方向、管理论文著作列表、更新授课信息、上传头像。
  • 管理员端:教师账号管理(禁用/启用)、院系信息维护、全局名片审核(可选开启)、数据统计概览。
  • 公共能力:统一登录认证、文件上传管理、操作日志记录。

这里面最核心的设计决策是:教师前台可搜可看,但所有信息修改必须通过后台认证进行,前台页面不开放任何写操作。这个边界划清楚之后,整个系统的安全模型和业务逻辑都跟着清晰了,不会出现"前台不小心暴露了修改接口"这种低级事故。

2. 核心细节解析与实操要点

2.1 数据库设计的五个关键表

这个系统的数据库表我设计了六张,但核心业务表是五张,还有一个是日志表。先把五张业务表的职责摆出来:

表名职责说明关键字段
teacher教师主表,存储登录账号和名片基础信息id, name, title, department_id, phone, email, photo, profile, status
department院系列表id, name, sort_order
course授课信息表,一名教师多门课程id, teacher_id, course_name, semester
paper论文著作表id, teacher_id, title, journal, year, link
operation_log操作日志id, user_id, action, detail, create_time

这里有个经验要分享:teacher表和用户认证信息是合并在一张表里的,没有单独拆出sys_user表。有人可能会说这样不优雅,但实际考虑是——这个系统的用户就是教师本人,不存在"一个教师账号绑定多个身份"的情况,拆出去反而多一次联表查询。项目里的每一个设计决定都要基于真实场景,而不是为了所谓的"规范"而规范。

字段类型上有一个坑提醒一下:手机号、工号这类字段,一定要用varchar而不是int或bigint。原因很简单,你永远不知道未来会不会有人需要在号码前面加个前缀,或者出现类似"转分机"这样的文本描述。用整型存号码,改一次表结构够你折腾半天的。

2.2 登录认证方案怎么选

登录这块,我在JWT和传统Session方案之间权衡了一阵子。最终选了JWT,原因有三:一是前台后台分离的场景下,JWT天然无状态,后端不需要维护会话存储;二是在后续如果有移动端接入需求,JWT可以无缝复用;三是SpringBoot生态里整合JWT的成熟度非常高。

不过用JWT要特别注意两个细节。

第一是密钥管理。JWT的密钥在代码里硬编码是最作死的行为,我见过太多人把secretKey直接写在配置类里然后提交到代码仓库,等于把整个系统的认证体系拱手送人。正确做法是放在application.yml的独立配置项中,部署时通过环境变量注入,项目里保留一个占位默认值但不用于生产环境。

第二是Token有效期设置。教师的登录态不宜太长,建议AccessToken设置为2到4小时,否则教师离职或者账号被注销后,旧Token还能在有效期内继续访问接口,这是一个容易被忽略的安全漏洞。我项目里用了双Token机制——AccessToken短时效、RefreshToken长时效,前端监测到AccessToken过期后用RefreshToken静默换新,教师账号被管理员禁用时,通过Redis存一个拒绝名单,拦住RefreshToken的续期请求。

2.3 权限控制的三层防线

这个项目里,权限控制如果不做好,后面上线就是灾难。我做了三层:

第一层是登录拦截器。所有/admin/**路径的请求必须携带有效Token,否则直接返回401。

第二层是角色校验。虽然只有三个角色,但教师和管理员能访问的接口边界必须清晰,管理员可以访问所有接口,教师只能访问自己的数据和公共数据,通过Spring AOP做一个自定义注解@RequireRole,在接口上加注解实现声明式权限控制。

第三层是数据级权限。这一步最容易漏,教师登录后修改名片信息,后端接口里必须校验操作资源的teacher_id是否等于登录用户的ID,不能只靠前端隐藏按钮来控制。我在更新接口里显式加了一层校验,防止有人绕过前端直接调接口改别人的资料。

这三层下来,整个系统的权限体系才算闭环。很多课程设计项目的通病就是只做了第一层,接口一旦被手撕,数据就裸奔了。

3. 实操过程与核心功能实现

3.1 从零搭建项目骨架

我用Spring Initializr的方式初始化工程,选择Spring Boot 2.7.x版本(搭配JDK 8,兼容性最好),依赖勾了Spring Web、MyBatis Framework、MySQL Driver、Lombok、Validation。

这里说下为什么不用Spring Boot 3.x。虽然3.x已经出来很久了,但它基于Jakarta EE,一些老牌的兼容性组件(特别是某些MyBatis周边工具)在迁移初期有坑。对于课程设计或者中小型生产项目,稳定压倒一切,2.7.x是经过大范围验证的一个版本。

项目结构我按常见的分层来组织:

src/main/java ├── controller/ # 控制层 ├── service/ # 业务逻辑层 ├── mapper/ # 数据访问层 ├── entity/ # 实体类 ├── config/ # 配置类(WebMvc、拦截器注册等) ├── common/ # 通用返回结果、异常处理、工具类 └── annotation/ # 自定义注解

一个比较重要的小细节:通用返回结果我用Result<T>统一封装,包含code、message、data三个字段。这样做的好处是前端可以对所有接口做统一处理,不用每个接口单独判断数据结构。异常处理也是全局的,用@RestControllerAdvice捕获业务异常、参数校验异常、兜底异常,并统一转成规范格式返回。这块看起来不起眼,但能省掉前端一堆冗余的错误处理代码。

3.2 实体类与数据库映射

以教师实体为例,核心代码大致长这样:

@Data @TableName("teacher") public class Teacher { @TableId(type = IdType.AUTO) private Integer id; private String username; private String password; private String name; private String title; private String phone; private String email; private String photo; @TableField("department_id") private Integer departmentId; private String profile; private Integer status; @TableField("create_time") private LocalDateTime createTime; @TableField("update_time") private LocalDateTime updateTime; }

这里用MyBatis Plus的@TableName和@TableField来映射数据库字段,密码字段我存的是BCrypt加密后的密文,而不是明文。加密这块是用spring-security-crypto单独引入的BCryptPasswordEncoder,只用了它的加密工具类,并没有引入完整的Spring Security——杀鸡不用牛刀,身份认证用JWT自己写,密码加密用工具类,两个东西刚好互补。

有一个细节容易踩坑:LocalDateTime和MySQL的datetime类型的映射,一定要确认驱动版本够新,否则会出现时间字段查出来是java.sql.Timestamp的诡异情况。我在项目里统一用LocalDateTime,配合MyBatis Plus的自动填充功能,插入和更新时自动写入create_time和update_time,避免每个Mapper方法里手动维护时间字段。

3.3 登录接口与JWT的工具类实现

登录接口的核心逻辑不复杂,但几个细节值得展开说。

@PostMapping("/login") public Result<LoginVO> login(@RequestBody LoginDTO loginDTO) { Teacher teacher = teacherMapper.selectOne( new LambdaQueryWrapper<Teacher>() .eq(Teacher::getUsername, loginDTO.getUsername())); if (teacher == null) { throw new BizException("用户名或密码错误"); } if (teacher.getStatus() == 0) { throw new BizException("账号已被禁用,请联系管理员"); } if (!passwordEncoder.matches(loginDTO.getPassword(), teacher.getPassword())) { throw new BizException("用户名或密码错误"); } String accessToken = jwtUtil.generateToken(teacher.getId(), teacher.getRole(), JwtProperties.ACCESS_TOKEN_EXPIRE); String refreshToken = jwtUtil.generateToken(teacher.getId(), teacher.getRole(), JwtProperties.REFRESH_TOKEN_EXPIRE); return Result.success(new LoginVO(accessToken, refreshToken, teacher)); }

注意几个设计点。

一是登录失败提示故意区分了"用户不存在"和"密码错误",但提示文案统一为"用户名或密码错误"。这是为了防止恶意用户通过接口探测系统里哪些用户名是真实存在的。细节虽小,但信息泄露往往就是从这些地方开始的。

二是用户被禁用status == 0时,要在密码校验之前就拦截。否则一个被禁用的账号还能用正确密码登录成功,只不过后续接口被拦,这种体验很别扭,管理员禁用账号的预期是"这个人根本进不去系统"。

三是Token里我塞了teacher.getId()和role,但绝对不塞密码、手机号这类敏感信息。JWT本身是base64编码的,等于明文,谁拿到Token都能解出来看,所以里面坚决不放敏感数据。

3.4 文件上传与头像处理

教师名片最直观的视觉元素就是头像,但这个功能做起来远比想象中繁琐。我在实现文件上传时,遇到的第一个问题就是"往哪里存"。本地存储、云OSS、FastDFS我都考虑过,最终选了本地存储 + Nginx静态代理的方案。

原因很现实:高校部署环境往往在内网,云服务不可控;FastDFS又重了,光部署一个tracker和storage就够写一篇博客了。本地存储的做法是:文件上传接口接收MultipartFile,写到服务器上一个固定的目录(比如/data/profile/),然后返回一个访问URL。Nginx配置里将这个路径映射为静态资源目录,图片就能通过http://服务器IP/上传的相对路径直接访问。

上传代码的核心逻辑用一个临时示例来说明:

@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { throw new BizException("上传文件不能为空"); } long maxSize = 5 * 1024 * 1024; // 5MB if (file.getSize() > maxSize) { throw new BizException("图片大小不能超过5MB"); } String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); List<String> allowedExts = Arrays.asList(".jpg", ".jpeg", ".png", ".gif"); if (!allowedExts.contains(ext.toLowerCase())) { throw new BizException("仅支持jpg、png、gif格式"); } String filename = UUID.randomUUID().toString().replace("-", "") + ext; File dest = new File(uploadPath + filename); file.transferTo(dest); return Result.success("/files/" + filename); }

这里有个血泪教训:文件名绝对不能直接用原始文件名。第一是中文文件名在部分服务器上会产生编码问题,第二是重名文件会互相覆盖,第三是恶意文件名可能包含路径穿越字符。用UUID重命名一劳永逸,但要注意原始文件名的扩展名必须保留,否则浏览器无法正确渲染图片。

图片大小限制我设置了5MB,头像这种场景其实1MB以内就够用,后续可以在前端压缩。后端一定要做二次校验,因为前端限制可以被绕过,一个恶意请求传个几百MB的文件进来,分分钟把磁盘塞满。

3.5 教师名片查询接口

前台名片列表的查询接口,我设计的是支持分页、院系筛选、关键词搜索三个维度。直接用MyBatis Plus的分页插件实现:

public PageResult<TeacherVO> getTeacherList(int page, int size, Integer deptId, String keyword) { Page<Teacher> pageParam = new Page<>(page, size); LambdaQueryWrapper<Teacher> wrapper = new LambdaQueryWrapper<>(); if (deptId != null) { wrapper.eq(Teacher::getDepartmentId, deptId); } if (StringUtils.hasText(keyword)) { wrapper.and(w -> w.like(Teacher::getName, keyword) .or().like(Teacher::getTitle, keyword) .or().like(Teacher::getProfile, keyword)); } wrapper.eq(Teacher::getStatus, 1); // 只展示启用的教师 wrapper.orderByAsc(Teacher::getId); Page<Teacher> result = teacherMapper.selectPage(pageParam, wrapper); // 转换VO、填充部门名称等 }

查询逻辑里最关键的是status == 1这个条件——前台展示绝对不能出现已经被管理员禁用或离职教师的信息。这个条件的优先级比关键词搜索和分页都要高,写的时候务必记住。

分页参数上有一个经验:列表接口返回的total字段前端很有用,用于渲染分页组件的总页数。MyBatis Plus的分页查询需要配置PaginationInnerInterceptor才能生效,否则selectPage查出来的total永远是0,这个坑我见过不少朋友踩。

3.6 前后端联调与跨域处理

这个项目的前端我用的是Vue 3 + Element Plus,开发环境下前端跑在8080端口,后端跑在8081端口,跨域问题必然会出现。

跨域采用的方案是后端通过CORS配置类统一解决,不搞前端代理那套(虽然也能做,但多一层代理就多一个排障复杂点):

@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); } }

有一个关键细节:预检请求(OPTIONS)必须放行,而且maxAge设置为3600秒,让浏览器在1小时内不需要重复发起预检,这个数值能显著提升前端在频繁请求时的响应速度。

但必须说明的是,allowedOriginPatterns("*")加allowCredentials(true)这个组合在生产环境下并不安全,等于允许任意来源携带凭证访问。我这个是面向内网部署场景、图省事才这么写的。如果系统要暴露到公网,这里必须配置成实际的域名白名单,而不是通配符。

3.7 项目打包与生产部署

部署方案其实很简单,我习惯用Maven打成jar包:

mvn clean package -DskipTests

然后直接把jar包扔到服务器上:

nohup java -jar teacher-card-system.jar --spring.profiles.active=prod > app.log 2>&1 &

我用一个prod的profile文件来区分生产环境配置,数据库地址、上传路径、JWT密钥这些全部从application-prod.yml读取,开发环境配置保持默认,互不干扰。

这里说一个真实踩坑:SpringBoot的jar包默认不会把src/main/resources下的application-prod.yml打进去吗?会打进去。但如果你的配置文件放在jar包外部,要在启动命令里显式指定路径,否则它会去找内置的配置文件。我在部署时就忘了把外部配置的路径指对,导致连的是开发库而不是生产库,这个问题排查了一个多小时。记住,外部配置文件优先级最高,路径写错不会有任何提示,只会安安静静用错库。

4. 常见问题排查与优化实录

4.1 Jackson序列化日期格式的坑

这个坑几乎每个做SpringBoot项目的人都会遇到。LocalDateTime字段默认序列化成ISO格式的数组,前端拿到后处理起来极其痛苦。解决方式是在配置文件里加一个格式约定:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

注意time-zone必须显式设置为GMT+8,否则服务器是UTC时区时,前端看到的时间整整比北京时间少了8个小时。这个问题的隐蔽性极强,因为本地开发时通常都是东八区,一上服务器就露馅。

4.2 接口幂等性与重复提交

教师保存名片资料时,如果网络抖动导致前端重复提交,后端可能会插入两条逻辑上相同的记录。我的解决方案有两个:一是前端在点击保存后立即禁用按钮,二是后端对关键的写操作接口做幂等校验。

后端做法用的是数据库层防止重复:比如保存论文记录时,先按teacher_id + title查一遍,如果已存在同名的论文,就返回一个明确提示,而不是默默插入一条重复记录。这个校验逻辑在Service层实现,而不是靠前端控制,因为后端必须假设所有请求都可能是恶意的。

4.3 头像上传偶尔报空指针

有段时间我发现上传头像接口偶尔报NPE,最终定位到原因是MultipartFile在部分情况下会拿到一个空的CommonsMultipartFile,而代码里直接调getOriginalFilename()时某些分支返回null。解决办法很简单,在调用前判空,同时校验文件名是否包含..路径穿越字符,这种边角校验不能偷懒。

处理完这个,我把文件上传的异常提示做得更友好了:"上传失败,请确认文件未损坏或登录状态未过期",用户的期待是看到人能看懂的话,而不是一坨堆栈信息。

4.4 JWT过期后前端表现怪异

前端有时候会出现请求一个接口成功、另一个接口401的怪异现象,排查后发现是RefreshToken的并发刷新问题——两个请求同时过期,同时触发刷新,后端返回两个新Token,后者覆盖前者,导致其中一个请求使用的旧Token失效。

解决办法是前端做单飞刷新,即多个接口同时401时,只有一个请求负责刷新Token,其他请求挂起等待刷新完成后再重放。这个逻辑说起来很简单,但实现细节不少,要处理好请求队列的Promise引用,不然容易出现死锁。这块是目前项目里前端最复杂的逻辑之一,值得花时间好好写。

4.5 常见问题速查表

问题现象可能原因解决方案
前端日期显示少8小时Jackson时区未设置配置time-zone: GMT+8
分页total始终为0缺少分页拦截器注册PaginationInnerInterceptor
登录后访问接口仍401Token过期或未携带检查请求头Authorization的Bearer前缀
图片上传后无法访问路径映射错误或权限不足检查Nginx静态目录映射与目录权限
更新资料后旧信息还在前端缓存接口响应加Cache-Control: no-store
数据库连接数过多连接池配置不合理调整HikariCP初始连接数与最大连接数
界面加载慢不必要的字段直出接口只返回前端需要的字段,剔除大文本

5. 可扩展方向与二次开发建议

5.1 从电子名片延伸到学术主页

做完这个基础版之后,我最大的感受是它的扩展空间远比想象的大。电子名片只是入口,真正有价值的是它背后的学术内容沉淀。

后续可以考虑增加论文管理的高级功能——支持多作者、支持DOI自动抓取、支持论文引用次数对接、支持从主流学术站点导入。这些功能本质上都是在现有的paper表上做增强,不需要改动系统的整体架构。对于老师来说,名片能展示的内容越丰富,它被使用的频率就越高,最终可能成为老师个人的学术主页,而不仅仅是一张静态卡片。

5.2 增加消息通知与审批流程

如果学校的管理粒度比较细,可以在名片修改环节加入审批流——教师提交修改后,管理员确认再生效。这个功能在现有的角色模型上扩展起来很容易,加一张audit_record表记录修改前后快照就行。

这类审批流设计的关键点是快照机制。我做过几个带审核的系统,最怕的就是"管理员看到的修改内容"和"教师实际提交的内容"对不上。所以在提交修改时要将领修改的内容整体存一份快照JSON,管理员审核的就是这份快照,而不是实时查库。否则教师在审核期间又改了资料,管理员批准后就出现"改了不是自己看的那一版"的混乱局面。

5.3 移动端适配

教师名片的使用场景有很大一部分在手机上——别人递过来一个二维码,扫码直接跳到名片页。所以前端页面的响应式适配优先级很高。我目前的做法是Element Plus的面板栅格配合visible条件判断,在手机端隐藏次要信息列,保证核心的名片字段在小屏上不折行、不遮挡。

后续如果要体验更好,可以单独做一套H5页面,或者直接生成小程序版本。但以我个人的经验,先把现有网页版的移动端手感调好,比兴师动众做小程序要实在得多。很多项目死在功能推倒重来上,而不是功能不够多。

6. 操作日志与审计功能

6.1 日志表结构与记录策略

说实话,一开始我没打算做操作日志,觉得一个名片系统记录操作日志有点小题大做。直到有次测试环境里有个老师的资料被改成乱码,排查了半天不知道是谁干的,我才决定补上这个功能。

日志记录策略遵循一个原则:写操作必记,读操作不记。具体来说就是登录、登出、修改资料、上传图片、删除论文、管理员禁用账号、修改院系信息,这些操作一律落日志。查询行为不做记录,否则日志表很快就会膨胀,而且真正出问题时查不到有效信息。

日志表结构简单一点,记录就够用:

字段说明
id主键自增
user_id操作人ID
action操作动作编码(LOGIN、UPDATE_PROFILE、DELETE_PAPER等)
detail操作详情JSON,包含修改前后的关键字段变化
ip操作来源IP
create_time操作时间

修改资料时,我会在Service层把修改前后的快照都塞到detail字段里,JSON序列化存进去。这样出问题时,能从日志里看到"谁在什么时间把电话从A改成了B",而不是只能看到一个孤零零的"保存成功",排查效率能提升好几个量级。

6.2 AOP实现日志的优雅姿势

操作日志如果用硬编码在每个Service方法里写,侵入性太强。我用了Spring AOP + 自定义注解的方式,实现一个@Log("修改资料")的注解,在需要记录的方法上加上即可。

核心代码结构大致是这样:

@Aspect @Component public class LogAspect { @Autowired private OperationLogMapper logMapper; @Around("@annotation(log)") public Object around(ProceedingJoinPoint joinPoint, Log log) throws Throwable { long startTime = System.currentTimeMillis(); // 从上下文中获取当前登录用户 Integer userId = UserContext.getUserId(); Object result = joinPoint.proceed(); long costTime = System.currentTimeMillis() - startTime; // 异步或同步记录日志 OperationLog operationLog = new OperationLog(); operationLog.setUserId(userId); operationLog.setAction(log.value()); operationLog.setDetail(buildDetail(joinPoint)); operationLog.setCostTimeMs((int) costTime); logMapper.insert(operationLog); return result; } }

这个方案有一个要注意的细节:如果一个写操作里既有成功分支又有异常分支,日志记录必须考虑异常情况下的记录方式。我在项目中约定异常时不记录"修改成功"日志,而是在全局异常处理器里统一记录一条"操作失败"日志,这样日志里能清晰看出失败操作,方便定位是哪次尝试造成的脏数据。

AOP切面里还有一个容易踩的坑——@annotation(log)对应的方法参数名必须和注解属性对应上,如果拼写不一致,切面会静默失效且不报错。排查这类问题通常会花掉比写代码还长的时间,所以我会在每个@Log注解方法上手动观察一次切面是否真的被触发,避免上线后才发现日志表一直是空的。

7. 个人经验总结与项目复盘

从需求分析到最终落地,这个高校教师电子名片系统前前后后花了大约三周时间,其中真正写代码的时间大概一周半,其余时间都耗在需求确认、方案调整和联调排错上。这个时间比例并不异常——架构设计和踩坑排查占用的精力,本来就应该比纯码代码多出不少。

有几个体会想分享给正在做类似项目的人。

第一,不要在项目开始时追求大而全的功能清单。我最初规划里有消息公告、在线问答、课表对接,后来全部砍掉了。名片系统的本质是"信息展示 + 简单管理",加的功能越多,每多一个功能就要多一份安全保障、多一份联调测试、多一份文档描述。项目评审时看重的不是你写了多少功能,而是核心功能做得是否扎实。

第二,权限设计哪怕再简单也要花时间理清楚。三类角色听起来很容易,但"教师只能看自己的数据"这个看似简单的需求,要在后端接口层面真正落地,远不是加一个if (userId != resourceId)那么简单。接口设计时要提前想清楚哪些数据是教师的私有数据、哪些是公开数据、哪些是管理员专属数据,这个边界划分决定了整个Service层的代码结构。

第三,异常处理要系统化,不要零散地try-catch。我见过很多项目在十几个地方各自捕获异常、各自拼接错误信息,结果是相同的业务异常在接口文档里出现了三种不同的错误提示文案。全局异常处理器加自定义业务异常的方案,统一了错误码和提示语,前后端联调时沟通成本大幅下降。

最后说一点关于代码可维护性的话。这个系统我用了一个很朴素的约定:Controller层只做参数接收和结果返回,Service层只做业务逻辑,Mapper层只做数据访问。这个分层看似老生常谈,但真正执行起来需要自律——比如把数据库查询逻辑写到Controller里这种"图方便"的行为,一旦出现一次,整个项目的分层就慢慢腐烂了。

名片系统这个方向本身不算新奇,但把它做得干净、稳定、好用,依然需要大量的细节打磨。如果这个项目对你的课程设计或者实际工作有帮助,照着上面的思路走一遍,你踩的坑应该能比当年少很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询