尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SSM框架与BS架构下的家庭理财管理系统开发实践

SSM框架与BS架构下的家庭理财管理系统开发实践 1. 这个题目为什么偏偏是SSM和BS架构说实话看到家庭理财管理系统这个题目很多人第一反应是这不就是个记账本吗但真正动手做的时候才发现这个题目考察的东西远比想象中全面。它既有基础的CRUD又有统计报表、预算预警这类偏逻辑的功能还涉及多用户、多账户的数据建模很适合作为课程设计或毕业设计的选题。而SSM框架也就是Spring加SpringMVC加MyBatis这组经典组合在这个题目里的定位相当清晰Spring管对象、管事务SpringMVC管请求路由MyBatis管数据库访问各司其职几乎是一套标准答案。为什么不是Spring Boot这是我在做方案选型时反复被问过的问题。Spring Boot确实开发效率更高上手更快但很多高校的教学大纲还是基于SSM来考核的。SSM这套东西的好处在于配置是显式的每个框架的边界感非常清楚你能从web.xml看到一个Web项目的骨架能从applicationContext.xml看到Spring容器是怎么被初始化的。这些对于理解JavaWeb的本质很有帮助。而且从答辩角度来说SSM让你有大量可讲的细节——每个配置文件的含义、每个框架的原理、事务传播机制怎么设置这些在Spring Boot里都被封装掉了反而没什么素材可以讲。BS架构也就是Browser/Server架构在这里的选择逻辑也很直白。家庭理财这种场景数据都在服务端浏览器只负责展示和录入家庭成员只要打开浏览器就能用不需要像C/S架构那样每台机器安装客户端维护成本低得多。把整个项目打成war包丢到Tomcat里同一局域网内任何一台设备都能访问这种部署方式对于家庭这个使用场景来说天然就是贴合度最高的。再补充一点关于IDEA的考虑。标题里特别带上了idea说明这个项目从开发到运行都是在IntelliJ IDEA里完成的。IDEA对Maven、Tomcat、MyBatis插件的支持都非常成熟尤其是它的智能提示和热部署机制比早期的Eclipse写SSM项目要舒服一大截。本文后面的部署排坑部分也都是基于IDEA这个开发环境来展开的用Eclipse的话有些操作细节会对不上。2. 数据库先行家庭理财的实体关系与表结构设计动手写代码之前我建议先把数据模型想透。这一块如果出了岔子后面改起来特别恶心因为Service层和Mapper层都是围绕表结构转的。我是在草稿纸上画了一遍实体关系图确认了五个核心实体之后才开始建库的。2.1 业务边界的确定家庭理财和个人记账的差异先说清楚一点家庭理财管理系统和个人记账软件的本质区别在于数据归属的层级。个人记账只需考虑我一个用户有哪些账目但家庭理财要考虑一个家庭下可能有多个成员每个成员有自己的账户和账目反过来某些账目又是全家共用的。不过大多数课程设计不会把权限体系做那么复杂折中的做法是系统支持多用户注册每个用户的数据相互隔离这一层权限通过登录后的用户ID来控制不搞复杂的角色权限模型。这样既体现了多用户家庭的概念又不至于让项目失控。我最终定的业务边界是四块账户管理银行卡、现金、支付宝、微信等资金载体每个账户有独立余额收支记录所有收入和支出的流水明细每条流水关联账户和分类分类管理收入分类和支出分类内置常用分类允许用户自定义预算与统计按分类和时间维度设定预算通过统计报表展示收支结构2.2 五张核心表的结构设计围绕上面的业务边界我设计了五张表用户表、账户表、分类表、交易流水表、预算表。这五张表的字段设计如下表名关键字段说明t_userid, username, password, nickname, create_time用户表password存加盐后的MD5值t_accountid, user_id, name, type, balance, create_time账户表type区分银行卡/现金/支付宝等t_categoryid, user_id, name, type, sort, create_time分类表type为1收入、0支出user_id为0表示系统内置t_transactionid, user_id, account_id, category_id, type, amount, remark, trans_time交易流水表type为1收入、0支出t_budgetid, user_id, category_id, amount, start_date, end_date预算表按分类和时间区间设定预算每个字段的选择都是有讲究的。举个例子金额字段我用的DECIMAL(10,2)而不是FLOAT或者DOUBLE。浮点类型在计算时会产生精度误差对于钱财相关的数据这是无法接受的。DECIMAL是定点数可以精确表示两位小数配合Java端用BigDecimal接收全链路精度都不会丢。用户表这里有个值得注意的细节分类表里我留了一个user_id0的标记表示这是系统内置分类。这样新用户注册后无需任何初始化脚本就能看到工资餐饮交通这些常用分类同时用户还能自己新增分类实现了内置和自定义的融合。交易流水表是整张系统的核心表字段设计要特别注意几个点trans_time虽然看起来是时间字段但业务上记账时间和交易发生时间可能是两回事——用户可能在月底补记月初的开销所以这里需要有可修改的时间而不是直接取系统当前时间。关联查询时account_id用于查从哪个账户花的钱category_id用于查这笔钱属于什么分类。2.3 事务与数据一致性余额和流水的联动在记账这个场景里最核心的数据一致性问题是新增一条支出记录时账户余额要同步减少删除或修改流水时余额也要对应调整。这个联动逻辑在单表操作里看不出什么问题但跨表业务就需要Spring事务来兜底。我的做法是在Service层把插入流水 更新账户余额放进同一个事务方法。任何一步抛异常事务回滚账户余额不会出现流水记了但钱没扣的尴尬状态。事务管理器的配置放在Spring配置里给需要事务的方法加上Transactional注解传播级别用默认的REQUIRED即可。实际测试的时候我专门做过一个验证故意在插入流水之后、更新余额之前抛一个运行时异常看最终数据库里是不是两条记录都没了。结果是预期的余额纹丝不动这就说明事务配置生效了。建议你也做一遍这个测试一方面验证自己的事务配置另一方面答辩时这是个非常好的加分点。3. 三层架构落地SSM在IDEA工程中的协作方式3.1 Maven项目的目录结构我习惯用Maven的war包结构来组织项目用IDEA新建项目时直接选Maven Archetype里的maven-archetype-webapp骨架。创建完之后手工补出标准的Java目录结构src/main/java com.example.familyfinance controller // 控制层负责接收请求和返回视图/数据 service // 业务层接口加实现类 impl mapper // MyBatis的Mapper接口 entity // 实体类和数据库表一一对应 vo // 视图对象用于页面展示的聚合数据 interceptor // 拦截器处理登录验证 util // 工具类MD5加密、分页等 src/main/resources mapper // MyBatis的XML映射文件 applicationContext.xml // Spring核心配置 spring-mvc.xml // SpringMVC配置 jdbc.properties // 数据库连接配置 src/main/webapp WEB-INF web.xml jsp // 页面文件 static // js、css、图片这个目录结构其实是三个阶段堆积出来的结果最初只有一个entity和mapper跑通了单表查询然后发现Controller里业务逻辑越来越多抽出了service层再后来发现有些聚合数据不好映射到单表实体才加了vo包。你如果刚开始写不需要一步到位按需拆分反而更自然。entity层的字段类型有个容易踩坑的地方数据库里的DECIMAL(10,2)千万别用Double或Float接收一定要用BigDecimal。数据库里的DATETIME对应java.util.Date或者LocalDateTime都可以但数据库驱动配置的字符串要匹配否则会在时间类型转换上摔跟头。3.2 Spring和SpringMVC的职责分工SSM项目里配置文件是一大堆很多人搞不清谁是谁。我的理解很简单web.xml是总入口它负责告诉Tomcat两件事——Spring的根容器怎么初始化所有请求交给谁处理。!-- web.xml 核心配置片段 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namespringMVC/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-namespringMVC/servlet-name url-pattern//url-pattern /servlet-mapping filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingapplicationContext.xml管的是业务层和持久层的Bean包括数据源、事务管理器、Mapper的扫描。spring-mvc.xml管的是控制层包括Controller的扫描、视图解析器、静态资源放行。这两个配置文件的分界线就对应着三层架构的边界。这里有一个经验性的配置静态资源放行。如果你使用url-pattern//url-pattern作为DispatcherServlet的映射那么浏览器请求的CSS、JS、图片全部会先经过SpringMVC不配置放行规则的话页面会变成裸HTML没有任何样式。所以在spring-mvc.xml里要加mvc:resources location/static/ mapping/static/**/或者用mvc:default-servlet-handler/让Tomcat的默认Servlet来兜底处理静态资源。3.3 MyBatis的Mapper层设计原则MyBatis的Mapper层由接口加XML组成接口里声明方法XML里写SQL。一个我自己觉得很有用的原则是能用XML写SQL的地方就不要用注解写SQL。注解方式虽然看起来代码更少但只要SQL超过三行拼接动态条件的时候就会非常难看而且没法复用SQL片段。XML里的sql标签支持片段复用比如分页查询的WHERE条件可以抽出来列表查询和总数查询共用避免两处维护同样的条件逻辑。Mapper接口和XML的绑定关系要注意两个细节XML的namespace必须是Mapper接口的全限定名XML里的id必须和接口方法名完全一致。我见过太多人栽在这两点上一启动就报Invalid bound statement其实说白了就是namespace写错了或者XML文件没有被打进编译目录。说到编译目录还有个Maven的经典坑默认情况下Maven只把src/main/resources下的文件作为资源打包如果你把Mapper的XML放在src/main/java目录下和接口放在一起那么编译后的target目录里根本没有这些XML文件。要么乖乖把XML放在resources/mapper目录下要么在pom.xml里显式配置资源过滤build resources resource directorysrc/main/resources/directory /resource resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources /build这里我强烈建议直接使用resources/mapper目录不仅省了配置查找文件也方便不同模块的SQL文件可以按实体名分开存放。4. 功能开发实录登录、记账、统计与预算预警4.1 登录认证与拦截器机制家庭理财系统虽然不像电商那样需要极其复杂的权限体系但登录拦截还是必须的。没有登录直接访问主页这在面向用户的系统里是不可接受的。我用的是传统做法Session加拦截器。用户登录成功后把用户对象放进Session写一个继承HandlerInterceptor的拦截器类在preHandle方法里判断Session中是否存在用户不存在就重定向到登录页存在才放行。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user (User) request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login); return false; } return true; } }SpringMVC里配置拦截器时要注意排除登录相关的路径否则你连登录功能都访问不了形成死锁mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/doLogin/ mvc:exclude-mapping path/register/ mvc:exclude-mapping path/static/**/ mvc:exclude-mapping path/captcha/ /mvc:interceptor /mvc:interceptors密码处理这块课程设计里时间有限最常用的是MD5加盐。大致做法是盐值用用户名加上一个固定的字符串拼接然后MD5摘要之后存库。验证时用相同的算法重新计算比对。注意MD5现在已经被证明不够安全了但作为课程设计用是OK的答辩时可以提一句生产环境建议改用BCrypt来展示你的视野。4.2 记账功能的实现思路记账是整个系统的核心业务选择收入和支出类型、选择账户、选择分类、填金额、填备注、填时间然后保存。看起来就是一张表单的事但落到代码里要处理三件事第一金额和类型的校验。支出金额不能大于账户余额吗这个业务规则要看你怎么定义。家庭理财的实际场景里信用卡是可以透支的所以我不建议在记账时强制校验余额但可以在页面上做一个红色警告提示该账户余额可能不足。这个细节在答辩时很加分因为它体现了一种理解业务的深度而不是简单地把所有字段设置为非空。第二余额联动更新。新增支出时账户余额减去金额新增收入时账户余额加上金额。这个逻辑一定要放在Service层的事务方法里Transactional(rollbackFor Exception.class) public void addTransaction(TransactionVO vo) { // 组装实体 Transaction t buildTransaction(vo); // 插入流水 transactionMapper.insert(t); // 更新账户余额 Account account new Account(); account.setId(vo.getAccountId()); if (vo.getType() TransactionType.EXPENSE) { account.setBalance(vo.getAmount().negate()); } else { account.setBalance(vo.getAmount()); } accountMapper.updateBalance(account); }这里的updateBalance用的SQL是UPDATE t_account SET balance balance #{delta} WHERE id #{id}不是先把余额查出来再改。两种方式的差别在于并发安全先查再改在高并发场景下会有丢失更新问题直接用SQL累加则是在数据库层面完成的原子操作。第三分类和账户的联动展示。页面上的分类下拉框不是写死的要根据当前选的是收入还是支出动态加载对应类型的分类数据。这个用JQuery的Ajax请求或者最简单的隐藏表单切换都能实现。如果是学框架原理建议把Ajax请求走一遍体会下SpringMVC返回JSON数据的流程。4.3 统计报表的实现从SQL到ECharts家庭理财系统里最能体现管钱价值的功能就是统计报表。如果只有流水列表而没有汇总分析这个系统的吸引力会大打折扣。我的做法是用SQL在数据库端做聚合统计查出结果后转成JSON数据返回页面用ECharts画图。按月统计收入和支出是每个理财系统必备的查询SELECT DATE_FORMAT(trans_time, %Y-%m) AS month, SUM(CASE WHEN type 1 THEN amount ELSE 0 END) AS income, SUM(CASE WHEN type 0 THEN amount ELSE 0 END) AS expense FROM t_transaction WHERE user_id #{userId} AND trans_time BETWEEN #{startDate} AND #{endDate} GROUP BY DATE_FORMAT(trans_time, %Y-%m) ORDER BY month这个SQL有几个细节DATE_FORMAT把日期格式化成年月SUM(CASE WHEN...)实现了同一行数据里同时算出收入和支出避免了两次查询。MyBatis里对应的Mapper方法返回值可以定义成一个VO类里面放month、income、expense三个属性一个vo包就派上了用场。分类占比的统计思路类似按category_id分组再关联分类表查出分类名称算出支出总额最后在前端用饼图展示SELECT c.name AS categoryName, SUM(t.amount) AS totalAmount FROM t_transaction t LEFT JOIN t_category c ON t.category_id c.id WHERE t.user_id #{userId} AND t.type 0 AND t.trans_time BETWEEN #{startDate} AND #{endDate} GROUP BY t.category_id, c.name ORDER BY totalAmount DESC页面端ECharts的接入很简单下载一个ECharts的js文件放进static/js目录然后在JSP页面里通过${pageContext.request.contextPath}/static/js/echarts.min.js引入。图表初始化代码放在$(document).ready事件里用Ajax拿到后端返回的JSON数组后setOption填充图表。这里要提醒一个ECharts中文显示的问题如果你的数据里有中文分类名JSON返回时如果编码没有统一为UTF-8图表上就会显示乱码。SpringMVC的ResponseBody默认使用MappingJackson2HttpMessageConverter来转JSON可以通过在spring-mvc.xml里的消息转换器配置中强制指定UTF-8保证中文不乱码。4.4 预算预警Service层的业务规则判断预算预警功能本质上是一层伪实时的判断逻辑。用户每次保存一笔支出之后立即统计当前月份该分类的累计支出和预算做对比如果超了就弹一个提醒。这个逻辑放在Service层做比在数据库做存储过程要灵活得多。实现方式上我在BudgetController里新增一个checkBudget方法在addTransaction事务提交之后调用它统计当月该分类的支出总和超过预算就返回一个提示标志。但是这里有个细节事务提交之后再查询才能查到刚插入的这条记录。如果用同一个事务里的查询因为事务隔离级别的问题可能查不到最新的数据。所以最简单稳妥的方案是前端的记账请求返回成功之后紧接着发一个Ajax请求单独去调预算检查接口。把记账和检查预算拆成两个HTTP请求业务边界反而更清晰。预算表的查询要支持一个场景用户在不确定精确日期的情况下只需要选择月份。所以我存的是start_date和end_date默认start_date是当月1日end_date是当月末日。查询时用trans_time startDate AND trans_time endDate这比只存一个月份字符串跨年时会出错要可靠得多。4.5 分页查询的两种方案交易流水随着时间累积会越来越多全表查询把几千条数据一次丢给浏览器显然不合理。分页查询有两个选择用PageHelper插件或手写分页参数。PageHelper的使用非常简洁在查询之前调用PageHelper.startPage(pageNum, pageSize)紧接着执行的第一个Mapper查询会被自动加上LIMIT语句。它的原理是MyBatis的Interceptor接口在Executor执行之前拦截SQL动态拼上分页参数。这是个人尽皆知的好工具但它有一个容易忽略的点PageHelper.startPage方法只对紧接着的下一条查询生效中间如果穿插了其他Mapper查询分页就会错乱。手写分页其实也不复杂就是传offset和limit两个参数给Mapper然后额外写一条SELECT COUNT(*)查询总数。我在增长列表加载性能时测试过手写方式最大的好处是SQL完全可控而且能在一个方法里同时查出总数和当前页数据。对于家庭理财系统这种用户量不大的场景手写分页完全够用而且答辩时你可以把SQL原理讲得很透彻。我最终选择了手写分页因为PageHelper的黑盒机制在面试时很难讲清楚不如直接自己控制来得踏实。5. 部署运行阶段的高频坑从Tomcat到数据库连接5.1 IDEA里的Tomcat部署配置IDEA里直接运行SSM项目和平时写Spring Boot项目点一下main方法完全不同。你要先配置一个Tomcat Server然后在Deployment选项卡里把Artifact添加进去。这里的坑在于Artifact到底选择war还是war exploded。我的经验是开发调试阶段用war exploded因为它支持热部署修改代码后IDEA会自动把新的class文件同步到Tomcat的解压目录刷新页面就能看到效果不用反复重启。正式部署时再打成war包扔到独立Tomcat里。还有一个频繁踩坑的点IDEA里的Tomcat实例默认会用较高的端口如果你的机器上有多个Tomcat服务或者端口被占用看看日志里的Port already in use把配置里的HTTP端口改一下就行。如果改完了还是报占用很可能是上次运行的Tomcat进程没有彻底停掉在任务管理器里把对应的java进程杀掉再试。5.2 Maven依赖的版本兼容问题SSM项目里最难缠的问题就是依赖版本不兼容。我在初始化项目时使用的是如下组合Spring 5.3.x、SpringMVC 5.3.x、MyBatis 3.5.x、MyBatis-Spring 2.0.x还有一个mybatis-pagehelper插件如果使用的话版本要配对。这一套组合在Tomcat 8.5和Tomcat 9上都运行得很稳定。一个特别容易翻车的地方如果你用的Tomcat版本是10及以上那么传统的javax.servlet包就变成jakarta.servlet了而Spring 5系列还不支持Jakarta命名空间所以SSM项目别用Tomcat 10老老实实用Tomcat 9。如果你是跟着最新的IDEA版本装的Tomcat注意下载Tomcat 9而不是10否则启动时一堆ClassNotFoundException会让你怀疑人生。5.3 数据库连接配置的三个硬性要求数据库连接这块的坑主要集中在jdbc.properties的配置上。如果你用的是MySQL 8驱动类名必须是com.mysql.cj.jdbc.Driver而不是MySQL 5时常用的com.mysql.jdbc.Driver。URL里必须带时区参数否则连接时会报The server time zone value的错误jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/family_finance?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的密码关于characterEncodingutf8这个参数我专门验证过它保证了从数据库到驱动、从驱动到Java这一段的字符编码转换是UTF-8但如果你的JSP页面本身没有声明UTF-8T
返回列表