在公用事业数字化转型的浪潮中,水务管理的现代化升级尤为关键。传统依赖纸质台账、人工抄表、营业厅现金缴费的模式,不仅运营成本高昂,且易出现数据差错、对账困难、用户体验差等问题。为此,我们设计并实现了一套基于SSH(Struts2 + Spring + Hibernate)技术栈的智慧水务计费征管平台。该系统旨在将水费收缴的全流程,包括用户档案、水表管理、抄表计费、账单生成、在线支付及财务对账等进行一体化整合,通过信息化手段提升水务公司的运营效率与服务质量,同时为居民用户提供便捷透明的缴费通道。
系统采用经典的三层架构,以实现高内聚、低耦合的设计目标。表现层由Struts2框架负责,它通过拦截HTTP请求,将其分发至对应的Action类进行处理,并负责将结果渲染至JSP视图页面。业务逻辑层由Spring框架的IoC(控制反转)容器统一管理,所有Service组件均以Bean的形式注册,由Spring负责其生命周期和依赖注入,从而实现了业务模块间的解耦。此外,Spring的声明式事务管理(Declarative Transaction Management)被应用于核心业务方法上,确保了数据操作的原子性和一致性。数据持久层则依托Hibernate框架实现,通过对象关系映射(ORM)技术,将Java实体类与后台MySQL数据库中的表结构关联起来,开发者可以面向对象的方式进行数据库操作,Hibernate会自动生成并执行优化的SQL语句,极大简化了数据访问层(DAO)的编码工作。
这种分层架构使得系统具备良好的可维护性和可扩展性。例如,一个完整的用户缴费流程始于前端的JSP页面,用户提交缴费请求后,Struts2的Action接收参数并调用Spring容器中的收费服务(ChargeService),该服务在事务边界内,通过Hibernate DAO(数据访问对象)完成用户账户扣款、缴费记录插入、水表状态更新等一系列数据库操作。任何环节的异常都会触发事务回滚,保证数据的完整性。
数据库架构设计与核心表解析
一个稳健的后台数据模型是系统可靠运行的基石。本系统共设计8张核心数据表,以下选取其中具有代表性的三张进行深入分析。
1. 用户信息表(t_user)
该表是系统的基础,存储了所有用水户的档案信息。其设计不仅包含了基本身份信息,还考虑了业务扩展性。
CREATE TABLE `t_user` (
`userId` int(11) NOT NULL AUTO_INCREMENT,
`userName` varchar(20) DEFAULT NULL,
`userPassword` varchar(20) DEFAULT NULL,
`userType` int(11) DEFAULT NULL,
`name` varchar(20) DEFAULT NULL,
`sex` varchar(10) DEFAULT NULL,
`idCard` varchar(20) DEFAULT NULL,
`tel` varchar(20) DEFAULT NULL,
`address` varchar(50) DEFAULT NULL,
`userNum` varchar(20) DEFAULT NULL,
`createTime` varchar(20) DEFAULT NULL,
PRIMARY KEY (`userId`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
设计亮点分析:
- 权限分离设计:通过
userType字段区分系统用户(如管理员、操作员)和普通居民用户。这种设计使得同一张表可以支撑后台管理登录和前台用户登录两种场景,简化了权限验证逻辑。 - 业务唯一标识:
userNum(用户编号)作为一个独立的业务字段,与技术主键userId解耦。这符合业务规范,便于人工识别和线下沟通,同时避免了技术主键(如自增ID)可能带来的信息泄露或业务逻辑依赖问题。 - 信息完整性:字段设计覆盖了用户管理的全要素,包括联系信息(
tel,address)、身份信息(idCard)及账户创建时间(createTime),为后续的用户分析和服务提供了数据支撑。
2. 水费信息表(t_waterrate)
此表是系统的核心业务表,记录了每一笔水费的产生、状态和结算详情。
CREATE TABLE `t_waterrate` (
`waterRateId` int(11) NOT NULL AUTO_INCREMENT,
`userId` int(11) DEFAULT NULL,
`waterMeterId` int(11) DEFAULT NULL,
`priceId` int(11) DEFAULT NULL,
`previousNum` float DEFAULT NULL,
`currentNum` float DEFAULT NULL,
`useNum` float DEFAULT NULL,
`price` float DEFAULT NULL,
`totalPrice` float DEFAULT NULL,
`isPay` int(11) DEFAULT NULL,
`payTime` varchar(20) DEFAULT NULL,
`payType` int(11) DEFAULT NULL,
`createTime` varchar(20) DEFAULT NULL,
PRIMARY KEY (`waterRateId`),
KEY `FK_waterRate_user` (`userId`),
KEY `FK_waterRate_waterMeter` (`waterMeterId`),
KEY `FK_waterRate_price` (`priceId`),
CONSTRAINT `FK_waterRate_price` FOREIGN KEY (`priceId`) REFERENCES `t_price` (`priceId`),
CONSTRAINT `FK_waterRate_user` FOREIGN KEY (`userId`) REFERENCES `t_user` (`userId`),
CONSTRAINT `FK_waterRate_waterMeter` FOREIGN KEY (`waterMeterId`) REFERENCES `t_watermeter` (`waterMeterId`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
设计亮点分析:
- 数据关联与一致性:通过外键(
userId,waterMeterId,priceId)紧密关联用户、水表和水价表。这种设计确保了每笔水费账单都基于准确的用户、正确的仪表和生效的单价,从数据层面杜绝了“张冠李戴”或计价错误。 - 账单状态追踪:
isPay字段是典型的状态标志位,用于清晰标识账单是待支付、已支付还是已逾期。结合payTime和payType,可以完整还原账单的生命周期,为对账和审计提供精确日志。 - 历史数据快照:
price字段存储了计费时的水价。这是一个重要的设计,即使未来水价政策调整,历史账单的金额也不会改变,保证了财务数据的准确性和不可篡改性。
3. 水价表(t_price)
该表管理着系统的计价规则,是计费业务的核心参数表。
CREATE TABLE `t_price` (
`priceId` int(11) NOT NULL AUTO_INCREMENT,
`waterType` int(11) DEFAULT NULL,
`priceType` int(11) DEFAULT NULL,
`price` float DEFAULT NULL,
`createTime` varchar(20) DEFAULT NULL,
PRIMARY KEY (`priceId`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
设计亮点分析:
- 灵活的计价策略:通过
waterType(用水类型,如居民、商业)和priceType(价格类型,如阶梯价、固定价)的组合,系统能够支持复杂的水价政策。这种结构化的设计使得新增水价类型无需修改数据库结构,只需增加数据记录即可。 - 价格版本化管理:
createTime字段隐式地实现了价格版本的记录。当水价调整时,插入一条新记录并更新生效时间,旧账单关联旧价格,新账单则使用新价格,实现了价格的平滑过渡和历史追溯。
核心功能实现与代码剖析
1. 用户登录与权限验证
系统登录是入口。下图展示了管理员登录界面。

登录功能由Struts2的UserLoginAction处理。它接收前端表单提交的用户名和密码,然后调用Spring管理的UserService进行验证。
Struts2 Action 代码示例:
public class UserLoginAction extends ActionSupport {
private User user; // 接收前端参数的实体对象
private String loginResult; // 返回给前端的消息
// Spring 注入的 Service
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
@Override
public String execute() throws Exception {
try {
// 调用业务层方法验证用户
User loginUser = userService.userLogin(user.getUserName(), user.getUserPassword());
if (loginUser != null) {
// 登录成功,将用户信息存入Session
ActionContext.getContext().getSession().put("loginUser", loginUser);
loginResult = "登录成功!";
// 根据用户类型跳转到不同主页
if (loginUser.getUserType() == 1) { // 管理员
return "adminSuccess";
} else { // 普通用户
return "userSuccess";
}
} else {
loginResult = "用户名或密码错误!";
return INPUT;
}
} catch (Exception e) {
loginResult = "系统错误,登录失败!";
e.printStackTrace();
return ERROR;
}
}
// Getter and Setter...
}
Spring Service 层代码示例:
@Service("userService") // 由Spring IoC容器管理
@Transactional // 声明式事务注解
public class UserService {
// Spring 注入的 DAO
@Autowired
private UserDao userDao;
public User userLogin(String userName, String userPassword) {
// 构建HQL查询语句
String hql = "FROM User u WHERE u.userName = ? AND u.userPassword = ?";
// 调用DAO执行查询
List<User> users = userDao.find(hql, userName, userPassword);
if (users != null && users.size() > 0) {
return users.get(0); // 返回第一个匹配的用户
}
return null;
}
}
Hibernate DAO 层代码示例:
@Repository("userDao") // 标识为数据访问层Bean
public class UserDao extends BaseDao<User> {
// 继承自BaseDao的通用方法,如find, save, update等
// 此处可编写特定的数据访问逻辑
}
// 通用的BaseDao实现
public class BaseDao<T> {
@Autowired
private HibernateTemplate hibernateTemplate;
// 通用的HQL查询方法
public List<T> find(String hql, Object... params) {
return (List<T>) hibernateTemplate.find(hql, params);
}
// 其他增删改查方法...
}
此登录流程清晰地展示了SSH三层的协作:Struts2 Action处理请求和跳转,Spring Service执行业务逻辑和事务控制,Hibernate DAO完成底层数据交互。
2. 水价策略管理
水价是计费的基础,其管理至关重要。管理员可以在后台对水价进行增删改查。

对应的Action负责接收管理操作请求。
水价管理 Action 代码示例:
public class PriceManageAction extends ActionSupport {
private Price price; // 水价实体
private List<Price> priceList; // 水价列表,用于前端显示
private String message;
@Autowired
private PriceService priceService;
// 查询所有水价策略
public String list() {
priceList = priceService.findAllPrices();
return "listSuccess";
}
// 保存或更新水价
public String save() {
try {
priceService.saveOrUpdatePrice(price);
message = "水价信息保存成功!";
return SUCCESS;
} catch (Exception e) {
message = "保存失败:" + e.getMessage();
return ERROR;
}
}
// 删除水价
public String delete() {
try {
priceService.deletePrice(price.getPriceId());
message = "水价信息删除成功!";
return SUCCESS;
} catch (Exception e) {
message = "删除失败:" + e.getMessage();
return ERROR;
}
}
// Getter and Setter...
}
3. 水费账单生成与支付
这是系统的核心业务流程。其实现涉及复杂的业务逻辑和事务控制。
账单生成与支付 Service 层核心代码示例:
@Service("chargeService")
@Transactional // 此注解确保该方法在一个事务内执行
public class ChargeService {
@Autowired
private WaterRateDao waterRateDao;
@Autowired
private UserDao userDao;
@Autowired
private PriceDao priceDao;
/**
* 生成水费账单
* @param userId 用户ID
* @param waterMeterId 水表ID
* @param currentReading 本期读数
*/
public WaterRate generateWaterBill(Integer userId, Integer waterMeterId, Float currentReading) {
// 1. 根据水表ID查询上期读数
WaterRate lastRecord = waterRateDao.findLatestByWaterMeterId(waterMeterId);
Float previousNum = (lastRecord != null) ? lastRecord.getCurrentNum() : 0.0f;
Float useNum = currentReading - previousNum;
// 2. 验证读数合理性(本期读数不能小于上期)
if (useNum < 0) {
throw new RuntimeException("本期读数不能小于上期读数!");
}
// 3. 获取适用的水价(这里简化处理,实际可能根据用水类型、阶梯等计算)
Price applicablePrice = priceDao.findApplicablePrice(userId, useNum);
if (applicablePrice == null) {
throw new RuntimeException("未找到适用的水价策略!");
}
// 4. 计算金额
Float totalPrice = useNum * applicablePrice.getPrice();
// 5. 创建水费账单对象并保存
WaterRate newBill = new WaterRate();
newBill.setUserId(userId);
newBill.setWaterMeterId(waterMeterId);
newBill.setPriceId(applicablePrice.getPriceId());
newBill.setPreviousNum(previousNum);
newBill.setCurrentNum(currentReading);
newBill.setUseNum(useNum);
newBill.setPrice(applicablePrice.getPrice()); // 快照价格
newBill.setTotalPrice(totalPrice);
newBill.setIsPay(0); // 0代表未支付
newBill.setCreateTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
waterRateDao.save(newBill);
return newBill;
}
/**
* 执行支付操作
* @param waterRateId 水费账单ID
* @param payType 支付方式
*/
@Transactional(rollbackFor = Exception.class) // 遇到任何异常都回滚事务
public void payWaterBill(Integer waterRateId, Integer payType) {
// 1. 查询账单
WaterRate bill = waterRateDao.get(waterRateId);
if (bill == null) {
throw new RuntimeException("水费账单不存在!");
}
if (bill.getIsPay() == 1) {
throw new RuntimeException("该账单已支付,请勿重复操作!");
}
// 2. 模拟支付网关调用(此处简化)
// boolean paySuccess = thirdPartyPayService.pay(bill.getTotalPrice(), ...);
// if (!paySuccess) { ... }
// 3. 更新账单状态
bill.setIsPay(1);
bill.setPayType(payType);
bill.setPayTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
waterRateDao.update(bill);
// 4. 可选的:更新用户账户余额等后续操作
// userDao.deductBalance(bill.getUserId(), bill.getTotalPrice());
}
}
@Transactional注解是Spring声明式事务管理的核心。在payWaterBill方法中,它保证了“查询账单、验证状态、更新支付信息”这几个数据库操作要么全部成功,要么全部失败。如果在水费账单生成或支付过程中任何一个步骤抛出异常,整个事务都会回滚,数据库将恢复到操作前的状态,有效防止了数据不一致的情况,例如用户已扣款但账单状态未更新的严重错误。
实体模型与对象关系映射
Hibernate的核心在于将数据库表映射为Java实体类。以下是WaterRate水费账单实体的部分代码,展示了其与数据库表t_waterrate的映射关系。
水费账单实体类代码示例:
@Entity
@Table(name = "t_waterrate") // 指定映射的表名
public class WaterRate implements java.io.Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 主键自增策略
@Column(name = "waterRateId", unique = true, nullable = false)
private Integer waterRateId;
// 多对一关联:多个水费账单属于一个用户
@ManyToOne(fetch = FetchType.LAZY) // 懒加载
@JoinColumn(name = "userId")
private User user;
// 多对一关联:多个水费账单对应一个水表
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "waterMeterId")
private WaterMeter waterMeter;
// 多对一关联:多个水费账单采用一种水价
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "priceId")
private Price price;
@Column(name = "previousNum", precision = 12, scale = 0)
private Float previousNum;
@Column(name = "currentNum", precision = 12, scale = 0)
private Float currentNum;
@Column(name = "useNum", precision = 12, scale = 0)
private Float useNum;
@Column(name = "price", precision = 12, scale = 0)
private Float price; // 快照价格
@Column(name = "totalPrice", precision = 12, scale = 0)
private Float totalPrice;
@Column(name = "isPay")
private Integer isPay;
@Column(name = "payTime", length = 20)
private String payTime;
@Column(name = "payType")
private Integer payType;
@Column(name = "