SpringBoot+Vue智慧云办公系统毕设全攻略:权限、数据库与部署
2026/9/16 4:09:27 网站建设 项目流程

简介:面向毕业设计场景的智慧云办公系统完整项目,采用SpringBoot与Vue前后端分离架构,覆盖中小企业日常办公中的员工管理、菜单权限、薪资管理等核心模块,适合计算机专业毕业生或Java全栈学习者参考,可用于课程设计、毕业答辩或实际项目二次开发。压缩包只有801KB,却包含790个文件,其中245个Java源码实现后端业务逻辑,37个Vue组件搭建前端页面,222个XML配置支撑持久层映射,另有2个SQL数据库脚本、yml环境配置及项目文档,目录结构清晰,便于按模块阅读和快速定位。项目整合了Swagger2自动生成接口文档、RabbitMQ自动发送入职邮件、Redis缓存员工信息等企业级特性,完整呈现从前端页面到后端服务的实现思路,配合项目文档可快速搭建运行环境。已有431人学习浏览,既能支撑毕业设计答辩,也可作为SpringBoot与Vue整合的实战范例。

1. SpringBoot+Vue 智慧云办公系统:毕设选题的真实边界

智慧云办公系统,听起来像是一个要把考勤、审批、会议、公告、日程全部塞进一个浏览器窗口的“大工程”。但放到毕业设计这个场景里,它真正的交付物并不是一个能对抗钉钉或飞书的产品,而是一套结构完整、逻辑自洽、能演示核心流程的 Web 应用。技术栈固定为 SpringBoot + Vue,意味着一端承担 RESTful API 和数据持久化,一端负责路由、状态和交互渲染。你从网上拿到源码后,第一件事不该是急着跑起来,而是先把模块边界划清楚:用户与权限、工作流、日程与会议、公告通知、文件分享,这几个子域里只要做透两三个,再配合数据库设计和项目文档,就已经能支撑一场不错的答辩。

这条技术路线的优势在于生态成熟:SpringBoot 提供自动配置和起步依赖,Vue 的前后端分离模式让接口文档成为沟通枢纽,而 MySQL 或 PostgreSQL 则负责把业务对象落到表里。标题里出现的“源码+数据库+项目文档”其实是三个可独立审查的交付物,评审老师大概率会分别检查代码能不能编译、数据库脚本能不能导入、文档能不能解释清设计决策。所以本文按“后端实现 → 前端交互 → 数据库设计 → 文档编写 → 部署验证”的顺序,把一套可落地的方案拆开讲,重点覆盖权限模型、动态路由、表结构、Swagger 集成和前后端联调这些高频关注点。适合正在做类似选题、想快速补全代码细节和踩坑经验的在校生,也适合帮师弟师妹把关代码质量的工程岗读者。

2. SpringBoot 后端:从零搭建可答辩的办公系统服务端

2.1 Maven 依赖与服务分层,先让项目能跑

拿到任何 SpringBoot 源码,第一步永远是确认依赖树和 JDK 版本。毕业设计最常见的场景是本地用 JDK 8 或 JDK 11,如果导入项目后遇到“springboot 版本太高”导致的不兼容报错,优先把pom.xml里的spring-boot-starter-parent降到 2.7.x,同时保证 Maven 编译器插件源和目标版本一致。

后端目录建议按“入口 → 配置 → 控制层 → 服务层 → 数据访问层”拆分:

com.example.office ├── OfficeApplication.java ├── config │ ├── SecurityConfig.java │ ├── CorsConfig.java │ └── SwaggerConfig.java ├── controller ├── service ├── mapper ├── entity ├── dto └── utils

核心依赖只需要五个起步模块。spring-boot-starter-web负责 MVC 和 Tomcat,spring-boot-starter-security做认证与授权,mybatis-plus-boot-starterspring-boot-starter-data-jpa二选一,mysql-connector-j提供驱动,lombok减少实体类样板代码。这里需要注意:MyBatis-Plus 在代码生成、分页插件和逻辑删除上比 JPA 更直观,大多数毕设源码选它是有道理的。

2.1.1 application.yml 里的四个关键配置
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/office_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 100MB max-request-size: 200MB mvc: pathmatch: matching-strategy: ant_path_matcher mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: your-long-secret-key-please-change-in-production expiration: 86400000

serverTimezone=Asia/Shanghai是 MySQL 8 必配项,否则日期字段会报错。matching-strategy: ant_path_matcher是 SpringBoot 2.6 以后必须为 Swagger 兼容而开启的配置。logic-delete-field让你的删除接口变成UPDATE语句,这在毕设答辩演示时能避免“删了就找不到”的尴尬。

