☰
SpringBoot+Vue校园竞赛管理系统:从需求建模到答辩演示全解析
2026/9/25 3:45:18 网站建设 项目流程

简介:基于Spring Boot和Vue的校园竞赛管理系统毕业设计项目,面向计算机相关专业学生与Java初学者,适用于毕业设计、课程设计和课设大作业。系统覆盖用户管理、竞赛发布、参赛报名、成绩管理等核心模块,学校可便捷发布竞赛信息、管理参赛学生及成绩,学生可随时查看竞赛、在线报名并查询个人成绩;同时支持竞赛信息的分类检索与成绩的自动统计。压缩包约27.36MB,内含前端Vue源码、后端Spring Boot源码、MySQL数据库SQL脚本及毕业论文文档,代码结构清晰、数据库设计合理,便于二次扩展与维护。系统采用前后端分离架构,开发环境配置简便,运行测试通过,稳定性有保障。项目附带论文参考,为撰写毕业设计文档提供理论支撑,开发者可在现有代码基础上按需增加功能模块。已有54人学习/下载,适合用作实战项目练习或毕业设计成果展示。

1. 基于SpringBoot+Vue的校园竞赛管理系统:这套毕业设计到底能解决什么

基于SpringBoot+Vue的校园竞赛管理系统,面向的是高校里竞赛管理全流程线上化这个高频需求:竞赛发布、学生在线报名、作品上传、评委打分、成绩公示和证书发放,如果还靠Excel和微信群来回传递,到评奖周基本是一片混乱。这类项目也是毕业设计里常青选题,资源包一般会打包后端源码、前端源码、数据库SQL脚本和毕业论文,看起来像个“全家桶”,但拆开看核心不复杂:权限模型加一套状态流转。适合两类人——准备交毕业设计或课程设计的学生,以及想快速给学校搭一个竞赛报名后台的开发者。接下来按做这类项目的实际顺序,把从需求拆分到能演示、能答辩的完整过程讲清楚。

2. 需求建模和技术选型:别急着写代码,先定角色和状态

2.1 为什么是SpringBoot+Vue:技术选型的真实理由

这类系统选SpringBoot+Vue,不是因为它“新”,而是因为交付效率高、答辩好讲。SpringBoot内置Tomcat,起步依赖把SpringMVC、Jackson、校验框架都带了,写一个能跑通的REST接口只需要很少的样板代码。对于校园竞赛管理这种标准CRUD加状态流转的系统,SpringBoot的资源配置和拦截器机制都足够直接,答辩时被问到“SpringBoot配置加载顺序”“自动配置原理”这类问题,也容易讲清楚。

Vue这边负责管理后台特别顺手,组件化写表单、列表、弹窗比JSP那套前后端混编改起来快很多。Vue Route管页面跳转,Axios封装接口请求,配合UI组件库能快速搭出管理员、评委、学生三个角色的操作界面。做毕设时最怕的不是功能难,而是时间不够,SpringBoot+Vue恰好是上手曲线平缓、资料齐全、遇到报错能搜得到解决方案的组合。

另外要提一句环境问题。这类毕业设计资源包多数按JDK8、Node 14、MySQL 5.7/8.0的版本写好,拿到手先确认本机环境匹配,别一上来用JDK17跑老工程,各种依赖报错会浪费半天。

2.2 三个角色的用例划分:管理员、评委、学生

先不考虑“超级管理员”这种花哨设计,把角色收敛成三个,用例图会干净很多,代码也容易写。

角色核心用例关键操作
管理员竞赛管理、用户管理、成绩审核创建竞赛、设置报名截止时间、分配评委、审核成绩、发布公告
评委(教师)作品评审、评分维护查看已分配作品、打分、填写评语、退回不符合赛制的作品
学生报名、作品提交、成绩查询浏览竞赛列表、在线报名、上传作品、查看自己的分数和获奖状态

