拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Spring Boot+Vue慈善捐赠平台实战:从数据库设计到部署避坑全解析

做慈善捐赠平台这件事我一开始其实是有点低估的。接这个项目的时候客户说“就是个捐赠管理系统后端管数据前端管展示”听着挺简单真拆开做才发现它既要处理复杂的角色权限普通用户、爱心机构、财务、超级管理员又要兼顾资金流向的透明展示还涉及在线捐赠模拟支付、物资匹配、进度公示这些业务细节。最后落地采用的技术栈是 Java 基于 Spring Boot 做后端接口Vue 做前端单页应用这也是目前做这类管理信息系统最成熟、最稳妥的组合。这篇我把整个项目从需求梳理、数据库设计到核心代码实现、部署避坑完整写一遍给正打算做类似系统或者想用 Spring Boot Vue 练手完整项目的朋友一个可直接抄作业的参考。1. 项目整体设计与需求拆解1.1 这个平台到底要解决什么问题很多公益组织到现在还在用“微信群接龙 Excel 登记”的方式管理捐赠这在实际运行中非常痛苦一笔捐款对应哪个项目发票怎么开物资怎么分捐赠人想查自己的记录怎么办管理端想统计某个时间段的募集总额光是翻表格就能翻到头皮发麻。所以这个平台的核心目标很明确把“发布项目—在线捐赠—记录留痕—资金公示—物资管理”这条链子串起来。捐赠人注册登录之后可以浏览平台上发布的慈善项目比如“山区小学图书角”“孤寡老人冬日关怀”选择项目进行捐赠系统记录每一笔捐赠的时间、金额、订单号并且把项目进度、资金使用情况公开透明地展示出来。管理端则负责项目审核、用户管理、订单对账、物资出入库、公告发布等工作。这个系统适合谁参考如果你是有 Java 基础、想学 Spring Boot Vue 前后端分离项目完整流程的开发者或者你所在机构正好有类似的信息化需求这篇的架构设计和核心代码基本可以拿过去改一改就能用。1.2 用户角色与权限边界系统不是只有“用户”和“管理员”两端实际业务要比这复杂一些。我在设计权限的时候把用户拆成了四类普通用户捐赠者注册、登录、浏览项目、在线捐赠、查看自己的捐赠记录和电子凭证。爱心机构项目发起方提交慈善项目申请、查看自己名下项目的募集进度和资金明细。财务人员审核捐赠订单、处理对账、发布资金使用公示。超级管理员整体系统管理包括用户管理、机构入驻审核、项目审核、数据统计、系统配置。这四类角色的权限是交叉重叠的比如项目审核既要管理员能看也要财务能复核。如果不在设计阶段把权限矩阵理清楚后面写接口的时候会反复改。我这里采用的是 RBAC基于角色的访问控制模型用户表、角色表、用户角色关联表、菜单权限表四件套后续做接口鉴权的时候只需要在 Spring Security 里配置角色对应的访问路径即可。1.3 功能模块一览从用户前台和管理后台两个视角来拆功能大致如下前台Vue 端用户注册登录、首页项目展示、项目详情与捐赠、我的捐赠记录、项目进度公示、物资捐赠登记、在线留言反馈。后台Vue 端按角色区分项目审核管理、用户/机构管理、捐赠订单管理、物资库存管理、资金公示发布、公告管理、数据可视化大屏募集总额、项目排行、月度趋势。这套功能看起来不多但涉及的前后端交互接口大概有六十多个。我实际开发中把接口按业务域拆分成 AuthController、ProjectController、DonationController、MaterialController、FinanceController、UserController 六个模块每个 Controller 都对应一组清晰的 RESTful 接口调试的时候定位问题非常方便。2. 技术选型背后的思考2.1 后端为什么选 Spring Boot 而不是其他框架项目启动的时候同事问过一句要不要用 RuoYi 这种现成的脚手架改我的意见是不用。RuoYi 虽然封装了权限、代码生成等很多功能但这也意味着你要花大量时间去理解它自己的约定而且它和 Vue 前端是绑定的后期想要替换某个模块反而束手束脚。用纯粹的 Spring Boot 初始项目起步所有东西都自己掌握该封装的地方自己封装整个项目的可控性高得多。Spring Boot 版本我最终选的是 2.7.x没有直接上 3.x。原因有三点第一3.x 是基于 JDK 17 的而目前很多生产环境的 JDK 还是 8 或者 11用 3.x 还要让运维改环境沟通成本高第二一些老的依赖对 Spring Boot 3.x 的 Jakarta 命名空间兼容还不够好第三2.7.x 还在社区维护周期内稳定性足够。当然如果你是新项目、环境本身就是 JDK 17直接用 3.x 也没问题这个取决于团队现状。ORM 层我选了 MyBatis-Plus 而不是 Spring Data JPA。原因很实际国内团队对 MyBatis 的接受度更高SQL 自己可控复杂报表类查询写 SQL 更容易优化。MyBatis-Plus 在 MyBatis 基础上加了通用 Mapper 和条件构造器单表 CRUD 几乎不用写 XML分页也提供了现成的分页插件。多表关联查询我仍然手写 SQL这种“简单操作靠封装复杂查询靠 SQL”的组合在开发效率上是目前最优解。2.2 前端Vue 3 Element Plus 的取舍前端技术栈我从一开始就锁定了 Vue 3。很多老项目还在用 Vue 2是因为生态和插件兼容性的顾虑但 2024 年的今天Vue 3 的生态已经非常成熟Element Plus、Vue Router 4、Pinia 这些配套组件早就稳定了。Vue 3 的组合式 APIComposition API对于这种带有大量表单和管理页面的项目来说逻辑组织比选项式 API 清晰得多。组件库选了 Element Plus主要看重它的表格、表单、分页组件很完善后台管理页面基本都是这套组件拼出来的。图表方面引入了 ECharts用来做后台的数据可视化大屏。HTTP 请求用的是 axios封装成一个 request 工具类统一处理请求头、Token 注入、错误拦截。如果你还在纠结 Vue 2 还是 Vue 3我建议直接 Vue 3。之前遇到过一个用 Vue 2 的老项目想引一个 TreeSelect 新组件发现作者已经不在维护 Vue 2 版本了最后还是得手搓。新项目不要给自己埋这种坑。2.3 数据库与中间件选型数据库用的是 MySQL 8.0InnoDB 引擎utf8mb4 字符集。utf8mb4 不只是为了存 emoji某些生僻字和特殊符号在 utf8 字符集下会报错或乱码捐赠人姓名里真的会出现生僻字这里不要图省事用默认字符集。Redis 在这个系统里的作用有两个一是存登录验证码和 Token 的黑名单二是给首页的热门项目做缓存。首页是访问量最大的页面每次刷新都去 MySQL 查项目列表压力没必要我把热门项目的接口设置了 30 秒的 Redis 缓存实际测下来接口响应从 180ms 降到了 20ms 左右。文件存储这块比较简单项目本身没有要求对接 OSS用的是本地磁盘存储nginx 做了静态资源映射。如果以后要扩展把上传地址换成 OSS/S3 的接口就行业务层不需要改。2.4 技术栈全景层次选型说明前端框架Vue 3 Vite比 Webpack 构建速度快一个量级UI 组件Element Plus后台管理页面首选状态管理PiniaVue 3 官方推荐替代 Vuex路由Vue Router 4支持动态路由权限加载HTTPaxios封装请求拦截器统一处理 Token后端框架Spring Boot 2.7稳定社区资源丰富权限认证Spring Security JWT无状态鉴权适合前后端分离ORMMyBatis-Plus 3.5内置分页插件单表零 SQL数据库MySQL 8.0主流生产配置缓存Redis 5验证码、热门数据缓存构建工具Maven生态最成熟部署Nginx Docker前端静态托管后端容器化3. 系统架构与数据库设计实操3.1 前后端分离的分层架构整个系统的架构是标准的前后端分离后端只提供 RESTful API前端通过 Nginx 托管静态页面同时把 /api 路径代理到后端服务。这种部署方式的好处是前后端可以独立扩展比如以后要做微信小程序端直接复用同一套后端 API 就行不用重新开发接口层。后端代码我分成五个层Controller 层接收请求、参数校验、返回统一响应体。Service 层业务逻辑核心事务控制在这里。Mapper 层数据访问继承 MyBatis-Plus 的 BaseMapper。Entity 层数据库实体映射。DTO/VO 层接口入参和出参的对象避免把 Entity 直接暴露给前端。这里要强调一个习惯Entity 和 VO 一定要分开。很多新手喜欢把表的实体类直接返回给前端这样会暴露不该暴露的字段比如密码哈希、内部状态标识。我在用户模块就专门定义了一个 UserVO里面的字段是前端真正需要的其他一概不返回。3.2 核心数据表结构设计我挑五张最核心的表贴出来这几张表基本撑起了整个系统的业务骨架。用户表sys_userCREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(255) NOT NULL COMMENT BCrypt密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, phone varchar(20) DEFAULT NULL COMMENT 手机号, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, user_type tinyint(4) DEFAULT 0 COMMENT 类型 0-普通用户 1-爱心机构 2-财务 3-管理员, status tinyint(4) DEFAULT 1 COMMENT 状态 1-正常 0-禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;注意密码字段是 varchar(255)因为 BCrypt 加密出来的字符串比较长如果你写成 varchar(64) 后面插入数据会直接报错这个坑我踩过。捐赠项目表donation_projectCREATE TABLE donation_project ( id bigint(20) NOT NULL AUTO_INCREMENT, org_id bigint(20) NOT NULL COMMENT 发起机构用户ID, title varchar(200) NOT NULL COMMENT 项目名称, description text COMMENT 项目详情, target_amount decimal(12,2) DEFAULT 0.00 COMMENT 目标金额, raised_amount decimal(12,2) DEFAULT 0.00 COMMENT 已募集金额, cover_image varchar(255) DEFAULT NULL COMMENT 封面图, category varchar(50) DEFAULT NULL COMMENT 项目分类, status tinyint(4) DEFAULT 0 COMMENT 0-待审核 1-募集中 2-已结束 3-已拒绝, audit_remark varchar(500) DEFAULT NULL COMMENT 审核备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT捐赠项目表;这里raised_amount字段的设计有一个关键点不要在每一次捐款的时候先读出来再加回去再更新高并发下会出问题。我在实际代码里用的是 SQL 自增UPDATE donation_project SET raised_amount raised_amount #{amount} WHERE id #{projectId}这样的原子更新操作在并发场景下不会丢更新。捐赠订单表donation_orderCREATE TABLE donation_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL COMMENT 捐赠人ID, project_id bigint(20) NOT NULL COMMENT 项目ID, amount decimal(12,2) NOT NULL COMMENT 捐赠金额, pay_status tinyint(4) DEFAULT 0 COMMENT 0-待支付 1-已支付 2-已取消, pay_time datetime DEFAULT NULL COMMENT 支付时间, certificate_no varchar(64) DEFAULT NULL COMMENT 电子证书编号, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT捐赠订单表;订单号是我在 Service 层手动生成的格式是日期 随机数20240612103052001 4位随机. 这里没有用数据库自增 ID 当订单号因为订单号在外面展示的频率很高直接用自增 ID 会暴露平台的订单量给别有用心的人拿来分析就不好了。资金公示表finance_publicityCREATE TABLE finance_publicity ( id bigint(20) NOT NULL AUTO_INCREMENT, project_id bigint(20) DEFAULT NULL COMMENT 关联项目ID空表示平台整体, title varchar(200) NOT NULL COMMENT 公示标题, content text COMMENT 公示内容, total_income decimal(14,2) DEFAULT 0.00 COMMENT 期间收入, total_expense decimal(14,2) DEFAULT 0.00 COMMENT 期间支出, balance decimal(14,2) DEFAULT 0.00 COMMENT 结余, publish_time datetime DEFAULT NULL COMMENT 发布时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT资金公示表;另外还有物资表material、物资出入库记录表material_record、公告表notice等这里不一一列出 DDL 了设计思路和上面几张大同小异核心都是“记录每次操作保留追溯链路”。3.3 数据库设计的几个关键思考第一金额字段一律用 DECIMAL绝对不能使用 DOUBLE 或 FLOAT。这个准则在涉及钱的系统里是死规矩二进制浮点数无法精确表示十进制小数用 DOUBLE 存储金额在累加过程中会出现 0.1 0.2 0.30000000000000004 这种问题对账的时候差一分钱都是事故。第二每一张业务表都要有 create_time能加 update_time 的加上。很多问题排查到最后都需要查“这条数据是什么时候变的”没有时间字段你就只能对着日志翻效率极低。第三软删除问题。用户数据和捐赠记录不能物理删除万一出现纠纷你要能追溯。MyBatis-Plus 自带TableLogic逻辑删除注解用起来很简单但要注意加了逻辑删除后唯一索引可能会冲突。解决方法是把唯一索引改成联合唯一索引把 deleted 字段加进去。4. 前后端核心功能实现4.1 Spring Boot 项目初始化与配置后端项目的创建不多说Maven 项目spring-boot-starter-parent 设成 2.7.18JDK 1.8。这里提一个坑如果你本机装了高版本的 Java比如 17直接用 IDEA 创建 Spring Boot 项目时它会默认用高版本编译运行时会报“源发行版 17 需要目标发行版 17”之类的错误。解决方法是确认 pom.xml 里的java.version是 1.8IDEA 的 Project Structure 和 Settings 里的 Java Compiler 全部改成 8。核心配置文件application.yml我贴一下server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/charity_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 servlet: multipart: max-file-size: 10MB max-request-size: 50MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0map-underscore-to-camel-case一定要开这样数据库的create_time字段才能自动映射到 Java 类的createTime属性不用写一堆冗长的 ResultMap。4.2 JWT 认证与拦截器实现前后端分离项目里Session 的体验并不好跨域的时候要配一堆 CORS 参数而且多端复用困难。我用的是 JWT 无状态认证流程概括成三步登录成功后服务端生成一个带用户 ID 和角色的 Token 返回前端前端把 Token 存入 localStorage每次 axios 请求时通过请求拦截器把 Token 塞进 Authorization 头后端用拦截器或 Spring Security 过滤器解析 Token获取当前用户信息。我这里用的是 Spring Security 自定义 JWT 过滤器核心代码不复杂Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { Autowired private UserDetailsServiceImpl userDetailsService; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); String username JwtUtil.parseToken(token); if (username ! null SecurityContextHolder.getContext().getAuthentication() null) { UserDetails userDetails userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } }注意一个细节注册接口、短信验证码接口这类不需要认证的路径要在 SecurityConfig 里放行否则会 401。我用了一个/api/auth/**前缀统一处理省事不少。4.3 捐赠业务的核心实现捐赠是整个平台的灵魂也是代码逻辑最严谨的一块。捐赠的完整流程是用户在项目详情页点击捐赠 → 填金额 → 后端创建订单状态为待支付 → 选择支付方式测试环境我接了模拟支付线上可以接微信/支付宝的官方接口→ 支付成功后回调 → 修改订单状态 → 项目募集金额 N → 生成电子证书编号 → 记录流水。后端创建订单的关键代码Service public class DonationServiceImpl implements DonationService { Autowired private DonationOrderMapper donationOrderMapper; Autowired private ProjectService projectService; Override Transactional(rollbackFor Exception.class) public String createOrder(Long userId, Long projectId, BigDecimal amount) { // 校验项目状态 DonationProject project projectService.getById(projectId); if (project null || project.getStatus() ! 1) { throw new BusinessException(项目不存在或不在募集中); } DonationProjectVO vo projectService.detail(projectId); if (vo.getLeftAmount().compareTo(amount) 0) { throw new BusinessException(捐赠金额超过项目剩余需募集金额); } // 生成订单 DonationOrder order new DonationOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setProjectId(projectId); order.setAmount(amount); order.setPayStatus(0); donationOrderMapper.insert(order); return order.getOrderNo(); } }注意这里Transactional注解创建订单和更新募集金额是强一致性的必须放到同一个事务里。后来我加了一个超时未支付自动取消的定时任务Spring 自带的Scheduled就能做每分钟扫一次超时订单。支付成功的回调处理里我用订单号作为幂等键。同一个回调可能会被支付平台推送多次如果每次都执行“修改状态 增加金额”逻辑金额就会多算。解决方式是在更新订单状态时带上条件UPDATE donation_order SET pay_status 1, pay_time NOW() WHERE order_no #{orderNo} AND pay_status 0这个 SQL 返回的受影响行数是 1 才继续后续操作增加金额、生成凭证如果是 0说明订单已经处理过了直接丢弃。4.4 Vue 前端环境搭建与页面实现前端我从创建项目开始说。开发环境需要先确保 Node.js 版本 ≥ 16建议 18 LTS。创建 Vue 3 项目npm create vitelatest charity-web -- --template vue cd charity-web npm install npm install vue-router4 pinia element-plus axios echartsElement Plus 的引入我这里采用了全局引入的方式因为后台管理系统组件用得比较全按需引入反而麻烦。在main.js里import { createApp } from vue import { createPinia } from pinia import ElementPlus from element-plus import element-plus/dist/index.css import zhCn from element-plus/es/locale/lang/zh-cn import App from ./App.vue import router from ./router const app createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus, { locale: zhCn }) app.mount(#app)路由设计上前台和管理后台我用了不同的布局组件通过嵌套路由挂载。管理后台的菜单是根据用户角色动态生成的核心思路是在后端登录接口的返回值里带上 roles 和 permissions前端拿到后通过router.addRoute动态添加路由。axios 封装是前端的基建工程必须做得顺手import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 15000 }) 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) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.response?.data?.message || 网络异常请稍后重试) return Promise.reject(error) } ) export default request统一响应码是我在后端自定义的ResultT结构code 为 200 表示成功401 交给前端拦截器做跳转登录。这里有个小细节401 的判断不能只看 HTTP 状态码因为很多后端框架会把“业务异常”也用 200 返回。我和后端同事约定的规范是HTTP 状态码只用来表示请求是否成功到达服务器业务成功与否看响应体里的 code。这样前端拦截逻辑更干净。4.5 管理后台数据大屏的实现数据大屏是客户额外提的需求要用图表展示平台的运行概况总捐款人数、累计捐款金额、项目完成度排行、近 7 天捐款趋势。我是用 ECharts 实现的。ECharts 的 Vue 3 用法很简单先安装包然后在组件里用init初始化import * as echarts from echarts import { onMounted, onBeforeUnmount, ref } from vue const chartRef ref(null) let chartInstance null const renderChart (data) { if (!chartInstance) { chartInstance echarts.init(chartRef.value) } chartInstance.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: data.dates }, yAxis: { type: value }, series: [{ name: 捐款金额, type: line, smooth: true, areaStyle: {}, data: data.amounts }] }) } onMounted(async () { const res await getTrendData() renderChart(res.data) }) onBeforeUnmount(() { chartInstance?.dispose() })有一个坑必须提醒用了 ECharts 的容器组件要给它一个明确的高度比如styleheight: 400px否则图表初始化后高度是 0什么都看不见。这种问题还不报错排查起来只能靠 F12 看元素很浪费时间。5. 关键业务逻辑与流程设计5.1 捐赠资金状态机设计捐赠订单不能只有一个“已支付”状态因为后续还牵扯退款、对账、公示这几个动作。我给订单状态定义了一套状态机逻辑待支付用户提交捐赠后创建默认状态。已支付支付回调验证成功资金进入平台待结算账户。已退款用户申请退款或重复支付被退回。已取消超时未支付定时任务自动取消。已公示该订单被纳入某期资金公示状态与公示批次关联。设计状态机的目的是防止非法操作比如“已取消”的订单不允许直接变成“已支付”“已退款”的订单不允许再次被公示。这些规则在代码里就是一组简单的 if 判断我写了一个枚举类public enum OrderStatusEnum { PENDING(0, 待支付), PAID(1, 已支付), REFUNDED(2, 已退款), CANCELED(3, 已取消), PUBLICIZED(4, 已公示); private final int code; private final String desc; // 枚举构造器和 getter 省略 }后续要加业务逻辑只需要在枚举里加状态码所有用到状态的地方都引用枚举不直接写魔法数字。这一点对于代码维护极其重要因为你不可能记得每个 0、1、2 代表什么枚举让你在读代码时一眼看明白状态含义。5.2 分页查询与 MyBatis-Plus 配置列表页是管理系统最常见的场景捐赠订单列表、项目列表、用户列表都要分页。MyBatis-Plus 的分页插件配置非常简单但有个常见的坑不配置分页插件分页 SQL 不会生效你会发现Page对象返回的 records 是全表的只是内存里截断。配置方式如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }查询代码示例比如给管理后台做项目分页Override public PageResultDonationProjectVO pageProjects(int page, int size, String keyword, Integer status) { PageDonationProject p new Page(page, size); LambdaQueryWrapperDonationProject wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), DonationProject::getTitle, keyword) .eq(status ! null, DonationProject::getStatus, status) .orderByDesc(DonationProject::getCreateTime); PageDonationProject result projectMapper.selectPage(p, wrapper); // 组装 VO关联查询机构名称等 return PageResult.from(result); }LambdaQueryWrapper的好处是类型安全字段名写错了编译期就报错不会像字符串方式的 QueryWrapper 那样等到运行时才抛异常。5.3 资金公示的透明机制做慈善平台最敏感的就是资金透明度。我们的设计是每一笔已支付的捐赠订单都可以在前台通过订单号或证书编号查询到具体信息每一期资金公示都包含“期间收入、期间支出、结余”三组数据并且公示发布后不允许修改或删除只能追加更正声明。这个逻辑本质上就是给资金流向建立了不可抵赖的记录链路。我甚至想过用区块链存哈希值但后来评估发现对于这种量级的应用中心化数据库 操作日志已经足够区块链的改造成本远大于实际收益。做项目要克制给客户创造实际价值的方案才是好方案。5.4 公告模块与前端路由传参公告模块不复杂但这里有一个值得记录的前端小技巧公告列表点击详情时Vue Router 传参我使用的是query方式router.push({ path: /notice/detail, query: { id: row.id } })详情页里读取import { useRoute } from vue-router const route useRoute() const noticeId route.query.id为什么不直接用params因为params在页面刷新后会丢失而query参数会保留在 URL 上刷新后依然可以正确加载详情。对于详情页这类场景用 query 是更稳妥的做法。6. 部署上线与运维要点6.1 后端打包与运行后端项目我建议使用 Maven 的 profile 区分环境配置在 pom.xml 里配置 dev/prod 两个 profile对应的配置文件是application-dev.yml和application-prod.yml。打包命令mvn clean package -DskipTests -Pprod打包产物是一个可执行的 jar 包放在服务器上直接用 Java 命令运行。为了让进程在后台稳定运行我写了启动脚本#!/bin/bash APP_NAMEcharity-admin.jar nohup java -Xms512m -Xmx1024m -jar $APP_NAME \ --spring.profiles.activeprod \ --server.port8080 \ /data/logs/charity.log 21 echo Application started, pid: $!日志输出一定要重定向到文件否则断开 SSH 后日志会丢。另外生产环境建议用Docker docker-compose部署把 MySQL、Redis、后端 jar 都编排在一起环境移植的时候会省很多事。6.2 前端构建与 Nginx 配置前端打包npm run build构建产物在dist目录上传到服务器后Nginx 配置如下server { listen 80; server_name charity.example.com; # 前端静态文件 root /data/www/charity-web; index index.html; # Vue Router history 模式配置所有非文件请求都指向 index.html location / { try_files $uri $uri/ /index.html; } # 后端 API 代理 location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态文件缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 7d; add_header Cache-Control public; } }这里最重要的一行是try_files $uri $uri/ /index.html;。如果不加这一行Vue Router 用 history 模式时用户直接访问www.example.com/project/detail/123这个地址会返回 404因为 Nginx 找不到对应的物理文件。加上后所有未知请求都会回退到index.html由前端路由接管。6.3 数据库备份策略这种系统最怕的不是被攻击而是数据丢失。捐赠记录、财务数据丢了是对捐赠人的不负责任。我写了一个简单的定时备份脚本#!/bin/bash BACKUP_DIR/data/backup/mysql DATE$(date %Y%m%d_%H%M%S) mysqldump -uroot -ppassword charity_db $BACKUP_DIR/charity_db_$DATE.sql find $BACKUP_DIR -type f -name *.sql -mtime 30 -delete配合 crontab 每天凌晨 3 点执行一次只保留最近 30 天的备份。有条件的建议再加一个异地备份或者定时同步到对象存储防止服务器磁盘坏了导致备份也丢了。7. 常见问题与排查技巧实录7.1 接口跨域问题开发联调阶段最常遇到的就是跨域。前端跑在 5173 端口后端跑在 8080直接访问必然被浏览器拦截。我解决跨域用的是后端配置CorsFilterConfiguration 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); } }但这里有个细节要提醒如果前端请求里带了Authorization头后端 Access-Control-Allow-Headers 必须包含Authorization否则跨域预检会失败。用上面的配置直接addAllowedHeader(*)可以解决但生产环境出于安全考虑可以收紧只放行实际需要的头。第二种更常见的方案是前端 Vite 配置代理后端完全不做跨域处理// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这两种方案我推荐第二种因为生产环境的 Nginx 本来就有反代开发环境的 Vite 代理和生产环境的 Nginx 代理形成了统一模式后端代码里不需要混入跨域逻辑。7.2 Spring Boot 版本引发的启动失败有朋友用我这份技术栈时遇到了启动问题排查后发现是 Spring Boot 3.x 的坑Spring Boot 3 要求 JDK 17并且自动配置变了原来很多spring.xx.xx配置项被迁移到spring.xxx.xxx。解决办法是让 pom.xml 维护统一的版本依赖不要混用 spring-boot 2.7 的 starter 和 3.x 的 starter。最稳妥的做法是直接用parent标签继承spring-boot-starter-parent所有依赖版本跟着 BOM 走不手动指定 starter 版本。另外引入第三方依赖时要检查版本兼容。比如mybatis-plus-boot-starter3.5.x 版本对应 Spring Boot 2.x 没问题但如果你的项目是 Spring Boot 3.x就要用mybatis-plus-spring-boot3-starter官方单独出了一个包很多人不知道就踩坑了。7.3 分页插件失效上线后有一次后台分页不对查了半天发现是 MyBatis-Plus 分页插件失效返回的总是全量数据页数也是错的。原因是在项目中引入了多个 MyBatis 相关依赖导致MybatisPlusInterceptor没有挂到真正使用的 SqlSessionFactory 上。检查target目录里的依赖树mvn dependency:tree删掉重复的 mybatis 依赖确保只有一个 MyBatis-Plus 版本。7.4 Element Plus 表格数据不更新列表页点搜索后表格没有刷新这类问题多半是响应式丢失本质是给对象新增了原本不存在的属性Vue 3 的 Proxy 虽然能代理嵌套对象但如果整行替换数组数据需要确保新数组本身是响应式的。我实际项目中遇到的是后端返回的数据结构里多了一层records赋值时写成了tableData.value res.data但 res.data 是PageResult对象而不是数组表格当然啥也渲染不出来。把赋值改成res.data.records就好了。这类数据嵌套问题建议前端先console.log看接口返回再吐槽不合理也不迟。7.5 常见问题速查表问题现象可能原因解决思路页面刷新 404Nginx 未配置 try_files加上try_files $uri $uri/ /index.html;登录状态一会就失效JWT 过期时间太短调整 Token 过期时间刷新机制上传图片失败Nginx 上传大小限制在 server 块加client_max_body_size 10m;列表查询慢没有走索引给 order_no、user_id、project_id 加普通索引后台 500 错误数据库字段与实体不对应开启 SQL 日志检查字段映射前后端联调失败接口字段命名不一致统一约定驼峰命名后端用 VO 规范输出8. 我的实操心得与扩展建议8.1 做这类平台最应该重视的一点慈善捐赠平台和普通商城系统最大的不同在于公信力。代码层面做不出公信力但代码可以做到“不敢腐”——通过完善的日志记录、订单追踪、公示机制让每一笔钱都有迹可循。我在项目里把所有资金操作创建订单、支付回调、退款、公示都写进了操作日志表sys_oper_log字段包括操作人、操作类型、业务单号、请求参数、返回结果、耗时。这个日志表在开发阶段看着是负担但一旦运营阶段出现数据对不上它就是唯一能还原现场的地方。8.2 如果可以重来我会简化什么如果重新做一遍我会砍掉物资管理模块里的部分定制功能。最初设计时给物资加了批次、有效期、供应商等一堆字段实际运营发现大部分物资都是批量采购后入库简单的一进一出就够用复杂的字段反而让管理人员觉得系统难用。项目做多了你会发现功能不是越多越好适合当前规模的管理效率才是第一位的。8.3 扩展方向这套架构的扩展空间很大。后续可以加入微信小程序端前端 API 完全复用可以接入真正的微信支付、支付宝支付替换掉模拟支付模块可以加一个志愿者招募模块把人与资源都纳入平台管理。技术的核心骨架搭好了业务的扩展都是水到渠成的事。就写到这里。做这个项目我最大的感受就是前后端分离的架构坑不少但只要环境配好、规范定好、约束建好后面写业务代码反而是一件顺畅的事情。希望这篇能让你少踩几个我踩过的坑。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门