首页 / 资讯中心 / 文章详情

Java+小程序+MySQL全栈就业平台实战项目(含RBAC权限与部署避坑指南)

Java+小程序+MySQL全栈就业平台实战项目(含RBAC权限与部署避坑指南) ★ FEATURED ARTICLE
简介本资源是一套完整的大学生就业服务平台微信小程序毕业设计项目面向计算机专业本科生及Java全栈初学者解决校园招聘场景下学生求职、企业招聘与管理员协同管理的全流程需求。压缩包共1270个文件涵盖231张界面图标png/svg、175个小程序逻辑脚本js、136个Vue组件vue、127个后端业务类java、96个配置与数据接口定义json、86个样式文件wxss/wxml及2个建表SQL脚本完整包含前后端源码、MySQL数据库文件与部署说明总大小20.02MB。已有90人学习下载适合课程设计参考、毕设快速启动与小程序SpringBootMySQL技术栈综合实践。读者可直接导入Eclipse/IDEA与微信开发者工具运行含bat一键安装/启动脚本、多端登录态管理、简历投递与面试邀请闭环、企业类型分类管理等典型功能模块目录结构规范注释较完整便于理解分层架构与跨端交互逻辑。1. 这不是又一个“毕设模板”而是一套能真跑起来的就业平台闭环Java后端 微信小程序 MySQL全栈可部署源码你是不是也见过太多标着“微信小程序毕业设计”的压缩包点开一看全是空文件夹、半截 Vue 组件、连数据库表都建不全的 SQL 脚本这次不一样——这个.zip包里塞进的是某高校计算机专业真实交付过、学生答辩用过的完整就业平台系统。它不是 Demo不是截图生成器而是从1-install.bat到微信开发者工具扫码预览全链路可走通的工程学生端能投简历、查面试邀请企业端能发岗位、收简历、发邀约管理员后台能增删改查所有角色数据连留言板和用户反馈都带审核流。技术栈干净利落JDK1.8 Spring Boot 2.1.x非 Spring Cloud 黑匣子、MySQL 5.7非高版本兼容陷阱、Vue 2.6非 Vue3 语法玄学小程序端用的是原生 WXML JS没上 UniApp 抽象层调试时变量在哪、请求在哪、报错在哪一眼可见。适合两类人一是大四学生想拿个稳过、能讲清每行逻辑的毕设项目二是刚转前端/全栈的新手想拆一个“有业务闭环、有真实权限分层、有前后端交互细节”的轻量级生产级参考。2. 搭建前必须确认的三件事环境边界、文件结构真相与启动脚本逻辑提示别急着双击1-install.bat先看清它到底干了什么否则后续2-run.bat报错时你会在ClassNotFoundException和Access denied for user之间反复横跳。2.1 环境不是“支持”而是硬性依赖为什么 JDK1.8 和 MySQL 5.7 不可替换这个项目用的是 Spring Boot 2.1.18.RELEASE从pom.xml中spring-boot.version可确认它底层依赖 Spring Framework 5.1.x而 Spring Framework 5.1.x 明确要求 JDK8不支持 JDK9 的模块化变更。如果你强行用 JDK11 启动会在org.springframework.context.support.AbstractApplicationContext.refresh()阶段抛出java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException—— 因为 JAXB 在 JDK11 中被彻底移除而本项目未添加jakarta.xml.bind:jakarta.xml.bind-api依赖补丁。MySQL 方面项目 SQL 文件如db.sql中大量使用datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP语法。MySQL 5.6 对ON UPDATE CURRENT_TIMESTAMP支持不完整仅允许一个TIMESTAMP字段而本项目student表、enterprise表均含多个带该修饰的字段MySQL 5.7 才真正支持多字段CURRENT_TIMESTAMP自动更新。若你用 MySQL 8.0虽语法兼容但默认开启的caching_sha2_password认证插件会导致 Java 连接报Unknown initial character set index 255错误——这不是代码问题是驱动版本与服务端认证协议不匹配。所以结论很明确必须用 JDK1.8u202或以上 8u 版本 MySQL 5.7.32推荐。别信“我改个配置就能跑”这是血泪经验换来的边界。2.2 解压后看到的.bak文件不是垃圾而是关键调试线索解压目录里一堆*.vue.bak、*.css.bak比如update-password.vue.bak、main.css.bak。新手常直接删掉结果运行时发现密码修改页样式错乱、按钮点击无响应。其实这些.bak是开发过程中保留的旧版组件备份而当前实际加载的是update-password.vue无.bak后缀——但注意update-password.vue文件本身可能为空或只有一行!-- TODO: 实现密码修改逻辑 --。此时.bak文件就是唯一可用的完整实现。我们来验证# 进入小程序源码目录通常是 /miniprogram/pages/update-password/ ls -la # 输出类似 # update-password.js # update-password.json # update-password.wxml # update-password.wxss # update-password.vue.bak ← 这才是真·业务逻辑所在打开update-password.vue.bak你会发现它包含完整的 Vue 实例定义、wx.request调用/api/student/updatePassword接口、表单校验规则含两次输入比对、成功后跳转逻辑。而update-password.vue无 bak可能是空文件或占位符。这不是疏忽是典型的学生项目迭代痕迹用.bak存活代码主文件留作重构入口。2.31-install.bat不是“一键安装”而是三步原子操作解压、建库、导入双击1-install.bat前请用记事本打开它看本质别用 IDE 直接执行echo off echo 正在解压依赖jar包... 7z x lib\dependencies.7z -olib\temp nul echo 正在创建数据库... mysql -uroot -proot -e CREATE DATABASE IF NOT EXISTS job_platform CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; echo 正在导入SQL... mysql -uroot -proot job_platform db\db.sql echo 安装完成请检查控制台是否出现 ERROR。 pause关键点有三它用7z解压lib\dependencies.7z到lib\temp说明项目未走 Maven 仓库拉取而是把spring-boot-starter-web、mybatis-spring-boot-starter等 jar 包全部打进了压缩包。如果你没装 7-Zip 或环境变量未配这一步会静默失败后续2-run.bat启动时直接报NoClassDefFoundError。数据库名硬编码为job_platform字符集强制utf8mb4支持 emoji但账号密码写死为-uroot -proot。如果你的 MySQL root 密码不是root或用了其他账号必须手动改 bat 文件里的-proot部分。db.sql是完整建库脚本但注意它不包含初始化数据如管理员账号admin/123456。初始化数据在另一个文件db\init-data.sql中而1-install.bat并未执行它——这是故意留的“安全阀”避免部署时自动创建高权限账号。所以正确流程是① 修改1-install.bat中 MySQL 密码为你的真实密码② 确保已安装 7-Zip 并加入 PATH③ 手动执行mysql -uroot -p你的密码 job_platform db\init-data.sql补全基础账号。3. 后端启动失败的五大高频现象从端口冲突到 MyBatis 映射断裂3.1 现象2-run.bat执行后控制台闪退日志末尾只有INFO o.s.b.w.e.t.TomcatServletWebServerFactory : Tomcat initialized with port(s): 8080 (http)无Started Application in X seconds原因Tomcat 7 默认端口 8080 被占用常见于 Skype、另一套 Java 项目、甚至某些国产软件。Spring Boot 启动时无法绑定端口但未显式报错而是静默退出。解决① 查端口占用netstat -ano | findstr :8080记下 PID② 查进程名tasklist | findstr PID③ 杀进程taskkill /f /pid PID④ 或改端口在src/main/resources/application.yml中加server: port: 8081注意改端口后小程序端utils/request.js中的baseUrl也要同步改为http://localhost:8081否则所有请求 404。3.2 现象控制台报Caused by: java.sql.SQLException: Access denied for user rootlocalhost原因application.yml中数据库配置写死为spring: datasource: url: jdbc:mysql://localhost:3306/job_platform?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8 username: root password: root但你的 MySQL 服务未启用rootlocalhost账号或密码不对。解决① 登录 MySQLmysql -u root -p② 创建专用账号更安全CREATE USER job_userlocalhost IDENTIFIED BY Job2024; GRANT ALL PRIVILEGES ON job_platform.* TO job_userlocalhost; FLUSH PRIVILEGES;③ 修改application.ymlusername: job_user password: Job20243.3 现象启动成功但访问http://localhost:8080/login返回 404或http://localhost:8080/api/student/login返回{code:500,msg:系统异常}原因MyBatis 的 XML 映射文件路径或命名不匹配。本项目采用Mapper XML方式非注解StudentMapper.java接口需对应StudentMapper.xml且 XML 中namespace必须与接口全限定名一致。检查src/main/resources/mapper/StudentMapper.xmlmapper namespacecom.job.mapper.StudentMapper select idlogin resultTypecom.job.entity.Student SELECT * FROM student WHERE phone #{phone} AND password #{password} /select /mapper若StudentMapper.java在com.job.dao.StudentMapper包下则namespace必须改为com.job.dao.StudentMapper否则 Spring Boot 启动时不会注册该 Mapper调用时抛Invalid bound statement (not found)。验证方法启动后查看日志中是否有Mapped Statements collection:后列出com.job.mapper.StudentMapper.login。没有即为映射失败。3.4 现象小程序登录时提示 “request:fail timeout” 或 “request:fail net::ERR_CONNECTION_REFUSED”原因微信开发者工具默认不允许http请求iOS/Android 小程序强制https但开发者工具可本地调试http。但本项目utils/request.js中baseUrl写的是http://localhost:8080而开发者工具运行时localhost指向的是开发者工具所在电脑的 localhost不是你后端服务所在的电脑。解决① 确保后端服务与开发者工具在同一台电脑② 在开发者工具顶部菜单栏 → 详情 → 本地设置 → 勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”③ 若后端在另一台机器如公司服务器baseUrl改为服务器 IPhttp://192.168.1.100:8080并确保服务器防火墙放行 8080 端口。3.5 现象管理员登录后台http://localhost:8080/admin后左侧菜单空白F12 查看 Network/api/admin/menu接口返回{code:401,msg:未登录}原因Spring Security 配置中/api/admin/**路径被拦截但登录接口/api/admin/login未放行导致登录请求本身就被 401 拦截。检查com.job.config.SecurityConfig.javaOverride protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(/api/admin/login).permitAll() // 必须有 .antMatchers(/api/admin/**).authenticated() .and() .formLogin().disable(); }若.antMatchers(/api/admin/login).permitAll()这一行缺失登录请求就会被 Security 拦截永远无法建立 session。修复补上该行并重启后端。4. 小程序端核心功能落地从首页轮播图到简历投递的四步链路4.1 首页轮播图不是静态图而是动态加载的 Banner 管理系统首页index/index.wxml中轮播组件swiper indicator-dots autoplay interval3000 duration500 swiper-item wx:for{{banners}} wx:keyid image src{{item.imgUrl}} modeaspectFill bindtapgoToBannerDetail>onLoad() { this.getBanners(); }, getBanners() { wx.request({ url: getApp().globalData.baseUrl /api/banner/list, method: GET, success: res { if (res.data.code 200) { this.setData({ banners: res.data.data }); } } }); }后端接口/api/banner/list对应BannerController.javaGetMapping(/list) public Result list() { ListBanner banners bannerService.list(); // 查询 banner 表 return Result.success(banners); }关键点banner表在db.sql中已建好但初始无数据。你需要手动插入INSERT INTO banner (id, title, imgUrl, linkUrl, sort, status) VALUES (1, 2024春招启动, https://example.com/banner1.jpg, /pages/recruit/recruit, 1, 1);linkUrl字段存的是小程序页面路径非 URL/pages/recruit/recruit即跳转到招聘列表页。status1表示启用否则不显示。4.2 招聘信息列表分页 关键词搜索 企业筛选的完整实现招聘信息页recruit/recruit.wxml使用scroll-view实现上拉加载scroll-view scroll-y bindscrolltolowerloadMore lower-threshold50 view wx:for{{jobs}} wx:keyid classjob-item bindtapgoToDetail>loadMore() { if (this.data.loading || this.data.hasMore false) return; this.setData({ loading: true }); const params { page: this.data.page 1, limit: 10, keyword: this.data.keyword, enterpriseId: this.data.enterpriseId }; wx.request({ url: getApp().globalData.baseUrl /api/job/page, data: params, method: GET, success: res { const jobs res.data.data.records; this.setData({ jobs: this.data.jobs.concat(jobs), page: params.page, hasMore: jobs.length 10 }); } }); }后端/api/job/page接口使用 MyBatis-Plus 的Page分页GetMapping(/page) public Result page(RequestParam Integer page, RequestParam Integer limit, RequestParam(required false) String keyword, RequestParam(required false) Long enterpriseId) { PageJob jobPage new Page(page, limit); QueryWrapperJob wrapper new QueryWrapper(); if (StrUtil.isNotBlank(keyword)) { wrapper.like(title, keyword).or().like(requirement, keyword); } if (enterpriseId ! null) { wrapper.eq(enterprise_id, enterpriseId); } wrapper.orderByDesc(create_time); IPageJob result jobService.page(jobPage, wrapper); return Result.success(result); }注意keyword搜索同时匹配title职位名和requirement任职要求这是学生项目里少有的合理搜索设计。4.3 简历投递一次请求完成「查简历状态 发送投递记录 更新企业通知」三件事投递按钮在job-detail/job-detail.wxmlbutton bindtapapplyJob classbtn-apply disabled{{applied}}立即投递/buttonapplyJob方法applyJob() { const jobId this.data.job.id; wx.request({ url: getApp().globalData.baseUrl /api/apply/submit, method: POST, data: { jobId: jobId }, header: { Authorization: wx.getStorageSync(token) }, // 携带学生 token success: res { if (res.data.code 200) { wx.showToast({ title: 投递成功, icon: success }); this.setData({ applied: true }); } } }); }后端ApplyController.submit()是事务性操作Transactional PostMapping(/submit) public Result submit(RequestBody ApplyDTO dto) { // 1. 检查学生是否已投递该职位 QueryWrapperApply applyWrapper new QueryWrapper(); applyWrapper.eq(student_id, getCurrentStudentId()) .eq(job_id, dto.getJobId()); if (applyService.count(applyWrapper) 0) { return Result.error(您已投递过该职位); } // 2. 创建投递记录 Apply apply new Apply(); apply.setStudentId(getCurrentStudentId()); apply.setJobId(dto.getJobId()); apply.setStatus(1); // 1-待处理 applyService.save(apply); // 3. 更新企业未读通知数 Job job jobService.getById(dto.getJobId()); Enterprise enterprise enterpriseService.getById(job.getEnterpriseId()); enterprise.setNoticeCount(enterprise.getNoticeCount() 1); enterpriseService.updateById(enterprise); return Result.success(); }亮点用Transactional保证三步原子性NoticeCount字段让企业端首页右上角小红点实时更新这是真实业务感的体现。4.4 我的收藏跨表关联查询的轻量级实现“我的收藏”页my-collection/my-collection.wxml展示收藏的职位view wx:for{{collections}} wx:keyid classcollection-item view classjob-title{{item.job.title}}/view view classjob-meta{{item.job.enterpriseName}} · {{item.job.salary}}/view button bindtapcancelCollect>getCollections() { wx.request({ url: getApp().globalData.baseUrl /api/collection/list, header: { Authorization: wx.getStorageSync(token) }, success: res { if (res.data.code 200) { // 后端已连表查出 job 信息前端无需二次请求 this.setData({ collections: res.data.data }); } } }); }后端CollectionController.list()使用 MyBatis 的resultMap连表!-- CollectionMapper.xml -- resultMap idCollectionWithJob typecom.job.entity.Collection id propertyid columnc.id/ result propertycreateTime columnc.create_time/ association propertyjob javaTypecom.job.entity.Job id propertyid columnj.id/ result propertytitle columnj.title/ result propertyenterpriseName columne.name/ result propertysalary columnj.salary/ /association /resultMap select idlistWithJob resultMapCollectionWithJob SELECT c.*, j.title, j.salary, e.name as enterpriseName FROM collection c LEFT JOIN job j ON c.job_id j.id LEFT JOIN enterprise e ON j.enterprise_id e.id WHERE c.student_id #{studentId} ORDER BY c.create_time DESC /select优势一次 SQL 查出所有需要字段避免 N1 查询对学生项目而言是性能意识的体现。5. 管理员后台权限体系解析RBAC 模型的极简落地与三个绕不开的坑5.1 权限控制不是靠 Session ID而是基于 JWT Token 的无状态鉴权管理员登录/api/admin/login成功后后端返回{ code: 200, data: { token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTcxNzYwMzIwMH0.abc123..., admin: { id: 1, username: admin, realName: 系统管理员 } } }此token由JwtUtil.generateToken(admin.getUsername())生成算法为 HS256密钥硬编码在JwtUtil.javaprivate static final String SECRET job_platform_admin_secret_key_2024;后续所有/api/admin/**请求必须在 Header 中携带Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...JwtFilter.java拦截请求解析 token 获取用户名再查admin表确认权限。为什么不用 Session因为管理员后台是纯前端HTMLJS无服务端渲染Session 依赖 Cookie而跨域请求前端域名 vs 后端域名下 Cookie 默认不发送。JWT 通过 Header 传递天然规避跨域限制。5.2 菜单权限不是写死而是数据库驱动的动态路由管理员登录后前端通过/api/admin/menu获取菜单GetMapping(/menu) public Result menu() { Long adminId getCurrentAdminId(); ListMenu menus menuService.getMenusByAdminId(adminId); return Result.success(menus); }menuService.getMenusByAdminId()执行 SQLSELECT DISTINCT m.* FROM menu m INNER JOIN role_menu rm ON m.id rm.menu_id INNER JOIN admin_role ar ON rm.role_id ar.role_id WHERE ar.admin_id ? ORDER BY m.sort ASCmenu表结构idnamepathcomponenticonsortparentId1个人中心/admin/profileProfile.vueuser102学生管理/admin/studentStudentList.vueusers20component字段值如StudentList.vue被前端router/index.js动态 importconst routes [ { path: /admin, component: () import(/views/Layout.vue), children: [] } ]; // 动态添加子路由 function addRoutes(menus) { menus.forEach(menu { routes[0].children.push({ path: menu.path, name: menu.name, component: () import(/views/${menu.component}) }); }); }这意味着你新增一个菜单项只需往menu表插一条记录无需改任何前端代码。5.3 企业类型管理下拉选择背后的三级联动陷阱企业发布职位时需选择“企业类型”该下拉框数据来自/api/enterprise/type/list。EnterpriseTypeController.list()返回[ { id: 1, name: 互联网, parentId: 0 }, { id: 2, name: 金融, parentId: 0 }, { id: 3, name: 人工智能, parentId: 1 }, { id: 4, name: 大数据, parentId: 1 } ]这是一个典型的树形结构parentId0为一级类目。但前端enterprise-add/enterprise-add.wxml的下拉框是扁平化的picker bindchangebindTypeChange value{{typeIndex}} range{{typeList}} range-keyname view classpicker{{typeList[typeIndex] ? typeList[typeIndex].name : 请选择企业类型}}/view /picker坑就在这里range-keyname只显示名称但提交时需要id。bindTypeChange事件中bindTypeChange(e) { const typeId this.data.typeList[e.detail.value].id; // ✅ 正确获取 id this.setData({ typeId: typeId, typeIndex: e.detail.value }); }如果误写成this.data.typeList[e.detail.value]没取.id提交的typeId就是整个对象后端RequestBody绑定失败报HttpMessageNotReadableException。5.4 避坑管理员后台三大血泪教训现象 1登录后点击“学生管理”页面空白Console 报Cannot find module /views/StudentList.vue原因menu表中component字段值为StudentList.vue但实际文件在src/views/student/StudentList.vue路径不匹配。解决统一menu.component字段格式为student/StudentList.vue或修改router/index.js的import路径为/views/${menu.component}.vue加.vue后缀。现象 2修改学生信息后列表页数据未刷新仍显示旧值原因前端StudentList.vue的editStudent方法调用update接口成功后未触发this.getList()重新拉取数据而是仅this.dialogVisible false关闭弹窗。解决在update的success回调中加this.$message.success(更新成功); this.dialogVisible false; this.getList(); // ✅ 强制刷新列表现象 3删除企业时关联的职位、简历未级联删除导致数据不一致原因enterprise表与job表是物理外键job.enterprise_id外键指向enterprise.id但 MySQL 5.7 默认存储引擎为 MyISAM不支持外键。项目db.sql中建表语句未指定ENGINEInnoDB。解决手动执行ALTER TABLE job ENGINEInnoDB; ALTER TABLE resume ENGINEInnoDB; -- 然后加外键约束谨慎先备份 ALTER TABLE job ADD CONSTRAINT fk_job_enterprise FOREIGN KEY (enterprise_id) REFERENCES enterprise(id) ON DELETE CASCADE;注意ON DELETE CASCADE是双刃剑确保业务允许级联删除。6. 从“能跑”到“能讲”答辩前必做的三件事与一个防翻车 checklist6.1 重构application.yml把所有硬编码参数抽离为可配置项答辩时老师最爱问“如果数据库密码变了你要改几个地方” 如果你答“改application.yml一个地方”老师会点头如果答“还要改1-install.bat”你就露馅了。所以必须做配置解耦① 在application.yml中定义 profilespring: profiles: active: dev --- spring: config: activate: on-profile: dev datasource: url: jdbc:mysql://localhost:3306/job_platform?... username: ${DB_USER:root} password: ${DB_PASS:root} --- spring: config: activate: on-profile: prod datasource: url: ${DB_URL:jdbc:mysql://prod-db:3306/job_platform...} username: ${DB_USER:job_user} password: ${DB_PASS:Job2024}② 创建application-dev.yml和application-prod.yml把敏感信息放进去③ 启动时加参数java -jar job-platform.jar --spring.profiles.activedev。这样答辩演示时你可以自信地说“所有配置已外部化切换环境只需改一个参数。”6.2 为关键接口补全 Swagger 文档让老师一眼看懂你的设计项目自带Swagger2配置SwaggerConfig.java但 Controller 方法缺少ApiOperation注解。补上后访问http://localhost:8080/swagger-ui.html就能看到专业文档。例如在StudentController.login()上加ApiOperation(学生登录) ApiResponses({ ApiResponse(code 200, message 登录成功返回 token), ApiResponse(code 401, message 账号或密码错误), ApiResponse(code 400, message 参数缺失) }) PostMapping(/login) public Result login(RequestBody StudentLoginDTO dto) { // ... }效果老师点开/api/student/login立刻看到请求体示例、返回格式、错误码含义——这比你口头解释“我这里做了校验”有力十倍。6.3 准备一份《答辩自测 checklist》覆盖老师 90% 的提问点序号检查项操作是否完成备注1后端能否独立启动访问http://localhost:8080/actuator/health返回{status:UP}2-run.bat→ 浏览器访问☐必须成功2小程序能否扫码预览首页轮播图、招聘列表是否正常加载微信开发者工具 → 预览☐确保baseUrl正确3学生注册→登录→投递简历→查看投递记录全流程走通手动操作一遍☐录屏备用4管理员登录→学生管理→禁用某学生账号→学生端再次登录是否提示“账号已禁用”模拟禁用流程☐权限控制验证5企业发布职位→学生投递→企业后台“面试邀请”页是否收到新记录两端联动测试☐业务闭环证明6修改application.yml中server.port为8081重启后端小程序baseUrl同步修改是否仍能通信端口变更测试☐配置能力证明提示这份 checklist 不是给老师看的是给你自己用的。答辩前一晚按表逐项操作卡在哪一项就重点准备那一项的原理和代码位置。我带过的 A同学就因漏测第 4 项在答辩时被问“怎么防止恶意注册”当场懵住——其实答案就在StudentController.register()里的一行if (studentService.count(wrapper) 0) throw new RuntimeException(手机号已存在);但他没定位到。从那以后我每次帮学生过毕设都强制他们把 checklist 打印出来用红笔一项项划掉直到所有 ☐ 变成 ☑。不是为了应付老师而是让你在答辩室坐下那一刻心里有底——你知道每一行代码长什么样知道每个 bug 曾在哪里翻过车知道这个系统不是拼凑的玩具而是你亲手搭起来、调通、并真正理解的工程。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站