背景

模拟银行监管报送系统(JSP + AJAX + XML 模板 + MySQL 存储过程),报表数据存储在数据库中,报表模板存储在文件系统中。

1. 系统目录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
src/
 └── main/
     ├── java/
     │   └── com/yutao/report/
     │       ├── servlet/
     │       │    └── ReportServlet.java       ← 控制层入口
     │       ├── service/
     │       │    └── ReportService.java       ← 业务逻辑层
     │       ├── dao/
     │       │    └── ReportDAO.java           ← 数据访问层
     │       └── util/
     │            └── DBUtil.java              ← 数据库连接工具
     ├── resources/
     │    └── db.properties                    ← 数据库配置
     └── webapp/
          ├── index.jsp                        ← 页面展示入口
          ├── report.jsp                       ← 报表展示入口
          └── WEB-INF                          
               └── templates                   ← xml 模版目录
               │    ├── report_balance_summary.xml    ← 报表模板1
               │    ├── report_deposit_summary.mxl    ← 报表模板2
               │    ├── report_large_exposures.xml    ← 报表模板3
               │    ├── report_liquidity_ratio.xml    ← 报表模板4
               │    ├── report_loan_exposure.xml      ← 报表模板5
               │    ├── report_large_exposures.xml    ← 报表模板6
               │    └── report_suspicious_transactions.xml     ← 报表模板7
               └── web.xml     ← Servlet 映射配置

2. 项目调用链路

项目访问地址:http://localhost:8080/bank-regulatory-reporting/

下面以 “用户在报表页面选择时间和分支,点击查询” 为例:

步骤 调用来源 → 目标 主要方法 / 行为 输入参数 输出结果 / 下一步
JSP 页面(report.jsp) 用户选择“报表类型 + 时间 + 分支”,点击“查询”按钮 reportCode, reportDate, branchId 发送 POSTGET 请求到 /report
ReportServlet doPost()doGet() 方法中接收参数,识别请求类型(查询/保存/提交) 从请求中获取参数 调用 ReportService 对应方法
ReportService 调用业务逻辑方法,比如:getReportData()callProcedure() 把 servlet 传入的参数封装后传给 DAO 调用 ReportDAODBUtil 执行 SQL
ReportDAO 构造 SQL 或存储过程语句,如 CALL PROC_GET_REPORT_DATA(?,?) 分支ID、日期 通过 Connection 执行 SQL,返回 ResultSet
ResultSet → ReportData / Map 把查询结果映射为 List<Map<String,Object» 或 List ResultSet 数据集合返回上层
ReportService → ReportServlet Service 把查询结果传回给 Servlet List Servlet 设置到 request attribute
ReportServlet → JSP Servlet 转发请求到 JSP,例如:request.getRequestDispatcher("report.jsp").forward(...) 数据集合 JSP 中用 JSTL / EL 表达式展示结果表格
最终呈现 页面显示报表数据 - 用户看到结果表格

💡 同理,“保存报表” 或 “提交报表” 操作流程完全相同,只是 Servlet 调用的是 saveReportData()submitReport(),DAO 执行不同的存储过程。

项目执行链路图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[report.jsp]
   ↓(提交表单)
[ReportServlet.doPost()]
[ReportService.getReportData() 或 callProcedure()]
[ReportDAO.callProcedure() / DBUtil 执行SQL]
[数据库存储过程 PROC_GET_REPORT_DATA / 普通SQL]
[返回结果 ResultSet → List<Map>]
[ReportServlet 把结果放入 request.setAttribute()]
[report.jsp 使用 JSTL <c:forEach> 渲染表格]

3. 项目代码

3.1 建表语句

建表语句
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
-- 创建数据库
CREATE DATABASE IF NOT EXISTS bank_reg_report CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE bank_reg_report;

-- 分支机构表
CREATE TABLE branch (
  id INT AUTO_INCREMENT PRIMARY KEY,
  code VARCHAR(20) NOT NULL,
  name VARCHAR(100) NOT NULL
);

-- 客户表(简化)
CREATE TABLE customer (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  id_no VARCHAR(50),
  industry varchar(50) DEFAULT '未知',
  risk_level varchar(20) DEFAULT '未知'
);

-- 账户/存款表(用于存款汇总)
CREATE TABLE account (
  id INT AUTO_INCREMENT PRIMARY KEY,
  branch_id INT,
  customer_id INT,
  account_type ENUM('DEPOSIT','LOAN','OTHER') DEFAULT 'DEPOSIT',
  balance DECIMAL(18,2) DEFAULT 0,
  created_at DATETIME DEFAULT NOW(),
  FOREIGN KEY (branch_id) REFERENCES branch(id),
  FOREIGN KEY (customer_id) REFERENCES customer(id)
);

-- 贷款表(用于贷款敞口)
CREATE TABLE loan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  branch_id INT,
  customer_id INT,
  original_amount DECIMAL(18,2),
  outstanding_amount DECIMAL(18,2),
  loan_type VARCHAR(50),
  status VARCHAR(20),
  created_at DATETIME DEFAULT NOW(),
  FOREIGN KEY (branch_id) REFERENCES branch(id),
  FOREIGN KEY (customer_id) REFERENCES customer(id)
);

-- 交易表(用于可疑交易统计)
CREATE TABLE transaction_log (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  branch_id INT,
  customer_id INT,
  amount DECIMAL(18,2),
  txn_type VARCHAR(50),
  txn_time DATETIME,
  channel VARCHAR(50),
  description VARCHAR(255),
  FOREIGN KEY (branch_id) REFERENCES branch(id)
);

