资讯详情

资讯详情

众筹互助网站搭建:SpringBoot+Vue前后端分离实战解析

1. 这类善筹网项目为什么每年都有学生选它到底在锻炼什么先说个现象每年毕业设计选题季SpringBootVue前后端分离的众筹互助网站这类题目几乎每个高校的软件工程、计算机科学专业都会出现。很多人觉得它烂大街但站在实际答辩和找工作两个角度看这个选题恰恰是被反复验证过的稳妥之选。为什么因为一个众筹互助平台善筹网的完整业务闭环覆盖了Web开发最核心的几件事用户注册登录、角色权限控制、项目发布与审核、捐赠下单、进度统计、数据可视化。它既有普通电商项目的基础操作又比纯图书管理、学生管理系统多了一层业务状态流转从发起、待审、筹款中、完成到失败这才像能上线跑的真实产品而不是纯粹的CRUD。从技术栈角度看SpringBoot负责后端APIVue负责前端交互天然贴合当前企业里主流的前后端分离开发模式。你在简历上写独立开发前后端分离平台面试官起码能跟你聊下去——因为它涉及JWT鉴权、跨域处理、文件上传、数据库表设计、事务与并发控制这些都是企业开发绕不开的基础功。这篇文章我就按我当时带A同学做这个题目的完整思路来写从架构划分、后端模块、数据库设计到前端联调、部署答辩把每一步为什么这么做讲清楚。已经做完或者正在做类似平台的同学可以直接对照你的实现查漏补缺。2. 从零搭建后端SpringBoot的模块划分与权限体系2.1 先分清包结构怎么拆后端才不会写成一坨很多人一上来就按controller、service、mapper三层拆包然后全部塞进去。做这种规模的项目确实能跑但后期改需求会非常痛苦。我更推荐按业务域归类尤其当一个系统里有前台用户操作和后台管理员操作两套逻辑时这种划分优势很大com.example.shanchou ├── config // 跨域、JWT拦截器、MyBatis-Plus分页插件等配置 ├── common // 统一返回结果 Result、异常处理、工具类 ├── entity // 数据库实体类 ├── mapper // MyBatis-Plus的 Mapper 接口 ├── service // 业务逻辑接口与实现 ├── controller │ ├── user // 前台登录注册、个人中心、发起项目、捐赠 │ └── admin // 后台项目审核、用户管理、数据统计 └── dto // 前端传入的对象避免直接用实体接收这里有个细节值得注意controller不要直接接收实体类尤其涉及金额、状态的写入操作。比如发起项目时前端传来的是一个CreateProjectDTO里面可以带title、description、targetAmount这些字段而数据库里的userId、status、createTime必须由后端自己填充。这是答辩时安全性的一个加分点也是实际开发里防止字段被恶意篡改的常规做法。2.2 登录注册与JWT拦截前后端分离项目的身份认证关键前后端分离后Session其实不太好用了因为前后端可能部署在不同域名或端口下Cookie的跨域行为很麻烦。所以善筹网这类项目最合理的方案就是JWTJSON Web Token。流程很简单用户登录成功后后端生成一个token返回给前端前端每次请求在Header里带上Authorization: Bearer token后端写一个拦截器统一解析token把用户信息取出来放到请求上下文。具体实现上注册时密码必须加密存储我建议直接用Spring Security自带的BCryptPasswordEncoder比MD5安全得多。登录时的核心逻辑类似下面这样public LoginResult login(String username, String password) { LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getUsername, username); User user userMapper.selectOne(wrapper); if (user null || !passwordEncoder.matches(password, user.getPassword())) { throw new BusinessException(用户名或密码错误); } if (user.getStatus() 0) { throw new BusinessException(账号已被禁用); } String token jwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole()); return new LoginResult(token, user.getNickname(), user.getRole()); }拦截器里要注意一个点需要放行的路径要提前列清楚比如/api/auth/login、/api/auth/register、首页项目列表、项目详情查询这些公开接口。我当时把公开接口和需要登录接口用了一个简单的路径前缀约定公开接口在/api/public/**下统一不做拦截需要登录取用户信息的接口走token解析。这样配置清晰也好答辩解释。2.3 从发起到募捐成功项目的状态流转要算清楚众筹项目的状态不能只有已发布/未发布两个否则业务逻辑会有漏洞。我在设计时给项目定了这么几个状态状态值含义触发条件0草稿用户发起项目时保存但未提交审核1待审核用户提交项目等待管理员审核2筹款中管理员审核通过展示在前台可被捐赠3已成功达到目标金额且未过截止时间4已失败截止时间到了但未达到目标金额5已下架管理员手动下架或违规项目被关闭很多人容易忽略的是状态流转的触发时机。比如已成功这个状态主要有两个触发点一是在用户下单捐赠后同步检查当前项目的累计金额是否达到targetAmount达到则改为已成功二是在每次查询项目详情时做一次惰性检查如果当前时间超过endTime且金额未达标就更新为已失败。这两个触发点缺一不可。前者保证实时性后者保证那些没人捐赠所以一直挂着的项目最终也会有个了结状态。答辩时把这个逻辑讲清楚再配合时间字段的索引设计会显得你考虑得很周全。3. 前端Vue路由设计、页面骨架与接口联调3.1 路由守卫与登录态维持别再每个页面判断一遍前端最忌讳的一件事在几十个页面的mounted里写判断有没有token没有就跳登录页。这种事应该交给路由守卫统一处理。善筹网项目我用Vue Router的beforeEach做了全局前置守卫效果很好router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }); } else if (to.path /login token) { next(/); } else { next(); } });同时在路由meta里给需要权限的页面打标记比如发起项目页requiresAuth为true后台管理页加一个meta.roles: [admin]守卫里再校验用户角色。这样新增页面的时候只要在路由配置里声明访问要求其他什么都不用管。这里我想多说一句前端只用localStorage.getItem(token)其实不够严谨因为用户被禁用后token可能仍然有效。所以更稳妥的做法是在全局的axios拦截器里遇到401状态码时清除本地token并跳转登录页。后端的JWT拦截器保证无效或过期token直接返回401前端拿到401统一处理这套闭环才能撑起整个登录态。3.2 三个核心页面的实现思路与关键交互善筹网前端的核心页面我总结下来是首页、项目详情页、发起众筹页。三个页面的技术侧重点不一样但都是答辩时会被深入追问的地方。首页按板块展示众筹项目。通过条件查询接口带category和sort参数前端用el-tabs切换最新/最热/即将结束等筛选底层其实是同一个list接口。这里值得注意的细节是分页参数不要硬编码固定数值我用的Element UI分页组件一页8条数据切换页时重新调用接口数据变化不会出现点击过快导致列表错乱的问题。项目详情页比较考验你对众筹业务的理解。页面除了项目信息外还得展示当前进度条、已筹金额、支持人数和剩余时间。进度百分比可以在前端根据接口返回的currentAmount / targetAmount计算但剩余天数建议用后端返回值不要前端自己用JS算——因为后端和前端服务器时间可能不一致会出现倒计时不准的情况。发起众筹页是表单密集型页面涉及项目标题、封面图、目标金额、截止时间、详细描述。封面图上传我用的是Element UI的Upload组件注意把action指到后端文件上传接口同时在后端返回文件访问URL后回填到表单字段里。详情描述可以用富文本编辑器如果不想引入太重的组件可以把description转成纯文本存储页面展示时用v-html渲染——但必须注意XSS风险建议后端做一次内容清洗或者简单点直接过滤掉script标签。3.3 Axios封装统一统一再统一Vue项目里如果每个页面直接axios.get(http://localhost:8888/api/project/list)你会遇到两个问题一是跨域配置写死二是错误处理散落各处。我当时给A同学封装了一个request.js把baseURL、请求超时、token注入、错误提示、Loading控制全部收拢到一个文件里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 ! 200) { Message.error(res.msg); return Promise.reject(new Error(res.msg)); } return res; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } Message.error(error.message || 请求失败); return Promise.reject(error); } );baseURL设成/api是为了开发环境和生产环境都能平滑切换。开发环境在vue.config.js里配置devServer的proxy把/api转到后端地址生产环境由Nginx做反向代理。这样代码里就不用写任何硬编码域名换环境只改代理配置非常干净。4. 数据库设计几张核心表的字段取舍与进度统计4.1 用户、项目、捐赠订单这三张表决定业务上限一个众筹互助网站最核心的数据表就是用户表、项目表、捐赠订单表。很多人的设计问题出在字段过多或者过少我建议照着下面的核心字段来控制范围用户表(user)id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, nickname VARCHAR(50) DEFAULT COMMENT 昵称, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像URL, role TINYINT DEFAULT 1 COMMENT 角色1普通用户 2管理员, status TINYINT DEFAULT 1 COMMENT 状态0禁用 1正常, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间项目表(project)id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 发起人, title VARCHAR(100) NOT NULL COMMENT 项目标题, category VARCHAR(50) DEFAULT NULL COMMENT 分类, cover VARCHAR(255) DEFAULT NULL COMMENT 封面图, description TEXT COMMENT 项目详情, target_amount DECIMAL(10,2) NOT NULL COMMENT 目标金额, current_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT 当前已筹金额, donate_count INT DEFAULT 0 COMMENT 参与捐赠人数, status TINYINT DEFAULT 0 COMMENT 状态0草稿 1待审核 2筹款中 3成功 4失败 5下架, start_time DATETIME COMMENT 开始筹款时间, end_time DATETIME COMMENT 截止时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_status_end (status, end_time)捐赠订单表(donation_order)id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号, project_id BIGINT NOT NULL COMMENT 项目ID, user_id BIGINT NOT NULL COMMENT 捐赠者ID, amount DECIMAL(10,2) NOT NULL COMMENT 捐赠金额, remark VARCHAR(255) DEFAULT NULL COMMENT 留言, status TINYINT DEFAULT 0 COMMENT 支付状态0未支付 1已支付, pay_time DATETIME DEFAULT NULL COMMENT 支付时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_project (project_id), INDEX idx_user (user_id)注意几个细节金额字段一律用DECIMAL不要用DOUBLE或FLOAT订单号要全局唯一可以简单用时间戳用户ID随机数生成但格式要稳定status字段不仅要存还要配合end_time建联合索引因为查出所有筹款中的项目并按剩余时间排序这个查询很频繁。4.2 众筹进度实时更新不要频繁SUM也别只存一个数众筹进度current_amount有两个落地方案一个是每次展示时SELECT SUM(amount) FROM donation_order WHERE project_id? AND status1另一个是直接在project表里维护一个current_amount字段。纯SUM方案在数据量小的时候没问题但每次列表查询都要做聚合计算性能会随着订单量增长明显下降。而单纯维护字段又面临一致性问题——如果同一时间有多个人捐赠后写的人可能覆盖先写的人的结果。我采用的是字段为主、事务内累加的做法捐赠成功后在同一个事务里先插入订单再用一条带条件的UPDATE累加项目金额Transactional public void donate(DonateDTO dto) { // 1. 插入订单状态为已支付这里模拟支付成功 // 2. 累加项目金额 int rows projectMapper.increaseAmount(dto.getProjectId(), dto.getAmount()); if (rows 0) { throw new BusinessException(项目不存在或不在筹款中); } // 3. 同步更新捐赠人数 projectMapper.increaseDonateCount(dto.getProjectId()); }increaseAmount对应的SQL是UPDATE project SET current_amount current_amount #{amount}, donate_count donate_count 1 WHERE id #{projectId} AND status 2。这样利用数据库行锁天然解决并发累加丢更新的问题比先select再update靠谱得多。每次查询项目列表时直接把current_amount返回给前端展示既快又准确。4.3 状态字段与时间字段的边界处理这里我想特别提醒一个坑项目截止时间的判断标准要统一。我在后端做了三处检查保证同一个项目不会出现一边显示筹款中一边已经过了截止时间却没更新状态的情况捐赠前校验end_time NOW()否则拒绝捐赠项目详情查询时做惰性状态刷新发现过期未达标就更新为失败后台管理员列表查询时不额外处理直接看status字段。如果前后端时间不同步、或者定时任务没写这套写入校验惰性刷新的组合已经足够撑起毕业设计的正确性。真要上生产环境加一个每分钟跑一次的定时任务把过期项目状态扫一遍即可。5. 实测最容易翻车的几个点事务、并发、金额精度5.1 同一项目同时被多人捐赠数据会不会丢这是答辩时老师最爱问的问题也是实际测试中最容易暴露的问题。如果你用先查项目当前金额加50再更新回去的方式写捐赠逻辑在并发情况下必然会丢更新。举个实际场景项目当前金额是100元A用户和B用户同时各捐50元。两条线程都先读到100各自加50变成150再写回数据库——最后金额是150而不是200白白丢了50元。解法就是我上一节说的原子UPDATE或者用乐观锁加version字段。我更推荐前者因为它把判断项目状态和更新累计金额合并成了一条SQL天然带行锁代码也简单。面试时顺带提一句通过数据库行锁保证并发安全比只说加synchronized要专业得多。5.2 金额精度Double算钱等于给自己埋雷我见过不少项目用double存金额前端显示99.99后端算完变成99.9900000001订单历史对不上。做任何涉及钱的系统必须用BigDecimal或者数据库DECIMAL。Java后端接收前端传来的金额时建议直接定义成BigDecimal字段并且在service层做校验金额必须大于0、不得超过项目剩余额度targetAmount - currentAmount。这里还有一个细节如果有最少捐赠1元的约束校验要统一在后端做不能只靠前端提示。前端展示时也要注意格式比如金额用toFixed(2)保留两位小数进度百分比要处理除数为0的情况。还有一点目标金额如果是100000.00元前端输入框要限制只能输入数字和小数点避免用户输入非法的字符导致后端解析报错。5.3 文件上传的路径与访问映射项目封面、用户头像都会用到文件上传。常见的坑是开发环境上传到本地磁盘D:/upload前端能访问换一台电脑跑路径变了图片全部404。我的做法是在后端配置里抽出一个上传路径配置项比如upload: path: ./upload然后写一个WebMvcConfigurer把该路径映射为静态资源registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath /);这样前端图片地址永远是/user/server/upload/xxx.jpg这种相对路径不管后端部署在哪台机器只要upload目录存在就行。有一点要提醒上传时文件名的生成不要用用户原始文件名用UUID或者时间戳重命名避免中文、特殊字符导致访问失败也避免同名文件互相覆盖。如果是大文件图片还可以压缩或限制大小但这个项目里控制一下文件类型jpg/png和大小比如5MB以内就够了。6. 前后端联调、部署与答辩准备6.1 开发环境代理和生产部署的差异要提前想清楚开发时前端跑在8080端口后端跑在8888端口直接跨域请求会因为CORS被浏览器拦截。我建议用两种方式配合解决一是后端配置全局CORSallowedOriginPatterns(*)配合allowCredentials(true)二是在前端devServer里配置代理devServer: { port: 8080, proxy: { /api: { target: http://localhost:8888, changeOrigin: true, pathRewrite: { ^/api: /api } } } }生产部署时一般用Nginx把静态打包产物和SpringBoot服务都代理到同一个域名下转发带/api前缀的请求到后端地址。这样彻底规避跨域问题。答辩时可以主动提一句生产环境通过Nginx反代保证前后端同域这是一个加分项。6.2 答辩被追问用户量大了怎么办怎么答不翻车如果老师问现在项目能用但如果用户量大了呢不要慌也不要吹牛说完全没问题。比较稳妥的回答思路是承认当前实现是单机版然后给出演进方向。可以从这几个点展开数据库层面给查询频繁的表加索引、把状态时间联合索引用到位先把慢查询优化做扎实引入Redis把项目详情、热门项目列表做缓存登录token也可以从JWT改成Redis存储实现服务端主动下线功能流量再大就把文件存储从本地磁盘换成云存储对象存储静态资源走CDN最后才是考虑服务拆分和集群部署。关键是回答逻辑要连贯体现我知道当前架构的边界在哪里也知道往哪个方向走而不是只说一个名词堆积。6.3 时间充足的话这几个扩展点是加分项如果核心功能做完还有富余时间我强烈建议挑一到两个扩展点落地数据可视化仪表盘后台加一个统计页面展示项目分类占比、每月募集金额趋势、用户增长曲线。用ECharts画图其实不难但能让系统看起来完整很多。站内消息通知项目审核通过后给发起人发一条站内信捐赠成功后给捐赠者展示感谢留言。这个功能逻辑简单但体现了对业务细节的关注。搜索功能首页加一个项目搜索框后端用模糊查询title LIKE %keyword%配合前端防抖和空状态展示实现起来时间成本很低。我个人的体会是毕业设计最忌为了炫技而堆功能倒不如把众筹业务本身的闭环走通发起、审核、展示、捐赠、统计、进度更新、状态变更每一步都有明确的验收标准。这套东西做完你对SpringBoot和Vue的整体理解尤其在事务、权限、跨域、部署这几个企业级必问方向上会比单纯背面试题扎实得多。最后再说一个小技巧Vue项目的package.json和SpringBoot的pom.xml里建议都加上版本注释比如Node 16 npm 8、JDK 1.8 SpringBoot 2.7 MyBatis-Plus 3.5这样换电脑或者复盘的时候不用靠回忆。我当年帮A同学调试时就是因为JDK版本不一致导致DateTimeFormatter报错折腾了一个下午。提前把这些环境边界钉死能帮你省掉很多玄学问题。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →