资讯动态

基于SpringBoot+JWT的高校教师电子名片系统设计与实现

发布时间:2026/10/13 13:51:19 来源:尧图企业网站定制
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, statusdepartment院系列表id, name, sort_ordercourse授课信息表一名教师多门课程id, teacher_id, course_name, semesterpaper论文著作表id, teacher_id, title, journal, year, linkoperation_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/ # 自定义注解一个比较重要的小细节通用返回结果我用ResultT统一封装包含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 ResultLoginVO login(RequestBody LoginDTO loginDTO) { Teacher teacher teacherMapper.selectOne( new LambdaQueryWrapperTeacher() .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 ResultString 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(.)); ListString 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 PageResultTeacherVO getTeacherList(int page, int size, Integer deptId, String keyword) { PageTeacher pageParam new Page(page, size); LambdaQueryWrapperTeacher 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); PageTeacher 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.activeprod app.log 21 我用一个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: GMT8注意time-zone必须显式设置为GMT8否则服务器是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: GMT8分页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操作人IDaction操作动作编码LOGIN、UPDATE_PROFILE、DELETE_PAPER等detail操作详情JSON包含修改前后的关键字段变化ip操作来源IPcreate_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里这种图方便的行为一旦出现一次整个项目的分层就慢慢腐烂了。名片系统这个方向本身不算新奇但把它做得干净、稳定、好用依然需要大量的细节打磨。如果这个项目对你的课程设计或者实际工作有帮助照着上面的思路走一遍你踩的坑应该能比当年少很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