资讯动态

基于SpringBoot的竞赛管理系统:从需求拆解到部署答辩全解析

发布时间:2026/9/27 1:01:06 来源:尧图企业网站定制
每年毕设季都能看到大量打着“基于SpringBoot”旗号的管理系统大学生科技竞赛管理系统是其中出场率最高的类型之一。你拿到的这个项目标题里挤满了Java、SpringBoot、SSM、源码、LW、调试文档、讲解这些关键词本质上就是一套用于高校赛事从发布、报名、作品提交到评审公示全流程管理的毕业设计作品。这篇文章我按自己带项目的经验把这个系统从需求拆解、技术选型、数据库设计、核心功能实现一直讲到部署调试、论文撰写和答辩演示完整走一遍。无论你是准备直接上手复现还是想搞明白这套系统内部到底怎么运转这篇都能给你省下不少弯路。1. 项目背景与需求拆解1.1 为什么需要一个专门的竞赛管理系统高校里的科技竞赛数量远超想象。除了校内的“挑战杯”选拔、电子设计大赛、程序设计竞赛还有各个学院自己组织的创新创业类赛事。在没有系统之前这些赛事的管理方式基本都是“表格大战”教务处发通知学生下载报名表填写后发到指定邮箱负责人手动汇总作品提交时用网盘或者U盘命名格式五花八门评审环节更是头疼评委拿到的可能是纸质材料也可能是压缩包打分结果靠Excel手工统计最后还要人工核对有没有漏掉谁。一旦赛事数量多、参赛队伍超过一百支这种管理方式就会全面失控。漏报、错报、作品丢失、成绩统计出错全是真实发生过的坑。所以我个人一直认为这类系统的核心价值不是“写一个网站”而是把赛事流程里的角色、状态、数据流转梳理清楚让每一步都有迹可循。这也就是为什么几乎所有竞赛管理系统的需求分析里角色都逃不开三类学生、评委教师、管理员。1.2 核心角色与业务流程梳理明确角色之后再看流程就会非常清晰。一套标准的大学生科技竞赛管理系统业务主线可以压缩成五步管理员创建赛事设置报名开始/结束时间、赛事类别、参赛要求并发布公告。学生在规定时间窗口内报名填写队伍信息和成员名单。报名截止后学生在作品提交阶段上传作品文件文档、图片、压缩包。评委在评审阶段查看作品逐项打分并填写评语。管理员汇总成绩发布获奖名单和公示公告。这五步里每一步都涉及状态流转。比如报名这个动作在系统里不是一条简单的插入记录而是需要校验赛事状态是否在报名期内、校验队伍人数上限、防止重复报名这些逻辑叠加起来才是完整的需求。很多初学者拿到这种项目第一反应是写页面、写增删改查实际上最容易出问题的是流程状态控制而不是CRUD本身。这部分后面我会专门展开说。2. 技术选型为什么是SpringBootSSM而不是别的2.1 SpringBoot与SSM的关系到底怎么理解很多同学看到“JavaSpringBootSSM”会觉得这是个矛盾的表述SpringBoot本身就是Spring体系SSM又是SpringSpringMVCMyBatis这不是重复了吗其实完全不冲突。通俗点说SSM指的是技术组合而SpringBoot是这个组合的落地方式。SpringBoot内置了SpringMVC的自动配置我们在项目里引入spring-boot-starter-web就等于同时拥有了Spring容器和SpringMVC再引入mybatis-spring-boot-starter持久层用MyBatis。所以一个SpringBoot项目套上MyBatis本质上就是一个“SSM升级版”。毕设项目选这套技术栈最大的好处是两个。第一生态成熟遇到问题搜索引擎一搜全是答案这对没有太多实战经验的同学非常重要。第二面试的时候这套技术栈几乎不会踩雷SpringBoot、SpringMVC、MyBatis都是Java后端岗位的高频考点做完这个项目面试谈资直接就有了。我在实际带项目时经常跟学生说哪怕你摆一个简单的管理系统只要能把SpringBoot的自动配置原理、MyBatis的SQL执行过程讲清楚面试官对项目的兴趣都会大很多。2.2 分层架构设计技术栈定了接下来是工程结构。这套系统我推荐按经典的四层结构来分包controller接收前端请求做参数校验后调用service层。service业务逻辑层处理报名、评审、成绩统计等核心流程。mapper数据访问层对应MyBatis的Mapper接口。entity/domain实体类对应数据库表结构。另外还要配一个common包放统一返回结果Result、全局异常处理器、拦截器、工具类。这里有个很多初学者容易犯的错把所有逻辑全写在Controller里Service层形同虚设。如果你打算用这个项目去面试或答辩Service层有没有承担真正的业务逻辑是面试官判断你代码水平的一个很直接的观察点。哪怕逻辑很简单比如“报名前检查赛事是否处于报名期”也应该写在Service层的registerCompetition()方法里而不是让Controller自己查完再判断。2.3 前端方案与页面渲染前端这块毕设管理系统最常见的做法是ThymeleafBootstrap/AdminLTE。为什么不推荐前后端分离原因很现实前后端分离意味着你要同时维护两套工程、处理跨域、做接口鉴权工作量直接翻倍对毕设来说完全没必要。而用Thymeleaf做服务端渲染模板和后端在一个项目里部署简单、调试方便、演示也流畅。管理员后台可以用AdminLTE这类现成的Admin模板学生端和评委端的页面则用Bootstrap写一套干净的响应式界面整体效果不会差。说句题外话如果你用的是页面模板注意Thymeleaf的语法一定要熟。我见过有人把th:each写错导致列表渲染不出来最后排查半天发现是标签闭合问题。后面调试章节我会专门讲这种坑。3. 数据库设计与核心功能实现3.1 核心表结构设计数据库设计是一套管理系统的地基。按上面的业务主线最少需要六张表表名用途核心字段user用户表学生/评委/管理员id, username, password, role, real_name, student_no, emailcompetition赛事表id, title, category, description, sign_start_time, sign_end_time, submit_start_time, submit_end_time, statussignup报名表id, competition_id, team_name, captain_id, member_names, signup_timework作品表id, competition_id, signup_id, file_path, file_name, submit_time, statusscore评审打分表id, work_id, judge_id, score_value, comment, create_timenotice公告表id, title, content, publisher_id, create_time字段设计上有几个关键点需要提醒。第一时间字段统一用datetime不要用varchar存时间字符串否则后面做赛事状态判断和排序时会非常痛苦。第二角色字段role用字符串STUDENT/TEACHER/ADMIN比用数字枚举更直观虽然性能上无差别但对调试和演示都友好。第三参赛成员字段member_names建议用逗号分隔的字符串存储因为一个队伍成员数量不固定为了一个不常查询的字段单独建队伍成员表不划算。这算是我在实际项目里妥协出来的方案可以省一张表代价是没法对成员做复杂的SQL查询但对毕设系统来说完全够用。数据库初始化脚本写好之后记得在application.yml里配置sql.init.mode: always这样项目第一次启动时能自动建表。但注意后续如果你改了表结构这个自动初始化反而会出问题所以稳妥的做法是项目里放一份schema.sql手动在Navicat里执行不要过度依赖自动初始化。3.2 权限模型用拦截器而不是Spring Security权限控制是这类系统的必须项核心要求是学生不能访问评委接口评委不能修改管理员数据。毕设项目这个级别我不建议引入Spring Security。一方面是学习成本高Security的过滤器链和配置方式对新手不友好另一方面是演示不方便默认登录页和自己写的页面风格不搭反而显得割裂。更务实的方案是自定义拦截器Session。具体做法是写一个AuthInterceptor实现HandlerInterceptor接口在preHandle里从Session取出当前登录用户判断其角色是否允许访问当前请求的URL。Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(/login); return false; } String uri request.getRequestURI(); if (uri.startsWith(/admin/) !ADMIN.equals(user.getRole())) { response.setStatus(403); return false; } if (uri.startsWith(/judge/) !TEACHER.equals(user.getRole())) { response.setStatus(403); return false; } return true; } }然后在WebMvcConfigurer里注册拦截器并配置放行路径比如登录页、静态资源、注册接口Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /register, /css/**, /js/**, /images/**); } }这样一个拦截器就覆盖了90%的权限场景。如果要更细致地控制按钮级权限可以在页面里用th:if${session.loginUser.role ADMIN}做条件渲染。这里有个实际经验拦截器放行静态资源时路径千万别写错。我见过有人把/static/**写成/static*结果CSS永远加载不出来页面跟裸奔一样。如果你遇到页面有内容但完全没有样式第一反应就应该是查拦截器放行路径。3.3 报名与作品提交流程的状态控制刚才提到这套系统的核心难点不在CRUD而在状态流转。我发现很多人写的报名接口就是一个简单的insert into signup完全不校验赛事状态结果就是比赛已经结束还能报名。真正的流程控制至少要考虑三件事赛事是否存在且状态为“报名中”。当前用户是否已经报过这个赛事防重复报名。报名时间是否在sign_start_time和sign_end_time之间。写成Service层的核心逻辑大概是public Result signUp(Integer competitionId, User user) { Competition comp competitionMapper.findById(competitionId); if (comp null) { return Result.error(赛事不存在); } Date now new Date(); if (now.before(comp.getSignStartTime()) || now.after(comp.getSignEndTime())) { return Result.error(不在报名时间内); } int count signupMapper.countByCompetitionAndCaptain(competitionId, user.getId()); if (count 0) { return Result.error(您已报名该赛事请勿重复操作); } Signup signup new Signup(); signup.setCompetitionId(competitionId); signup.setCaptainId(user.getId()); signup.setTeamName(默认队伍名); signupMapper.insert(signup); return Result.success(报名成功); }这里有个容易被忽略的细节赛事的“报名中”“评审中”“已结束”这些状态不要专门建一个字段然后靠人工更新而是应该通过时间字段动态判断。因为一场赛事的报名截止时间是可预知的用now signEndTime就能得出“报名结束”的结论根本不需要有一个字段来同步维护。强行加字段反而会出现“时间已经过了但状态还是报名中”这种数据不一致的尴尬情况。当然如果你想在界面上展示“评审中”这类阶段也可以加一个status字段但它的值应该定时从时间计算出来而不是让管理员手动去改。3.4 文件上传与下载的实现细节作品提交必然涉及文件上传这也是本系统里比较能体现“实战感”的功能。SpringBoot里文件上传本身不复杂PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file, RequestParam(workId) Integer workId) { if (file.isEmpty()) { return Result.error(上传文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String storeName UUID.randomUUID().toString().replace(-, ) ext; File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } File dest new File(uploadDir storeName); file.transferTo(dest); // 更新work表的file_path和submit_time return Result.success(上传成功); }这里有几个我一再强调的细节。第一保存到服务器的文件名一定要用UUID重命名绝不能用用户上传的原始文件名。否则两个人都传项目报告.docx后上传的会把先上传的覆盖掉而且中文文件名在部分环境下会乱码。第二配置文件上传大小限制spring: servlet: multipart: max-file-size: 50MB max-request-size: 100MB不配置这个默认只有1MB作品文档稍微大点就传不上去。第三也是很多人忽略的上传目录要独立于项目目录和生产环境解耦。我习惯在application.yml里配置一个自定义的upload.dir属性推荐配成绝对路径如D:/upload/这样项目重新部署时文件不会丢也方便评委下载时直接拼接路径。文件下载的方式有两种。一种是让浏览器直接访问文件在服务器上的URL简单但暴露真实路径另一种是通过接口读取文件流输出能实现权限控制比如只有评委和管理员才能下载。毕设答辩时如果面试官问“如何防止学生下载别人的作品”你回答“下载接口做了权限校验”会是个很亮眼的细节。4. 完整部署与调试过程实录4.1 环境准备与版本选择老规矩先列环境JDK 1.8SpringBoot 2.x项目必须用8或者11不建议直接上17因为很多老版本依赖会出兼容问题Maven 3.6MySQL 5.7或8.0我用的是5.7字符集务必选utf8mb4IDEA 2021及以上版本Navicat或DataGrip作为数据库客户端这里重点说JDK版本。如果你拿到的源码是SpringBoot 2.xJDK 8是最稳的选择。SpringBoot 3.x要求JDK 17且javax包名改成jakarta如果源码里大量用了javax.servlet说明它是SpringBoot 2.x硬用JDK 17编译会报一堆包找不到的错。拿到源码第一步先看pom.xml里的spring-boot-starter-parent版本号再决定装哪个JDK这是最省时间的做法。4.2 配置文件的正确修改姿势项目的核心配置基本都集中在src/main/resources/application.yml里。你需要关注的无非这几项server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/competition_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.competition.entity upload: dir: D:/upload/这里我想重点提醒一件事数据库连接串里的serverTimezone参数一定要加。不加这个MySQL 8.0连接时大概率报The server time zone value is unrecognized错误。另外characterEncodingutf8也要有否则插入中文数据会变成???。如果你拿到的源码用的不是application.yml而是application.properties格式记得改成spring.datasource.urljdbc:mysql://localhost:3306/competition_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.passwordyour_password顺便说一句我看到标题里提到“调试文档”这东西在毕设场景下其实就是一份你修改过的配置记录和踩坑记录。后面第6章我会专门讲怎么写。4.3 项目导入到启动的全流程假设你拿到了完整源码最稳妥的导入步骤是这样IDEA里选择File - New - Project from Existing Sources选中项目根目录的pom.xml以Maven项目方式导入。等待Maven下载依赖。这一步考验耐心也考验网络。如果下载特别慢去settings.xml里配阿里云镜像。在MySQL里创建数据库执行sql/schema.sql初始化脚本。修改application.yml里的数据库用户名密码。找到带SpringBootApplication注解的主类右键运行。控制台出现Started Application in x.xxx seconds启动成功。浏览器访问http://localhost:8080。如果你发现启动报错九成是下面几种情况之一。我直接给你对应排查顺序报错现象排查方向Access denied for user rootlocalhost数据库密码配置错误或root账户不允许远程连接Unknown database competition_db还没有执行建库脚本Port 8080 was already in use端口被占用改server.port或者杀掉占用进程Table doesnt exist没执行schema.sql或者连错了数据库Mapper method not foundMapper接口和XML文件没绑定成功检查mapper-locations配置和XML的namespace这些坑我在带项目的过程中几乎每一届都会遇到一遍尤其是最后一个。很多同学的Mapper接口明明写了方法但运行时报Invalid bound statement (not found)根本原因是XML文件的namespace没有写全成接口的全限定名或者select标签的id跟接口方法名不一致。这个错很隐蔽因为编译是过的只有运行到那一步才炸。4.4 运行时调试技巧项目能启动只是第一步接下来你肯定要调试各种功能。IDE里打断点、看变量是基本功我不多废话说几个别人不太讲但很实用的技巧第一个善用日志而不是System.out.println。项目里用sout打日志在毕设里很常见但它是同步的会影响性能而且在排查问题时没有级别区分。我更推荐在关键方法里用Slf4j打日志Slf4j Service public class CompetitionServiceImpl implements CompetitionService { public Result signUp(Integer competitionId, User user) { log.info(用户 {} 正在报名赛事 {}, user.getId(), competitionId); // ... log.error(报名失败原因赛事不存在); } }这样启动日志里能看到完整的业务流转过程答辩演示时也更专业。第二个用Postman调试接口而不是全靠浏览器。浏览器只能发起GET请求POST、PUT这类操作用Postman更可控。尤其是登录接口、报名接口这类需要带Session的Postman可以用Cookie方式保持会话调试效率高出一大截。第三个前端页面报错时打开浏览器F12看Console和Network。我见过太多人反馈“页面点按钮没反应”结果打开Console一看是JS文件路径404或者接口返回500。这些信息在Network面板里一目了然比自己瞎猜快得多。5. 常见问题与避坑清单5.1 中文乱码从请求到数据库的全面排查中文乱码是JavaWeb项目的经典问题搞不定它会非常劝退。按我经验乱码出现在三个环节第一页面显示乱码。检查HTML头部有没有meta charsetUTF-8Thymeleaf模板要确保pageEncodingUTF-8。第二请求参数乱码。SpringBoot 2.x里内置的CharacterEncodingFilter默认就会把请求编码设为UTF-8所以POST表单一般不会乱码。如果你用了老SpringMVC项目才需要在web.xml里手动配置编码过滤器。第三数据库存储乱码。这个最麻烦。确保三处一致数据库连接串带characterEncodingutf8建库语句指定DEFAULT CHARSETutf8mb4表字段也是utf8mb4。我在实际项目中遇到过一种奇葩情况数据库连接字符串没带编码参数但是页面和请求都是UTF-8结果数据库里的中文依然变成问号。所以记住排查乱码的顺序永远是页面 → 请求 → 连接串 → 数据库表一层层看不要跳过任何一层。5.2 文件上传中的连锁坑文件上传相关的坑我至少能列出五个文件大小超限默认才1MB配置不放大上传稍大文件就报错。路径不存在File.transferTo()之前不检查父目录是否存在会报FileNotFoundException。文件名冲突不用UUID重命名用户上传同名文件就互相覆盖。下载404如果你把文件存在本地磁盘但通过http://localhost:8080/upload/xxx去访问SpringBoot默认不映射外部目录。解决办法是写一个资源映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file:D:/upload/); } }跨平台路径问题Windows路径分隔符是\Linux是/。写路径时尽量用File.separator或者直接用Paths.get()避免写死。我在Windows上开发好好的代码部署到Linux服务器上就报找不到文件的错多半就是这里出了问题。5.3 MyBatis常见问题汇总MyBatis这块被问得最多的就是分页。很多毕设项目都会用PageHelper插件。引入方式很简单dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency然后在查询前调用PageHelper.startPage(pageNum, pageSize); ListCompetition list competitionMapper.selectList(condition); PageInfoCompetition pageInfo new PageInfo(list);用PageHelper有一个非常隐蔽的坑PageHelper.startPage()只对紧跟着的第一个查询生效。如果你在它和selectList()之间调用了另一条SQL分页就会作用到错误的查询上导致返回的数据不对。这是官方文档都强调过的“仅对一次查询有效”但实际项目里踩的人依然不少。还有一个常见问题是if动态SQL条件拼接select idselectList resultTypeCompetition SELECT * FROM competition where if testcategory ! null and category ! AND category #{category} /if if testtitle ! null and title ! AND title LIKE CONCAT(%, #{title}, %) /if /where ORDER BY create_time DESC /select注意条件和条件之间要加AND很多人第一次写动态SQL时会漏掉导致拼接出错。用where标签可以自动处理多余的AND但这个习惯需要刻意培养。5.4 数据一致性一个在答辩时能加分的细节如果你做的系统涉及评分统计、获奖名单生成一定会遇到数据一致性的问题。最简单的场景多个评委同时给一个作品打分管理员要看平均分。如果你在业务代码里先查出所有评分记录然后在Java里求平均逻辑没错但效率低、代码丑。更优的做法是直接让数据库算SELECT AVG(score_value) FROM score WHERE work_id #{workId}MyBatis里返回Double类型就行。这种“能数据库干的事不要拿到Java里做”的习惯在答辩时很容易被面试官认可。我自己带过的一个学生就是因为在作品详情页用了一条子查询统计平均分和评分人数被面试官追问了两轮最后评价很高。技术点不在新在于用得恰当。6. 毕业设计配套论文、调试文档与答辩讲解要点6.1 论文LW核心撰写脉络标题里的“LW”就是“论文”的缩写很多同学把它理解成毕业论文或者课程设计报告。写这类文档我一般建议按这个骨架走绪论选题背景高校竞赛规模扩大、人工管理效率低、国内外现状、研究内容与意义。相关技术介绍Java、SpringBoot、MyBatis、MySQL。不要写成教科书每项技术写“是什么、能解决什么问题、为什么本项目用它”就够了。系统分析可行性分析技术/经济/操作三方面、需求分析角色、业务流程、功能模块。系统设计架构设计、功能模块划分、数据库设计ER图、表结构。系统实现每个模块结合核心代码截图和运行截图展示代码不必贴全但要把关键逻辑说清楚。系统测试功能测试用例表、测试结果。写在这份文档里最重要的一句话需求分析里说了什么设计里就要有什么设计里有什么实现里就必须展示什么。我审毕设的时候最怕看到需求分析写了十个功能模块实现部分只写了四个剩下六个说“篇幅所限略”这种直接就不合格了。6.2 调试文档的整理技巧调试文档听起来是小事但企业里管这个叫“开发运维手册”质量高不高一眼就能看出来。我的建议是调试文档不需要写成一本大书做成一张问题速查表最实用问题类型现象原因解决办法数据库连不上启动报Access denied密码错误检查application.yml配置中文乱码数据存为???连接串缺编码参数url加characterEncodingutf8文件上传失败报文件大小超限默认上限1MB配置max-file-sizeMapper报错Invalid bound statementXML的namespace错误修改namespace为接口全限定名表格一列问题、原因、方案清清楚楚。这份调试文档不仅是你自己排障的记录答辩时老师看到你做事有条理印象分会直接拉高。6.3 答辩现场演示的7个加分细节答辩演示环节是整个毕设的临门一脚我总结几个实测有效的细节提前准备好测试账号学生端、评委端、管理员端各一个不要在演示现场注册。用无痕窗口打开系统避免登录状态残留导致演示时跳过了登录页。演示时先说一句“这是管理员视角”让评委知道你在哪个角色下操作。报名功能演示前先展示赛事的报名时间范围再演示成功报名和重复报名被拦截形成对比。文件上传尽量用一个2-3MB的小文件秒传成功不浪费时间。演示评审打分时展示评委只能看到分配给自己的作品换到学生账号后发现打不开评委页面这一步权限对比的演示效果极佳。准备一张系统流程图或架构图答辩开始时贴出来帮助评委快速建立对项目的整体认知。这些细节单独看都不起眼组合起来能让整个演示过程非常流畅。我印象很深的一个学生演示时故意先用一个错误密码登录失败然后再登录成功还顺带解释了一句“登录失败有全局异常处理”答辩老师当场就笑了气氛一下就活了。6.4 从课程设计升级为企业项目的三个扩展方向如果你做完这套系统还有余力我建议往这三个方向扩展无论是丰富简历还是面试加分都有用第一个方向是引入工作流引擎。比如用Flowable实现竞赛审批流程让“赛事创建→指导教师审核→教务处通过”变成可配置的流程节点。这个扩展点能让你在面试时把话题引向工作流引擎这是很多真实企业项目都会用到的东西。第二个方向是文件存储迁移到MinIO或阿里云OSS。本地存储虽简单但生产环境里都是对象存储。花两天时间把上传下载功能从本地迁移到MinIO你的项目就从“课程设计级”变成了“接近生产级”这也是简历上的一个加分项。第三个方向是引入日志切面与全局异常处理。用ControllerAdvice统一处理业务异常用AOP记录操作日志做成一张可查的操作审计表。我曾经帮一个学生把这个模块讲给面试官听面试官直接追问“AOP的切面表达式怎么写的”答上来之后面试当场通过。这些细节才是让项目“活起来”的关键。我个人这几年带项目的最大感受是好系统不是功能堆得多而是每个核心流程都有清晰的状态控制每次操作都有对应日志和异常处理每个权限点都做了该做的校验。你只要把前面几章捋顺再按最后一章把论文、调试文档和答辩演示准备好这套竞赛管理系统绝对能稳稳过关。真到了答辩现场放轻松项目是你一笔一划写出来的没人比你更懂它。

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

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

免费获取报价 →
↑