简介本资源是一套完整的校园兼职信息管理平台源码面向Java全栈初学者与课程设计者解决高校学生、商家与管理员三方协同发布、接单、评价兼职任务的数字化需求。项目采用SSMSpringSpringMVCMyBatis后端架构与Vue前端框架基于MySQL 5.7数据库实现配套完整说明文档适合作为毕业设计、实训项目或Spring Boot过渡学习的实战案例。压缩包共863个文件涵盖142个Java业务逻辑类、156个JS交互脚本、52个Vue组件、46个CSS样式文件及79个GIF动效资源整体25.91MB预览可见.bat启动脚本、Vue静态组件备份及Eclipse项目配置文件体现工程可直接导入IDEA或Eclipse运行。已有186人学习下载提供从首页展示、多角色权限管理管理员/学生/商家、兼职全流程闭环发布→接单→咨询→评价到后台系统维护的全功能模块源码含SQL建表语句与Navicat兼容结构开箱即用。1. 为什么校园兼职系统成了 Java SSM Vue 技术栈的“练手标靶”它真能跑通真实业务闭环你不是在写一个“学生管理系统”的简化版而是在模拟一个微型劳动力市场——学生发布简历、企业发布岗位、管理员审核资质、双方在线签约、工时自动统计、工资按周结算。这个场景里SSMSpring SpringMVC MyBatis不是教科书里的三层分层示例而是要扛住每学期开学前 3 天的简历洪峰单日新增 2000 条、处理多角色并发操作学生改简历、HR 刷岗位、管理员封禁黑中介、还要和微信服务号做 OAuth2 登录对接Vue 不是只渲染个表格得实现带拖拽排序的岗位收藏夹、实时更新的“已投递/待面试/已录用”状态机、以及用 ECharts 渲染的“各学院兼职参与率热力图”。项目源码之所以被高频检索Java SSM Vue 校园兼职系统 源码根本原因在于它把 Web 开发中 80% 的典型痛点——权限粒度控制学生只能看自己简历HR 只能管自己发布的岗位、文件上传安全学生传 PDF 简历、企业传营业执照扫描件、事务一致性录用成功必须同步扣减岗位剩余人数 生成合同 PDF 发送站内信——全塞进一个可本地启动的工程里。适合刚学完 SSM 和 Vue 基础、但没碰过真实业务流的开发者用它当“脚手架”去补全缺失的工程化肌肉记忆而不是当玩具跑个 Hello World。2. 从零拉起项目用 Maven Vue CLI 搭建双端骨架避开“npm install 卡死在 node-sass”这类玄学翻车2.1 后端用 Spring Boot 替代原始 SSM XML 配置5 分钟初始化干净的 Maven 工程原始 SSM 项目常依赖大量 XML 配置spring-context.xml、spring-mvc.xml、mybatis-config.xml新手极易因命名空间写错或路径拼错导致ClassNotFoundException。我一般会直接用 Spring Boot 重构后端——它本质仍是 SSM 内核Spring IoC 容器 SpringMVC 调度 MyBatis ORM但用SpringBootApplication注解替代 XML用application.yml统一管理配置。这样既保留 SSM 的技术认知链又规避了 XML 的脆弱性。# 创建 Spring Boot 工程JDK 8 Maven 3.6 curl -s https://start.spring.io/starter.tgz \ -d dependenciesweb,jdbc,mysql,mybatis,thymeleaf,lombok \ -d javaVersion1.8 \ -d packagingjar \ | tar -xzvf -提示thymeleaf是为后续集成前端静态资源预留的模板引擎非必须但比纯 REST API 更易调试页面跳转lombok用Data省去 POJO 的 getter/setter避免手写出错。关键配置application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/campus_parttime?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false # 开发期关闭模板缓存改 HTML 立即生效 mybatis: mapper-locations: classpath:mapper/*.xml # 指定 Mapper XML 文件位置 configuration: map-underscore-to-camel-case: true # 自动转换下划线字段名user_name → userName逻辑说明map-underscore-to-camel-case: true是血泪经验——MySQL 表字段习惯用user_idJava 实体类用userId不开启此配置会导致查询结果无法映射到对象属性且无报错静默失败排查耗时超 2 小时。2.2 前端用 Vue CLI 4.5 创建标准项目禁用 ESLint 以绕过新手语法误判原始 Vue 项目常因vue-loader版本不匹配或webpack.config.js手动配置错误导致Cannot find module vue-template-compiler。Vue CLI 是官方封装的成熟方案开箱即用。# 全局安装 Vue CLI需 Node.js ≥ 12.0 npm install -g vue/cli4.5.15 # 创建项目禁用 ESLint避免新手被 Missing semicolon 卡住 vue create campus-vue --default cd campus-vue npm install axios element-ui echarts --save关键修改main.jsimport Vue from vue import App from ./App.vue import router from ./router import store from ./store import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css import axios from axios // 设置全局请求 baseURL指向后端 Spring Boot 接口 axios.defaults.baseURL http://localhost:8080/api Vue.prototype.$http axios // 挂载到 Vue 原型组件内可直接 this.$http.get() Vue.use(ElementUI) Vue.config.productionTip false new Vue({ router, store, render: h h(App) }).$mount(#app)参数说明baseURL必须设为http://localhost:8080/apiSpring Boot 默认端口 8080而非/api——后者是相对路径在vue-router的 history 模式下会被解析成http://localhost:8080/api但实际后端接口是http://localhost:8080/api/xxx若设成/api会导致跨域请求失败浏览器实际发的是http://localhost:8080/api/xxx但开发服务器未代理。2.3 双端联调用 Vue CLI 的 proxy 配置解决跨域拒绝用CrossOrigin硬编码Spring Boot 默认不允许跨域若前端直接访问http://localhost:8080/api/login浏览器会拦截。常见错误方案是给每个 Controller 方法加CrossOrigin(origins http://localhost:8081)但一旦部署到 Nginx域名变更就得改代码。正确做法是前端开发期用 proxy 代理生产期由 Nginx 统一处理。修改vue.config.js根目录新建module.exports { devServer: { port: 8081, // 前端端口设为 8081避免和后端 8080 冲突 proxy: { /api: { target: http://localhost:8080, // 代理到后端 changeOrigin: true, // 修改 Origin 请求头 pathRewrite: { ^/api: // 把 /api/login 重写为 /login后端无需识别 /api 前缀 } } } } }逻辑说明pathRewrite是关键——前端请求this.$http.get(/api/user/info)proxy 会转发为http://localhost:8080/user/info后端 Controller 只需写GetMapping(/user/info)无需加RequestMapping(/api)。这样前后端路径解耦部署时只需改 Nginx 的location /api { proxy_pass http://backend; }即可。3. 核心业务落地用 MyBatis Plus 替代原生 MyBatis把“学生投递岗位”流程压缩到 3 个 SQL原始 SSM 项目中MyBatis 的 XML 写法冗长select idselectByUserIdSELECT * FROM apply WHERE user_id #{userId}/select且易漏写resultMap导致字段映射失败。MyBatis Plus 是增强版通过注解自动生成 CRUD让业务代码聚焦在“做什么”而非“怎么查”。3.1 数据库设计三张核心表决定系统健壮性上限表名关键字段业务约束避坑点t_userid,username,password,role(1学生,2企业,3管理员),status(0禁用,1启用)username唯一索引role必须用 tinyint(1)不能用 varchar(student)—— 否则 MyBatis Plus 的TableField无法自动映射枚举t_jobid,title,company_id,salary_min,salary_max,deadline,status(0下架,1招聘中)company_id外键关联t_user.iddeadline类型必须是datetime不能用date—— 否则 MyBatis Plus 的TableField(fill FieldFill.INSERT)无法自动填充当前时间t_applyid,user_id,job_id,status(0已投递,1已面试,2已录用,3已拒),apply_time联合唯一索引(user_id, job_id)status字段必须设默认值0否则插入时 MySQL 报错Field status doesnt have a default value注意所有表必须加ENGINEInnoDB否则外键约束无效字符集统一用utf8mb4避免微信昵称含 emoji存不进去。3.2 “学生投递岗位”全流程MyBatis Plus 的 Service 层写法实体类标注TableName和TableIdimport com.baomidou.mybatisplus.annotation.*; TableName(t_apply) public class Apply { TableId(type IdType.AUTO) // 主键自增 private Long id; TableField(user_id) private Long userId; TableField(job_id) private Long jobId; private Integer status; // 0已投递 TableField(fill FieldFill.INSERT) // 插入时自动填充 private LocalDateTime applyTime; // getter/setter 省略Lombok Data 已生成 }Mapper 接口继承BaseMapperimport com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface ApplyMapper extends BaseMapperApply { // 无需写 XMLBaseMapper 已提供 insert(), selectById(), updateById() 等方法 }Service 实现事务控制 状态校验import org.springframework.transaction.annotation.Transactional; Service public class ApplyService { Autowired private ApplyMapper applyMapper; Autowired private JobService jobService; // 用于检查岗位是否有效 Transactional // 保证“扣减岗位剩余数”和“创建投递记录”原子性 public boolean applyJob(Long userId, Long jobId) { // 1. 检查岗位是否存在且状态为招聘中 Job job jobService.getById(jobId); if (job null || !job.getStatus().equals(1)) { return false; // 岗位已下架 } // 2. 检查学生是否已投递过该岗位防重复提交 QueryWrapperApply wrapper new QueryWrapper(); wrapper.eq(user_id, userId).eq(job_id, jobId); if (applyMapper.selectCount(wrapper) 0) { return false; // 已投递 } // 3. 创建投递记录 Apply apply new Apply(); apply.setUserId(userId); apply.setJobId(jobId); apply.setStatus(0); applyMapper.insert(apply); // MyBatis Plus 自动生成 INSERT SQL // 4. 更新岗位投递数可选用于统计 job.setApplyCount(job.getApplyCount() 1); jobService.updateById(job); return true; } }逻辑说明Transactional是硬性要求——若applyMapper.insert()成功但jobService.updateById()失败数据库会回滚投递记录避免数据不一致。QueryWrapper替代了手写 SQL且类型安全字段名写错编译期报错而非运行时报Unknown column user_id。3.3 前端投递按钮用 Element UI 的el-button触发 Axios 请求!-- StudentJobList.vue -- template div el-button typeprimary sizemini clickhandleApply(job.id) :disabledjob.applyStatus 1 !-- 已投递则禁用 -- {{ job.applyStatus 1 ? 已投递 : 投递 }} /el-button /div /template script export default { methods: { handleApply(jobId) { this.$http.post(/apply, { jobId }) // POST /api/apply .then(res { if (res.data.success) { this.$message.success(投递成功请关注面试通知) // 刷新当前岗位状态 this.jobList.find(j j.id jobId).applyStatus 1 } else { this.$message.error(res.data.msg || 投递失败) } }) .catch(err { this.$message.error(网络错误请重试) }) } } } /script参数说明/apply是前端路由路径经 proxy 代理后实际请求http://localhost:8080/api/apply后端 Controller 只需RestController RequestMapping(/api) public class ApplyController { Autowired private ApplyService applyService; PostMapping(/apply) public Result apply(RequestBody MapString, Object params) { Long jobId Long.valueOf(params.get(jobId).toString()); Long userId getCurrentUserId(); // 从 JWT Token 解析后续章节讲 boolean success applyService.applyJob(userId, jobId); return success ? Result.success() : Result.fail(投递失败); } }4. 避坑指南SSM Vue 校园兼职系统里最常踩的 5 个深坑附定位命令和修复命令4.1 现象登录后跳转到/home页面空白控制台报TypeError: Cannot read property name of undefined原因Vue Router 的beforeEach全局守卫中to.matched[0].components.default试图访问未定义的组件通常因路由懒加载路径写错如component: () import(/views/Home.vue)中/views/Home.vue文件实际不存在或大小写不符Home.vuevshome.vue。解决# 1. 检查文件路径是否存在Linux/macOS ls -l src/views/Home.vue # 2. 若文件存在检查 import 路径是否与文件系统大小写完全一致 # 3. 临时关闭懒加载用同步 import 定位 // router/index.js // component: () import(/views/Home.vue) // 注释掉 component: () require(/views/Home.vue).default // 改为此行错误会直接抛出4.2 现象MyBatis 查询返回null但日志显示 SQL 正确执行且 MySQL Workbench 查有数据原因实体类字段名与数据库列名不匹配且未开启map-underscore-to-camel-case: true导致user_name列无法映射到userName属性。解决# 1. 查看 MyBatis 日志确认字段名在 application.yml 中加 logging: level: com.example.mapper: debug # 替换为你的 mapper 包名 # 2. 日志中若出现 Mapped to null立即检查 application.yml 的 mybatis.configuration.map-underscore-to-camel-case # 3. 强制指定 resultMap终极方案 !-- UserMapper.xml -- resultMap idUserResultMap typecom.example.entity.User id propertyid columnid/ result propertyuserName columnuser_name/ !-- 显式映射 -- /resultMap select idselectById resultMapUserResultMapSELECT * FROM t_user WHERE id #{id}/select4.3 现象学生上传简历 PDF后端用MultipartFile接收但file.isEmpty()恒为true原因前端FormData未正确 append 文件或enctypemultipart/form-data缺失。解决// 正确写法Vue 组件中 const formData new FormData() formData.append(file, this.file) // this.file 是 input[typefile] 的 files[0] // ❌ 错误formData.append(file, this.file.name) —— 传的是文件名字符串不是文件对象 this.$http.post(/upload/resume, formData, { headers: { Content-Type: multipart/form-data } // ⚠️ 此行必须删除浏览器会自动设置 boundary })提示headers中删掉Content-Type否则boundary丢失后端无法解析 multipart。4.4 现象ECharts 图表不渲染控制台报Error: Initialize failed: invalid dom原因图表容器div idchart/div在 Vuemounted()时 DOM 尚未挂载完成尤其用了v-if或异步数据。解决template div v-ifchartData.length 0 !-- 确保数据加载后再渲染 -- div idchart stylewidth: 600px; height: 400px;/div /div /template script export default { mounted() { this.$nextTick(() { // 确保 DOM 更新完毕 const chartDom document.getElementById(chart) const myChart echarts.init(chartDom) myChart.setOption({ /* 配置 */ }) }) } } /script4.5 现象Spring Boot 启动报Caused by: java.lang.ClassNotFoundException: javax.servlet.Filter原因JDK 11 移除了javax.servlet包而旧版spring-boot-starter-web依赖的 Servlet API 版本过低。解决!-- pom.xml 中升级 web starter -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version2.7.18/version !-- 用 2.7.x 最终版兼容 JDK 8~17 -- /dependency !-- 并显式添加 Servlet API -- dependency groupIdjakarta.servlet/groupId artifactIdjakarta.servlet-api/artifactId scopeprovided/scope /dependency5. 权限系统实战用 JWT Spring Security 实现“学生/企业/管理员”三级隔离拒绝硬编码 if-else校园兼职系统的权限不是简单的“登录后可见”而是学生能看到所有岗位但只能投递企业只能管理自己发布的岗位管理员能删岗位、封用户、导出数据。用if (user.getRole() 1)判断角色是反模式——它把权限逻辑散落在各 Controller难以维护且易漏。5.1 JWT 认证流程登录成功后返回 token前端存 localStorage每次请求带Authorization: Bearer xxx后端生成 token使用jjwt-api!-- pom.xml -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency// AuthService.java public String login(String username, String password) { User user userMapper.selectOne(new QueryWrapperUser().eq(username, username)); if (user ! null passwordEncoder.matches(password, user.getPassword())) { // 生成 JWTpayload 包含用户 ID 和角色 String token Jwts.builder() .setSubject(user.getId().toString()) // 用户 ID .claim(role, user.getRole()) // 角色1/2/3 .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) // 24小时 .signWith(SignatureAlgorithm.HS256, campus-secret-key) // 密钥 .compact(); return token; } return null; }前端登录后存储并设置请求头// Login.vue this.$http.post(/login, { username, password }) .then(res { const token res.data.token localStorage.setItem(token, token) // 持久化存储 // 后续所有请求自动带 token axios.defaults.headers.common[Authorization] Bearer token })5.2 Spring Security 配置用PreAuthorize注解实现方法级权限控制Configuration EnableWebSecurity EnableGlobalMethodSecurity(prePostEnabled true) // 启用 PreAuthorize public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态 .and() .authorizeRequests() .antMatchers(/login, /register, /public/**).permitAll() // 公共接口放行 .anyRequest().authenticated(); // 其他请求需认证 } Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); // 自定义过滤器解析 token 并设 SecurityContext } }关键过滤器JwtAuthenticationFilterpublic class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token getTokenFromRequest(request); if (token ! null jwtUtil.validateToken(token)) { Long userId jwtUtil.getUserIdFromToken(token); User user userService.getById(userId); UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken(user, null, Collections.singleton(new SimpleGrantedAuthority(ROLE_ user.getRole()))); SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } private String getTokenFromRequest(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (bearerToken ! null bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }5.3 用PreAuthorize控制具体接口让权限规则可读、可测、可审计RestController RequestMapping(/api/job) public class JobController { Autowired private JobService jobService; // 学生和管理员都能查看岗位列表 GetMapping PreAuthorize(hasRole(1) or hasRole(3)) // ROLE_1 或 ROLE_3 public Result listJobs() { return Result.success(jobService.list()); } // 企业只能发布自己的岗位 PostMapping PreAuthorize(hasRole(2)) // 仅 ROLE_2企业 public Result publishJob(RequestBody Job job, Authentication auth) { User user (User) auth.getPrincipal(); job.setCompanyId(user.getId()); // 自动绑定企业 ID jobService.save(job); return Result.success(); } // 管理员才能删除岗位 DeleteMapping(/{id}) PreAuthorize(hasRole(3)) // 仅 ROLE_3管理员 public Result deleteJob(PathVariable Long id) { jobService.removeById(id); return Result.success(); } }参数说明hasRole(1)对应ROLE_1Spring Security 会自动在角色名前加ROLE_前缀PreAuthorize在方法执行前校验不满足直接返回 403无需 Controller 内写if判断。5.4 前端路由守卫根据 token 中的 role 字段动态生成菜单// router/index.js const routes [ { path: /student/home, component: () import(/views/student/Home.vue), meta: { role: [1] } }, { path: /company/job, component: () import(/views/company/JobManage.vue), meta: { role: [2] } }, { path: /admin/user, component: () import(/views/admin/UserManage.vue), meta: { role: [3] } } ] router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token) return next(/login) try { const payload JSON.parse(atob(token.split(.)[1])) // 解析 JWT payload const userRole payload.role // 得到 1,2,3 if (to.meta.role !to.meta.role.includes(userRole)) { next(/403) // 无权限跳转 } else { next() } } catch (e) { next(/login) } })注意JWT payload 解析只是前端校验真正的权限控制必须在后端PreAuthorize前端校验仅用于体验优化避免点击无权限菜单后才弹 403。6. 生产部署 checklist把本地能跑的 SSM Vue 项目变成可交付的 Docker 镜像附 Nginx 配置模板一个能本地跑通的校园兼职系统离真正可用还差最后一步它得能在服务器上稳定运行且支持多人同时访问。手工配 Tomcat MySQL Nginx 极易出错Docker 是目前最可靠的交付方案。我坚持用 Docker Compose 一键启停而不是写一堆docker run命令——因为真实运维中你永远需要同时管理后端、前端、数据库三个容器。6.1 后端打包用 Maven 插件生成可执行 jar跳过 war 包部署Spring Boot 的spring-boot-maven-plugin能把整个应用含内嵌 Tomcat打成一个 fat jar这是 Docker 部署的基石。!-- pom.xml -- build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration executabletrue/executable !-- 生成可执行 jar -- /configuration /plugin /plugins /build打包命令mvn clean package -Dmaven.test.skiptrue # 输出 target/campus-parttime-0.0.1-SNAPSHOT.jar6.2 前端构建用npm run build生成静态文件交给 Nginx 托管cd campus-vue npm run build # 输出 dist/ 目录含 index.html 和 static/ 资源6.3 Docker Compose 编排定义 mysql、backend、nginx 三个服务新建docker-compose.ymlversion: 3.8 services: mysql: image: mysql:8.0 container_name: campus-mysql environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: campus_parttime ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql # 初始化数据库脚本 backend: image: openjdk:8-jre-slim container_name: campus-backend depends_on: - mysql ports: - 8080:8080 volumes: - ./target/campus-parttime-0.0.1-SNAPSHOT.jar:/app.jar command: java -Dspring.profiles.activedocker -jar /app.jar environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/campus_parttime?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: 123456 nginx: image: nginx:alpine container_name: campus-nginx depends_on: - backend ports: - 80:80 volumes: - ./dist:/usr/share/nginx/html - ./nginx.conf:/etc/nginx/nginx.conf关键点说明backend容器中SPRING_DATASOURCE_URL的 host 是mysqlDocker 内部服务名不是localhostcommand: java -Dspring.profiles.activedocker -jar /app.jar指定 Spring Boot 使用docker配置文件新建src/main/resources/application-docker.ymlspring: datasource: url: ${SPRING_DATASOURCE_URL} username: ${SPRING_DATASOURCE_USERNAME} password: ${SPRING_DATASOURCE_PASSWORD} redis: host: redis # 若用 Redis同理设为服务名6.4 Nginx 配置反向代理 API 请求静态资源直出nginx.confevents { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; upstream backend { server campus-backend:8080; # 指向 backend 容器 } server { listen 80; server_name localhost; # 前端静态资源 location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; # 支持 Vue Router history 模式 } # API 请求代理到后端 location /api/ { proxy_pass http://backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } }提示try_files $uri $uri/ /index.html是 Vue Router history 模式的救命配置——否则刷新/student/home页面会 404。6.5 一键部署命令从零到上线只需 3 条命令# 1. 构建后端 jar确保 Maven 已配置好 mvn clean package -Dmaven.test.skiptrue # 2. 构建前端 dist确保已安装 Node.js cd campus-vue npm install npm run build cd .. # 3. 启动全部服务自动拉取镜像、创建网络、启动容器 docker-compose up -d # 验证访问 http://your-server-ip 看到登录页即成功最后说句实在话我带过 12 届实习生90% 的人第一次部署失败不是因为代码写错而是卡在docker-compose up后docker logs campus-backend看到Communications link failure——其实只是SPRING_DATASOURCE_URL里写的localhost没改成mysql。所以现在我强制要求新人在docker-compose.yml里把所有环境变量都用${}包起来再写个env.example文件里面清清楚楚写着SPRING_DATASOURCE_URLjdbc:mysql://mysql:3306/xxx。这种细节才是从“能跑”到“能交”的分水岭。希望帮到你。本文还有配套的精品资源点击获取