-- 报表存储结果表:每个报表类型通用一张(这里简单)
CREATE TABLE report_store (
  id INT AUTO_INCREMENT PRIMARY KEY,
  report_code VARCHAR(50) NOT NULL,
  branch_id INT,
  data_key VARCHAR(100),
  data_value TEXT,
  period DATE,
  created_at DATETIME DEFAULT NOW(),
  updated_at DATETIME DEFAULT NOW() ON UPDATE NOW()
);

-- 报表提交记录表
CREATE TABLE report_submission (
  id INT AUTO_INCREMENT PRIMARY KEY,
  report_code VARCHAR(50),
  period DATE,
  branch_id INT,
  status ENUM('DRAFT','SUBMITTED','APPROVED','REJECTED') DEFAULT 'DRAFT',
  submitter VARCHAR(100),
  submit_time DATETIME,
  remark VARCHAR(255)
);

-- 示例数据
INSERT INTO branch (code,name) VALUES ('B001','总行'),('B002','分行A'),('B003','分行B');
INSERT INTO customer (name,id_no) VALUES ('张三','ID1001'),('李四','ID1002'),('王五','ID1003');

INSERT INTO account (branch_id,customer_id,account_type,balance) VALUES
(1,1,'DEPOSIT',1000000.00),(1,2,'DEPOSIT',500000.00),(2,3,'DEPOSIT',200000.00),
(2,1,'DEPOSIT',120000.00),(3,2,'DEPOSIT',300000.00);

INSERT INTO loan (branch_id,customer_id,original_amount,outstanding_amount,loan_type,status) VALUES
(1,1,500000,200000,'CORPORATE','ACTIVE'),(1,2,200000,100000,'RETAIL','ACTIVE'),
(2,3,300000,120000,'RETAIL','ACTIVE');

INSERT INTO transaction_log (branch_id,customer_id,amount,txn_type,txn_time,channel,description) VALUES
(1,1,150000,'TRANSFER',NOW(),'ONLINE','大额转账'),(2,3,50000,'WITHDRAW',NOW(),'ATM','取款'),(3,2,200000,'TRANSFER',NOW(),'ONLINE','大额转账');

-- 存储过程:计算余额汇总(Balance Summary)
DROP PROCEDURE IF EXISTS sp_calc_balance_summary;
DELIMITER $$
CREATE PROCEDURE sp_calc_balance_summary(IN p_period DATE, IN p_branch_id INT)
BEGIN
  -- 返回分支当日/当月的存款总额和贷款余额总额,并将结果写入 report_store
  DECLARE v_deposits DECIMAL(18,2) DEFAULT 0;
  DECLARE v_loans DECIMAL(18,2) DEFAULT 0;

  SELECT IFNULL(SUM(balance),0) INTO v_deposits FROM account WHERE branch_id = p_branch_id AND account_type='DEPOSIT';
  SELECT IFNULL(SUM(outstanding_amount),0) INTO v_loans FROM loan WHERE branch_id = p_branch_id AND status='ACTIVE';

  -- 写入 report_store: key=TOTAL_DEPOSITS, TOTAL_LOANS
  INSERT INTO report_store(report_code,branch_id,data_key,data_value,period)
  VALUES('BALANCE_SUMMARY', p_branch_id, 'TOTAL_DEPOSITS', CAST(v_deposits AS CHAR), p_period);
  INSERT INTO report_store(report_code,branch_id,data_key,data_value,period)
  VALUES('BALANCE_SUMMARY', p_branch_id, 'TOTAL_LOANS', CAST(v_loans AS CHAR), p_period);

  -- 返回结果集(供直接查询)
  SELECT v_deposits AS total_deposits, v_loans AS total_loans;
END$$
DELIMITER ;

-- 存储过程:计算贷款敞口(Loan Exposure)
DROP PROCEDURE IF EXISTS sp_calc_loan_exposure;
DELIMITER $$
CREATE PROCEDURE sp_calc_loan_exposure(IN p_period DATE, IN p_branch_id INT)
BEGIN
  SELECT loan_type, COUNT(*) AS cnt, IFNULL(SUM(outstanding_amount),0) AS exposure
  FROM loan WHERE branch_id = p_branch_id AND status='ACTIVE' GROUP BY loan_type;
END$$
DELIMITER ;

-- 存储过程:可疑交易统计(大额交易)
DROP PROCEDURE IF EXISTS sp_calc_suspicious_transactions;
DELIMITER $$
CREATE PROCEDURE sp_calc_suspicious_transactions(IN p_period DATE, IN p_branch_id INT, IN p_threshold DECIMAL(18,2))
BEGIN
  SELECT customer_id, COUNT(*) AS txn_count, IFNULL(SUM(amount),0) AS total_amount
  FROM transaction_log
  WHERE branch_id = p_branch_id AND amount >= p_threshold
  GROUP BY customer_id HAVING SUM(amount) >= p_threshold;
END$$
DELIMITER ;

