ARTICLE DETAIL

资讯详情

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

SSM实战:智慧社区缴费报修平台的设计与开发

SSM实战:智慧社区缴费报修平台的设计与开发 刚拿到智慧社区缴费报修服务平台这个需求时我心里其实有点复杂。SSMSpring SpringMVC MyBatis这套组合在今天的Java生态里已经算老古董了身边不少同事已经换上Spring Boot全家桶。但项目背景摆在那里公司现有资产中还有大量基于SSM的传统企业级系统客户的基础设施里部署环境老旧不希望引入过重的容器化改造。于是我只能在这个相对古典的Stack上把一个面向小区业主的缴费和报修业务完整落地。这个平台到底解决什么问题说白了就是三件事让业主线上缴费让报修工单不被淹没在微信群聊里让物业人员不用拿Excel统计催缴记录。它包含业主端、物业端和维修工端三个视角核心业务围绕缴费和报修两条主线展开。如果你也在做类似的SSM项目或者是刚接触JavaWeb课程设计想从中学一点实战思路那么这篇从需求拆解、数据库设计到上线维护的记录应该能给你一些参考。下面按我实际开发顺序来讲。1. 为何要做一个智慧社区缴费报修平台1.1 从需求方抱怨中梳理核心场景我第一次去小区物业调研物业经理打开手机给我看了三个被置顶的微信群催缴群、报修群、投诉群。群里的消息基本是x栋x单元今天停水了维修工师傅我家厨房漏水师傅什么时候来之类。报修信息靠人工接龙物业客服每天要花上午两小时把散落在聊天记录里的工单抄到Excel表里。而缴费更头痛每个月除了贴通知单还要挨个打电话催物业费、水电公摊费。这个看似简单的场景梳理下来就两个核心闭环缴费闭环物业发布账单 - 业主收到应缴费用 - 在线支付 - 财务核销 - 开具电子收据。报修闭环业主提交报修单 - 客服派单 - 维修工接单 - 现场处理并上传结果 - 业主确认 - 服务评价。只要这两条线跑通物业日常运营至少能节省一半人力。我们的平台边界也就清楚了不做智能门禁不做社区电商只聚焦和钱与维修相关的两个核心点。做产品最怕什么都想做最后哪个都没做好。社区平台虽然名字叫智慧社区但不能为了智慧二字硬塞功能。1.2 三个角色的核心诉求都被我列成了账本在设计功能时我没有一上来就画用例图而是以每个角色的利益点出发列需求。后来发现这张表比任何原型图都管用因为它是数据库和接口设计的直接依据。角色最急迫的痛点平台必须提供的功能业主缴费要跑物业中心报修后不知道进度手机上查账单、在线支付、随时查看报修进度、评价物业客服催缴和派单全靠电话Excel记录易出错批量账单管理、智能派单、统一工单看板维修工接单靠微信群抢材料/费用说不清工单列表、完工回执、材料费用登记物业财务收款记录与第三方支付对账难支付订单明细、每日对账单、导出报表我们给系统规划了四种角色业主ROLE_OWNER、客服ROLE_SUPPORT、维修工ROLE_REPAIR、财务ROLE_FINANCE再加上管理员ROLE_ADMIN。其实客服和财务很多时候是同一个物业人员兼任但权限层面我建议分开因为支付网关的退款操作不能和账单编辑混在一起这是资金安全的基本要求。权限划分清楚以后后面做拦截器和越权防护才不会乱。1.3 业务边界怎么控制很多做课设或实训的小伙伴喜欢把系统做大社区平台要么加二手市场要么加业主论坛。我的建议是克制。智慧社区的核心价值在于数字化已有流程而不是重组业务流程。我们只保留了和缴费、报修强相关的附属功能公告通知、业主房屋绑定、电子收据。像社区团购这种和当地商家深度绑定的业务先不做避免运营风险和开发风险同步扩大。在技术实现上我把整个系统划分成两个相对独立的领域模块billing缴费和repair报修。它们共用一个用户中心模块user但彼此的表不直接join。为什么因为在后期迭代中两个业务的变更频率完全不同缴费功能要跟着支付网关接口调整报修功能要跟着物业管理流程调整。如果不做领域隔离改起来很容易互相污染。比如支付网关接口升级时你可能只改billing模块却因为代码耦合动了repair模块那上线回归范围就失控了。2. 技术选型为什么用SSM而不是照着Spring Boot模板抄2.1 先承认Spring Boot更好但不是每个场合都适合先承认一个事实如果让我从零做新项目Spring Boot MyBatis Plus 是更高效的选择。启动快、自动配置、内嵌Tomcat几乎没有样板代码。但这个项目之所以用SSM有三个现实原因。第一运行环境是客户自建机房的一台老RHEL服务器Java 8版本已固定系统里还有多个历史Web应用共用同一个Tomcat 8实例。Spring Boot的嵌入式容器、端口管理、依赖传递在这种旧环境下反而容易和现有应用冲突SSM打成war包扔进现有Tomcat的webapps目录里是最稳妥的部署方式。第二团队里兄弟们的技能栈刚好停留在SSM阶段培训成本为零。如果引入Spring Boot还需要时间熟悉新的starter机制、配置项命名方式项目排期不允许。软件开发不是只有技术先进性一个指标交付风险和团队可持续性同样重要。第三客户后续希望源码可维护甚至要拿去二次开发。SSM的XML配置虽然啰嗦但每一处bean依赖都看得明明白白对年龄较大的维护工程师相对友好。Spring Boot的自动配置虽然方便但出问题时黑盒感更强需要更深的框架知识才能排查。2.2 SpringMVC、Spring、MyBatis各自在项目里的职责SSM不是三个工具的堆砌而是有明确分工的Spring负责容器管理所有Service对象、DAO对象都由它托管事务边界也在Service层SpringMVC负责表现层接收HTTP请求绑定参数调用Service返回视图或JSONMyBatis负责持久层把Java对象映射成SQL把结果集映射回Java对象。对应到目录结构上我们使用标准的单模块多包结构com.estate.platform ├── common # 工具类、通用异常、分页 ├── config # spring配置、mybatis配置 ├── controller # 表现层 ├── service # 业务层接口实现 ├── mapper # MyBatis的Mapper接口 ├── model # 实体对象、DTO、VO └── interceptor # 登录/权限拦截器、全局异常这里要注意一个点很多初学者把业务逻辑写在Controller里这是SSM项目后期最痛苦的事情。我们的原则是Controller只做参数接收和视图转发所有业务判断、事务控制都封装在Service接口中。这样一来后面加微信小程序端时只要复用Service层就行Controller甚至可以被替换成另一个暴露REST API的适配层。我们目前已经用这个方法把业主端从JSP页面扩展到H5移动端成本大概只花了两天。2.3 为什么不引入MyBatis-Plus可能有人问用SSM也还能配MyBatis-Plus简化CRUD。我们评估后决定不用原因有两个。其一是MyBatis-Plus的自动填充、逻辑删除等特性虽然省时间但让SQL的可控性降低。缴费平台涉及资金每一条update语句我都要自己核对字段不想依赖第三方自动拼接否则一旦出现批量更新把逻辑删字段误伤查起来非常麻烦。其二是老版本MyBatis-Plus和原生MyBatis在特殊SQL例如复杂的动态表名、批量插入优化上有些坑为了少写几行代码给排错埋雷不值当。最终我们手写Mapper XML虽然多写了五百行但每一条SQL都清清楚楚。3. 数据库设计缴费和报修两条主线不能乱3.1 用户、房屋、楼栋的关系模型社区平台一定绕不开归属关系业主是房屋的住户房屋属于楼栋物业费按建筑面积或房屋数量计算。我们设计了四张基础表t_user用户基础信息包括账号、密码hash、手机号、真实姓名、角色。t_building楼栋信息包括楼栋号、总层数、建筑面积等信息。t_room房屋信息所属楼栋、房号、建筑面积、业主用户ID可空空表示未绑定。t_room_user用户与房屋的关系表。为什么不用外键因为一个业主可能拥有同一小区多套房也可能一个房屋对应多个居住人夫妻二人都有账号用关联表可以支持多对多后续缴费账单按房屋维度出登录用户按关联表找名下房屋。DDL示例简化CREATE TABLE t_room_user ( id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, user_id INT NOT NULL, role_in_room VARCHAR(20) NOT NULL DEFAULT OWNER, is_deleted TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, UNIQUE KEY uk_room_user (room_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;主键用自增int还是雪花id由于是内部系统并发量低自增主键就够。但是对外导出订单时不要暴露自增ID而是用业务单号形如PAY202501150001对外展示。业务单号的好处是不仅方便客服定位问题还避免了自增ID被遍历抓取的风险。3.2 缴费单表设计的关键字段缴费单表t_payment_order是缴费闭环的核心。最重要的字段不是金额而是状态。我们用了以下状态集INIT账单已生成等待支付PAYING用户正在支付前端标记后端一般不等这个状态PAID支付成功以支付回调为准CLOSED超时未支付自动关闭REFUNDING退款中REFUNDED已全额退款金额字段除了pay_amount实付金额外还设计了bill_amount应缴金额、discount_amount减免金额、late_fee滞纳金。为什么要分开因为财务需要统计应缴率和减免率。例如本年度有哪些业主申请了空置房减免、老幼减免如果不单独存这几个字段等月底做报表时就得从日志里反推减免额很费劲。另外缴费单必须有原始来源标识billing_typeWATER/ELECTRICITY/PROPERTY/MATERIAL等、billing_period账期如2025-01。这两个字段便于月底生成汇总报表。我们在这张表上建了联合索引(user_id, status, billing_period)查询业主待缴账单走这个索引效率很高数据量到十万级也不会有压力。3.3 报修单表用状态字段贯穿全流程报修单表t_repair_order设计时最忌讳把状态做成一个字符串随意写。我们定义了严格状态集SUBMITTED已提交等待客服审核/派单ASSIGNED已派单给维修工REPAIRING维修工开始处理PENDING_CONFIRM维修工已上报完成等待业主确认CONFIRMED业主已确认待评价EVALUATED已评价流程结束CANCELED业主或客服取消每个状态流转都要在代码中校验不能跳状态。比如从SUBMITTED可以直接到CANCELED但不能从SUBMITTED直接到PENDING_CONFIRM。数据库表里除了status之外还保存当前处理人current_repair_id、派单时间assign_time、完成时间finish_time这些都是报表和消息通知的必填字段。报修单的图片附件如何保存我们没有把图片直接存数据库的BLOB字段而是上传到服务器本地存储目录数据库里保存相对路径。这样方便直接给前端输出图片URL。考虑到Tomcat重启后的绝对路径变化我们在配置文件中设置了upload.dir变量然后通过SpringMVC的ResourceHandler把这个目录映射为URL路径。这里有个细节如果文件路径写在Tomcat的webapps目录下重新部署war包可能会被清理所以我们放到外部独立目录不跟着war包走更安全。3.4 防止重复缴费和重复报修的约束在设计表时就要考虑防重。防止重复缴费一个账单只能对应一条有效的支付单。在t_payment_order表中增加bill_id字段并建立唯一索引uk_bill_id(bill_id)让同一个账单只能生成一条支付记录。如果业务上允许分多次付款比如大修基金那就不能简单加唯一索引需要在业务层设计分期逻辑。我们当前不做分期所以用唯一索引足够。防止重复报修业主在未完成流程的情况下不能为同一个房屋同一维修类型如漏水创建内容相似的新单。我们在业务层做校验查询最近10分钟内是否存在状态在(SUBMITTED,ASSIGNED,REPAIRING)的同类型维修单有则提示您有未完成的工单。数据库层不需要做这种复杂约束业务层判断足够因为报修单的重复性不像资金单那么严格。4. 缴费模块开发从账单生成到支付成功的完整链路4.1 账单生成与定时任务缴费的基础是账单先行。物业的收费计划一般有两种周期性每月物业费和临时性某次维修材料费、公摊水费。我们提供一个后台接口供财务录入也可以导入Excel批量生成。为了支持每月自动生成功能用Spring自带的TaskScheduler配置了一个定时任务task:annotation-driven schedulertaskScheduler/ task:scheduler idtaskScheduler pool-size2/在每月1日凌晨系统扫描所有房屋根据房源面积和物业费单价计算出应缴金额再为每户生成t_payment_order。这里有一个小坑批量生成时要在事务内执行但若一次生成几千条事务会过长影响数据库资源。我们按楼栋分片提交每处理一个楼栋提交一次事务出现失败只回滚当前楼栋记录异常日志。这是典型的大事务拆小事务思路可避免某栋楼数据异常导致整个月的账单全部生成失败。4.2 支付回调必须幂等支付流程我们选了市面上比较通用的接口模式前端调用支付下单接口后端生成支付参数用户支付成功后支付平台以异步通知callback方式通知我们系统。这里最核心的是回调处理。回调接口设计成接收两个要素商户订单号我们自己生成的payment_no和支付平台交易号。回调处理逻辑如下校验签名防止伪造通知根据payment_no查询数据库订单若订单状态已经是PAID直接返回成功不重复更新幂等判断更新订单状态为PAID记录支付流水更新账单已缴状态。这个幂等判断非常关键。因为支付平台会因网络原因多次发送回调如果我们不判断就会重复加钱记录财务对账时很难处理。我们在update语句里用乐观锁UPDATE t_payment_order SET status PAID, pay_time NOW(), third_trade_no #{thirdTradeNo} WHERE payment_no #{paymentNo} AND status IN (INIT,PAYING)通过受影响行数判断是否更新成功。如果影响0行说明订单已经不是初始状态可能是重复通知也可能是支付金额不一致。此时要记录日志交给人工处理不能默默吞掉。4.3 超时未支付的订单处理用户提交支付但中途退出订单一直留在INIT状态会导致账实不符。我们设计了定时任务每10分钟扫描一次超时未支付的订单把创建时间超过30分钟的INIT订单自动置为CLOSED。为什么是30分钟因为支付平台的支付链接有效期一般也在15~30分钟之间两边时间对齐避免用户已经支付成功我们这边订单却被超时关闭产生退款争议。这里有个实际踩过的坑如果用户先调起收银台然后手机切后台超过30分钟后回来支付支付平台依然能成功。这时我们的订单已经CLOSED。怎么办我们的对策是回调处理时如果发现订单已经是CLOSED不直接忽略而是走支付成功单分支自动恢复为PAID但记录一条异常标记让财务在下一次对账时复核。这样做虽然有一点操作风险但比直接给用户退款更友好毕竟用户确实付了钱物业也确实提供了服务。如果你的项目不允许这种策略最简单的方式是直接原路退款再让用户重新下单但用户体验会差一些。4.4 对账每天凌晨拉取支付流水最后一步是对账。每天凌晨后台拉取支付平台前一日账单与我们本地订单状态为PAID、支付时间在前一日的订单明细比对。发现有平台有而我们没有的长款要标记为异常订单有我们已扣款但平台没有的短款则可能是状态同步延迟先不处理第二天再查。这个对账流程虽然简单但能有效发现漏单。我们曾经因为服务器时钟偏移导致对账一直差一条后来把数据库连接参数里的serverTimezone统一成Asia/Shanghai才解决。对账不是可有可无是资金系统的最后一道防线。5. 报修模块的状态机从提交到完成的每一步都不能漏5.1 状态机定义与业务强绑定前面章节提过状态集合这里细化流转。我强烈建议不要在Service代码里到处写if判断状态而是用枚举配合一张状态流转Mappublic enum RepairStatus { SUBMITTED, ASSIGNED, REPAIRING, PENDING_CONFIRM, CONFIRMED, EVALUATED, CANCELED; private static final MapRepairStatus, ListRepairStatus TRANSITIONS new HashMap(); static { TRANSITIONS.put(SUBMITTED, Arrays.asList(ASSIGNED, CANCELED)); TRANSITIONS.put(ASSIGNED, Arrays.asList(REPAIRING, CANCELED)); TRANSITIONS.put(REPAIRING, Arrays.asList(PENDING_CONFIRM, CANCELED)); TRANSITIONS.put(PENDING_CONFIRM, Arrays.asList(CONFIRMED)); TRANSITIONS.put(CONFIRMED, Arrays.asList(EVALUATED)); } public void canTransitionTo(RepairStatus target) { if (!TRANSITIONS.getOrDefault(this, Collections.emptyList()).contains(target)) { throw new BizException(非法状态流转: this - target); } } }这个枚举把所有允许的迁移都放在一个地方。后续加需求时改动可以集中评审而不是像很多项目那样状态流转分散在二十个方法里加一个状态得全局搜索。曾经有同事在REPAIRING状态直接接到EVALUATED用户端没看到完工确认页误以为没完成后来我们靠这个枚举卡住了这种非法流转。5.2 派单策略不是简单抢单而是客服手动 系统推荐最初我们想让业主上传报修单后自动派单但客户拒绝了。原因是维修工有所属区域比如东区、西区不同工单类型需要对应不同工种水电、木工、家电全自动派单规则太复杂。最终方案是客服在后台看到SUBMITTED工单后系统根据维修工的空闲度和擅长类型推荐三个候选人客服按推荐列表点击派单。为什么这样设计因为平台初期的数据不够支撑机器学习但可以基于简单规则维修工最近30天工单数量少的优先来排序。这比纯抢单公平也比全自动安全。派单接口设计为PUT /repair/order/{id}/assign请求体包含repair_user_id。后端要校验被指派的维修工当前未完成工单数不能超过5个否则返回提示。如果不做这个限制就会出现一个师傅接到10单、其他师傅空着的失衡情况。我们在维修工表上加了current_pending_count字段每次派单和完工回执时更新避免实时统计导致的性能开销。5.3 消息通知除了数据库状态还得让人看得见报修状态变化必须通知到相关人。传统SSM项目用WebSocket吗可以但为了简单我们先用前端定时轮询 后端消息表t_notification的方案。虽然不够实时但足够可靠。当状态变化时Service层在同一个事务内写入消息记录前端每30秒调一次接口拉取当前用户未读消息数。因为社区报修不是高频业务30秒延迟完全可接受还避开了WebSocket连接管理、心跳保活、断开重连等一系列麻烦。消息内容模板用简单字符串拼接您的报修单#RB20250101001已派单给张师傅请关注进度。这里注意凡是拼接用户输入的位置必须参数化不能直接把报修描述拼进通知文案防止XSS风险。数据库存取用MyBatis的#{}占位符防SQL注入前端渲染时也转义双保险。5.4 报修完成后还要做好评价闭环最后一步是业主确认并进行服务评分。评分包含star1~5和comment。为了报表统计我们在维修工维度上记录平均分和工单数。这里有一个容易忽略的点业主可能很久不操作确认状态就卡在PENDING_CONFIRM。我们加了自动确认机制维修工完工回执提交后72小时业主没有主动确认或投诉系统自动置为CONFIRMED并允许评价。这样做是为了避免维修工一直挂怀着待验收工单影响派单算法的空闲判断。产品上这个逻辑要跟客户提前说明避免纠纷。6. 权限与安全SSM框架里最容易翻车的地方6.1 登录与会话管理SSM项目常见的登录方案是Session Cookie我们也是。用户登录成功后将用户对象和角色列表放入SessionSpringMVC的拦截器判断URL是否需要登录。为了避免明文密码存储密码使用BCryptPasswordEncoder加密这个依赖很小且安全。绝不要用MD5裸哈希在线彩虹表一查就破。如果要兼容老系统的旧密码可以做一个过渡策略登录时先按BCrypt校验失败再按旧哈希校验并提示用户下次登录强制改密。Session和Cookie还要设置合理超时时间。我们设置默认30分钟无操作失效并在登出时主动清除Session。曾遇到用户反馈我什么都没干就掉线原因是物业工作人员习惯开着页面半天不动后来我们延长到120分钟并为操作频繁的后台人员提供记住我选项。6.2 RBAC权限模型落地RBAC的关系是用户拥有角色角色拥有权限。我们简化为用角色字符串直接判断是否可以访问某URL。拦截器里维护一个mapprivate static final MapString, String[] URL_ROLE_MAP new HashMap(); static { URL_ROLE_MAP.put(/admin/payment/batch, new String[]{ROLE_FINANCE, ROLE_ADMIN}); URL_ROLE_MAP.put(/repair/order/*/assign, new String[]{ROLE_SUPPORT, ROLE_ADMIN}); }如果当前用户角色不匹配返回403页面。这里要特别注意URL通配符的匹配顺序。我们使用AntPathMatcher规则匹配从精确到模糊避免/repair/order/**把权限放开到业主。权限配置不是一劳永逸的每上线一个新接口都要检查拦截器里是否加了对应规则。我们内部定了一个开发规约新增Controller接口时必须同步更新权限表否则代码评审不过。6.3 接口越权防护这是最常见的逻辑漏洞比SQL注入更容易出现的是水平越权。比如业主A登录后直接调用/repair/order/{id}/update修改了业主B的工单内容。根本原因就是Service层没有校验资源归属。我们的规范是凡是修改类接口传入的订单ID必须先从当前登录用户的角度做一次权限校验。怎么校验一句话根据请求者的角色和其绑定数据先查一次数据库看资源是否属于他。比如业主只能操作room_id归属于自己的工单维修工只能操作assigned_repair_id等于自己的工单客服可以操作所有未取消工单。这个校验放在Service层的第一步而不是Controller里。这样即使某个Controller被组装绕过比如定时任务直接调用Service业务逻辑也安全。实现时在Service层定义一个方法checkOwnerPermission(orderId, userId, role)先查询order判断owner_val等于当前用户否则抛异常。如果项目中需要校验的接口很多可以抽取AOP切面统一处理。我们的项目接口数量少用手动方法调用就够不引入AOP的额外复杂开关。6.4 参数校验与全局异常参数校验是安全问题里最容易被忽视的一块。SSM里如果用JSR-303 Bean Validation需要引入hibernate-validator配置一个MethodValidationPostProcessor。但考虑到老项目中依赖版本冲突我们直接用Spring的Validator接口配合手写工具类。比如手机号格式、日期格式、金额必须大于0、分页参数上限100。所有校验统一放在Service入口Controller只做粗糙的非空判断。这样保证不同入口调用Service时都会被校验不会因为某个新入口漏了判断而录入脏数据。全局异常用ControllerAdvice通过ExceptionHandler返回统一JSON或错误页。在异常里要区分业务异常BizException和系统异常。对客户端只返回业务提示系统异常记录日志返回系统繁忙请稍后重试。切记不要把SQL异常信息原样返回给前端因为数据库表结构暴露会引来更严重的安全问题。我们在生产环境做过一次演练故意触发SQL注入请求日志里记录到异常输入前端拿到的是统一提示没有泄漏任何表名字段信息。7. 上线前我踩过的坑SSM整合的五个经典错误7.1 Mapper扫描不到启动报Invalid bound statement (not found)这是SSM项目最经典的问题。当时启动不报错但一调用Mapper的方法就提示Invalid bound statement (not found)。根因有几种Mapper接口没加MapperScan或没配MapperScannerConfigurerXML文件没放在Mapper接口对应的包路径下或者在Maven构建时没把XML文件打进去。我们项目是Maven结构src/main/java下的Mapper接口XML放在resources/mapper目录居然没打包成功。原因是没有在pom.xml中配置buildresources包含xml后缀。加上后就好了resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources另外还有一种情况Spring配置里mybatis.mapper-locations写的是classpath:mapper/*.xml但实际XML在另一个classpath路径下导致Mapper接口找不到statement。排查时最好打开构建后war包里的目录结构对比不要只看开发环境IDE里的路径。这个坑我花了两个小时才定位后来总结出一个排查顺序先看war包里的xml是否存在再看mybatis配置的mapper-locations是否匹配最后看Mapper接口的全限定名和XML中的namespace是否一致。7.2 Spring事务只对Service层生效且回滚条件要小心我们曾在Controller里直接调用paymentService.createOrder后又调paymentService.paySuccess以为两个方法都在事务里结果前者提交后者失败出现了账单已生成但支付记录缺失的半成品状态。这在注解事务中很常见Spring AOP默认只对public方法生效且事务按代理传播。当在同一个Service类中调用另一个带Transactional方法事务不会生效因为自调用绕过代理。正确做法是跨Service的原子操作放在一个新Service方法中并加上Transactional例如Transactional(rollbackFor Exception.class) public void handlePaymentCallback(...) { paymentService.markPaid(...); billService.markSettled(...); }还要注意默认事务只回滚RuntimeException如果方法抛出受检异常比如支付平台接口的IOException是不会自动回滚的要显式声明rollbackFor Exception.class。很多老项目的资损bug就是这样来的。我们在总结规范时要求所有写操作的Service方法统一使用Transactional(rollbackFor Exception.class)不能只写Transactional。7.3 JSON序列化循环引用由于MyBatis关联查询Order对象里有User对象User对象里可能又有List Room里又有List 在把对象转成JSON时出现循环引用直接栈溢出。处理办法有两种一是设计DTO不使用实体直接返回二是使用Jackson的JsonIgnoreProperties注解忽略反向引用。我们最终选择前者因为直接返回实体会把不该暴露的字段比如密码、数据库字段也暴露出去。所以从Service返回给Controller的都是VO视图对象比如PaymentOrderVO复制必要的字段。虽然代码量大一点但安全性和可维护性提高了。后来我们还发现某些实体里存在List字段前端根本不需要显示但是返回JSON时却把整个列表带上了导致接口响应慢。换成VO之后按需装配字段接口体积直接减少一半。7.4 日期类型转换的连环坑SSM MyBatis老版本处理LocalDateTime会有兼容问题我们统一用java.util.Date再配合Jackson的DateSerializer。前端传时间字符串2025-01-15 10:20:30SpringMVC默认绑不到Date上会报400错误。解决方式是在Controller的InitBinder里注册CustomDateEditor或者在实体字段上加DateTimeFormat(patternyyyy-MM-dd HH:mm:ss)。日期还有一个更隐蔽的问题MyBatis查询结果里的date字段和MySQL数据库时区不一致可能相差8小时。在JDBC连接地址上加serverTimezoneAsia/Shanghai同时数据库连接参数加useSSLfalsecharacterEncodingutf8mb4。这个差异在本地开发环境不容易出现但一到云服务器上就频繁暴露尤其是跨时区访问时更明显。我们后来把所有日期字段统一在Service层转成yyyy-MM-dd HH:mm:ss字符串给前端不再让前端自己格式化从根源上规避了时区和格式的混乱。7.5 数据库连接池耗尽上线初期出现过系统每隔几小时所有接口超时排查发现是数据库连接池被打满。原因有两个一是MyBatis每次执行查询时如果结果集没有及时关闭连接不释放二是定时任务中for循环逐条调用多次查询每个查询占一个连接循环1000次就是1000个连接。解决一是确保所有SqlSession都用try-with-resources关闭二是调整Druid连接池参数初始连接数5最小空闲5最大活跃50testWhileIdle设为true三是在循环中改为批量查询减少数据库往返。Druid自带的监控页面很直观可以看到活跃连接数、当前等待数。我们把监控页面限定了内网IP访问避免生产环境把连接池监控暴露到外网。这个问题属于性能场景但在SSM老项目中频繁出现值得每个维护老系统的同学注意。写到这里这个智慧社区缴费报修服务平台的主体已经完整跑通从需求到数据库从缴费流程到报修状态机最后是安全防护与排错经验。如果明年让我重新做一遍大概率会有更好的技术方案但SSM时代的这些细节仍然值得记录。希望正在做类似毕业设计、课程设计或老项目维护的你能避开我踩过的这些坑。最后再提醒两点数据库设计时一定把状态字段的取值离散化并集中管理任何涉及钱的系统幂等和事务边界都要反复审查。祝你的项目早日上线。
返回列表