简介本资源为基于JavaWeb开发的个人网上银行系统完整项目包采用MVC模式分层设计面向计算机相关专业的在校学生、课程设计或毕业设计开发者以及需要银行类业务系统参考案例的入门进阶学习者。包内共368个文件涵盖52个Java源文件、31个JSP页面、84个JavaScript脚本、48个CSS样式及83张PNG截图另附SQL数据库脚本、项目报告文档与war部署包压缩包约64.8MB结构清晰便于按模块查阅。系统功能覆盖用户登录、存款存取与增删改查、信息展示、转账、定期存款、活期转定期及日志查看等核心银行业务并配有需求分析、总体设计、详细设计与测试章节说明。目前已有147人学习关注适合作为课程大作业、毕设立项或二次开发的基础模板帮助读者快速理解MVC架构下的业务实现与数据库设计思路。1. 个人网上银行系统从 JavaWeb 源码到 MVC 分层落地很多同学做课程设计或毕业设计时一看到「个人网上银行系统」就觉得是个大工程其实拆开看它就是一个典型的 JavaWeb 增删改查项目只不过业务对象换成了账户、交易流水和转账记录。这个标题里的关键词是 JavaWeb、MVC 模式、源码、数据库、网上银行系统核心诉求是拿到一套能跑起来、能改、能写进报告里的完整案例。我一般会把它定位成「MySQL Servlet/JSP 三层 MVC」的经典组合适合刚学完 JavaWeb 想找一个完整案例练手的人也适合需要交课程设计、要截图和报告的人。它解决的不是高并发金融级问题而是让你把登录鉴权、账户查询、转账事务、流水记录这几条主线串起来真正理解 MVC 模式在真实项目里怎么分层。下面我按「先讲清结构再动手跑通最后说坑」的顺序展开中间会给出可抄的建表语句、目录结构和关键代码片段。2. 先搞懂这套 MVC 网上银行系统的分层与选型2.1 为什么是 Servlet JSP MySQL 这套组合JavaWeb 项目完整案例里最常见的骨架就是 Servlet 做控制器、JSP 做视图、JavaBean 做模型数据库用 MySQL。这套组合的好处是依赖少、IDE 支持好、资料多IDEA 里新建一个 Web 项目就能跑。MVC 模式在这里的落点是Servlet 接收请求并调用 ServiceService 处理业务逻辑并操作 DAODAO 用 JDBC 或连接池访问 MySQLJSP 只负责把数据渲染成页面。很多人写课程设计时把 SQL 直接写在 JSP 里那就是典型的反面教材后面改一个字段要翻十几个页面。选型上数据库连接池我一般用 Druid 或 C3P0前者监控页面方便排查慢查询。前端不用上框架Bootstrap 加 jQuery 足够因为重点是后端分层而不是页面炫技。JDK 用 8 或 11 都行Tomcat 用 8.5 或 9MySQL 用 5.7 或 8.0。这里要注意 MySQL 8.0 的驱动类名和时区参数跟 5.7 不一样后面避坑章节会细说。2.2 三层 MVC 的目录结构与职责边界一个能直接放进报告里的目录结构大致如下我按包名分层读者可以照着建bank-system/ ├── src/main/java/com/bank/ │ ├── controller/ # Servlet 控制器层 │ │ ├── LoginServlet.java │ │ ├── AccountServlet.java │ │ └── TransferServlet.java │ ├── service/ # 业务逻辑层 │ │ ├── AccountService.java │ │ └── TransferService.java │ ├── dao/ # 数据访问层 │ │ ├── AccountDao.java │ │ └── TransactionDao.java │ ├── entity/ # 实体类 │ │ ├── Account.java │ │ └── Transaction.java │ └── util/ # 工具类 │ └── DBUtil.java ├── src/main/webapp/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── views/ # JSP 页面 │ │ ├── login.jsp │ │ ├── dashboard.jsp │ │ └── transfer.jsp │ └── static/ # css/js └── pom.xml 或 lib/职责边界要卡死Controller 只做参数校验和跳转Service 管事务和业务规则DAO 只管 SQL。转账这种涉及两条 UPDATE 的操作必须放在 Service 里加事务否则一边扣钱一边没到账就是血泪事故。2.3 数据库表设计账户表与流水表的最小集合网上银行系统的数据库不用太复杂两张核心表就能撑起主要功能。账户表存余额和用户信息流水表存每一笔交易。建表语句如下-- 账户表一个用户对应一个账户 CREATE TABLE t_account ( id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(19) NOT NULL UNIQUE COMMENT 卡号, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(64) NOT NULL COMMENT 密码哈希, balance DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT 余额, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0冻结, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 流水表记录每笔转账 CREATE TABLE t_transaction ( id INT PRIMARY KEY AUTO_INCREMENT, from_card VARCHAR(19) NOT NULL COMMENT 转出卡号, to_card VARCHAR(19) NOT NULL COMMENT 转入卡号, amount DECIMAL(15,2) NOT NULL COMMENT 金额, remark VARCHAR(100) DEFAULT NULL COMMENT 备注, status TINYINT NOT NULL DEFAULT 1 COMMENT 1成功 0失败, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_from_card (from_card), INDEX idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;金额字段一定用 DECIMAL 而不是 FLOAT浮点数算钱会出现 0.10.2 不等于 0.3 的玄学问题。卡号加唯一索引流水表给转出卡号和创建时间加索引后面查最近流水会快很多。密码字段长度给 64 是为了存 SHA-256 哈希别存明文。3. 把源码跑起来IDEA 配置、连接池与登录鉴权3.1 IDEA 运行 JavaWeb 项目的完整配置步骤拿到源码压缩包后第一步不是急着点运行而是把环境对齐。我一般按这个顺序来先确认 JDK 版本再配 Tomcat最后检查 Artifact。具体操作如下File → Project Structure → Project把 SDK 设成 1.8 或 11Language level 对应。Project Structure → Modules确认 src/main/java 被标记为 Sourceswebapp 被标记为 Web。Run → Edit Configurations → 加一个 Tomcat Local在 Deployment 里添加 Artifact选war exploded。检查 Application context一般设成/bank这样访问路径就是http://localhost:8080/bank/login.jsp。如果启动时报 404八成是 Artifact 没配或者 web.xml 里 welcome-file 写错。如果报 ClassNotFound检查 lib 目录下的 jar 有没有被加到 Artifact 的 WEB-INF/lib 里。用 Maven 的话直接mvn clean package再部署 war 包更省事。3.2 数据库连接池配置与 DBUtil 写法连接池我一般用 Druid配置文件放src/main/resources/druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/bank_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot passwordyour_password initialSize5 maxActive20 maxWait3000对应的 DBUtil 负责加载配置并返回连接public class DBUtil { private static DataSource dataSource; static { try { Properties props new Properties(); // 从类路径加载配置文件 props.load(DBUtil.class.getClassLoader() .getResourceAsStream(druid.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(连接池初始化失败: e.getMessage()); } } // 获取连接调用方负责 close public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 依次关闭忽略异常避免掩盖业务异常 try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }参数说明initialSize是启动时建立的连接数maxActive是最大连接数maxWait是获取连接的超时毫秒数。这三个值在课程设计里够用生产环境要按 QPS 调。注意serverTimezone必须写否则 MySQL 8.0 会报时区错误。3.3 登录鉴权与 Session 拦截登录逻辑放在 LoginServlet 里校验通过后把用户信息写进 SessionWebServlet(/login) public class LoginServlet extends HttpServlet { private AccountService accountService new AccountService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cardNo req.getParameter(cardNo); String password req.getParameter(password); // 密码先做 SHA-256 再比对避免明文传输存储 Account account accountService.login(cardNo, sha256(password)); if (account null) { req.setAttribute(msg, 卡号或密码错误); req.getRequestDispatcher(/WEB-INF/views/login.jsp).forward(req, resp); return; } req.getSession().setAttribute(loginUser, account); resp.sendRedirect(req.getContextPath() /account); } }关键点是登录成功后用sendRedirect而不是forward避免刷新页面重复提交。Session 里存的是 Account 对象后续页面用 EL 表达式取。为了防止未登录直接访问 dashboard可以加一个 Filter 统一拦截/account和/transfer路径检查 Session 里有没有 loginUser没有就跳回登录页。这个 Filter 是 MVC 里容易被忽略的一层但它是安全底线。4. 转账事务与流水查询核心业务代码怎么写4.1 转账的 Service 事务边界转账是整个系统里唯一必须用事务的地方因为它涉及两条 UPDATE。Service 层写法如下public class TransferService { private AccountDao accountDao new AccountDao(); private TransactionDao transactionDao new TransactionDao(); public boolean transfer(String fromCard, String toCard, BigDecimal amount, String remark) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 检查转出账户余额是否充足 Account from accountDao.findByCardNo(conn, fromCard); if (from null || from.getBalance().compareTo(amount) 0) { conn.rollback(); return false; } // 2. 扣减转出方余额 accountDao.updateBalance(conn, fromCard, amount.negate()); // 3. 增加转入方余额 accountDao.updateBalance(conn, toCard, amount); // 4. 写入流水 transactionDao.insert(conn, fromCard, toCard, amount, remark); conn.commit(); return true; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ignored) {} e.printStackTrace(); return false; } finally { DBUtil.close(conn, null, null); } } }逻辑说明setAutoCommit(false)之后所有操作在同一个连接里任何一步抛异常都 rollback。参数amount用 BigDecimalnegate()取负数表示扣款。DAO 方法都接收同一个 Connection保证事务不跨连接。这里最容易翻车的是 DAO 内部自己又去DBUtil.getConnection()那样事务就断了扣款成功但入账失败。4.2 流水查询的分页与条件筛选流水查询要支持按卡号和时间段筛选SQL 用动态拼接public ListTransaction query(String cardNo, String startDate, String endDate, int page, int pageSize) { StringBuilder sql new StringBuilder( SELECT * FROM t_transaction WHERE (from_card ? OR to_card ?)); ListObject params new ArrayList(); params.add(cardNo); params.add(cardNo); if (startDate ! null !startDate.isEmpty()) { sql.append( AND create_time ?); params.add(startDate 00:00:00); } if (endDate ! null !endDate.isEmpty()) { sql.append( AND create_time ?); params.add(endDate 23:59:59); } sql.append( ORDER BY create_time DESC LIMIT ?, ?); params.add((page - 1) * pageSize); params.add(pageSize); // 执行查询并映射成 ListTransaction return jdbcTemplate.query(sql.toString(), params.toArray(), new TransactionMapper()); }参数说明page从 1 开始pageSize一般给 10 或 20。LIMIT的两个参数是偏移量和条数偏移量是(page-1)*pageSize。时间段查询要把结束日期补到 23:59:59否则当天的记录查不到。索引idx_create_time在这里会生效数据量上万条也不慢。4.3 JSP 页面如何只做展示不做逻辑JSP 里只允许出现 EL 和 JSTL不允许写 Java 代码块。dashboard 页面展示余额和最近流水% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table classtable thead trth时间/thth对方卡号/thth金额/thth备注/th/tr /thead tbody c:forEach items${transactions} vartx tr td${tx.createTime}/td td${tx.fromCard loginUser.cardNo ? tx.toCard : tx.fromCard}/td td${tx.fromCard loginUser.cardNo ? - : }${tx.amount}/td td${tx.remark}/td /tr /c:forEach /tbody /table这里用三元表达式判断当前用户是转出方还是转入方决定显示对方卡号和金额正负。逻辑很轻适合放在视图层。如果发现页面报javax.el.PropertyNotFoundException检查实体类字段名和 getter 是否对应EL 取的是 getter 而不是字段。5. 避坑与排查这套源码最容易翻车的五个地方5.1 中文乱码POST 和 GET 要分开处理现象登录页输入中文用户名后数据库里存的是问号。原因POST 请求体默认用 ISO-8859-1 解码GET 请求参数在 Tomcat 8 以后由 server.xml 的 URIEncoding 决定。解决在 Filter 或 Servlet 最前面加req.setCharacterEncoding(UTF-8)只对 POST 有效GET 请求要么改 Tomcat 配置要么在代码里手动new String(name.getBytes(ISO-8859-1), UTF-8)。数据库连接串里也要带characterEncodingutf8三处都对齐才不会乱码。5.2 MySQL 8.0 驱动类名和时区报错现象启动时报Loading class com.mysql.jdbc.Driver is deprecated或者The server time zone value is unrecognized。原因MySQL 8.0 的驱动类是com.mysql.cj.jdbc.Driver旧类名虽然能用但会警告时区不配会直接连不上。解决把 driverClassName 改成新类名URL 里加serverTimezoneAsia/Shanghai。如果还报 SSL 警告加useSSLfalse。这三个参数是 MySQL 8.0 的标配缺一个都可能翻车。5.3 事务失效DAO 自己拿了新连接现象转账时扣款成功但对方没收到或者两边都没变。原因Service 里开了事务但 DAO 方法内部又调用DBUtil.getConnection()拿了一个新连接两条 SQL 不在同一个事务里。解决DAO 方法统一接收外部传入的 ConnectionService 负责开关事务和关闭连接。这是 MVC 分层里最隐蔽的坑代码看着没问题跑起来就是数据不一致。5.4 JSP 放在 WEB-INF 外导致直接访问现象不登录也能直接访问dashboard.jsp看到数据。原因JSP 放在 webapp 根目录下浏览器可以绕过 Servlet 直接请求。解决把所有业务 JSP 移到WEB-INF/views/下WEB-INF 目录浏览器无法直接访问只能通过 Servlet forward 进入。这是 MVC 模式的基本安全约定很多人图省事放外面答辩时被问一句就露馅。5.5 连接池耗尽连接没关或 maxActive 太小现象用一会儿就报GetConnectionTimeoutException。原因要么 DAO 里忘了 close要么maxActive设得太小要么慢查询占着连接不放。解决用 try-with-resources 或在 finally 里统一 close把maxActive调到 20 以上打开 Druid 监控页面看活跃连接数定位是哪条 SQL 慢。连接泄漏是黑匣子监控页面就是后悔药。6. 进阶技巧用报告和截图把项目讲成一个完整故事课程设计或毕业设计不只是代码能跑报告和截图同样占分。我一般会先跑通三条主流程再截图登录成功进 dashboard、转账成功看流水、查询流水带条件筛选。截图要包含浏览器地址栏和关键数据证明是真跑起来的而不是画的。报告结构按「需求分析 → 系统设计 → 数据库设计 → 核心代码 → 测试截图」写数据库设计部分把 E-R 图和建表语句放上去核心代码只贴 Service 事务和登录鉴权这两段其他用文字描述。进阶一点的做法是给转账加一个幂等控制用流水表的唯一索引或者请求号去重防止用户连点两次提交转两笔。还可以把密码哈希从 SHA-256 换成 BCrypt加盐更安全。如果想让报告更有说服力可以画一张 MVC 请求流转图标注 Servlet、Service、DAO、JSP 各自的位置答辩时对着图讲比念代码强得多。我自己做这类项目最大的教训是别一上来就写页面先把数据库表和 Service 事务写对页面最后套模板。表结构改一次后面所有代码都要跟着动前期多花半小时设计后期省半天返工。希望帮到你。本文还有配套的精品资源点击获取