-- 存储过程:保存/更新报表草稿
DROP PROCEDURE IF EXISTS sp_save_report_data;
DELIMITER $$
CREATE PROCEDURE sp_save_report_data(IN p_report_code VARCHAR(50), IN p_branch_id INT, IN p_period DATE, IN p_key VARCHAR(100), IN p_value TEXT)
BEGIN
  DECLARE v_id INT;
  SELECT id INTO v_id FROM report_store WHERE report_code = p_report_code AND branch_id = p_branch_id AND period = p_period AND data_key = p_key LIMIT 1;
  IF v_id IS NULL THEN
    INSERT INTO report_store(report_code,branch_id,data_key,data_value,period) VALUES(p_report_code,p_branch_id,p_key,p_value,p_period);
  ELSE
    UPDATE report_store SET data_value = p_value WHERE id = v_id;
  END IF;
END$$
DELIMITER ;

-- 存储过程:提交报表
DROP PROCEDURE IF EXISTS sp_submit_report;
DELIMITER $$
CREATE PROCEDURE sp_submit_report(IN p_report_code VARCHAR(50), IN p_branch_id INT, IN p_period DATE, IN p_submitter VARCHAR(100))
BEGIN
  INSERT INTO report_submission(report_code,period,branch_id,status,submitter,submit_time)
  VALUES(p_report_code,p_period,p_branch_id,'SUBMITTED',p_submitter,NOW());
END$$
DELIMITER ;

-- 索引
CREATE INDEX idx_report_store_code ON report_store(report_code);
CREATE INDEX idx_report_submission ON report_submission(report_code);

COMMIT;

3.2 POM文件

POM文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.yutao.report</groupId>
  <artifactId>bank-regulatory-reporting</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>银行监管报送系统</name>
  <description>基于JSP的银行监管报表系统</description>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <mysql.version>8.0.30</mysql.version>
    <gson.version>2.8.9</gson.version>
    <servlet.api.version>4.0.1</servlet.api.version>
  </properties>

  <dependencies>
    <!-- MySQL数据库驱动 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>${mysql.version}</version>
    </dependency>

    <!-- Gson(JSON处理) -->
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>${gson.version}</version>
    </dependency>

    <!-- Servlet API(Web容器提供,打包时排除) -->
    <!-- <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>${servlet.api.version}</version>
      <scope>provided</scope> 
    </dependency> -->

    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>6.0.0</version>
    </dependency>

  </dependencies>

  <build>
    <finalName>bank-regulatory-reporting</finalName>
    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <filtering>false</filtering>
      </resource>
    </resources>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to
      parent pom) -->
      <plugins>
        <!-- 编译插件 -->
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.1</version>
          <configuration>
            <source>${maven.compiler.source}</source>
            <target>${maven.compiler.target}</target>
            <encoding>UTF-8</encoding>
          </configuration>
        </plugin>
        <!-- Web打包插件 -->
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.3.2</version>
          <configuration>
            <webResources>
              <!-- 确保XML模板和JSP被正确打包 -->
              <resource>
                <directory>src/main/webapp</directory>
                <filtering>true</filtering>
              </resource>
            </webResources>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

3.3 项目java代码

com.yutao.report.util.DBUtil.ReportDAO.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.yutao.report.dao;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.yutao.report.util.DBUtil;

public class ReportDAO {

    // 读取 xml 模板(由 servlet 负责读取文件) - 这里是数据层操作示例

    // 调用存储过程并返回 ResultSet 转成 List<Map>
    public List<Map<String, Object>> callProcedure(String proc, Object... params) throws Exception {
        try (Connection c = DBUtil.getConnection()){
            // 构建占位符
            StringBuilder sb = new StringBuilder();
            for (int i=0;i<params.length;i++) sb.append("?,");
            String ph = params.length>0? sb.substring(0,sb.length()-1):"";
            String sql = "CALL "+proc+"("+ph+")";
            try (CallableStatement cs = c.prepareCall(sql)){
                for (int i=0;i<params.length;i++){
                    cs.setObject(i+1, params[i]);
                }
                boolean has = cs.execute();
                List<Map<String,Object>> list = new ArrayList<>();
                if (has) {
                    try (ResultSet rs = cs.getResultSet()){
                        ResultSetMetaData md = rs.getMetaData();
                        int cols = md.getColumnCount();
                        while (rs.next()){
                            Map<String,Object> row = new LinkedHashMap<>();
                            for (int i=1;i<=cols;i++){
                                row.put(md.getColumnLabel(i), rs.getObject(i));
                            }
                            list.add(row);
                        }
                    }
                }
                return list;
            }
        }
    }

    // 保存报表数据 (调用存储过程 sp_save_report_data)
    public void saveReportData(String reportCode, int branchId, java.sql.Date period, String key, String value) throws Exception{
        try (Connection c = DBUtil.getConnection(); CallableStatement cs = c.prepareCall("CALL sp_save_report_data(?,?,?,?,?)")){
            cs.setString(1, reportCode);
            cs.setInt(2, branchId);
            cs.setDate(3, period);
            cs.setString(4, key);
            cs.setString(5, value);
            cs.execute();
        }
    }

    public void submitReport(String reportCode, int branchId, java.sql.Date period, String submitter) throws Exception{
        try (Connection c = DBUtil.getConnection(); CallableStatement cs = c.prepareCall("CALL sp_submit_report(?,?,?,?)")){
            cs.setString(1, reportCode);
            cs.setInt(2, branchId);
            cs.setDate(3, period);
            cs.setString(4, submitter);
            cs.execute();
        }
    }

    // 直接读取 report_store 表中指定报表和 period 的 key-value 列表
    public Map<String,String> loadReportStore(String reportCode, int branchId, java.sql.Date period) throws Exception{
        try (Connection c = DBUtil.getConnection(); PreparedStatement ps = c.prepareStatement("SELECT data_key,data_value FROM report_store WHERE report_code=? AND branch_id=? AND period=?")){
            ps.setString(1, reportCode);
            ps.setInt(2, branchId);
            ps.setDate(3, period);
            try (ResultSet rs = ps.executeQuery()){
                Map<String,String> m = new LinkedHashMap<>();
                while (rs.next()){
                    m.put(rs.getString("data_key"), rs.getString("data_value"));
                }
                return m;
            }
        }
    }
}
com.yutao.report.service.ReportService.java
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package com.yutao.report.service;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.yutao.report.util.DBUtil;

public class ReportService {
    
    // 获取报表数据
    public List<Map<String, Object>> getReportData(String reportCode, String reportDate) throws SQLException {
        String tableName = getTableNameByCode(reportCode);
        if (tableName == null) {
            throw new IllegalArgumentException("无效的报表代码: " + reportCode);
        }
        
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        List<Map<String, Object>> result = new ArrayList<>();
        
        try {
            conn = DBUtil.getConnection();
            String sql = "SELECT * FROM " + tableName + " WHERE report_date = ?";
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, reportDate);
            rs = pstmt.executeQuery();
            
            int columnCount = rs.getMetaData().getColumnCount();
            while (rs.next()) {
                Map<String, Object> row = new HashMap<>();
                for (int i = 1; i <= columnCount; i++) {
                    String columnName = rs.getMetaData().getColumnName(i);
                    row.put(columnName, rs.getObject(i));
                }
                result.add(row);
            }
        } finally {
            if (rs != null) rs.close();
            if (pstmt != null) pstmt.close();
            DBUtil.closeConnection(conn);
        }
        
        return result;
    }
    
    // 执行存储过程获取统计数据
    public Map<String, Object> executeProcedure(String procedureName, String reportDate) throws SQLException {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        Map<String, Object> result = new HashMap<>();
        
        try {
            conn = DBUtil.getConnection();
            String sql = "CALL " + procedureName + "(?)";
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, reportDate);
            rs = pstmt.executeQuery();
            
            if (rs.next()) {
                int columnCount = rs.getMetaData().getColumnCount();
                for (int i = 1; i <= columnCount; i++) {
                    String columnName = rs.getMetaData().getColumnName(i);
                    result.put(columnName, rs.getObject(i));
                }
            }
        } finally {
            if (rs != null) rs.close();
            if (pstmt != null) pstmt.close();
            DBUtil.closeConnection(conn);
        }
        
        return result;
    }
    
    // 更新报表数据
    public int updateReportData(String reportCode, Map<String, Object> data) throws SQLException {
        String tableName = getTableNameByCode(reportCode);
        if (tableName == null) {
            throw new IllegalArgumentException("无效的报表代码: " + reportCode);
        }
        
        Connection conn = null;
        PreparedStatement pstmt = null;
        int rowsAffected = 0;
        
        try {
            conn = DBUtil.getConnection();
            // 构建更新SQL
            StringBuilder sql = new StringBuilder("UPDATE " + tableName + " SET ");
            List<Object> params = new ArrayList<>();
            
            for (Map.Entry<String, Object> entry : data.entrySet()) {
                if (!"id".equals(entry.getKey()) && !"report_date".equals(entry.getKey())) {
                    sql.append(entry.getKey()).append(" = ?, ");
                    params.add(entry.getValue());
                }
            }
            
            // 移除最后一个逗号和空格
            if (sql.toString().endsWith(", ")) {
                sql.setLength(sql.length() - 2);
            }
            
            sql.append(" WHERE id = ? AND report_date = ?");
            params.add(data.get("id"));
            params.add(data.get("report_date"));
            
            pstmt = conn.prepareStatement(sql.toString());
            for (int i = 0; i < params.size(); i++) {
                pstmt.setObject(i + 1, params.get(i));
            }
            
            rowsAffected = pstmt.executeUpdate();
        } finally {
            if (pstmt != null) pstmt.close();
            DBUtil.closeConnection(conn);
        }
        
        return rowsAffected;
    }
    
    // 根据报表代码获取对应的表名
    private String getTableNameByCode(String reportCode) {
        Map<String, String> codeToTable = new HashMap<>();
        codeToTable.put("BALANCE_SHEET", "balance_sheet");
        codeToTable.put("LOAN_DETAILS", "loan_details");
        codeToTable.put("DEPOSIT_DETAILS", "deposit_details");
        codeToTable.put("INTERBANK_BUSINESS", "interbank_business");
        codeToTable.put("RISK_EXPOSURE", "risk_exposure");
        codeToTable.put("CAPITAL_ADQUACY", "capital_adequacy");
        
        return codeToTable.get(reportCode);
    }
    
    // 获取所有报表类型
    public List<Map<String, Object>> getAllReportTypes() throws SQLException {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        List<Map<String, Object>> result = new ArrayList<>();
        
        try {
            conn = DBUtil.getConnection();
            stmt = conn.createStatement();
            rs = stmt.executeQuery("SELECT report_id, report_name, report_code, description FROM report_type");
            
            while (rs.next()) {
                Map<String, Object> reportType = new HashMap<>();
                reportType.put("reportId", rs.getInt("report_id"));
                reportType.put("reportName", rs.getString("report_name"));
                reportType.put("reportCode", rs.getString("report_code"));
                reportType.put("description", rs.getString("description"));
                result.add(reportType);
            }
        } finally {
            if (rs != null) rs.close();
            if (stmt != null) stmt.close();
            DBUtil.closeConnection(conn);
        }
        
        return result;
    }
}
com.yutao.report.servlet.ReportServlet.java
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
package com.yutao.report.servlet;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.sql.Date;
import java.util.List;
import java.util.Map;

import com.yutao.report.dao.ReportDAO;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet(name = "ReportServlet", urlPatterns = { "/report/*" })
public class ReportServlet extends HttpServlet {
    private ReportDAO dao = new ReportDAO();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        handleRequest(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        handleRequest(req, resp);
    }

    private void handleRequest(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String path = req.getPathInfo();
        if (path == null)
            path = "/";
        resp.setContentType("application/json;charset=UTF-8");
        PrintWriter out = resp.getWriter();

        try {
            switch (path.toLowerCase()) {
                case "/template":
                    handleTemplate(req, resp, out);
                    break;
                case "/query":
                    handleQuery(req, resp, out);
                    break;
                case "/save":
                    handleSave(req, resp, out);
                    break;
                case "/submit":
                    handleSubmit(req, resp, out);
                    break;
                default:
                    resp.setStatus(404);
                    out.write("{\"error\":\"not found\"}");
            }
        } catch (Exception e) {
            resp.setStatus(500);
            e.printStackTrace();
            out.write("{\"error\":\"" + escapeJson(e.getMessage()) + "\"}");
        }
    }

    private void handleTemplate(HttpServletRequest req, HttpServletResponse resp, PrintWriter out) throws Exception {
        String code = req.getParameter("code");
        if (code == null) {
            resp.setStatus(400);
            out.write("{\"error\":\"missing code\"}");
            return;
        }

        String xml = readTemplate(code);
        out.write("{\"template\":\"" + escapeJson(xml) + "\"}");
    }

    private void handleQuery(HttpServletRequest req, HttpServletResponse resp, PrintWriter out) throws Exception {
        String code = req.getParameter("code");
        int branch = Integer.parseInt(req.getParameter("branch"));
        Date period = Date.valueOf(req.getParameter("period"));

        // <<< 在这里添加日志,打印前端传来的参数
        System.out.println("ReportServlet.handleQuery -> code=" + code + ", branch=" + branch + ", period=" + period);

        String xml = readTemplate(code);
        String proc = parseProcedureFromXml(xml);
        if (proc != null && !proc.isEmpty()) {
            List<Map<String, Object>> result = dao.callProcedure(proc, period, branch);
            out.write(mapListToJson(result));
        } else {
            Map<String, String> store = dao.loadReportStore(code, branch, period);
            out.write(mapToJson(store));
        }
    }

    private void handleSave(HttpServletRequest req, HttpServletResponse resp, PrintWriter out) throws Exception {
        String code = req.getParameter("code");
        int branch = Integer.parseInt(req.getParameter("branch"));
        Date period = Date.valueOf(req.getParameter("period"));
        String key = req.getParameter("key");
        String value = req.getParameter("value");

        dao.saveReportData(code, branch, period, key, value);
        out.write("{\"ok\":true}");
    }

    private void handleSubmit(HttpServletRequest req, HttpServletResponse resp, PrintWriter out) throws Exception {
        String code = req.getParameter("code");
        int branch = Integer.parseInt(req.getParameter("branch"));
        Date period = Date.valueOf(req.getParameter("period"));
        String submitter = req.getParameter("submitter");

        dao.submitReport(code, branch, period, submitter);
        out.write("{\"ok\":true}");
    }

    // ---------------- 辅助方法 ----------------
    private String readTemplate(String code) throws Exception {
        // 使用 ServletContext 获取绝对路径
        String basePath = getServletContext().getRealPath("/WEB-INF/templates/");
        if (basePath == null) {
            throw new FileNotFoundException("无法找到模板目录:/WEB-INF/templates/");
        }

        String fileName1 = "report_" + code.toLowerCase() + ".xml";
        String fileName2 = code.toLowerCase() + ".xml";
        File f1 = new File(basePath, fileName1);
        File f2 = new File(basePath, fileName2);

        File target = f1.exists() ? f1 : (f2.exists() ? f2 : null);
        if (target == null)
            throw new FileNotFoundException("template not found for code: " + code);

        byte[] bytes = java.nio.file.Files.readAllBytes(target.toPath());
        return new String(bytes, StandardCharsets.UTF_8);
    }

    private String parseProcedureFromXml(String xml) {
        try {
            int i = xml.indexOf("<procedure>");
            int j = xml.indexOf("</procedure>");
            if (i >= 0 && j > i)
                return xml.substring(i + "<procedure>".length(), j).trim();
        } catch (Exception ignored) {
        }
        return null;
    }

    private String escapeJson(String s) {
        if (s == null)
            return "";
        return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
    }

    private String mapListToJson(List<Map<String, Object>> list) {
        StringBuilder sb = new StringBuilder("[");
        boolean first = true;
        for (Map<String, Object> row : list) {
            if (!first)
                sb.append(",");
            first = false;
            sb.append(mapToJson(row));
        }
        sb.append("]");
        return sb.toString();
    }