这版设计里最重要的是“管理员分配评委”这一步。如果不做分配,所有评委能看到所有作品,评审环节就没法控制公平性。数据库里用一张评委分配表或直接在竞赛表里加多个评委字段都能实现,优先用单独的关系表,后面要扩展“一个竞赛多个评委、一个评委多个竞赛”会方便很多。

用例定清楚之后,才能动手画时序图和E-R图。论文里的架构图、功能模块图都要以这个阶段的产出为基础,后面代码和论文对不上,答辩被问“这个功能在哪个类里实现的”会很尴尬。

2.3 核心流程的状态闭环:从发赛到成绩公示

校园竞赛管理系统的业务本质是一个状态机。下面这个链路基本覆盖所有模块:创建竞赛(草稿)→ 发布竞赛(报名中)→ 报名截止(评审中)→ 评委打分(评审中)→ 管理员审核并公示(已结束)。学生报名记录的状态单独再走一条:已报名 → 已提交作品 → 已评分 → 已获奖/未获奖。

状态字段不要用String随便存,建议用TINYINT或INT,配合明确的注释:0草稿、1报名中、2评审中、3已结束,这样写SQL查询条件时干净,写前端下拉框也好映射。常见做法是在一个枚举类里统一管理,Java端用常量类或枚举,前端用字典数组,两边的数字含义必须一致。

数据库表设计就是对这个状态机的落库。先画好状态流转图,再定表结构,你会发现主表不过五六张:用户、竞赛、报名、作品、评分、公告。很多人一上来先堆了二十张表,后面改需求时光改外键关系就崩溃了。按“先跑通主流程,再补扩展字段”的顺序建表,才是这类毕设项目的正确打开方式。

3. 数据库与SQL设计:表结构定好了,后端少写一半

3.1 六张核心表的结构清单

数据库设计是这个系统里最值得花时间的部分,表结构稳定了,实体类、Mapper、前端页面都能顺下来。我通常按下面这六张表起步。

表名用途关键字段
user用户表,存三种角色id、username、password、role、real_name
competition竞赛主表id、title、description、type、status、start_time、end_time
registration报名表,关联学生与竞赛id、competition_id、student_id、status、create_time
work作品表,学生提交的成果id、registration_id、title、file_url、submit_time
score评分表,评委打分记录id、work_id、judge_id、score、comment、create_time
notice公告表id、title、content、publish_time

用户表里role用TINYINT存数字,1管理员、2评委、3学生,方便权限拦截器做判断。competition表里的status就是上一节说的竞赛状态机。score表的主键建议用自增id,不用work_id加judge_id做联合主键,原因后面会讲。理论上还应该有证书表或获奖记录表,但毕设阶段把获奖状态挂在registration表里就够用了,少一张表少一摊事。

3.2 初始化SQL脚本与关键参数说明

拿到资源包里的SQL文件,先别急着在Navicat里整个导入,打开看一眼字符集和表结构。下面是按生产习惯写的一个最小建表脚本,表结构精简过,方便讲解。

CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL COMMENT '登录账号', `password` VARCHAR(100) NOT NULL COMMENT 'BCrypt密文', `role` TINYINT NOT NULL DEFAULT 3 COMMENT '1管理员 2评委 3学生', `real_name` VARCHAR(50) DEFAULT NULL COMMENT '真实姓名', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

几处参数需要特别说明。密码字段长度设成100,是因为BCrypt加密结果有60位,MD5只有32位,如果你打算用BCrypt,VARCHAR(64)够用但你设100更保险;如果用MD5,VARCHAR(32)就行。但答辩时建议说BCrypt,MD5加盐已经不算安全实践了。create_time用DATETIME而不是TIMESTAMP,是避免2038年问题和时区换算带来的坑。DEFAULT CURRENT_TIMESTAMP是MySQL 5.6以上支持的写法,老版本会语法报错,SQL导不进去先查一下MySQL版本。