2.1.2 分页与统一返回体

办公系统列表页极多,分页不是可选项。MyBatis-Plus 里配置一个分页插件,然后在 Controller 返回IPage<T>即可。统一返回体建议定义为{ code, message, data, timestamp },枚举里至少包含200400401500。很多网上的源码会把返回体写成Map,短期能凑合,但答辩问“为什么 data 可能是对象也可能是数组”时容易答不上来,所以一开始就定义泛型类。

2.2 Spring Security + JWT:登录、鉴权与 RBAC 权限模型

智慧云办公系统的核心不是“考勤”而是“权限”——谁能看到哪份审批单、谁能发布公告、谁能编辑日程。用 Spring Security 做认证授权,配合 JWT 承担无状态会话,是这类项目最常见的组合。

2.2.1 登录接口与 JWT 工具类
@Service public class AuthService { @Autowired private UserMapper userMapper; public String login(LoginDTO dto) { // 1. 根据用户名查询用户 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, dto.getUsername()); User user = userMapper.selectOne(wrapper); if (user == null) { throw new BusinessException("用户不存在"); } // 2. 密码加密校验,BCrypt 不用解密,只比对哈希 if (!BCrypt.checkpw(dto.getPassword(), user.getPassword())) { throw new BusinessException("密码错误"); } // 3. 生成 token,把用户 id 和角色编码塞进 payload Map<String, Object> claims = new HashMap<>(); claims.put("userId", user.getId()); claims.put("role", user.getRoleCode()); return JwtUtil.createToken(claims); } }

逻辑要点:密码用BCryptPasswordEncoder加密存储,登录成功只生成 token,不保存服务端会话。claims里放用户基本标识,不要放手机号等隐私信息。JWT 过期时间建议 24 小时,expiration以毫秒为单位设置,前端拿到 token 后应存在localStoragePinia/Vuex中,而不是sessionStorage,否则刷新页面会白屏。

2.2.2 SecurityConfig 把接口权限收口
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .cors().and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/auth/login", "/doc.html", "/webjars/**", "/v3/api-docs").permitAll() .antMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(new JwtAuthenticationEntryPoint()); http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }

这里有三点值得展开:一是STATELESS表示服务端不使用 Session,每次请求通过过滤器解析 token;二是/api/auth/login必须放行,同时 Swagger 文档通常也要放行,否则调试接口时看着“401”发懵;三是hasRole("ADMIN")会自动拼前缀ROLE_,数据库里角色字段应存ROLE_ADMIN这样的值。毕业设计里,RBAC 做到“用户-角色-资源”三层就足够,不要强行引入组织维度,除非你要做多租户,那是另一个工作量。

2.2.3 自定义权限校验注解

除了 URL 级别的控制,办公系统还常见“数据权限”,比如普通员工只能查自己的审批单。可以在 Service 层直接判断userId,可以用@PreAuthorize("hasRole('ADMIN')")做方法级控制。更优雅的做法是自定义一个@RequirePermission("sys:approval:audit")注解,配合 AOP 或 Spring Security 的GlobalMethodSecurityConfiguration使用。但毕设答辩不会要求你写中间件框架,只要能把 RBAC 的关系说清、把越权访问封死,就已经超过大多数源码质量。

2.3 审批/日程/公告模块的接口设计套路

这些业务模块在接口设计上高度相似,都是“增删改查 + 分页 + 条件过滤”。以审批模块为例,常见表是approval(主表)和approval_record(流转记录),接口大致是:

接口方法路径说明
发起审批POST/api/approval/create创建主单,初始状态为 PENDING
待办列表GET/api/approval/todo查询当前用户待审核的单子
审批通过POST/api/approval/approve修改状态,插入记录
审批驳回POST/api/approval/reject附带驳回原因
已办列表GET/api/approval/done历史记录分页

状态流转不要引入ActivitiFlowable这类重量级工作流引擎,它们的学习成本和控制反转会让源码变得难以解释。用一张status字段 + 状态机枚举就能应付 90% 的办公审批场景:

public enum ApproveStatus { PENDING(0), APPROVED(1), REJECTED(2); private final int value; }

把状态转换规则写在一个 Service 私有方法里,不接受任意跳转。这样后续写单元测试时只需要测“PENDING 只能变 APPROVED 或 REJECTED”这一条规则。公告和日程模块更简单,所有返回结果都需要带create_timeupdate_time两个审计字段,MyBatis-Plus 的MetaObjectHandler可以自动填充。

3. Vue 前端:从vue安装及环境配置到动态路由和视频预览

3.1 Vite 项目创建与环境变量配置

拿到源码后,前端部分最常见的执行顺序是:安装 Node.js → 配置 npm 镜像 → 安装依赖 → 启动 dev server。涉及“vue 安装依赖”报错时,多半是 Node 版本太高导致某些包编译失败,比如node-sass。解决方案是改用sass包,或者直接用npm install而不是npm ci,遇到 peer dependency 冲突时加--legacy-peer-deps

npm create vite@latest office-web -- --template vue cd office-web npm install npm install vue-router@4 pinia element-plus axios npm run dev

.env.development.env.production分别放不同环境的接口地址:

# .env.development VITE_API_BASE_URL=/api # .env.production VITE_API_BASE_URL=https://your-domain.com/api

在项目里通过import.meta.env.VITE_API_BASE_URL读取,Vite 会静态替换,因此不要把这些值驼峰混用。脚手架的vite.config.js里要配置代理,避免开发环境下跨域:

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

注意:代理只在开发服务器生效,部署时要用 Nginx 的location /api { proxy_pass ... }同源转发,否则就会出现“vue 路由刷新 404”和“跨域”双重问题。

3.2 登录态存储、Axios 封装与路由守卫

前端权限逃不过“路由守卫 + 接口拦截”。登录后把 token 存到localStorage,Axios 请求拦截器从localStorage取出 token 并塞进请求头:

// src/utils/request.js import axios from 'axios' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, 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 => { // 后端统一返回体 { code, message, data } const { code, message, data } = response.data if (code === 200) return data ElMessage.error(message) return Promise.reject(new Error(message)) }, error => { if (error.response.status === 401) { router.push('/login') } return Promise.reject(error) } )

这里的重点不是代码本身,而是“拦截器返回的层级”。很多新手在响应拦截器直接return response.data.data,导致业务代码拿到的对象层级对不上。建议统一return data,业务侧直接const list = await getApprovalList()得到的就是后端data字段内容。

路由守卫做三件事:未登录跳登录页、已登录访问登录页跳首页、根据用户角色过滤无权限路由。动态菜单路由一般依赖后端返回的菜单列表:

// router/index.js router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') } else if (token && to.path === '/login') { next('/') } else { next() } })

3.3 动态菜单与按钮权限指令

办公系统的菜单通常是“父级菜单 + 子菜单 + 按钮”三层。数据库里sys_menu表存目录、菜单、按钮三类,前端登录后通过/api/auth/menus获取,然后递归生成路由和侧边栏。按钮级权限用 Vue 自定义指令 v-permission:

// main.js app.directive('permission', { mounted(el, binding) { const required = binding.value const userPerms = store.getters.perms if (!userPerms.includes(required)) { el.parentNode?.removeChild(el) } } })

模板中使用<el-button v-permission="'sys:user:add'">新增</el-button>。注意 v-if 和指令的差异:v-if 是渲染前销毁,指令是渲染后删除 DOM,对于 Element Plus 组件,移除元素后 Vue 实例会泄漏,所以更稳妥的做法是封装一个<Auth>组件用v-if控制。这里直接告诉别人“我用了两种方案的对比,最终选择组件方式”,在答辩里是不错的亮点。

3.4 Vue 播放 M3U8:hls.js 的封装与常见坑

“vue播放m3u8”是近期检索量较高的需求。云办公系统里录播会议回放、培训视频会用到 HLS 流,<video>标签原生支持 MP4 但不支持 M3U8,所以要用hls.js

// components/HlsPlayer.vue <template> <video ref="videoEl" controls autoplay muted style="width: 100%"></video> </template> <script setup> import { ref, onMounted, onBeforeUnmount, watch } from 'vue' import Hls from 'hls.js' const props = defineProps({ src: { type: String, required: true } }) const videoEl = ref(null) let hls = null const play = () => { if (Hls.isSupported()) { hls = new Hls({ enableWorker: true, lowLatencyMode: true, fragLoadingMaxRetry: 6 }) hls.loadSource(props.src) hls.attachMedia(videoEl.value) hls.on(Hls.Events.ERROR, (event, data) => { if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: hls.startLoad(); break case Hls.ErrorTypes.MEDIA_ERROR: hls.recoverMediaError(); break default: hls.destroy(); break } } }) } else if (videoEl.value.canPlayType('application/vnd.apple.mpegurl')) { // iOS Safari 原生支持 videoEl.value.src = props.src } } onMounted(play) onBeforeUnmount(() => hls && hls.destroy()) watch(() => props.src, play) </script>

几个调试点:autoplay在 Chrome 下必须muted才生效,否则要用户手势触发;如果流地址不是 HTTPS 或者带跨域限制,会在 Network 里看到大片红色请求,需要在 Nginx 里加add_header Access-Control-Allow-Origin *fragLoadingMaxRetry设置太小会导致弱网下黑屏但不报错,这是最常见的线上问题。

4. 数据库设计:直接决定“智慧”二字的表结构

4.1 用户-角色-部门的关联模型

办公系统的数据库设计几乎都是同一个骨架:sys_usersys_rolesys_user_rolesys_deptsys_user_dept。不要省掉用户与角色的中间表,否则后续做多角色切换会非常痛苦。下面是精简过的核心表结构:

CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(100) NOT NULL COMMENT 'BCrypt加密', `real_name` varchar(50) DEFAULT NULL, `email` varchar(100) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `status` tinyint DEFAULT 1 COMMENT '1正常 0禁用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted` tinyint DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

字段类型上注意三点:用户名用varchar(50)够了,但密码哈希要留varchar(100);所有表都加deleted字段配合 MyBatis-Plus 逻辑删除;时间字段统一datetime而不是timestamp,后者只能存到 2038 年,虽然数据库课程设计可能没人追究,但答辩老师看到细节会加分。

角色表、部门表与用户表的关联:

表名作用关键字段
sys_role角色定义role_code, role_name
sys_user_role用户角色关联user_id, role_id
sys_dept部门树parent_id, dept_name
sys_user_dept用户部门关联user_id, dept_id

注意部门是树形结构,parent_idbigint,根节点为0。查询部门列表时用递归 CTE(MySQL 8)或 Java 端递归,如果源码里用了一条for循环拼树,那也说得通,但性能问题要在文档里说明“数据量小于 1 万时无影响”。

4.2 办公业务表:审批单、公告、日程、会议

业务表的数量取决于你想做几个模块。审批单表里不要把所有业务字段揉在一起,而是用一张主表和一张扩展表:

CREATE TABLE `oa_approval` ( `id` bigint NOT NULL AUTO_INCREMENT, `approval_no` varchar(32) NOT NULL COMMENT '审批单号', `user_id` bigint NOT NULL COMMENT '发起人', `type` varchar(20) NOT NULL COMMENT '请假/报销/用章', `title` varchar(200) NOT NULL, `content` text, `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待审 1通过 2驳回', `current_node` varchar(50) NOT NULL DEFAULT 'manager' COMMENT '当前节点编码', `create_time` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

审批流转记录表oa_approval_record存放每一步的操作人、动作、意见和时间。这样“你的审批单走到哪了”就能通过一条 SQL 查出来:

SELECT ar.*, u.real_name FROM oa_approval_record ar LEFT JOIN sys_user u ON ar.operator_id = u.id WHERE ar.approval_id = #{approvalId} ORDER BY ar.create_time DESC;

公告模块注意附件存储,不要用blob存文件,数据库只存附件路径attachment_url,文件放在本地磁盘或对象存储。日程模块要处理重复日程(每日、每周、每月),但毕设里一般忽略循环事件,只做单次日程,否则会引入日历计算复杂度。会议模块如果做会议室预约,就要考虑锁粒度问题:同一时间段会议室不能重复预约,可以通过INSERT时在会议表中建立“会议室 + 开始时间”唯一索引来规避并发冲突。

4.3 初始化数据与增量脚本管理

交付的数据库脚本至少要有三个层级:表结构schema.sql、初始数据data.sql、示例业务数据sample.sql。很多源码把管理员账号直接写死在上面的data.sql里:

INSERT INTO sys_user (username, password, real_name, status) VALUES ('admin', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ql33YbG0MUQ6Lz0Y7hA9w2e', '系统管理员', 1);

这段哈希值是对应admin123的 BCrypt 密文,你在写文档时一定要说明初始账号密码。数据同步工具在团队协作时很有用,但毕设单人开发不需要,你只需要一个能在 MySQL 里执行无误的.sql文件。注意导出时选择“转储 SQL 格式”而不是“Excel”,否则编码和引号会让导入直接失败。

5. 项目文档:从开题到答辩的写作顺序与模板

5.1 文档结构:需求、设计、测试、部署各写什么

项目文档是“数据库+源码”之外最容易拉开差距的交付物。标准结构通常是六章:绪论、需求分析、系统设计、详细设计与实现、系统测试、总结展望。但写作顺序千万不要从第一章开始,而是先写需求分析中的用例表,再写数据库表结构说明,最后回头补绪论。需求分析部分要有“系统角色”和“用例描述”:

用例编号用例名称角色描述
UC-001用户登录所有人输入用户名密码,校验通过后进入系统
UC-002发起审批普通员工选择审批类型,填写内容并提交
UC-003审批操作部门经理看到待办,通过或驳回并填写意见

系统设计部分重点画两张图:SpringBoot 的“模块架构图”和“数据库 ER 图”。不用画太复杂,图是用来给答辩老师快速建立印象的,模块拆到controller/service/mapper即可。详细设计里每一个功能的描述模板是:功能界面 → 核心代码片段 → 流程图 → 关键 SQL。代码片段不要直接复制整个类,只贴核心逻辑,否则查重率和阅读体验都会崩溃。

5.2 接口文档用 Swagger 还是离线 Markdown?

如果源码里集成了springdoc-openapiknife4j,接口文档可以自动生成,但离线文档对毕设仍然必要。最省事的方案是导出一份 Markdown 或 Word 版的接口说明表:

## 2.1 登录接口 请求路径:POST /api/auth/login 请求参数: | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | username | string | 是 | 用户名 | | password | string | 是 | 密码 | 响应示例: { "code": 200, "message": "成功", "data": { "token": "xxx" } }

写每个接口时,把“鉴权方式”写清楚。是Header携带Authorization: Bearer token,还是登录接口白名单?这些信息在 Swagger 页面能看到,但离线文档里要单列一节说明。否则答辩时老师问“这个接口需要带 token 吗”你很容易卡壳。

5.3 查重与细节:图表、术语、参考文献

文档查重要注意数据库设计部分的“字段说明”、“ER 图注释”这类内容与其他网站高度雷同。解决方法是把表结构表格化,用自定义注释描述业务含义,避免照抄《软件工程》教材的原话。参考文献必须引用真实的 SpringBoot 官方文档、Vue Router 官方文档和至少两篇期刊论文,没有引用过就不写。最后,插入页码、目录自动生成、图表编号连续这些基础排版属于拿分点,但这部分不是给导师看的,是给教务查格式用的。

6. 部署验证与源码交付:让老师看到就能跑

6.1 本地联调:后端跨域与前端代理

前后端分离项目的联调问题基本集中在跨域和接口路径。开发环境用 Vite 代理解决跨域,生产环境用 Nginx。如果后端的CorsConfig里允许了localhost:5173,但前端代理到localhost:8080后仍然报 CORS,先看浏览器请求的Origin头是不是localhost:5173。一旦走了代理,请求变成了同源,后端其实不需要配 CORS。两者同时存在的配置可能导致 “preflight 请求返回 403”。

6.2 打包与部署:SpringBoot 的 jar 和 Vue 的 dist

# 后端打包 mvn clean package -DskipTests # 会生成 target/office-system-0.0.1-SNAPSHOT.jar # 前端打包 npm run build # 生成 dist/ 目录,把静态文件拷贝到 Nginx/html 下

后端启动用nohup java -jar office-system.jar --spring.profiles.active=prod &。配置文件里数据库密码不要用明文写死,用环境变量注入。把 jar 和后端的mapper/目录打包成一个 zip 时,要注意application.yml里的mybatis-plus.mapper-locations路径是否与 JAR 内路径匹配,常见的报错是Invalid bound statement (not found),那就是mapper.xml没被编译到 classpath 下。

6.3 自检清单:账号、缓存、端口

交付前一遍完整走查,按清单逐项打勾比盲目操作更稳:

  • 初始账号:确认data.sql中的 BCrypt 密码与文档中的密码一致,通常提供一个admin/admin123
  • 缓存一致性:如果用了 Redis 做菜单缓存,检查 Redis 是否作为必选组件,若老师没有 Redis 环境,源码要能退化为“缓存开关关闭”
  • 端口约定:前端 vite 默认 5173,后端 8080,Nginx 80;已存在的冲突在application.ymlnginx.conf中保持一致
  • 空白页排查:Vue 打包后出现“布局异常”,先用router是否开启createWebHistory判断,历史模式需要后端配置try_files $uri $uri/ /index.html;
  • 文档核对:数据库脚本导出的 SQL 里若有CREATE DATABASE,在执行时先确认连接到的实例名

交付时最好附带一个README.md,写清楚“环境要求、启动顺序、演示账号、常见错误”。这条不是凑字数,而是让评审老师用最短时间跑起来的关键。启动顺序是 MySQL → 后端 → 前端,「先npm installnpm run dev」这样的步骤对熟悉技术的人来说是废话,但写下来是对非技术背景的管理人员友好,也是项目完整性的体现。唯一的额外建议是:在 final pass 时把 console.log 清掉,你的源码在老师眼里会干净很多。

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

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

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

立即咨询