    private String mapToJson(Map<?, ?> m) {
        StringBuilder sb = new StringBuilder("{");
        boolean first = true;
        for (Object k : m.keySet()) {
            if (!first)
                sb.append(",");
            first = false;
            Object v = m.get(k);
            sb.append('"').append(k.toString()).append('"').append(":");
            if (v == null)
                sb.append("null");
            else if (v instanceof Number)
                sb.append(v.toString());
            else
                sb.append('"').append(escapeJson(v.toString())).append('"');
        }
        sb.append("}");
        return sb.toString();
    }
}
com.yutao.report.util.DBUtil.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.yutao.report.util;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

public class DBUtil {
    private static String url;
    private static String user;
    private static String pass;

    static {
        try {
            System.out.println("[DBUtil] 开始加载数据库配置...");
            InputStream in = DBUtil.class.getClassLoader().getResourceAsStream("db.properties");
            if (in == null) {
                System.err.println("[DBUtil] db.properties 未找到!请确认文件是否在 classpath 下(如 src/main/resources/)");
                throw new RuntimeException("db.properties not found in classpath");
            } else {
                System.out.println("[DBUtil] 成功找到 db.properties");
            }

            Properties p = new Properties();
            p.load(in);
            System.out.println("[DBUtil] 加载属性完成:");
            System.out.println("  jdbc.url = " + p.getProperty("jdbc.url"));
            System.out.println("  jdbc.user = " + p.getProperty("jdbc.user"));
            System.out.println("  jdbc.driver = " + p.getProperty("jdbc.driver"));

            url = p.getProperty("jdbc.url");
            user = p.getProperty("jdbc.user");
            pass = p.getProperty("jdbc.password");

            System.out.println("[DBUtil] 尝试加载数据库驱动:" + p.getProperty("jdbc.driver"));
            Class.forName(p.getProperty("jdbc.driver"));
            System.out.println("[DBUtil] 数据库驱动加载成功!");

        } catch (Exception e) {
            System.err.println("[DBUtil] 数据库配置加载失败!");
            e.printStackTrace();
            throw new RuntimeException("数据库配置加载失败", e);
        }
    }

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(url, user, pass);
    }

    public static void closeConnection(Connection conn) {
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}
src/main/resources/db.properties
1
2
3
4
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/bank_reg_report?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8&useUnicode=true
jdbc.user=root
jdbc.password=root

3.4 webapp

src/main/webapp/js/report.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
var current = { code:null, branch:1, period:null, template:null, storeData:{} };

function initReport(code, branch, period){
    current.code = code; current.branch = branch; current.period = period;
    loadTemplate(code, function(ok){
        if (ok) queryReport();
    });
}

function loadTemplate(code, cb){
    var xhr = new XMLHttpRequest();
    xhr.open('GET','/bank-regulatory-reporting/report/template?code='+encodeURIComponent(code));
    xhr.onreadystatechange = function(){
        if (xhr.readyState===4){
            if (xhr.status===200){
                var res = JSON.parse(xhr.responseText);
                current.template = res.template;
                cb(true);
            } else {
                alert('加载模板失败: '+xhr.responseText);
                cb(false);
            }
        }
    };
    xhr.send();
}

function queryReport(){
    var code = current.code; var branch = current.branch; var period = current.period;
    var xhr = new XMLHttpRequest();
    xhr.open('GET','/bank-regulatory-reporting/report/query?code='+encodeURIComponent(code)+'&branch='+branch+'&period='+period);
    xhr.onreadystatechange = function(){
        if (xhr.readyState===4){
            if (xhr.status===200){
                // 结果可能是 array 或 object
                try{
                    var data = JSON.parse(xhr.responseText);
                    renderReportFromData(data);
                } catch(e){
                    document.getElementById('reportContainer').innerText = '解析结果失败';
                }
            } else {
                alert('查询失败:'+xhr.responseText);
            }
        }
    };
    xhr.send();
}

function renderReportFromData(data){
    // 简单根据 template 渲染表格
    // 解析 template 的 columns
    var xml = current.template.replace(/\\n/g,'\n');
    var parser = new DOMParser();
    var xmlDoc = parser.parseFromString(xml,'application/xml');
    var cols = xmlDoc.getElementsByTagName('column');
    var html = '<table border="1" cellpadding="6"><tr>';
    for (var i=0;i<cols.length;i++){
        html += '<th>'+cols[i].getAttribute('title')+'</th>';
    }
    html += '</tr>';

    // data 有两类:proc result array of rows  或 key-value map
    if (Array.isArray(data)){
        for (var r=0;r<data.length;r++){
            html += '<tr>';
            for (var c=0;c<cols.length;c++){
                var key = cols[c].getAttribute('key');
                var val = data[r][key] || data[r][key.toLowerCase()] || data[r][cols[c].getAttribute('key').toLowerCase()];
                if (val===undefined) val = '';
                html += '<td>'+escapeHtml(val)+'</td>';
            }
            html += '</tr>';
        }
    } else {
        // object map 或 {key:value}
        html += '<tr>';
        for (var c=0;c<cols.length;c++){
            var key = cols[c].getAttribute('key');
            var val = data[key] || data[key.toLowerCase()] || '';
            html += '<td contenteditable="true" data-key="'+key+'">'+escapeHtml(val)+'</td>';
        }
        html += '</tr>';
    }
    html += '</table>';
    document.getElementById('reportContainer').innerHTML = html;
}