CREATE TABLE `registration` ( `id` INT NOT NULL AUTO_INCREMENT, `competition_id` INT NOT NULL, `student_id` INT NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0已报名 1已提交作品 2已评分 3未获奖 4已获奖', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_comp_student` (`competition_id`, `student_id`), KEY `idx_student` (`student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='竞赛报名表';

报名表的联合唯一索引uk_comp_student是防重复报名的第一道关卡。就算前端按钮做了置灰,两个学生同时提交,或者同一学生开了两个页面操作,数据库层面依然能拦住重复记录。这条索引在答辩时很加分,讲出来就是“我在数据库层面做了幂等控制”,比只说前端判断高级一个档次。单列KEY索引idx_student是给“查某学生报名了哪些竞赛”这个高频查询准备的,否则每次都要扫全表。

3.3 SQL注入与慢SQL优化:答辩老师最爱问的两个点

先说SQL注入。为什么MyBatis里建议用#{}而不是${},因为#{}走预编译,传参被当作字符串处理;${}是直接拼接SQL,把用户输入原样塞进语句,一旦参数里带OR 1=1这类内容就出问题。毕业设计里常见的翻车点就是有人图省事在ORDER BY或动态表名处用了${},却没校验参数。如果确实要动态排序字段,必须做白名单校验,只允许传入预设好的字段名。

再说慢SQL优化。竞赛列表页按状态查询、报名记录按学生查询,这两条是系统里最高频的SQL。遇到响应变慢,别瞎猜,先用EXPLAIN看执行计划。

EXPLAIN SELECT r.id, r.status, c.title FROM registration r LEFT JOIN competition c ON r.competition_id = c.id WHERE r.student_id = 17 ORDER BY r.id DESC LIMIT 20;

EXPLAIN返回的type列如果出现ALL,说明当前查询在走全表扫描。解决方式就是给WHERE条件里的student_id加索引,就是上一节建表时加的idx_student。type从ALL变成ref,查询成本立刻降下来。还有一个容易被忽略的点:SELECT后面只查需要的字段,不要SELECT *。student表里有密码字段,列表页根本用不到,查出来还平白增加数据传输量。

3.4 SQL脚本在毕业设计材料里的作用

SQL文件不只是让项目能跑起来,它还是论文里数据库设计的直接证据。论文第一章要写“数据库设计”,一般会要求给出E-R图和核心表的结构说明。答辩时老师很可能抽查某个字段的含义或某条查询语句的设计理由,这时候能指着SQL文件讲清楚就行。

评分验收时有一个常见现象:老师会直接打开源码里的SQL文件看表设计,表过大、命名混乱、没有注释会影响印象分。建议每个字段都写COMMENT,每张表都用utf8mb4字符集,主键统一叫id,业务表的时间字段统一叫create_time或update_time。风格一致的表结构,比多写几个非业务功能更容易拿到好评。

4. SpringBoot与Vue联调:从登录到报名评分的主链路实现

4.1 SpringBoot工程骨架与关键配置

工程结构建议按controller、service、mapper、entity、config、common这几层拆,不要图省事把逻辑全写在Controller里。校园竞赛系统的逻辑虽然不重,但“登录→鉴权→报名→评分→公示”这条链路很长,分层清楚,后面论文画架构图也方便。

核心配置集中在application.yml里,地方不大但坑不少。数据源、MyBatis映射路径、文件上传大小、JWT密钥,全在这一份配置里调。下面是常见配置片段。

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/competition?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 自定义参数 jwt: secret: 替换成你自己的长随机字符串 expire-hours: 8

MySQL 8以后驱动类要从com.mysql.jdbc.Driver换成com.mysql.cj.jdbc.Driver,连接串不带serverTimezone会直接报时区错误,这是每年毕设掉坑的重灾区。map-underscore-to-camel-case这个配置打开后,数据库字段user_name能自动映射成Java属性userName,不用在结果映射里一个个写。

JWT密钥不要写“abc123”这种,被老师在源码里看到会问你怎么保证安全性。实际做法是随便生成一段64位随机字符串,用的时候从配置里读,不要把密钥硬编码在代码里。expire-hours设8小时是照顾白天连续开发,对于生产场景偏短,但毕业设计够用。

4.2 JWT登录、路由守卫与接口拦截

登录接口本身没什么技术含量,重点是登录后怎么让前端记住状态、后续请求怎么带凭证、后端怎么拦截非法请求。最顺手的一套组合是JWT加Axios请求拦截器加后端HandlerInterceptor,三个部分配起来,整个系统的鉴权就闭环了。

@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private UserMapper userMapper; @PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { User user = userMapper.selectByUsername(dto.getUsername()); if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.error("用户名或密码错误"); } Map<String, Object> claims = new HashMap<>(); claims.put("userId", user.getId()); claims.put("role", user.getRole()); String token = JwtUtil.createToken(claims, jwtProperties.getExpireHours()); return Result.success(token); } }

