简介:这是一套基于SpringBoot开发的众筹平台前后台管理系统完整源码,面向计算机相关专业学生与缺乏实战经验的初级开发者,可用于课程设计、毕业设计参考或项目练习。系统涵盖前台用户注册登录、发起众筹、支持项目、个人中心,以及后台项目审核、用户管理、订单管理等核心流程,形成完整功能闭环。资源包共823个文件,约27.33MB,以167个Java源文件与167个JS脚本为主体,辅以42个HTML页面、42个CSS样式及大量图片素材,另有SQL脚本、XML配置与依赖jar包,分层清晰、依赖精简,便于快速运行与二次开发。技术栈采用JDK1.8、MySQL、SpringBoot、SpringSecurity+OAuth2、Swagger2,前端使用HTML、Vue、jQuery与Ajax。目前已有702人学习,适合需要完整项目案例、排错思路与目录结构参考的读者。
1. 众筹平台前后台系统:从项目结构到可运行的最小闭环
众筹平台听起来像是一个典型的“业务系统”,但真正动手做的时候,很多人会卡在一个很尴尬的位置:后台管理页面能打开,数据库表也建好了,但前台项目列表、发起众筹、订单支付、后台审核这几条链路始终串不起来。尤其是当你拿到一份基于 Spring Boot 的众筹平台前后台管理系统源码时,第一反应往往是“文件挺全”,第二反应是“从哪开始跑”。
这个标题对应的核心场景其实很明确:一套用 Spring Boot 做后端、带前后台两套界面的众筹业务系统,前台给普通用户浏览项目、发起众筹、下单支持,后台给运营人员做项目审核、订单管理、用户管理、资金流水查看。它适合两类人:一类是正在做课程设计或毕业设计,需要一套能跑通、能讲清楚业务链路的参考实现;另一类是想拿一个完整业务系统练手,理解 Spring Boot 在真实项目里怎么做分层、怎么做权限、怎么做状态流转。
我见过太多人把这类项目当成“下载即完成”,结果卡在数据库连不上、前端请求跨域、后台登录 401、项目状态不流转这些具体问题上。这篇笔记不打算复述源码目录,而是按“能跑起来、能改得动、能讲得清”的顺序,把众筹平台前后台系统的落地路径拆开。你跟着走一遍,至少能做到:本地启动后端、初始化数据库、跑通前后台登录、走完一次众筹发起到后台审核的最小闭环。
2. 众筹平台的技术选型与分层:为什么用 Spring Boot 做前后台分离
2.1 前后台分离下 Spring Boot 的职责边界
众筹平台的前后台分离不是赶时髦,而是业务本身决定的。前台面向普通用户,需要频繁展示项目列表、项目详情、众筹进度、订单状态;后台面向运营,需要做审核、导出、状态变更、权限控制。两套界面的交互节奏和权限模型完全不同,如果还用传统 JSP 混在一起写,后期改一个审核按钮都可能影响前台页面。
常见做法是后端只提供 REST 接口,前台和后台各自独立部署或放在不同静态目录下。Spring Boot 在这里承担的是统一入口、统一鉴权、统一数据出口。具体来说,Controller 层只负责接收参数和返回结果,Service 层处理众筹业务规则,Mapper 或 Repository 层负责数据库读写。这样做的直接好处是:后台要加一个“批量审核”功能,只需要新增一个接口,不用动前台任何代码。
我一般会把项目分成这几个包:controller、service、mapper、entity、dto、vo、config、interceptor、util。其中 dto 用于接收前端参数,vo 用于返回给前端展示,entity 和数据库表一一对应。很多源码包会把 dto 和 vo 混在一起,短期看不出问题,等到项目状态字段需要在前台展示中文、后台展示英文枚举时,就会到处改。
2.2 数据库表设计:众筹业务绕不开的六张核心表
众筹平台的表不多,但关系必须清楚。下面这张表是我在多个类似项目里总结出来的最小核心表集合,少了任何一张,业务链路都会断。
| 表名 | 作用 | 关键字段 |
|---|---|---|
| user | 前台用户与后台管理员 | id, username, password, role, status |
| project | 众筹项目主表 | id, user_id, title, target_amount, raised_amount, status, deadline |
| project_detail | 项目详情与回报配置 | id, project_id, content, reward_desc, reward_amount |
| order | 支持订单 | id, user_id, project_id, amount, status, create_time |
| payment_flow | 资金流水 | id, order_id, amount, type, create_time |
| audit_log | 后台审核记录 | id, project_id, admin_id, action, remark, create_time |
其中 project.status 是整个系统的状态机核心。常见状态包括:待审核、审核通过、审核拒绝、众筹中、众筹成功、众筹失败。前台只能看到审核通过且处于众筹中的项目,后台可以看到全部状态。order.status 则控制支付流程:待支付、已支付、已退款、已关闭。
注意:很多源码把项目状态和订单状态写成纯数字,没有枚举类。后期加一个“审核中”状态,就要全局搜数字替换。建议一开始就定义 ProjectStatusEnum 和 OrderStatusEnum。
2.3 用 Maven 搭出可运行的后端骨架
拿到源码后,不要急着改业务代码,先把后端跑起来。下面是一个最小可运行的 pom.xml 依赖片段,适用于 Spring Boot 2.7.x 和 JDK 8 或 11。版本号不要照抄,按你本地环境调整,但依赖组合是经过验证的。
<dependencies> <!-- Web 接口能力 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis 持久层,众筹项目常用 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- 分页插件,项目列表和订单列表都要用 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> <!-- JWT 做前后台登录态 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> </dependencies>这段依赖里,spring-boot-starter-web 提供内嵌 Tomcat 和 MVC 能力;mybatis-spring-boot-starter 负责把 Mapper 接口和 XML 映射文件关联起来;pagehelper 用于项目列表分页,众筹平台的项目列表几乎一定会分页;jjwt 用于生成和解析 token,前后台登录后返回 token,后续请求放在 Header 里。
参数说明:MyBatis 的 mapper-locations 一般配置为 classpath:mapper/*.xml,type-aliases-package 配置为你的 entity 包路径。PageHelper 只需要在查询前调用 PageHelper.startPage(pageNum, pageSize),后面紧跟的查询就会自动分页。JWT 的 secret 不要写死在代码里,放到 application.yml 中,长度至少 32 位。
2.4 前台与后台的接口隔离:用拦截器区分两套登录态
前后台共用一个后端时,最容易翻车的地方是权限串了。前台用户登录后能调后台接口,后台管理员登录后能改前台订单。常见做法是用两套拦截器,按 URL 前缀区分。
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 前台接口拦截器,只校验普通用户 token registry.addInterceptor(new FrontAuthInterceptor()) .addPathPatterns("/api/front/**") .excludePathPatterns("/api/front/login", "/api/front/register", "/api/front/project/list"); // 后台接口拦截器,只校验管理员 token registry.addInterceptor(new AdminAuthInterceptor()) .addPathPatterns("/api/admin/**") .excludePathPatterns("/api/admin/login"); } }逻辑说明:前台接口统一走 /api/front/ 前缀,后台接口统一走 /api/admin/ 前缀。两个拦截器分别解析 token 中的 role 字段,前台只允许 role=user,后台只允许 role=admin。排除路径是登录、注册和前台项目列表,这些不需要登录就能访问。
参数说明:token 建议放在请求头 Authorization 中,格式为 Bearer xxx。拦截器里解析失败直接返回 401,不要抛异常到全局,否则前端拿到的错误信息不明确。后台登录接口本身也要做频率限制,防止暴力尝试,简单做法是用 Redis 记录同一 IP 的失败次数。
3. 从零跑通众筹核心链路:发起、审核、支持、支付
3.1 项目发起接口:参数校验与状态初始化
前台用户发起众筹,是整条链路的起点。这个接口看起来简单,但参数校验不严会导致后台审核时看到一堆脏数据。下面是一个典型的项目发起接口实现。
@PostMapping("/api/front/project/create") public Result createProject(@RequestBody @Valid ProjectCreateDTO dto, HttpServletRequest request) { // 从 token 中取当前用户 Long userId = JwtUtil.getUserId(request); if (userId == null) { return Result.fail("请先登录"); } // 校验截止时间必须晚于当前时间 if (dto.getDeadline().before(new Date())) { return Result.fail("截止时间不能早于当前时间"); } // 校验目标金额必须大于 0 if (dto.getTargetAmount() == null || dto.getTargetAmount().compareTo(BigDecimal.ZERO) <= 0) { return Result.fail("目标金额必须大于 0"); } Project project = new Project(); project.setUserId(userId); project.setTitle(dto.getTitle()); project.setTargetAmount(dto.getTargetAmount()); project.setRaisedAmount(BigDecimal.ZERO); project.setStatus(ProjectStatusEnum.PENDING_AUDIT.getCode()); project.setDeadline(dto.getDeadline()); project.setCreateTime(new Date()); projectService.save(project); return Result.success(project.getId()); }逻辑说明:这个接口做了三件事——取当前用户、校验关键参数、初始化项目状态。状态初始化为“待审核”,而不是“众筹中”,这是众筹平台和普通商品系统的核心区别。项目必须经过后台审核才能对外展示。
参数说明:ProjectCreateDTO 里 title 长度建议限制在 5 到 50 字,targetAmount 用 BigDecimal 而不是 double,避免金额精度问题。deadline 用 Date 或 LocalDateTime,注意时区配置。raisedAmount 初始化为 0,后续每次订单支付成功后累加。
3.2 后台审核接口:状态流转与审核日志
后台审核是众筹平台的关键控制点。审核通过后,项目才进入众筹中状态,前台才能看到。审核拒绝后,项目回到用户手里,可以修改后重新提交。
@PostMapping("/api/admin/project/audit") public Result auditProject(@RequestBody AuditDTO dto, HttpServletRequest request) { Long adminId = JwtUtil.getUserId(request); Project project = projectService.getById(dto.getProjectId()); if (project == null) { return Result.fail("项目不存在"); } // 只有待审核状态才能被审核 if (!ProjectStatusEnum.PENDING_AUDIT.getCode().equals(project.getStatus())) { return Result.fail("当前项目状态不允许审核"); } if (dto.getPass()) { project.setStatus(ProjectStatusEnum.FUNDING.getCode()); } else { project.setStatus(ProjectStatusEnum.AUDIT_REJECT.getCode()); } projectService.update(project); // 写入审核日志,方便追溯 AuditLog log = new AuditLog(); log.setProjectId(project.getId()); log.setAdminId(adminId); log.setAction(dto.getPass() ? "PASS" : "REJECT"); log.setRemark(dto.getRemark()); log.setCreateTime(new Date()); auditLogService.save(log); return Result.success(); }逻辑说明:审核接口先判断项目当前状态,只有待审核状态才能被操作。审核通过后状态变为“众筹中”,审核拒绝后状态变为“审核拒绝”。每次审核都写入 audit_log,记录管理员、动作、备注和时间。
参数说明:AuditDTO 包含 projectId、pass、remark。pass 用 Boolean 而不是 int,避免 0/1 歧义。remark 在拒绝时建议必填,方便用户知道原因。审核日志表不要和项目表合并,否则一个项目多次审核会覆盖历史记录。
3.3 用户支持项目:订单创建与支付回调
用户支持项目,本质是创建一个订单,支付成功后累加项目已筹金额。这里最容易出问题的是并发:两个人同时支持同一个项目,raised_amount 可能少加。
@Transactional(rollbackFor = Exception.class) public void paySuccess(Long orderId) { Order order = orderMapper.selectById(orderId); if (order == null || !OrderStatusEnum.UNPAID.getCode().equals(order.getStatus())) { return; } // 更新订单状态 order.setStatus(OrderStatusEnum.PAID.getCode()); order.setPayTime(new Date()); orderMapper.updateById(order); // 累加项目已筹金额,使用 SQL 原子更新 projectMapper.increaseRaisedAmount(order.getProjectId(), order.getAmount()); // 写入资金流水 PaymentFlow flow = new PaymentFlow(); flow.setOrderId(orderId); flow.setAmount(order.getAmount()); flow.setType("SUPPORT"); flow.setCreateTime(new Date()); paymentFlowMapper.insert(flow); }逻辑说明:支付回调里先判断订单状态,防止重复回调。然后更新订单为已支付,再用原子 SQL 累加项目已筹金额,最后写资金流水。整个过程加 @Transactional,任何一步失败都回滚。
参数说明:increaseRaisedAmount 对应的 SQL 是 update project set raised_amount = raised_amount + #{amount} where id = #{projectId},不要先查再改,否则并发下会丢更新。资金流水的 type 字段区分支持、退款、提现等类型。
3.4 众筹进度计算与前台展示
前台项目列表和详情页都需要展示众筹进度。进度不是存出来的,而是算出来的:raised_amount / target_amount。但要注意目标金额为 0 的情况,以及进度超过 100% 时的展示。
public ProjectVO toVO(Project project) { ProjectVO vo = new ProjectVO(); vo.setId(project.getId()); vo.setTitle(project.getTitle()); vo.setTargetAmount(project.getTargetAmount()); vo.setRaisedAmount(project.getRaisedAmount()); // 计算进度百分比,保留两位小数 if (project.getTargetAmount().compareTo(BigDecimal.ZERO) > 0) { BigDecimal percent = project.getRaisedAmount() .multiply(new BigDecimal("100")) .divide(project.getTargetAmount(), 2, RoundingMode.HALF_UP); vo.setProgress(percent); } else { vo.setProgress(BigDecimal.ZERO); } // 计算剩余天数 long remainDays = (project.getDeadline().getTime() - System.currentTimeMillis()) / (1000 * 60 * 60 * 24); vo.setRemainDays(Math.max(remainDays, 0)); return vo; }逻辑说明:进度百分比用 BigDecimal 计算,避免浮点误差。剩余天数用截止时间减当前时间,小于 0 时展示 0。前台展示时,进度条宽度直接用这个百分比。
参数说明:divide 的第二个参数是小数位数,第三个参数是舍入模式。HALF_UP 是四舍五入,符合展示习惯。如果项目已经众筹成功,进度可能超过 100%,前台展示时可以封顶 100%,但数据库里保留真实值。
4. 前后台联调避坑:登录态、跨域、状态不一致
4.1 登录态丢失:token 存哪、怎么带、怎么续
现象:前台登录成功后,刷新页面就退出登录;或者后台能登录,但点任何菜单都提示未登录。
原因:token 存储位置不统一。有的代码存在 localStorage,有的存在 sessionStorage,有的存在 cookie。前端请求拦截器没有统一加 Authorization 头,后端拦截器又只认 Header。
解决:统一约定 token 存在 localStorage,key 为 token。前端封装 request 工具,每次请求从 localStorage 取 token 放到 Header。后端拦截器只从 Header 取,不兼容 cookie 和参数。token 过期时间建议前台 2 小时,后台 30 分钟,后台可以更短。
4.2 跨域配置:开发环境能跑,部署后 404
现象:本地开发时前端 8080,后端 8081,请求正常。部署到服务器后,前端请求全部 404 或 403。
原因:开发环境用了 @CrossOrigin 注解或代理,部署后没有配 Nginx 反向代理,或者 Nginx 配置里没有转发 /api 前缀。
解决:开发环境用 Spring Boot 的 CorsConfig 统一放行,生产环境用 Nginx 把 /api 转发到后端端口。不要同时用两种方式,否则响应头里会出现两个 Access-Control-Allow-Origin,浏览器直接拒绝。
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }参数说明:addAllowedOriginPattern 用 * 而不是 addAllowedOrigin("*"),后者在 allowCredentials 为 true 时会报错。生产环境建议把 * 换成具体域名。
4.3 项目状态不一致:前台看到已筹满,后台还是众筹中
现象:项目已筹金额已经超过目标金额,但前台还显示众筹中,后台也没有自动变更状态。
原因:状态流转只靠人工操作,没有定时任务或事件驱动。众筹成功或失败应该在截止时间到达时自动判定。
解决:加一个定时任务,每天凌晨扫描截止时间已过且状态为众筹中的项目。如果 raised_amount >= target_amount,状态改为众筹成功;否则改为众筹失败。同时给发起人发站内信。
@Scheduled(cron = "0 0 1 * * ?") public void checkProjectStatus() { List<Project> list = projectMapper.selectFundingExpired(); for (Project project : list) { if (project.getRaisedAmount().compareTo(project.getTargetAmount()) >= 0) { project.setStatus(ProjectStatusEnum.SUCCESS.getCode()); } else { project.setStatus(ProjectStatusEnum.FAIL.getCode()); } projectMapper.updateById(project); } }参数说明:cron 表达式 0 0 1 * * ? 表示每天凌晨 1 点执行。selectFundingExpired 查询 status=众筹中 且 deadline < 当前时间 的项目。状态变更后,前台项目详情页要能立刻看到,所以不要缓存项目状态太久。
4.4 订单重复支付:回调幂等与前端防抖
现象:用户点击支付按钮后,订单被重复回调,raised_amount 加了两次。
原因:支付回调没有做幂等,或者前端按钮没有防抖,用户快速点了两次。
解决:后端在 paySuccess 里先判断订单状态,只有待支付才处理。前端在点击支付后立即禁用按钮,直到收到响应。另外,支付平台回调可能重试,所以幂等必须做在后端,不能只靠前端。
注意:幂等判断和状态更新要在同一个事务里,否则并发下两个回调可能同时通过状态判断。
5. 后台管理进阶:权限、导出与数据看板
5.1 基于角色的菜单权限控制
后台管理系统绕不开权限。众筹平台的后台一般分超级管理员、运营、财务三种角色。超级管理员看全部菜单,运营看项目和用户,财务看订单和流水。
常见做法是角色表 + 菜单表 + 角色菜单关联表。登录后根据用户角色查询菜单列表,前端动态渲染路由。后端接口也要做权限校验,不能只靠前端隐藏菜单。
@GetMapping("/api/admin/menu/list") public Result<List<MenuVO>> menuList(HttpServletRequest request) { Long adminId = JwtUtil.getUserId(request); List<Menu> menus = menuService.selectByAdminId(adminId); return Result.success(menus.stream().map(this::toVO).collect(Collectors.toList())); }参数说明:selectByAdminId 关联 admin_role 和 role_menu 两张表。菜单表里要有 path、component、icon、sort 字段,前端根据这些字段生成路由。接口权限用注解 @RequireRole("admin") 或拦截器里判断 URL 和角色的映射。
5.2 订单与流水导出:大数据量下的分页与流式写出
后台经常需要导出订单和资金流水。数据量小的时候一次性查出来没问题,数据量大了会内存溢出。常见做法是分页查询,每页 1000 条,用 POI 的 SXSSFWorkbook 流式写出。
@GetMapping("/api/admin/order/export") public void exportOrder(HttpServletResponse response) throws IOException { response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment;filename=orders.xlsx"); SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 保留100行在内存 Sheet sheet = workbook.createSheet("订单"); // 写表头 Row header = sheet.createRow(0); header.createCell(0).setCellValue("订单号"); header.createCell(1).setCellValue("项目名称"); header.createCell(2).setCellValue("金额"); // 分页查询并写入 int pageNum = 1; while (true) { PageHelper.startPage(pageNum, 1000); List<Order> orders = orderMapper.selectList(); if (orders.isEmpty()) break; for (int i = 0; i < orders.size(); i++) { Row row = sheet.createRow(sheet.getLastRowNum() + 1); row.createCell(0).setCellValue(orders.get(i).getOrderNo()); row.createCell(1).setCellValue(orders.get(i).getProjectTitle()); row.createCell(2).setCellValue(orders.get(i).getAmount().doubleValue()); } pageNum++; } workbook.write(response.getOutputStream()); workbook.dispose(); }逻辑说明:SXSSFWorkbook 是 POI 的流式工作簿,只保留指定行数在内存,其余写入临时文件。分页查询每页 1000 条,循环写入,最后 dispose 清理临时文件。
参数说明:new SXSSFWorkbook(100) 中的 100 是内存中保留的行数,太小会频繁写磁盘,太大会占内存。分页大小 1000 是经验值,可以根据服务器内存调整。导出接口要加超时时间,避免大导出把 Tomcat 线程占满。
5.3 数据看板:用 SQL 聚合代替 Java 循环
后台首页通常有一个数据看板,展示今日新增项目、今日订单金额、累计用户数等。这些数据不要用 Java 查全量再循环统计,直接用 SQL 聚合。
-- 今日新增项目数 SELECT COUNT(*) FROM project WHERE DATE(create_time) = CURDATE(); -- 今日订单总金额 SELECT IFNULL(SUM(amount), 0) FROM `order` WHERE status = 'PAID' AND DATE(pay_time) = CURDATE(); -- 各状态项目占比 SELECT status, COUNT(*) AS num FROM project GROUP BY status;参数说明:DATE(create_time) 会导致索引失效,数据量大时建议用 create_time >= CURDATE() AND create_time < CURDATE() + INTERVAL 1 DAY。IFNULL 防止空结果返回 null。各状态占比可以在前端用饼图展示,后端只返回状态和数量。
5.4 审核日志追溯:出问题时怎么查
众筹平台涉及资金,审核日志必须可追溯。每次后台审核、状态变更、退款操作都要写日志。日志表字段包括:操作人、操作类型、目标 ID、变更前状态、变更后状态、备注、时间。
查问题时,先根据项目 ID 查 audit_log,按时间倒序,看最近一次状态变更是什么、谁操作的、备注写了什么。如果日志里没有记录,说明代码里漏写了,这是最常见的翻车点。我一般会在 Service 层的关键方法上加 AOP,自动记录操作日志,避免手动写漏。
6. 把系统跑稳的几个具体习惯
众筹平台这类系统,功能跑通只是第一步,真正花时间的是状态一致性和资金安全。我自己的习惯是:任何涉及金额变更的操作,先写流水再改状态,流水是唯一不会骗人的东西。订单支付回调、退款、提现,全部走同一个流水表,出问题时按流水对账,比查代码快得多。
另一个习惯是状态机不要散落在各个 Service 里。项目状态、订单状态、审核状态,全部用枚举类集中管理,状态流转写在一个方法里,任何地方要改状态都调这个方法。这样加一个“审核中”状态时,只需要改枚举和流转方法,不用全局搜数字。
还有一点是接口幂等。支付回调、审核提交、订单创建,这些接口都要考虑重复请求。简单做法是用唯一业务号 + 数据库唯一索引,插入失败就返回已处理。不要依赖前端防抖,前端防抖只能减少重复,不能杜绝。
最后,本地跑通后,一定要用 Postman 或 curl 把核心链路走一遍:注册、登录、发起项目、后台审核、前台支持、支付回调、状态变更。每一步都看数据库里的数据对不对,不要只看接口返回。接口返回成功但数据库没变的情况,在事务配置错误时经常出现。
希望帮到你。
本文还有配套的精品资源,点击获取