function saveReport(){
    // 读取表格可编辑列,逐个保存
    var t = document.getElementById('reportContainer');
    if (!t) return;
    var cols = t.getElementsByTagName('td');
    var saves = [];
    for (var i=0;i<cols.length;i++){
        var k = cols[i].getAttribute('data-key');
        if (k){
            var v = cols[i].innerText;
            saves.push({key:k,value:v});
        }
    }
    if (saves.length===0){ alert('无可保存的字段'); return; }
    // 逐条 AJAX 保存
    var idx=0;
    function next(){
        if (idx>=saves.length){ alert('保存完成'); return; }
        var s = saves[idx++];
        var xhr = new XMLHttpRequest();
        xhr.open('POST','/bank-regulatory-reporting/report/save');
        xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
        xhr.onreadystatechange = function(){ if (xhr.readyState===4){
            if (xhr.status===200) next(); else alert('保存失败:'+xhr.responseText);
        }};
        var body = 'code='+encodeURIComponent(current.code)+'&branch='+encodeURIComponent(current.branch)+'&period='+encodeURIComponent(current.period)+'&key='+encodeURIComponent(s.key)+'&value='+encodeURIComponent(s.value);
        xhr.send(body);
    }
    next();
}

function submitReport(){
    var xhr = new XMLHttpRequest();
    xhr.open('POST','/bank-regulatory-reporting/report/submit');
    xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
    xhr.onreadystatechange = function(){ if (xhr.readyState===4){
        if (xhr.status===200) alert('提交成功'); else alert('提交失败:'+xhr.responseText);
    }};
    var body = 'code='+encodeURIComponent(current.code)+'&branch='+encodeURIComponent(current.branch)+'&period='+encodeURIComponent(current.period)+'&submitter='+encodeURIComponent('admin');
    xhr.send(body);
}