逻辑说明:passwordEncoder是SpringSecurity里BCryptPasswordEncoder的实例,matches方法校验明文密码和数据库密文是否一致,返回true才放行。JwtUtil是手写的工具类,createToken第一个参数是token里要携带的用户信息,这里只放userId和role,不放密码。过期时间从配置类读取,统一管理。

token返回给前端之后就出现一个新问题:后端怎么知道每个接口要不要鉴权?HandlerInterceptor可以解决。

@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 登录接口、验证码等白名单路径放行 if (request.getRequestURI().startsWith("/api/auth/")) { return true; } String token = request.getHeader("Authorization"); if (token != null && JwtUtil.parseToken(token) != null) { return true; } response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\"}"); return false; } }

注意这段代码里白名单路径的判断比较粗,实际项目会把白名单路径收集成一个Set,统一检查。这里最容易被问倒的是“JWT无状态,服务端怎么让token失效”,答案是JWT天然带过期时间,token过期后parseToken返回null,自然被拦截。如果要求用户注销后token立即失效,就得引入黑名单或Redis,属于加分项,毕设阶段不强制。

前端Vue这一侧,重点在Axios请求拦截器和Vue Router的路由守卫。

// src/utils/request.js axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = token; } return config; }, error => Promise.reject(error)); // src/router/index.js router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (!token && to.path !== '/login') { next({ path: '/login', query: { redirect: to.fullPath } }); } else { next(); } });

这里有个细节值得说:localStorage存token在XSS攻击面前不安全,理论上应该用httpOnly Cookie或者内存存储,但毕业设计里localStorage仍然是主流,因为简单。如果你在论文里写了“安全性考虑”,却用localStorage存token,答辩会被挑刺。不想深入安全话题就统一说“采用token鉴权,前端本地存储”,别把安全拔太高。

路由守卫配合后端401拦截,就实现了“前端跳登录页、后端拒绝访问”的双重防线。带redirect参数是为了登录后能跳回用户本来要访问的页面,这个细节很显工程师思维。

4.3 竞赛报名与作品提交的数据流

报名和作品提交看似两个功能,实际是一条数据流。学生看竞赛列表,点击报名,registration表插入一条记录;竞赛状态没到报名截止,这条记录status是0。然后学生进入“我的报名”,上传作品,work表插入记录,同时registration表的status改成1。评委端看到的就是状态为1的报名记录,对应去作品表拿文件来评。

// 前端报名调用 async submitRegistration(competitionId) { const { data } = await axios.post('/api/registration', { competitionId: competitionId }); if (data.code === 200) { this.$message.success('报名成功'); this.loadCompetitionList(); } else { this.$message.error(data.message); } }
// 后端报名接口 @PostMapping("/api/registration") public Result register(@RequestBody RegisterDTO dto, @RequestAttribute("userId") Integer userId) { // 判断竞赛是否处于报名中状态 Competition comp = competitionMapper.selectById(dto.getCompetitionId()); if (comp == null || comp.getStatus() != 1) { return Result.error("当前不在报名时间范围内"); } // 数据库索引保证同一学生不能重复报名 Registration reg = new Registration(); reg.setCompetitionId(dto.getCompetitionId()); reg.setStudentId(userId); reg.setStatus(0); registrationMapper.insert(reg); return Result.success(); }

