资讯动态

SpringBoot+Vue前后端分离的大创项目管理系统实战详解

发布时间:2026/10/20 3:18:14 来源:尧图企业网站定制
前后端分离的大学生创新创业训练项目管理系统技术栈是SpringBoot Vue MyBatis MySQL。这种组合在校园项目、毕设、实验室管理系统里极其常见但常见不代表简单项目要管申报、评审、中期检查、结题验收还有学生、老师、评审专家、管理员多种角色权限逻辑和业务状态流很容易写成一团乱麻。这篇文章就把我做这套系统的完整思路、踩过的坑和部署过程全盘写出来从数据库设计到前后端代码再到服务器上线一步一步说清楚不管是拿来交作业还是应付真实业务场景都够用。1. 大创项目管理系统到底要管什么需求复盘与方案选型1.1 核心需求拆解我说的大学生创新创业训练项目管理系统指的是高校里最常见的“大创”业务学生团队发起项目申报指导老师审核学院组织专家评审评审通过后立项接着是中期检查和结题验收。整个过程还涉及经费分配、成果登记、结题材料上传。听上去简单但只要真坐在电脑前写这套系统就会发现业务逻辑比想象中要拧巴。最大难点不是某一个页面多复杂而是状态流转和角色权限交织在一起。举个例子一个项目在“申报中”时学生能改表单一旦进入“评审中”学生就应该只能看不能动评审专家只能看到分配给自己评审的项目管理员要能随时调整项目阶段。这些规则如果没有在设计阶段想清楚后面写接口就是一坨改来改去的补丁。所以我在做需求复盘时先把用户分成了四类学生、指导老师、评审专家、系统管理员。学生做申报和进度维护老师审批自己指导的项目专家负责评审打分和填写意见管理员负责账号管理、项目分配、阶段切换和数据统计。整个系统围绕这四个角色去设计菜单、路由和接口权限后面开发会省心很多。1.2 为什么选择前后端分离 SpringBoot Vue这个选择我不是拍脑袋定的。早期很多同类系统是JSPServlet那套页面和后端代码混在一起改个按钮都要重新编译部署更别提多人协作时前后端互相阻塞。前后端分离的核心好处在于前端和后端各干各的接口定义清楚后可以并行开发上线部署也能分开处理。再聊技术选型。SpringBoot就是拿来快速搭后端的内置Tomcat打包成jar直接运行不用装外置容器这对写管理系统来说太舒服了。它的starter机制让依赖管理也变得非常简单加个Web、加个MyBatis、加个MySQL驱动几行配置就完事。Vue用来写前端页面组件化的思维特别适合页面复用。像项目列表、用户表单、评审弹窗这种东西写成组件后在不同页面里直接用代码量能压缩很大一截。MyBatis半自动化ORMSQL自己写控制力很强。大创系统里有很多多表联查和动态条件筛选比如按项目状态、按年份、按专业方向过滤MyBatis的where和if动态SQL写起来非常顺手。MySQL不用多说免费、资料多学校服务器随便装适合这种读多写少、事务不复杂的管理系统。1.3 完整功能模块清单我把自己最终实现的功能整理成了一个表这样你核对需求时候有对照模块名称主要功能涉及角色登录认证账号密码登录、JWT签发、退出登录全部用户管理用户信息维护、角色分配、账号启用禁用管理员项目管理项目申报、修改、提交审核学生项目审核指导老师审核、退回修改指导老师评审管理评审专家分配、在线打分、填写意见、评审汇总管理员、评审专家过程管理中期检查材料提交、检查结果登记学生、指导老师结题管理结题材料上传、结题审核学生、指导老师公告通知公告发布、列表查看管理员、全部数据统计项目状态统计、按学院分布统计管理员这套功能设计参考的是我自己接触过的实际业务流程。如果你的项目另有要求比如想加“经费报销”“成果转化”在这个框架上继续扩展接口和页面就行不会伤筋动骨。2. 数据库设计与核心表结构2.1 数据库设计原则数据库是一切业务的地基。大创系统这种业务我的建议是先画状态机再建表。为什么因为项目管理最核心的就是状态草稿、已提交、待老师审核、老师审核通过、待专家评审、评审通过立项、中期检查、待结题、已结题、已终止。每个状态对应哪些角色能做什么操作都应该是确定的。状态机画清楚之后再建表每张表会非常自然。我画了一张简单的项目状态流转图这里用文字描述方便你在文档里画 草稿 → 提交审核 → 指导老师审核不通过则退回草稿→ 学院推荐 → 专家评审不通过则终止→ 立项 → 中期检查 → 结题申请 → 已结题。基于这个流程核心表就浮现出来了用户表、角色表、项目表、项目成员表、评审记录表、中期检查表、结题申请表、附件表、公告表。接着还要考虑一对多关系一个项目有多名学生成员一个专家评审多个项目一个项目有多次阶段记录。2.2 用户表和角色表用户表不用设计太复杂大创系统里的用户字段无非是学号/工号、姓名、密码、邮箱、电话、所属学院、类型。密码存的是BCrypt加密后的密文不要用MD5明文打死也不要。角色这块我当时没有单独弄一套复杂的RBAC权限表用户-角色-菜单-权限4张表而是精简成了用户表中的user_type字段。为什么因为大创系统的角色是固定的不像企业系统需要动态配置权限点。用user_type 1/2/3/4分别表示学生、指导老师、评审专家、管理员拦截器里判断角色代码即可代码简单维护成本低。如果你希望角色能灵活扩展也可以把user_role、role_permission做成标准RBAC结构但那种复杂度对这个项目来说有点过度设计。用到真实企业级多部门场景时再考虑。2.3 项目主表和成员表项目主表是系统的核心字段大致如下CREATE TABLE project ( id BIGINT PRIMARY KEY AUTO_INCREMENT, project_code VARCHAR(32) COMMENT 项目编号, project_name VARCHAR(200) COMMENT 项目名称, project_year YEAR COMMENT 立项年度, category VARCHAR(50) COMMENT 项目类别如创新训练、创业训练, level VARCHAR(20) COMMENT 国家级/省级/校级, status TINYINT COMMENT 项目状态 0草稿 1待审核 2审核通过 3评审中 4已立项 5中期检查 6待结题 7已结题 8已终止, student_id BIGINT COMMENT 项目负责人学生ID, teacher_id BIGINT COMMENT 指导教师ID, budget DECIMAL(10,2) COMMENT 预算金额, intro TEXT COMMENT 项目简介, create_time DATETIME, update_time DATETIME );项目成员单独一张project_member表因为一个项目可能有3到5个学生不能在一个字段里逗号分隔存ID后面查成员、统计人数都会非常痛苦。成员表里带一个is_leader字段用来区分谁是大创系统的项目负责人。2.4 评审表和阶段表评审表要和项目表关联设计时有个建议一份项目在多个专家之间评审所以评审表主体是expert_id project_id review_status score opinion review_time一个专家对同一项目只保留一条评审记录后面加一个唯一索引(project_id, expert_id)防止重复提交。中期和结题我合并思路做成了一张阶段表project_stage_log里面有stage_type区分中期/结题/变更content存内容提交说明file_ids存关联附件status记录该阶段是否通过。这个设计的好处是未来新增“年度进展报告”之类的阶段不需要再加一张表改枚举值就行。附件我单独抽出来一张file_info表存文件名、存储路径、上传人、关联业务类型和业务ID。这样项目申报材料、中期报告、结题报告都能复用同一个上传下载逻辑不用每个模块都写一套文件管理。3. 后端实现细节SpringBoot MyBatis 怎么搭才顺手3.1 后端工程结构与依赖我的后端工程结构大致如下分包清晰排查问题时顺着controller-service-mapper一条线找下去非常快src/main/java/com/xxx/innovate ├── controller # 接口层只做参数接收和结果返回 ├── service # 业务逻辑层事务都写在这层 ├── mapper # MyBatis数据访问层 ├── entity # 数据库实体 ├── dto # 前端交互用的数据对象 ├── vo # 视图对象比如项目详情返回给前端展示 ├── config # 配置类比如拦截器、跨域配置、CORS ├── common # 统一返回结果、常量、异常处理 └── util # 工具类比如JWT工具、文件上传工具pom.xml里最核心的依赖就这些dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependencyJWT我是用的java-jwt这个库比较轻量。你也可以用jjwt都行。密码加密用spring-security-crypto里的BCrypt工具但注意不要引入整套Spring Security否则拦截器配置和你的原始方式冲突处理不好会变成灾难。3.2 统一响应与全局异常处理前后端分离的项目接口返回格式必须统一。我定义了一个ResultT类里面就三个字段code0成功非0失败、message、data。所有controller一律返回这个对象。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 0; r.message success; r.data data; return r; } public static T ResultT error(String message) { ResultT r new Result(); r.code 500; r.message message; return r; } }全局异常处理用RestControllerAdvice把业务异常、参数校验异常、未知异常分开处理。比如参数校验失败返回400提示权限不足返回403提示避免直接把一堆堆栈信息丢给前端。3.3 登录认证与角色权限控制这个系统最容易被忽略又最重要的就是权限。我用的方案是JWT 拦截器流程用户登录成功后签发一个JWT把userId和userType写进token里。前端把token存到localStorage之后每个请求都在请求头带上Authorization: Bearer token。后端写一个拦截器拦截除登录接口外的所有请求解析token并校验有效期把用户信息放到ThreadLocal或Request attribute里供后续代码使用。在需要角色判断的接口里从用户信息里取出userType判断有没有权限。拦截器核心代码public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求否则前端跨域调用会被卡 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { throw new BusinessException(未登录或token缺失); } String token authHeader.substring(7); // 校验并解析token失败会抛出异常由全局异常处理器兜底 LoginUser user JwtUtil.parseToken(token); request.setAttribute(loginUser, user); return true; } }在WebMvcConfig里注册拦截器并排除登录接口、静态资源等不需要认证的路径。角色权限判断我写得比较简单在service层先getLoginUser()然后if判断。比如评审专家提交评审结果时先查出该项目是不是当前评审人的不是就抛异常。// 只有专家角色能调用 if (loginUser.getUserType() ! 3) { throw new BusinessException(无评审权限); } ReviewRecord record reviewMapper.selectByProjectAndExpert(projectId, loginUser.getUserId()); if (record null) { throw new BusinessException(该项目未分配给你评审); }3.4 项目申报接口的完整实现链拿最核心的“学生申报项目”接口举个例子。前端传来的不是单张表数据而是一个项目的基本信息、成员列表和上传的文件ID列表需要在一个事务里同时操作三张表。Controller层接收一个ProjectApplyDTOPostMapping(/project/apply) public Result? apply(RequestBody ProjectApplyDTO dto, HttpServletRequest request) { LoginUser user (LoginUser) request.getAttribute(loginUser); projectService.applyProject(dto, user); return Result.success(null); }Service层就是业务逻辑重点Transactional(rollbackFor Exception.class) public void applyProject(ProjectApplyDTO dto, LoginUser user) { // 1. 校验当前用户是学生 if (user.getUserType() ! 1) { throw new BusinessException(仅学生可申报项目); } // 2. 插入项目主表状态初始为草稿或待提交 Project project new Project(); BeanUtils.copyProperties(dto, project); project.setStudentId(user.getUserId()); project.setStatus(0); project.setCreateTime(new Date()); projectMapper.insert(project); // 3. 插入项目成员 if (dto.getMembers() ! null) { for (MemberDTO member : dto.getMembers()) { ProjectMember pm new ProjectMember(); pm.setProjectId(project.getId()); pm.setStudentName(member.getStudentName()); pm.setStudentNo(member.getStudentNo()); projectMemberMapper.insert(pm); } } // 4. 关联上传的文件更新文件表的业务ID字段 if (dto.getFileIds() ! null) { for (Long fileId : dto.getFileIds()) { fileMapper.updateBiz(fileId, PROJECT, project.getId()); } } }注意Transactional(rollbackFor Exception.class)很多新手写事务时不指定rollbackFor导致自定义业务异常不会触发回滚项目主表插入了成员表插入失败后留下半截数据就很尴尬。状态流转的update操作就更要小心。比如“提交审核”这个操作updateProjectStatus要带条件where id ? and status 0防止前端连续点击导致状态错乱。UPDATE project SET status 1, submit_time NOW() WHERE id #{projectId} AND status 0如果影响行数为0就说明状态已经不是草稿服务端直接报“当前状态不允许提交”。3.5 文件上传与下载文件上传我用的SpringBoot自带的MultipartFile存储路径放在服务器某个目录而不是存进数据库的blob字段。生产环境文件一旦多起来数据库会非常臃肿。FileService里面把文件保存到本地磁盘或OSS数据库只存元数据。上传接口接收MultipartFile返回文件的ID和访问地址前端再把ID关联到项目表单里。这里有个容易被坑的点如果你使用Nginx部署前端并在后端接口路径用/api做前缀文件上传后要保证Nginx也能正确代理到后端跨域和路径重写都要提前测好。4. 前端实现细节Vue Element UI 怎么组织不失控4.1 前端项目结构与接口封装前端我用Vue CLI创建的项目配合Element UI做组件库。代码组织分了views页面、components通用组件、router路由、store状态管理、api接口封装、utils工具。API封装是前端工程化的重点。我把所有接口请求统一封装成一个模块axios实例带上baseURL和拦截器。请求拦截器里自动把localStorage的token塞进header响应拦截器里统一处理业务code非0则弹出错误提示401则跳转登录页。这样页面代码里就不会到处写token获取逻辑了。// api/request.js import axios from axios import { Message } from element-ui import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 0) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } else { Message.error(网络异常) } return Promise.reject(error) } ) export default requestbaseURL用/api而不是写死后端地址是为了配合开发环境的proxy和上线后的Nginx统一代理。开发时在vue.config.js里配置代理到localhost:8080上线时Nginx把/api转发到后端服务这样前端代码不需要改动。4.2 路由与前端登录守卫路由表按角色拆分是这套系统的关键。管理员能看到用户管理、项目分配菜单学生看到申报和我的项目专家看到评审任务。我把路由分为公共路由、学生路由、教师路由、专家路由、管理员路由然后在router.beforeEach里根据登录用户角色动态拼接。一个简单的路由守卫实现router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() return } if (!token) { next(/login) return } const user JSON.parse(localStorage.getItem(userInfo) || {}) // 校验路由meta中是否包含当前用户角色 if (to.meta to.meta.roles to.meta.roles.indexOf(user.userType) -1) { next(/403) return } next() })路由守卫只做页面层的权限遮挡真正的数据安全还是后端拦截器。前端隐藏菜单只是为了体验不能信任前端权限这是我一直强调的原则。4.3 核心页面实现申报表单、评审打分、进度看板项目申报表单我用的动态表单加表单校验。项目成员数量不固定用一个v-for循环渲染成员输入行用户可以增加和删除行。表单校验规则通过rules配置。Element UI的el-form配合el-form-item校验错误信息会自动展示在输入框下方交互体验比较标准。评审打分页是评审专家最重要的操作界面。专家从待评审列表点击项目名弹出一个详情抽屉里面展示项目资料和成员信息底部是评分星星组件和意见textarea。提交评审时调接口后端校验项目是否分配给该专家评分范围0到100分。进度看板是给学生和老师看的我把项目状态阶段做成一个步骤条el-steps根据status字段显示当前在第几步。同时旁边展示每个阶段的材料提交情况如果还没提交中期材料就显示红色的待办标签。这种可视化非常受用指导老师一打开就能看出哪些学生项目卡在哪个环节。4.4 前后端接口约定前后端联调时接口约定最好文档化。我常用的格式是列表GET /api/project/my我的项目列表返回分页数据list totalPOST /api/project/apply提交项目申报POST /api/project/{id}/submit提交审核GET /api/review/tasks获取我的评审任务列表POST /api/review/do提交评审结果PUT /api/user/{id}更新用户信息所有分页接口约定返回{ code:0, data: { list: [], total: 0 } }结构。所有日期时间返回字符串格式yyyy-MM-dd HH:mm:ss避免时间格式解析的差异。数字状态用数字而非字符串做枚举前端映射表维护在常量文件里。5. 完整部署教程从零到能跑的每一步5.1 环境准备清单部署这套系统你需要以下基础环境JDK 1.8或11建议JDK 8兼容性最稳Maven 3.6以上Node.js 14或16Vue CLI5要求Node高版本但过高的Node20可能遇兼容问题建议14/16MySQL 5.7或8.0Nginx 1.20以上放前端静态资源并充当反向代理一个能访问到服务器的Linux环境CentOS7或Ubuntu20.04都行我这边用的是CentOS7开发阶段你也可以在Windows本机跑后端用IDEA启动前端用VSCode启动devServer数据库用Navicat管理。上线部署可以参考下一小节的Linux流程。5.2 数据库初始化把项目里的sql文件上传到服务器MySQL或者直接本地导入后导出线上导入mysql -uroot -p innovate_db.sql导入成功后确认所有表已经创建。然后修改后端配置文件application.ymlserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/innovate_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/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-days: 75.3 后端打包与启动后端项目在服务器上编译打包。如果没有源码构建环境可以在本地用IDEA打包成jar再上传如果服务器装了Maven和JDK直接执行mvn clean package -DskipTests打包后target目录下会生成一个innovate-server-1.0.0.jar。上传到服务器建议放到/opt/innovate/目录然后启动nohup java -jar innovate-server-1.0.0.jar --spring.profiles.activeprod server.log 21 用tail -f server.log查看启动日志看到Started ... in ... seconds就是成功了。检查端口netstat -ntlp | grep 80805.4 前端构建与Nginx配置前端本地执行依赖安装和构建npm install --registryhttps://registry.npmmirror.com npm run build构建产物在dist/目录。上传dist目录到服务器比如放到/opt/innovate/dist。然后修改Nginx配置我是这样写的server { listen 80; server_name 你的服务器IP或域名; # 前端静态文件根目录 root /opt/innovate/dist; index index.html; # 解决vue router刷新404问题 location / { try_files $uri $uri/ /index.html; } # 后端接口代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传文件访问路径 location /files/ { alias /opt/innovate/upload/; } }注意location /api/里的proxy_pass写法末尾带不带斜杠差别很大。proxy_pass http://127.0.0.1:8080/;会把/api/project/list重写为/project/list转发给后端所以后端的controller不需要写/api前缀前端请求时带/api即可。改完配置执行nginx -t检查语法然后nginx -s reload生效。访问服务器IP如果看到登录页恭喜部署成功了。5.5 部署后验证流程我的习惯是部署完按一条链路走通访问登录页用管理员账号登录。创建一个测试学生账号退出管理员用测试学生登录提交一个项目。切管理员账号把项目分配一个评审专家。登录评审专家账号进入评审页面打分。检查附件上传下载是否正常。查看数据库里项目状态是否跟着变化。这几步全部通过说明前后端连通、数据库读写、JWT认证、权限控制、文件存储都没有问题。6. 常见问题与避坑记录6.1 后端常见故障问题一数据库连接时区报错。MySQL 8.x的驱动对时区敏感如果连接串不加上serverTimezoneAsia/Shanghai启动时会报SQLException: The server time zone value ...。另一个隐藏问题是MySQL连接时区设置与JVM时区不一致导致接口返回的时间差8小时。统一在JDBC配置里加参数解决。问题二MyBatis的Mapped Statement绑定找不到。十有八九是以下几点XML文件的namespace和Mapper接口全套类名不一致接口方法名和XML的id不一致mapper-locations路径写错。排查时打开SpringBoot的debug日志确认加载了哪些XML文件。问题三jar包启动后端口被占。先netstat -ntlp | grep java看清楚端口PID再kill -9杀掉旧进程。注意用了nohup启动的进程如果直接关终端窗口进程也可能被hang up正确应使用nohup方式启动这样才能后台持续运行。6.2 前端常见故障问题一跨域请求失败。开发环境用vue.config.js的proxy线上环境用Nginx的proxy_pass只要统一请求前缀到/api就不会有跨域问题。如果直接让前端请求后端8090端口后端就要写CorsFilter。我这边选择的是Nginx代理理由是不需要在代码里开放跨域线上更安全。问题二打包部署后刷新404。Nginx配置里必须加try_files $uri $uri/ /index.html;因为Vue是History路由刷新时会向服务器请求真实路径服务器没有这个路径就返回404了。不加这段话用户点浏览器刷新按钮就会白屏。问题三build出来的资源路径不对。Vite或Vue CLI默认资源引用是绝对路径/js/xxx.js部署在域名子路径下就会404。需要在vue.config.js里设置publicPath: ./或部署时配置对应的base。6.3 业务逻辑层的隐藏问题并发重复提交。用户双击“提交审核”按钮前端往往来不及禁用按钮就可能触发两次请求。解决方法是后端update语句里带状态条件或者利用数据库唯一索引兜底。我前后端都做了按钮loading禁用 后端状态条件更新。事务失效。这是最常见但是最隐蔽的坑。Transactional默认只对RuntimeException回滚如果你在事务方法里catch了异常没往外抛事务也不会回滚。还有一点事务方法和被调用方法不能在同一个类里用内部调用否则动态代理不生效事务等于摆设。SQL注入。MyBatis使用${}拼接参数会存在注入风险能用#{}就用#{}。动态排序字段、动态表名这种特殊场景非要使用${}时必须在前端传入前做白名单校验。我在项目里排序字段做了一张允许排序的白名单Map非法字段直接忽略。7. 扩展思路与我的实际心得这套系统交付到后面其实已经不只是“能跑就行”。我把它的能力继续扩展了几处这里分享出来给你作参考。第一消息通知机制。大创系统里最有价值的体验提升就是状态变化要主动通知。学生在项目被退回时如果没人提醒拖上一周才看到就是事故。我给项目加了一张message表状态变更时除了更新状态同时插入一条待办消息。前端顶部导航栏显示未读消息红点点击进入消息中心。实现成本不大但对用户好感提升非常巨大。第二导出Excel。管理员经常要导出项目汇总表给学院存档。我用EasyExcel写了一个导出接口把项目列表、负责人、指导老师、状态导出成一个Excel文件前端通过window.location跳转下载接口即可。这个功能毕设答辩时非常能加分。第三评审汇总自动化。多个专家评同一项目时后台需要一个项目评审总分汇总功能。我写了一个SQL按项目ID分组求平均分。注意这里不要用AVG对NULL进行错误处理评审状态不等于已评审的专家记录要过滤掉否则汇总会被空记录拉低。最后说一下我最深的体会写大创系统这种业务系统60%的时间都在处理“边界情况”。数据库边界状态非法值、外键关联、权限边界越权操作、接口边界参数为空、格式错误、部署边界路径不对、端口被占。真正把边界全处理干净系统才谈得上可用。如果你是从零开始复现这套项目我是真心建议先把项目状态机画清楚、把角色权限矩阵列明白哪怕多花半天时间后端的实现速度会快一两倍。我后来把这套系统的源码、SQL脚本和部署文档整理成了一个完整包。如果你想直接拿来改改当课设或者毕设重点不是跑起来而是把每个模块背后的设计原因讲清楚答辩的时候这反而是最亮的加分点。

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

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

免费获取报价 →
↑