function escapeHtml(s){ if (s==null) return ''; return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
src/main/webapp/WEB-INF/templates/report_balance_summary.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<report>
  <meta>
    <code>BALANCE_SUMMARY</code>
    <name>资产负债表汇总(模拟)</name>
    <description>返回分支的存款与贷款汇总</description>
    <procedure>sp_calc_balance_summary</procedure>
  </meta>
  <columns>
    <column key="TOTAL_DEPOSITS" title="总存款(元)" type="money"/>
    <column key="TOTAL_LOANS" title="贷款余额(元)" type="money"/>
  </columns>
</report>
src/main/webapp/WEB-INF/templates/report_deposit_summary.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<report>
  <meta>
    <code>DEPOSIT_SUMMARY</code>
    <name>客户存款类型汇总报表</name>
    <description>统计不同存款类型、分支、客户群的存款余额情况</description>
    <procedure>sp_calc_deposit_summary</procedure>
  </meta>
  <columns>
    <column key="BRANCH_NAME" title="分支机构" type="string"/>
    <column key="DEPOSIT_TYPE" title="存款类型" type="string"/>
    <column key="CUSTOMER_TYPE" title="客户类型" type="string"/>
    <column key="BALANCE" title="存款余额(元)" type="money"/>
  </columns>
</report>
src/main/webapp/WEB-INF/templates/report_large_exposures.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<report>
  <meta>
    <code>LARGE_EXPOSURES</code>
    <name>大额信用敞口报表</name>
    <description>列示超过监管标准的单一客户或集团客户授信情况</description>
    <!-- 本报表不调用存储过程,直接使用自定义 SQL -->
    <procedure>sp_calc_large_exposures</procedure>
  </meta>
  <columns>
    <column key="CUSTOMER_NAME" title="客户名称" type="string"/>
    <column key="GROUP_NAME" title="集团名称" type="string"/>
    <column key="CREDIT_LIMIT" title="授信额度(元)" type="money"/>
    <column key="OUTSTANDING_BALANCE" title="当前余额(元)" type="money"/>
    <column key="EXPOSURE_RATIO" title="敞口占资本比例(%)" type="percent"/>
  </columns>
</report>
src/main/webapp/WEB-INF/templates/report_liquidity_ratio.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<report>
  <meta>
    <code>LIQUIDITY_RATIO</code>
    <name>流动性指标报表</name>
    <description>计算银行整体及分支流动性指标,如流动性覆盖率、净稳定资金比率</description>
    <procedure>sp_calc_liquidity_ratio</procedure>
  </meta>
  <columns>
    <column key="BRANCH_NAME" title="分支机构" type="string"/>
    <column key="HIGH_QUALITY_ASSETS" title="高流动性资产(元)" type="money"/>
    <column key="NET_CASH_OUTFLOW" title="净现金流出量(元)" type="money"/>
    <column key="LCR_RATIO" title="流动性覆盖率(%)" type="percent"/>
    <column key="NSFR_RATIO" title="净稳定资金比率(%)" type="percent"/>
  </columns>
</report>
src/main/webapp/WEB-INF/templates/report_loan_exposure.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<report>
  <meta>
    <code>LOAN_EXPOSURE</code>
    <name>贷款风险暴露报表</name>
    <description>展示贷款客户的风险敞口,按行业、评级、风险级别分类</description>
    <procedure>sp_calc_loan_exposure</procedure>
  </meta>
  <columns>
    <column key="CUSTOMER_NAME" title="客户名称" type="string"/>
    <column key="INDUSTRY" title="所属行业" type="string"/>
    <column key="RISK_LEVEL" title="风险等级" type="string"/>
    <column key="EXPOSURE_AMOUNT" title="风险敞口(元)" type="money"/>
    <column key="PROVISION" title="拨备金额(元)" type="money"/>
  </columns>
</report>
src/main/webapp/WEB-INF/templates/report_suspicious_transactions.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<report>
  <meta>
    <code>SUSPICIOUS_TX</code>
    <name>可疑交易分析报表</name>
    <description>分析大额或异常交易,协助反洗钱监测</description>
    <procedure>sp_calc_suspicious_transactions</procedure>
  </meta>
  <columns>
    <column key="TX_DATE" title="交易日期" type="date"/>
    <column key="CUSTOMER_NAME" title="客户名称" type="string"/>
    <column key="TX_AMOUNT" title="交易金额(元)" type="money"/>
    <column key="TX_TYPE" title="交易类型" type="string"/>
    <column key="FLAG_REASON" title="可疑原因" type="string"/>
  </columns>
</report>
src/main/webapp/WEB-INF/web.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<web-app
  version="4.0"
  xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:javaee="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xml="http://www.w3.org/XML/1998/namespace"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd">
  <display-name>bank-reg-reporting</display-name>

  <servlet>
    <servlet-name>ReportServlet</servlet-name>
    <servlet-class>com.yutao.report.servlet.ReportServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>ReportServlet</servlet-name>
    <url-pattern>/report/*</url-pattern>
  </servlet-mapping>

  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>report.jsp</welcome-file>
  </welcome-file-list>
</web-app>
src/main/webapp/index.jsp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>

    <head>
        <title>银行监管报送系统 - 仪表盘</title>
    </head>

    <body>
        <h1>银行监管报送系统模拟</h1>
        <p>选择报表
            <select id="reportSelect">
                <option value="BALANCE_SUMMARY">资产负债表汇总</option>
                <option value="DEPOSIT_SUMMARY">存款汇总</option>
                <option value="LOAN_EXPOSURE">贷款敞口</option>
                <option value="LARGE_EXPOSURES">大额敞口</option>
                <option value="LIQUIDITY_RATIO">流动性比率</option>
                <option value="SUSPICIOUS_TRANSACTIONS">可疑交易统计</option>
            </select>
            分支<select id="branchSelect">
                <option value="1">总行</option>
                <option value="2">分行A</option>
                <option value="3">分行B</option>
            </select>
            期间<input type="date" id="period" value="<%= java.time.LocalDate.now() %>" />
            <button onclick="openReport()">打开报表</button>
        </p>
        <hr />
        <div id="main"></div>

        <script>
            function openReport() {
                var code = document.getElementById('reportSelect').value;
                var branch = document.getElementById('branchSelect').value;
                var period = document.getElementById('period').value;
                var url = 'report.jsp?code=' + encodeURIComponent(code) + '&branch=' + branch + '&period=' + period;
                window.location.href = url;
            }
        </script>
    </body>

    </html>
src/main/webapp/report.jsp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<%@ page import="java.sql.*" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String code = request.getParameter("code");
    String branch = request.getParameter("branch");
    String period = request.getParameter("period");
    if (code == null) code = "BALANCE_SUMMARY";
    if (branch == null) branch = "1";
    if (period == null) period = java.time.LocalDate.now().toString();

    // ---- 新增:根据 branch id 查询分支名称 ----
    String branchName = branch; // 默认显示数字
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        try (
            Connection conn = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/bank_reg_report?useSSL=false&serverTimezone=UTC",
                "root",
                "root"
            );
            PreparedStatement ps = conn.prepareStatement("SELECT name FROM branch WHERE id = ?")
        ) {
            ps.setInt(1, Integer.parseInt(branch));
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    branchName = rs.getString("name");
                }
            }
        }
    } catch (Exception e) {
        // 查询失败则回退显示数字
        branchName = branch;
        e.printStackTrace();
    }
%>

<html>
<head>
    <title>报表 - <%= code %></title>
    <script src="js/report.js"></script>
</head>

<body onload="initReport('<%=code%>','<%=branch%>','<%=period%>')">
    <h2>报表<%= code %> 分支<%= branchName %> 期间<%= period %></h2>

    <div>
        <button id="btnQuery" onclick="queryReport()">查询</button>
        <button id="btnSave" onclick="saveReport()">保存草稿</button>
        <button id="btnSubmit" onclick="submitReport()">提交报表</button>
        <span id="status"></span>
    </div>

    <hr />
    <div id="reportContainer">Loading...</div>
</body>
</html>

4. 项目环境

项目开发环境配置表

配置类别 工具/组件名称 版本号 说明
项目构建工具 Maven 3.9.9 用于项目依赖管理与构建自动化
数据库 MySQL 8.0 关系型数据库,存储项目业务数据
Web服务器 Tomcat 11.0.11 部署Java Web应用的服务器环境
开发语言环境 JDK 21 Java开发核心运行环境
开发IDE Visual Studio Code(VSCode) - 轻量级代码编辑器,用于项目开发
VSCode插件 Tomcat for VSCode - 用于在VSCode中集成Tomcat,支持Web应用部署与调试

关于 Tomcat for VSCode 使用,安装后在底部有 Tomcat 按钮,第一次启动要求指定本机Tomcat的安装目录,以及jdk的安装目录,点击部署的时候选择Maven进行编译项目。

总结

了解 Jsp、Servlet、Tomcat、Maven、VSCode 的安装使用。