后端接口里的userId不是前端传的,是从JWT解析后通过RequestAttribute注入的,这是一种典型的信任链设计:用户身份信息统一从token取,不用前端再传一遍user_id,防止有人改参数报名到别人名下。这个点能讲清楚,至少在“不信任前端输入”这个维度上站得住。

4.4 联调时的黑匣子排查习惯

前后端联调时,前端页面只显示一个“接口报错”,看不到后端哪里挂了,这时候别瞎猜,按“请求有没有到后端→后端有没有报异常→返回的数据前端有没有解析对”的顺序排查。先看浏览器Network面板里接口的HTTP状态码,再看后端控制台日志,最后看Response里的数据结构。这个习惯能解决大多数所谓“联调不通”的玄学问题。

常规做法是后端提前把统一返回结构固定下来,比如Result类里code、message、data三个字段,所有接口都走这一个壳。这样前端Axios响应拦截器只要判断code是否等于200就行,不用每个页面单独处理异常数据。联调时如果发现前端拿不到数据,先打印完整响应,十有八九是data字段缺少一个属性。

5. 常见问题与避坑:毕业设计包最容易翻车的五个点

5.1 压缩包代码跑不起来:JDK、Node、MySQL版本对不上

现象:解压按README执行,前端npm install报错,后端启动直接抛UnsupportedClassVersionError,SQL脚本导入也报语法错误。

原因:旧毕业设计多数基于JDK8、Node 14或更老版本生成,包里的依赖版本和2025年的新环境不兼容。MySQL从5.7升到8.0后,驱动类名和时区配置都变了,老SQL脚本里如果用了ENGINE=MyISAM或旧语法,导入过程也会中断。

解决:先看pom.xml里的java.version,再看package.json里的vue版本,按对应的环境安装。不要追求最新版,按包内锁定的版本走。如果包内没有说明,把JDK装到8、Node装到14或16、MySQL保持8.0,是兼容性最高的组合。MySQL连接串记得加serverTimezone=Asia/Shanghai。

5.2 跨域报错:前端能打开、接口全被拦

现象:Vue项目npm run dev起的页面能正常显示,但调用后端接口时浏览器报blocked by CORS。

原因:前端运行在localhost:5173,后端运行在localhost:8080,端口不同就跨域了。Vite或Webpack开发服务器没配代理时,浏览器直接向8080发请求,后端没允许跨域来源,请求被浏览器拦下。

解决:开发环境优先用Vite代理,把/api开头的请求转发到8080,这样浏览器看到的还是同源请求。

// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } });

项目上线走Nginx时也要配一份同样的反向代理,前后端同源,跨域问题就不存在了。后端加CorsFilter是兜底方案,不过毕业设计里用代理更干净,论文里也更好解释。

5.3 时间字段显示异常:返回一串数字或格式不对

现象:接口返回的create_time是一串毫秒值,或者前端显示成“2025-05-01T10:30:00”,和页面风格不搭。

原因:Java 8的LocalDateTime经过Jackson默认序列化规则后,可能变成数组或ISO字符串;前端如果直接渲染它,就出现这种不直观的显示。

解决:在application.yml里统一指定Jackson的日期格式。

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

注意这个配置只对java.util.Date生效,对LocalDateTime需要配合jackson-datatype-jsr310模块并设置write-dates-as-timestamps为false。很多新项目直接全局配置就够用,但LocalDateTime场景需要在实体字段上再加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")。两个都写上最稳妥,答辩被问也能说明白区别。

5.4 作品上传失败:文件超限和PDF的XSS风险

