资讯动态

SSM家庭理财系统源码解析:配置文件、动态SQL与部署实战

发布时间:2026/9/14 15:06:16 来源:尧图企业网站定制
简介基于SSM框架的家庭理财系统完整项目包内含可运行的前后端源码与配套设计说明文档面向计算机专业学生、Java Web开发者及需要完成课程设计或毕业设计的读者。系统采用SpringSpringMVCMyBatis的经典分层架构包含管理员后台与App端用户功能涵盖用户管理、收支记录、统计报表等常见理财模块目录中配有初始化数据库SQL、安装/运行/构建脚本以及详细开发文档便于本地部署和二次开发。压缩包共1033个文件整体约18.94MB主要文件类型包括Vue组件、Java源码、XML/JSON配置、JavaScript以及大量界面截图和说明文档结构清晰可按需检索。已有239人在线学习适合作为SSM项目实战参考或理解家庭理财业务逻辑的入门案例。1. 别急着换Spring Boot这套SSM家庭理财源码先把账算清楚拿到这个压缩包的时候你大概率会在心里嘀咕一句都什么年头了怎么还在给SSMSpring SpringMVC MyBatis做交付解压后看到一串.vue.bak、一个main.css.bak以及1-install.bat、2-run.bat、3-build.bat三个脚本反而让人踏实了——这是一套文档齐全、能独立跑通的 JavaWeb 课程设计级项目。后端 SSM 框架提供 REST 接口前端是 Vue 2 Element UI 单页应用MySQL 存储整体 B/S 架构核心链路把一笔账、查月流水、汇总收支、对比预算全部串了起来。它没有 Spring Cloud 那套注册中心但恰好因为这样SSM 整合的边界问题暴露得非常清楚适合拿来做课设交差也适合想看 SSM 配置细节的人做二次开发。说明文档里那节“app端”实际指的就是浏览器端属于模板残留别被目录绕进去。2. SSM整合第一道坎三个配置文件分别管什么配错一个就白页2.1 从web.xml看Spring与SpringMVC的父子容器SSM 项目启动时第一个被读到的配置是web.xml它决定了 Spring 和 SpringMVC 谁先起来、各自加载什么。常见写法是ContextLoaderListener加载applicationContext.xml管 service、dao、数据源DispatcherServlet加载spring-mvc.xml管 controller、视图解析器、静态资源。这两个容器是父子关系Spring 父容器先初始化SpringMVC 子容器后初始化子容器能看到父容器的 Bean父容器看不到子容器的。listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param servlet servlet-namedispatcherServlet/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcherServlet/servlet-name url-pattern//url-pattern /servlet-mapping关键是url-pattern用了/把包括静态资源在内所有请求都交给 SpringMVC。如果配成*.do那前端 axios 请求路径也要全部加上.do后缀Vue 代码里很容易漏改启动后页面能打开但接口全 404。另外注意如果 service 和 dao 的扫描范围误写进了spring-mvc.xml事务切面会失效因为Transactional的代理是在 Spring 父容器里创建的父子容器 Bean 不互通时Controller 拿到的 service 不是被事务代理包过的那一个。2.2 applicationContext.xml数据源、Mapper扫描与事务这份文件是 SSM 整合的中枢核心是四个东西数据源、SqlSessionFactoryBean、MapperScannerConfigurer、事务管理器。这套家庭理财系统用的是 MySQL Druid配置如下bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/family_finance?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghaiamp;useSSLfalse/ property nameusername valueroot/ property namepassword valueroot/ property namemaxActive value20/ property nameinitialSize value5/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.family.entity/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.family.mapper/ /bean bean idtxManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertxManager/几个容易踩的点driverClassName在新版 MySQL 驱动下必须写com.mysql.cj.jdbc.Driver写老版的com.mysql.jdbc.Driver会直接启动失败URL 里的在 XML 里必须转义成amp;否则解析报错serverTimezoneAsia/Shanghai不加的话插入日期字段会差 8 小时。typeAliasesPackage配好后Mapper XML 里就可以直接用Bill而不是全限定类名com.family.entity.Bill。2.3 spring-mvc.xml与mybatis-config.xml的边界spring-mvc.xml管的是 Controller 层的东西最基础的三件套是包扫描、注解驱动、静态资源放行。我给这份系统补配置的时候一般会把 JSON 转换也放这里context:component-scan base-packagecom.family.controller/ mvc:annotation-driven mvc:message-converters bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter/ /mvc:message-converters /mvc:annotation-driven mvc:default-servlet-handler/!-- mybatis-config.xml -- configuration settings setting namemapUnderscoreToCamelCase valuetrue/ setting namelogImpl valueSTDOUT_LOGGING/ /settings /configurationmapUnderscoreToCamelCase把数据库的bill_date自动映射成实体属性billDate这个不开的话查询结果里带下划线的字段全是 null。System 配置这块三个文件的职责边界可以记成下面这张表配置文件加载方式管辖范围配错后的典型症状web.xml容器启动时加载Servlet 路由、全局监听器、字符编码过滤器请求进不到 Controller直接 404applicationContext.xmlContextLoaderListener 加载数据源、Service、Mapper、事务启动报 NoSuchBeanDefinitionException 或事务不生效spring-mvc.xmlDispatcherServlet 加载Controller、拦截器、消息转换器返回 JSON 变成乱码或视图解析失败mybatis-config.xmlSqlSessionFactoryBean 引入MyBatis 全局设置、插件下划线字段查出来为 null、日志不输出logImpl设成STDOUT_LOGGING是因为课设项目没接 logback用 MyBatis 自带的控制台输出最快看 SQL 参数不用翻文件。做完这套配置后启动时如果日志里出现了Root WebApplicationContext、然后跟着MappingServlet的初始化过程说明容器关系已经理顺了。3. 家庭记账的MyBatis持久层建表SQL、动态SQL与PageHelper分页3.1 四张业务表怎么设计家庭理财系统的核心是“人—账—分类—预算”四个维度。用户表存登录信息分类表定义收入和支出的类型账单表记录每一笔流水预算表按月份和分类控制花销。直接复用这套系统的建表思路表名和字段严格用下划线风格create table family_user ( id bigint primary key auto_increment, username varchar(64) not null unique, password varchar(128) not null, real_name varchar(64), create_time datetime default current_timestamp ) engineinnodb default charsetutf8mb4; create table family_category ( id bigint primary key auto_increment, user_id bigint not null, name varchar(32) not null, type tinyint not null comment 0支出 1收入, sort_order int default 0 ) engineinnodb default charsetutf8mb4; create table family_bill ( id bigint primary key auto_increment, user_id bigint not null, category_id bigint not null, amount decimal(10,2) not null, bill_type tinyint not null comment 0支出 1收入, bill_date date not null, remark varchar(255), create_time datetime default current_timestamp, key idx_user_date (user_id, bill_date) ) engineinnodb default charsetutf8mb4; create table family_budget ( id bigint primary key auto_increment, user_id bigint not null, category_id bigint comment null表示总预算, month varchar(7) not null, amount decimal(10,2) not null, unique key uk_user_month_cat (user_id, month, category_id) ) engineinnodb default charsetutf8mb4;账单金额必须用decimal(10,2)而不是 floatfloat 在累加时会出现 0.10.2 不等于 0.3 的精度问题预算表的unique约束保证了同一个用户同一个月同一个分类下不会插入两条预算idx_user_date是给第 4 章的月度统计用的索引where 条件按 user_id 和 bill_date 过滤时这个组合索引能直接命中最左前缀原则。3.2 动态SQL实现多条件流水查询首页的水流表格要支持按日期范围、收支类型、分类三个条件筛选MyBatis 的动态 SQL 是这个功能的主力select idselectBillPage parameterTypemap resultTypemap select b.id, b.amount, b.bill_type, b.bill_date, b.remark, c.name as category_name from family_bill b left join family_category c on b.category_id c.id where if testuserId ! null and b.user_id #{userId} /if if testcategoryId ! null and b.category_id #{categoryId} /if if testbillType ! null and b.bill_type #{billType} /if if teststartDate ! null and b.bill_date gt; #{startDate} /if if testendDate ! null and b.bill_date lt; #{endDate} /if /where order by b.bill_date desc, b.id desc /selectwhere标签的作用是自动处理第一个条件前面的and没条件时直接生成不带 where 的语句。gt;和lt;是 XML 转义写法直接写会把这个 mapper 文件解析挂掉。返回类型用了map好处是省去连表 VO 的编写查询结果里的category_name会直接成为 map 的 key。3.3 Service层事务与分页的坑Service 层把写操作和读操作分开标注Service Transactional(readOnly true) public class BillServiceImpl implements BillService { Autowired private BillMapper billMapper; Override Transactional public int addBill(Bill bill) { if (bill.getAmount() null || bill.getAmount().compareTo(BigDecimal.ZERO) 0) { throw new ServiceException(金额必须大于0); } return billMapper.insertSelective(bill); } }类上的readOnly true是给查询优化的但要注意它只对当前类里Transactional生效的方法起作用。新增和修改方法用Transactional覆盖保证记账失败时不会产生脏数据。分页用的 PageHelper 是课设里的标准选择Controller 端写法固定两种一种传 Page 参数一种在 service 查询前起拦截RestController RequestMapping(/api/bill) public class BillController { Autowired private BillService billService; GetMapping(/page) public Result page(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, BillQuery query) { PageHelper.startPage(pageNum, pageSize); ListMapString, Object list billService.queryPage(query); PageInfoMapString, Object pageInfo new PageInfo(list); return Result.ok(pageInfo); } }PageHelper.startPage必须紧挨着 MyBatis 查询方法调用中间不能穿插其他查询否则分页线程变量的数据会被下一条查询消费掉导致分页错乱或者查不出数据。这个坑在列表页和统计页共用同一个 service 方法时特别容易触发建议在 Controller 里调用分页查询后立刻把PageInfo组装好不要再做其他数据库操作。4. 收支统计与预算预警MySQL聚合查询与Vue视图态同步4.1 按分类汇总收支的SQL聚合月度统计页需要展示“这个月吃饭花了多少、交通花了多少”。用 GROUP BY 配合 SUM 和 COUNT 能一次拿到全部分类的汇总数据不用在 Java 里循环加select c.id as category_id, c.name as category_name, sum(b.amount) as total_amount, count(1) as bill_count, max(b.bill_date) as last_date from family_bill b inner join family_category c on b.category_id c.id where b.user_id #{userId} and b.bill_type #{billType} and date_format(b.bill_date, %Y-%m) #{month} group by c.id, c.name order by total_amount descdate_format(b.bill_date, %Y-%m)直接按月过滤前端传2025-01这样字符串进来SQL 层就完成了月份匹配。要注意date_format会让bill_date上的索引失效数据量超过几万条后建议改成b.bill_date #{monthStart} and b.bill_date #{monthEnd}的范围查询既走索引语义也清晰。inner join在这里比left join合适因为统计只关心有账单的分类没有流水的分类没必要出现。4.2 返回Map类型后前端怎么收Mapper 接口返回ListMapString, Object这就是 MyBatis 的弱类型用法。SQL 里查出来的total_amount是 BigDecimal 类型前端 JSON 序列化后可能变成带很多位小数的数字最好在 SQL 层处理精度select c.id as category_id, c.name as category_name, cast(sum(b.amount) as decimal(10,2)) as total_amount, count(1) as bill_count from family_bill b inner join family_category c on b.category_id c.id group by c.id, c.namecast(... as decimal(10,2))让返回字段保持在两位小数省去前端再toFixed(2)的处理。Vue 页面里拿到数组后直接遍历渲染即可monthReport({ month: this.currentMonth, billType: 0 }).then(({ data }) { this.expenseList data const total this.expenseList.reduce((acc, item) { return acc parseFloat(item.total_amount) }, 0) this.expenseTotal total.toFixed(2) })注意parseFloat(item.total_amount)因为 JSON 中的数字也可能被某些拦截器转成字符串代码里显式转一次最稳。如果后端返回的字段名是category_id而前端模板里写categoryId那 Vue 会渲染出 undefined这是连表查询不写实体类时最常见的低级错误字段名以 SQL 别名为准前端别自动给你换驼峰。4.3 预算预警Java层算还是SQL层算预算预警有两种实现思路第一种是 SQL 层直接 JOIN 预算表算超支比例第二种是查询后 Java 内存中比对。课程设计一般数据量小第二种维护起来更直观也更方便后期扩展“超支提醒短信”这类功能public ListMapString, Object monthReportWithBudget(Long userId, String month) { ListMapString, Object report statisticMapper.selectMonthSummary(userId, month); ListMapString, Object budgets budgetMapper.selectByMonth(userId, month); MapLong, BigDecimal budgetMap budgets.stream() .collect(Collectors.toMap( m - ((Number) m.get(category_id)).longValue(), m - new BigDecimal(m.get(amount).toString()) )); for (MapString, Object row : report) { Long categoryId ((Number) row.get(category_id)).longValue(); BigDecimal budget budgetMap.getOrDefault(categoryId, BigDecimal.ZERO); BigDecimal actual new BigDecimal(row.get(total_amount).toString()); row.put(budget, budget); if (budget.compareTo(BigDecimal.ZERO) 0) { BigDecimal rate actual.multiply(BigDecimal.valueOf(100)) .divide(budget, 2, RoundingMode.HALF_UP); row.put(used_rate, rate); } else { row.put(used_rate, null); } } return report; }这里的类型转换比看起来要啰嗦MySQL 的category_id查出来可能是 Long也可能是 Integer取决于驱动和字段类型统一用((Number) ...).longValue()转换最安全amount用toString()转 BigDecimal 是避免直接把Object强转时崩掉。预算状态判断放到前端做状态划分如下used_rate状态推荐展示样式null未设预算灰色标签0 ~ 60正常绿色60 ~ 80留意黄色80 ~ 100接近上限橙色100 以上超支红色Vue 端对应这段逻辑状态在computed里做比在mounted里改来改去要干净computed: { tableWithStatus() { return this.expenseList.map(item { const rate item.used_rate let status 未设预算 if (rate ! null rate ! undefined) { if (rate 60) status 正常 else if (rate 80) status 留意 else if (rate 100) status 接近上限 else status 超支 } return { ...item, status } }) } }5. 部署脚本与.bak文件恢复把二次开发的时间省下来这套源码自带的三个.bat基本把环境的坑提前踩完了。建议先跑1-install.bat再跑2-run.bat跳过 install 直接 run 的话本地仓库缺依赖会报一堆Could not resolve artifact。三个脚本的内容大致是这样echo off rem 1-install.bat先把项目依赖装进本地Maven仓库 mvn clean install -DskipTests pause-DskipTests跳过测试是为了不让课设里可能写坏的 JUnit 用例卡住打包但注意这一步只编译不运行测试不会生成可执行的 war。真正的打包在3-build.bat里做把-DskipTests换成-Dmaven.test.skiptrue更彻底连测试代码编译都跳过。2-run.bat里常见做法是mvn tomcat7:run -Dmaven.tomcat.port8080用 tomcat7 插件跑的项目端口改在这里而不是去改web.xml或者找server.xml。跑起来如果提示端口被占用先用netstat -ano | findstr :8080找到占用进程别直接改端口绕过后面前端代理全都对着 8080 的话改端口就要连带改 Vue 的 baseURL。.vue.bak这批文件的含义是发布者改代码前留的备份恢复方式不是删掉.bak就能生效而是要把备份文件覆盖回正常文件名copy /Y update-password.vue.bak src\views\system\update-password.vueLinux 下对应cp update-password.vue.bak src/views/system/update-password.vue。恢复之后记得强制刷新浏览器缓存Vue 单页应用对资源做的 hash 是基于文件内容生成的IDE 里改了文件但没触发 Webpack 重新编译刷新也看不到变化。实际二次开发时更推荐用diff去比对.bak和当前文件能直接看出发布方改了什么这比从头啃文档快得多。比对完确定不需要旧版再删留着.bak还能当版本回滚点数据库连接要改的话applicationContext.xml里的username和password改完记得同步检查前端代理配置里的跨域地址否则后端连上了页面上的接口还是 401。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价