ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

乡镇自来水收费系统源码:SSM+JSP+MySQL从建表到部署全解析

乡镇自来水收费系统源码:SSM+JSP+MySQL从建表到部署全解析 简介面向Java毕业设计与课程设计的乡镇自来水收费系统完整源码包基于SSMJSPMySQL技术栈在Eclipse/IDEA中即可导入运行。系统围绕乡镇水费管理场景展开管理员可进行水表管理、抄表与缴费审核、公告及留言管理用户能在线申请更换水表、缴纳水费、查看公告并维护个人信息。资源共1942个文件包含Java源码、JSP页面、数据库SQL脚本、Maven配置与说明文档主要文件类型有js、css、png、jpg、xml、jsp、java、jar及sql等压缩包大小42.56MB目录结构清晰适合用于毕业设计答辩或课程设计参考。已有60人学习下载。资料内提供完整前后端实现与数据库初始化脚本可直接运行演示也便于按模块研究SSM框架的分层写法、MySQL表设计及用户-管理员交互流程对快速完成系统开发与论文撰写均有帮助。1. 乡镇自来水收费系统源码从纸质台账到线上计费为什么SSM老项目依然值得照做一遍乡镇水厂的日常到现在很多还停留在纸质台账阶段抄表员拿着本子入户抄数回来后由会计照着手写记录算水费缴费日再翻账本一笔笔核对错账漏账全凭人力扛。乡镇自来水收费系统SSMJSPMySQL要做的就是把用户档案、水表编号、每月读数、水费计算、缴费流水全部装进同一套数据库应用里让收费员、管理员在不同电脑上看到同一本账。这个选题在毕业设计和中小型运维项目里出现频率很高原因是业务有清晰的资金边界技术栈又是Java开发里最通用的SSM组合能从建表、接口、事务一路走到部署收益比很高。下面按我实际做这类系统的顺序展开。2. SSMJSPMySQL技术栈选择这套组合在乡镇场景的真实理由与最小骨架2.1 框架分工与选型理由不是不能上Spring Boot是没必要先回答很多人会问的问题都什么年代了为什么不直接上Spring Boot我在做乡镇水费这类小系统时习惯做减法不追新。乡镇部署现场往往是一台老旧Windows服务器甚至是一台淘汰下来的办公电脑维护的人没有精力理解Docker和DevOps。把源码打成WAR包丢进Tomcat的webapps目录数据库手工导入SQL文件启动后打开浏览器输入地址就能看到登录页这套流程在乡镇几乎不需要额外培训。SSMJSPMySQL的底气在于任何一环出了问题网上都有对应解决方案不容易卡死在冷门技术点上。框架分工要先理清后面排查才能按图索骥。Spring负责Bean生命周期和事务尤其对缴费这类多步写操作非常关键SpringMVC负责把HTTP请求分发给Controller再把Controller返回的字符串交给视图解析器渲染成JSPMyBatis负责SQL映射决定了数据库字段和Java属性之间的对应关系。在这个水费系统里MyBatis存在的意义尤其大涉及金额和读数的SQL需要能写带条件的UPDATE和关联查询MyBatis可以把SQL控制到每一个字符而不是交给框架自动生成。下面是我常用的最小配置骨架可以当作起点。先看Spring容器配置!-- applicationContext.xml 片段 -- context:component-scan base-packagecom.water.service/ bean iddataSource classorg.springframework.jdbc.datasource.DriverManagerDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/water_db?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value123456/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.water.mapper/ /bean这里有个容易被忽略的细节component-scan只扫描com.water.service不扫Controller。原因是如果service和controller都在同一个包下Spring容器和SpringMVC容器可能把同一个类初始化两遍轻则产生多余代理对象重则导致事务注解不生效。MapperScannerConfigurer会把com.water.mapper包下所有接口自动注册成Mapper之后在Service里直接Autowired注入即可不用手写实现类。再看SpringMVC配置!-- springmvc.xml 片段 -- mvc:annotation-driven/ mvc:default-servlet-handler/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /beanInternalResourceViewResolver负责视图拼接Controller里返回user/list会对应到/WEB-INF/views/user/list.jsp。default-servlet-handler解决的是静态资源问题没有这一行Tomcat的默认Servlet会被DispatcherServlet屏蔽CSS、JS、图片全部404。这个配置经常被漏掉很多人页面打开没有样式以为是JSP路径写错了其实是这里缺了一行。2.2 跑通第一条查询链路用用户列表验证Controller到SQL全通框架配好之后先不做复杂业务用最简单的“用户列表”验证整条链路是Controller到JSP都能连通。这样后面再填业务时不会出现“不知道哪一层断了”的情况。Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; RequestMapping(/list) public String list(Model model) { model.addAttribute(users, userService.findAll()); return user/list; } }Service public class UserService { Autowired private UserMapper userMapper; public ListUser findAll() { return userMapper.findAll(); } }public interface UserMapper { ListUser findAll(); }mapper namespacecom.water.mapper.UserMapper select idfindAll resultTypecom.water.entity.User SELECT user_id, user_name, address, phone, water_no FROM t_user WHERE status 1 ORDER BY user_id DESC /select /mapper这段代码的关键点是resultType直接指定了com.water.entity.User它要求数据库的user_id、user_name能映射到Java属性userId、userName。最省事的做法是在MyBatis配置里开启下划线转驼峰而不是给每个SQL字段起别名settings !-- 下划线转驼峰user_id 自动映射到 userId省去手工别名和 resultMap -- setting namemapUnderscoreToCamelCase valuetrue/ /settings加上这个设置后查询SQL可以保持数据库原有字段写法实体类属性用驼峰命名即可。如果是一个历史包袱比较重的项目已有大量SQL没有起别名先确认这个开关是否已经打开再决定要不要补别名。逐一改几百行SQL没有意义开着开关大概率能一次性解决映射问题。2.3 数据库初始化用户、水表、账单三张核心表的设计与DDL接下来建表。收费系统最终要对得上账用户、水表、账单这三张表之间至少要有一条完整的追溯链从人找到水表编号从水表编号找到每次抄表读数从抄表读数找到每一笔水费和缴费状态。所以字段设计上要保证water_no不重复且所有业务表都保留这个业务键。一个够用的建表脚本如下CREATE TABLE t_user ( user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, user_name VARCHAR(50) NOT NULL COMMENT 用户姓名, address VARCHAR(200) COMMENT 家庭地址, phone VARCHAR(20) COMMENT 联系电话, water_no VARCHAR(32) NOT NULL UNIQUE COMMENT 水表编号业务唯一键, status TINYINT DEFAULT 1 COMMENT 1在用0停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 建档时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户台账表; CREATE TABLE t_meter ( meter_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 抄表记录ID, user_id INT NOT NULL COMMENT 关联用户, water_no VARCHAR(32) NOT NULL COMMENT 水表编号, last_read DECIMAL(10,2) DEFAULT 0 COMMENT 上次抄表读数, current_read DECIMAL(10,2) DEFAULT 0 COMMENT 本次抄表读数, read_date DATE COMMENT 本次抄表日期, KEY idx_meter_water_no (water_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT水表读数表; CREATE TABLE t_charge ( charge_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 账单ID, user_id INT NOT NULL COMMENT 用户ID, water_no VARCHAR(32) NOT NULL COMMENT 水表编号, begin_read DECIMAL(10,2) DEFAULT 0 COMMENT 周期起始读数, end_read DECIMAL(10,2) DEFAULT 0 COMMENT 周期结束读数, water_usage DECIMAL(10,2) DEFAULT 0 COMMENT 用水量, unit_price DECIMAL(10,2) DEFAULT 0 COMMENT 计费单价, total_amount DECIMAL(10,2) DEFAULT 0 COMMENT 应收金额, pay_status TINYINT DEFAULT 0 COMMENT 0未缴1已缴, charge_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 计费时间, KEY idx_charge_user (user_id), KEY idx_charge_status (pay_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT水费账单表;字段选择上最重要的原则金额字段类型必须用DECIMAL(10,2)不要用FLOAT或DOUBLE。浮点数在计算机里本身就是近似值水费这种按月累计对账的场景一旦出现0.01的误差查账能把人逼疯。water_no在t_user上加了UNIQUE约束所有涉及水表的地方统一使用这个字符串避免以后系统扩展时出现同一用户两套编号的情况。t_meter的last_read和current_read分别是周期首尾读数。每次抄表时把current_read更新为当次读数同时把上一次的current_read写入last_read这样当月用水量的基数一直在。t_charge里的begin_read和end_read存的是周期快照后续就算用户换了水表历史账单也不会被当前数据污染。建表这一步做完数据库、连接、映射全部连通整个系统地基就完整了。3. 核心业务模块实现抄表、计费与缴费三条链路3.1 抄表录入更新读数与生成账单放进同一个事务业务链条的第一步收费员填写水表编号和新的读数点击提交。系统在背后要完成两件事一是更新最新读数二是自动生成水费账单。这两步必须保证原子性也就是要么都成功要么都失败。最容易出现的脏数据是更新读数的SQL执行成功但生成账单时抛了个异常页面报了错数据库里却是“读数已经变了账单没生成”。人对账时最先发现的就是“数字变了钱没变”这种脏数据比“钱变了数字没变”难查得多。原因就是没有加事务。实际的Service代码这样写Service public class MeterReadingService { Autowired private MeterMapper meterMapper; Autowired private ChargeService chargeService; Transactional(rollbackFor Exception.class) public void submitReading(String waterNo, BigDecimal currentRead, LocalDate readDate) { Meter meter meterMapper.selectByWaterNo(waterNo); if (meter null) { throw new BusinessException(水表编号不存在); } if (currentRead.compareTo(meter.getLastRead()) 0) { throw new BusinessException(本次读数不能小于上次读数); } meterMapper.updateCurrentReading(waterNo, currentRead, readDate); chargeService.generateCharge(meter, currentRead, readDate); } }Transactional(rollbackFor Exception.class)是这里最容易被忽略的细节。Spring默认只回滚RuntimeException和Error如果业务代码抛出的是IOException这类受检异常事务不会回滚前面的UPDATE已经提交。为了一次性挡住所有意外我一般直接写成rollbackFor Exception.class。但这有一个前提Spring容器里必须配好事务管理器并开启注解驱动。很多课程设计项目只写了注解完全忘了加下面这段XMLbean idtxManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertxManager/缺了这一段Transactional就是空摆设。排查这种问题时不用怀疑代码逻辑先确认这个bean和命名空间是否存在于配置文件里。生成账单的逻辑放在另一个Service里用Propagation.MANDATORY约束调用方必须处于事务中Service public class ChargeService { Autowired private ChargeMapper chargeMapper; Transactional(propagation Propagation.MANDATORY) public void generateCharge(Meter meter, BigDecimal currentRead, LocalDate readDate) { BigDecimal usage currentRead.subtract(meter.getLastRead()); if (usage.compareTo(BigDecimal.ZERO) 0) { throw new BusinessException(用水量不能为负数); } ChargeRecord record new ChargeRecord(); record.setUserId(meter.getUserId()); record.setWaterNo(meter.getWaterNo()); record.setBeginRead(meter.getLastRead()); record.setEndRead(currentRead); record.setWaterUsage(usage); record.setUnitPrice(TieredPriceCalculator.calculateUnitPrice(usage)); record.setTotalAmount(TieredPriceCalculator.calculate(usage)); record.setPayStatus(0); chargeMapper.insert(record); } }Propagation.MANDATORY表示如果调用者不在事务里方法会直接抛异常。这样可以防止后续有人把这个方法单独拿出来调用导致只插入了账单数据而没有对应的抄表动作关联账目溯源就会断掉。3.2 阶梯水价计费把价格规则做成独立可测试的工具类水费计费最常见的场景是阶梯计价第一阶梯20吨以内按基础价20到40吨按第二档超过40吨再往上走。价格规则如果散落在JSP页面或SQL片段里答辩时不好解释改价也容易漏改。我一般把它写成一个独立计算器放Service层调用/** * 阶梯水价计算器只负责算钱不负责其他业务。 * 档位一≤20吨2.80元/吨 * 档位二20-40吨3.60元/吨 * 档位三40吨4.80元/吨。 * 金额保留两位小数四舍五入。 */ public class TieredPriceCalculator { private static final BigDecimal TIER1_LIMIT new BigDecimal(20); private static final BigDecimal TIER2_LIMIT new BigDecimal(40); private static final BigDecimal PRICE1 new BigDecimal(2.80); private static final BigDecimal PRICE2 new BigDecimal(3.60); private static final BigDecimal PRICE3 new BigDecimal(4.80); public static BigDecimal calculate(BigDecimal usage) { if (usage null || usage.compareTo(BigDecimal.ZERO) 0) { return BigDecimal.ZERO; } BigDecimal amount; if (usage.compareTo(TIER1_LIMIT) 0) { amount usage.multiply(PRICE1); } else if (usage.compareTo(TIER2_LIMIT) 0) { amount TIER1_LIMIT.multiply(PRICE1) .add(usage.subtract(TIER1_LIMIT).multiply(PRICE2)); } else { amount TIER1_LIMIT.multiply(PRICE1) .add(TIER2_LIMIT.subtract(TIER1_LIMIT).multiply(PRICE2)) .add(usage.subtract(TIER2_LIMIT).multiply(PRICE3)); } return amount.setScale(2, RoundingMode.HALF_UP); } }这段逻辑的关键是不要用double换算。比如19.9吨乘2.80元double算出来会有一长串尾数页面上显示的金额就成了没法向用户解释的数字。BigDecimal配合setScale(2, RoundingMode.HALF_UP)得到的金额和人工用计算器按出来的结果一致。实际工程里价格很少是雷打不动的。如果未来要支持按片区、按月调整价格可以把三档价格和上限放到数据库费率表里按生效日期取最新一条。毕业设计不用把复杂度堆到这种程度但在说明文档里提一句“价格支持后续扩展”会显得设计意识更完整。3.3 缴费记账条件更新加流水记录并发下防重复支付缴费模块是整个系统最敏感的一环。用户拿着账单号来交钱收费员点击确认收款系统需要把账单状态改为已缴同时写一条支付流水。多步操作仍然需要事务但还有一个容易忽略的问题重复点击。用户提交一次缴费请求后如果页面没有防重处理第二个请求会再次进入业务逻辑。要挡住这种情况最好靠数据库层面的条件更新而不是在Java里先查再改。MyBatis里的UPDATE这样写update idupdateStatusPaid UPDATE t_charge SET pay_status 1 WHERE charge_id #{chargeId} AND pay_status 0 /update注意AND pay_status 0这个条件。两个并发请求同时进来第一个UPDATE成功影响一行第二个执行时因为状态已经变成1影响行数为0。Service层拿到这个结果就知道账单已被处理直接中止。这种写法比先SELECT再UPDATE更可靠因为先查再改的窗口期里另一个事务可能已经把它改掉了。Transactional(rollbackFor Exception.class) public void pay(Long chargeId, BigDecimal payAmount) { ChargeRecord charge chargeMapper.selectById(chargeId); if (charge null) { throw new BusinessException(账单不存在); } if (charge.getPayStatus() 1) { throw new BusinessException(账单已支付请勿重复操作); } int row chargeMapper.updateStatusPaid(chargeId); if (row ! 1) { throw new BusinessException(账单已被处理); } PaymentLog log new PaymentLog(); log.setChargeId(chargeId); log.setWaterNo(charge.getWaterNo()); log.setPayAmount(payAmount); log.setPayTime(LocalDateTime.now()); paymentLogMapper.insert(log); }这里还有一个共识支付的实收金额不能完全信任前端传参。应收金额应该以数据库里的total_amount为准前端传入的金额只用于展示和人工核对。如果收费员手误少收了钱流水和账单之间会对不上这种问题在月结对账时会变成血泪现场。至少要做到金额不一致时抛异常而不是默默存进去。到这里抄表到计费再到缴费的闭环就通了。页面交互和按钮防重属于前端范畴但服务端这条链路如果按上述写法已经能做到账目清晰、事务完整、并发不重复入账。4. 部署与配置避坑从MySQL时区到Tomcat WAR包五个真实排查记录4.1 MySQL 8.x 驱动的时区报错项目能启动一查表数据就翻车现象本地用Navicat连MySQL正常程序启动也没报错但第一次执行查询时报The server time zone value ... is unrecognized或者插入的时间字段比本地时间晚了8小时。原因MySQL 8.0以上的JDBC驱动要求连接串里显式声明serverTimezone否则驱动不知道数据库在哪个时区直接拒绝建立连接。而时间差8小时通常是数据库服务器使用UTC时间而程序运行在中国时区造成的。解决在JDBC URL中加上时区参数jdbc.urljdbc:mysql://localhost:3306/water_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseuseSSLfalse也要带上本地开发没有任何SSL证书默认加密握手除了拖慢连接速度没有实际意义。如果部署环境对安全有要求再单独配置证书不要为了省事直接把这个参数去掉而被迫在启动时处理证书警告。4.2 Tomcat下启动报ClassCastExceptionservlet-api的scope写错现象项目在开发工具内置Tomcat下运行正常但把WAR包丢到正式Tomcat后启动控制台出现和HttpServletRequest相关的ClassCastException或NoClassDefFoundError。原因pom.xml里引入了servlet-api、jsp-api依赖但是没有把scope设为provided。编译时这些类被打进WAR包Tomcat运行时自己的lib下也有同名包两个类加载器里都存在同名的Servlet类实例化时就产生了类型冲突。解决给这类依赖加上provideddependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependencyprovided的含义是“编译和测试时给我用打包时不要带进去”。这样WAR包里不包含Servlet API也就不会和Tomcat自带的类打架。很多部署问题表面上是环境玄学实际把依赖树看一遍多出来的jar就是元凶。4.3 javax与jakarta命名空间老项目在Tomcat 10上的兼容性踩坑现象同一份源码在Tomcat 9上跑得好好的换到新装的Tomcat 10后Controller全部404后台没有任何明显异常。原因Tomcat 10开始实现Servlet 5.0规范Java包名从javax.*全面迁移到jakarta.*。旧项目编译出来的class里引用的还是javax包Tomcat 10的类路径里找不到这些类请求自然进不了Controller。解决老SSM项目优先用Tomcat 9.x这是Java EE 8的兼容版本和javax命名空间完全匹配。如果坚持用Tomcat 10需要把代码和依赖里的javax.servlet.*全部替换成jakarta.servlet.*JSTL依赖也要换成jakarta版本。这个坑对传统JSP项目打包WAR影响很大因为这些项目一般没有专门做过容器适配。源码包交付时建议在说明文档开头就写清楚JDK 1.8 Tomcat 9 MySQL 5.7/8.0不要随意升大版本。4.4 数据库连接池耗尽系统跑几天后页面集体卡死现象系统刚上线时一切正常连续运行几天后页面打开越来越慢最后Tomcat日志出现Connection is not available, request timed out一类的错误。原因连接池的连接被耗尽。典型根因是没有正确释放连接比如用原生JDBC时忘了在finally里close或者是某个操作长时间占着连接不归还。Spring MyBatis的环境里SqlSession一般由框架自动管理最容易出问题的是自己手动开SqlSession却没有关的场景。解决先确认连接池大小和空闲超时设置合理。以c3p0为例# c3p0 典型参数乡镇场景并发不高连接数不需要开得很大 c3p0.maxPoolSize20 c3p0.minPoolSize5 c3p0.maxIdleTime1800不要把连接池开得越高越好。连接数越大数据库要维护的空闲连接越多内存占用反而上升。真正常见的坑不是数量不够而是连接没归还。排查时优先找那些执行时间超过一秒的SQL看是不是漏了WHERE条件全表扫描或者把大量数据一次性加载进内存。4.5 静态资源404JSP页面没有样式图片加载不出来现象浏览器打开页面后CSS样式完全没有控制台一排404页面结构像纯文本一样竖着排列。原因DispatcherServlet的url-pattern配置成/它捕获了所有请求包括css、js、图片文件。SpringMVC没有配置静态资源处理器默认Servlet又没机会处理于是全部404。很多人以为是JSP路径写错了去检查了半天ViewResolver配置。解决在springmvc.xml中加一行静态资源映射mvc:resources mapping/static/** location/static//或者mvc:default-servlet-handler/两种选其一即可。如果项目静态资源放在/static目录下推荐第一种路径更明确。排查这类404时有个习惯直接在浏览器里访问/static/css/app.css这种完整URL。如果在浏览器里能打开那就是路由被DispatcherServlet拦了如果直接打开也404那说明文件本身没有部署到WAR包的正确位置先检查目录结构。5. 说明文档与LW整理把源码交付变成能跑能验证的完整方案5.1 说明文档四件套环境要求、数据库初始化、启动步骤、功能清单源码交付时如果只有代码和SQL文件接手的人第一件事就会卡在启动上。我做交付时说明文档最少包含四个部分每个部分的写法都有目的文档模块最少要写的内容这么写的原因环境要求JDK版本、Tomcat版本、MySQL版本、操作系统兼容说明提前把版本锁死避免用户花一整天在环境上数据库初始化新建数据库命令、导入SQL命令、需要修改的配置项位置数据库不对后面所有功能都无法演示启动步骤WAR包放置位置、Tomcat启动命令、浏览器访问地址、默认账号密码让拿到文档的人能在10分钟内跑起来功能清单管理员、收费员、抄表员各自的可用操作答辩前对照文档就能知道系统全貌不需要逐行读代码如果部署服务器是Linux环境初始化数据库的命令要一并写进文档不要假设每个人都会用Navicat# 初始化数据库脚本 mysql -uroot -p water_db_init.sql # 验证表是否创建成功 mysql -uroot -p -e use water_db; show tables; # 将打好的WAR包放到Tomcat的webapps目录 cp water-fee.war $CATALINA_HOME/webapps/ # 启动Tomcat $CATALINA_HOME/bin/startup.sh # 实时查看启动日志出现 completed 表示启动成功 tail -f $CATALINA_HOME/logs/catalina.outwater_db_init.sql要在文档里明确说明执行顺序。导入失败时第一步看是不是MySQL版本差异导致SQL语法不兼容第二步看是不是没有先创建数据库。这些内容看起来基础却决定了文档使用者能否真正把系统跑起来。5.2 LW里的图用例图、ER图、时序图各画一张主线图LW一般需要配套用例图、ER图、时序图。很多同学建模时不知道画什么把系统所有功能都塞进去最后图乱得没法看。我的做法是按角色画骨干收到几个关键用例就好。乡镇水费系统的核心用例就三条线收费员登录、用户缴费、抄表录入、查看月度账单管理员用户台账维护、水表编号管理、账单审核、系统配置抄表员记录水表读数、确认读数异常ER图不需要另外造概念直接根据第二章的建表脚本反向生成工具用Navicat或DBDesigner都可以把t_user、t_meter、t_charge、t_payment之间的关系展示出来就够了。时序图只画最核心的一条主线抄表员提交读数系统更新水表读数计算水价生成账单用户完成缴费。这个流程走完答辩时讲需求分析、概要设计、数据库设计都能复用同一套图和词。画图最容易犯的错是把代码细节画进时序图比如把MyBatis的Mapper方法当作一个参与者列出来。时序图表达的是业务顺序不是方法调用链画太细反而暴露架构混乱。5.3 现场演示路径按业务故事线走不要按菜单从上往下点答辩现场演示最怕逐个菜单点过去既浪费时间又没重点。我建议按“抄表→计费→缴费→流水核对”这条业务故事线走每一幕都说明白了系统做了什么、改了哪张表。具体步骤管理员登录系统创建一条用户档案输入姓名、地址、水表编号以收费员身份进入抄表录入输入该水表本月的当前读数界面显示用水量和应缴金额进入缴费页面选择这笔账单输入实收金额看账单状态从“未缴”变为“已缴”切到管理员视角打开月度报表确认这笔收入进入当日汇总导出Excel后核对金额。每一步后面补一句“这一步系统做了什么也就是保存了哪张表、更新了什么字段”。比如抄表提交后要强调事务保证读数和账单同时生效缴费后要强调条件更新防止重复支付。这样一套流程下来评委看到的不是一个页面集合而是一个完整业务闭环这个是最大的加分项。6. 从能验收进一步给系统加微信缴费和月度报表导出6.1 微信缴费回调接口的最小实现乡镇水费系统做到线上缴费最常见的需求是小程序或公众号里发起支付然后由微信回调后台接口完成入账。真实的微信支付需要商户号资质毕业设计用不到那么完整但回调接口的结构可以先搭好。这里给出一个最小实现Controller RequestMapping(/pay) public class PayNotifyController { Autowired private ChargeService chargeService; RequestMapping(value /notify, method RequestMethod.POST) ResponseBody public String notify(RequestBody String xmlData) { // 生产环境必须先验签和解密本地演示可以先打印报文确认回调到达 MapString, String params XmlUtil.parseXml(xmlData); String outTradeNo params.get(out_trade_no); String totalFee params.get(total_fee); // 假定外部单号里携带了chargeId例如 WATER1001 Long chargeId Long.valueOf(outTradeNo.replace(WATER, )); // 微信金额单位是分除以100转成元 chargeService.pay(chargeId, new BigDecimal(totalFee).divide(new BigDecimal(100))); // 按微信协议返回成功标记否则微信会重复推送回调 return xmlreturn_code![CDATA[SUCCESS]]/return_code/xml; } }微信回调里的金额单位是分必须除以100转成元后再和系统内应收金额比对。如果金额不一致直接抛异常不能按回调报文里的数字无脑入账。回调成功返回的XML要求是SUCCESS否则微信会认为是投递失败反复推送。这个接口要配合缴费服务里pay_status的条件更新做幂等即使回调重复到达第二个请求也会因为状态已改而中止。6.2 月度报表导出用Apache POI生成可以在答辩现场展示的Excel报表模块是体现工作量的一页。用Apache POI导出Excel的做法在Java面试题里也常被问到实际写起来并不复杂public void exportMonthly(HttpServletResponse response, Integer year, Integer month) throws IOException { ListMonthlySummary summaryList chargeMapper.selectMonthlySummary(year, month); Workbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(year 年 month 月收费汇总); Row header sheet.createRow(0); String[] titles {水表编号, 用户姓名, 用水量, 应收金额, 实收金额, 缴费状态}; for (int i 0; i titles.length; i) { header.createCell(i).setCellValue(titles[i]); } for (int i 0; i summaryList.size(); i) { Row row sheet.createRow(i 1); MonthlySummary item summaryList.get(i); row.createCell(0).setCellValue(item.getWaterNo()); row.createCell(1).setCellValue(item.getUserName()); row.createCell(2).setCellValue(item.getWaterUsage().doubleValue()); row.createCell(3).setCellValue(item.getTotalAmount().doubleValue()); row.createCell(4).setCellValue(item.getPaidAmount().doubleValue()); row.createCell(5).setCellValue(item.getPayStatus() 1 ? 已缴 : 未缴); } response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenamemonthly.xlsx); workbook.write(response.getOutputStream()); workbook.close(); }文件名的中文缓存处理是常见失误。如果Content-Disposition里直接写中文文件名部分浏览器下载时会乱码建议用URLEncoder.encode(filename, UTF-8)编码后再拼到header里那个小动作为答辩现场省去不少尴尬。做这类系统做到现在我回头想最值得保留的习惯是每建一张表同步更新设计文档每改一次接口就重新跑一遍建库启动流程。这个习惯比掌握多少新框架特性更能决定一个项目是否交付得顺利。乡镇自来水收费系统本身的逻辑不复杂但把事务边界、并发幂等、部署兼容这些问题一个个磨清楚之后再看任何SSM项目都不会觉得是黑匣子这也是我希望这篇笔记能帮到你的地方。本文还有配套的精品资源点击获取
返回列表