现象:学生上传竞赛作品时,文件稍微大一点就报MultipartException,直接返回500。还有另一种情况,上传的PDF文件名里包含特殊字符,列表页展示时页面结构被破坏。

原因:SpringBoot默认单文件上传上限是1MB,竞赛作品的PDF、打包作品压缩包动辄几十兆,必然超限。文件名问题在于后端未对原始文件名做清洗,特殊字符被前端渲染成HTML。

解决:调大上传限制,同时在后端保存文件时对文件名做处理。

spring: servlet: multipart: max-file-size: 50MB max-request-size: 60MB
String originalFilename = file.getOriginalFilename(); String safeName = originalFilename.replaceAll("[<>\"'()]", "_"); String storePath = uploadDir + "/" + System.currentTimeMillis() + "_" + safeName;

常见做法是保存文件时只在数据库存相对路径,展示时用专用的下载接口让浏览器强制下载,而不是直接把文件路径当链接拼到页面上。文件名里的特殊字符用replaceAll清洗成下划线,既防止路径穿越也防XSS。这类安全细节写在论文里,比空谈“系统安全”更有说服力。

5.5 重复报名与状态乱掉:前端接口被绕过怎么办

现象:同一个学生报名同一竞赛报了两条记录,或者竞赛还没发布,学生就能通过直接调接口报名成功。

原因:前端只做了按钮置灰和提示,后端没有校验。任何人只要会用Postman,就能绕过前端直接调接口,把数据写进去。

解决:后端报名接口里做两件事。第一,检查竞赛status必须等于报名中;第二,靠数据库联合唯一索引兜底重复报名。另外在用户表初始化时预置一条管理员账号,密码用BCrypt加密,不要在SQL脚本里写明文密码。答辩现场老师很可能现场开Postman模拟一个越权请求,这两个校验做了,他挑不出毛病。

6. 答辩演示前这样验证:部署、演示脚本与二次开发加分点

6.1 答辩必演示的主链路脚本

答辩演示最怕临场点开一个模块发现数据是空的,或者流程走不通。提前准备一套完整演示数据,按照下面脚本连走一遍。

步骤操作预期结果
1管理员登录,创建竞赛并发布竞赛出现在公开列表,状态变为报名中
2学生注册登录,报名该竞赛报名成功,重复报名被拦截
3学生上传作品文件作品列表出现记录,报名状态变为已提交
4管理员分配评委,评委登录打分分数落库,可填写评语
5管理员审核并公示成绩学生端能看到分数和获奖状态

这套脚本跑通,核心功能就演示完了。建议提前录一份备用视频,防止现场网络或环境出问题。

6.2 三个不超纲的加分功能

第一个是数据导出,用EasyExcel把报名名单导出成Excel,老师经常手动收集报名信息,这个功能很实用。第二个是通知模块,竞赛状态变化时给相关学生发站内消息,用WebSocket或轮询都能实现。第三个是公告置顶和竞赛分类筛选,代码量不大,但能让系统看起来更像生产项目,而不只是课程作业。

6.3 用jar包加Nginx把项目跑起来

构建部署这块答辩不一定要求,但配置过和没配置过,对项目的理解深度完全不同。前后端分离的项目最终要能在服务器上跑起来。

# 前端打包 npm run build # 后端打包,跳过测试减时间 mvn clean package -DskipTests # 启动后端jar包 java -jar target/competition-server.jar --spring.profiles.active=prod # 前端dist目录放到Nginx html目录后,配置反向代理
location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

部署的关键点是Nginx托管前端静态文件、反向代理后端接口、前端通过相对路径访问服务,这样浏览器不跨域。我自己带毕业设计有个习惯:项目能跑、能演示、能部署只是及格线,答辩前一定要把核心模块的代码从头翻一遍,确保每个接口都讲得出“为什么这么设计”。很多学生栽在“代码是网上找的,自己没通读过”这一关。做这类系统,先跑通主链路,再谈附加功能,顺序别颠倒。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询