资讯动态

基于SSM框架的保险业务管理系统设计与实现:从权限到部署全解析

发布时间:2026/10/22 6:04:33 来源:尧图企业网站定制
1. 为什么选“SSM 保险业务”——从一个真实的管理场景说起有很长一段时间我接触的保险业务管理系统都不是什么高大上的东西。很多中小团队或保险中介机构日常用的就是Excel加微信群今天谁续保、哪个客户生日要回访、哪张保单核保卡住了、佣金什么时候发全靠表格在几个人手里传来传去。项目一多数据一乱月底对账时几个人对着同一张表也能算出三个不同结果。后来真正动手做“基于Java的SSM框架保险业务管理系统”才把这个问题彻底想明白了保险业务的核心不是卖保单而是流转。从客户投保到核保通过从出单到续保提醒从佣金计算到业绩统计每个环节都要有记录、有状态、有责任人。Excel能记结果但记不住过程能存数但管不了权限。所以这类系统的价值不在于界面上有多花哨而在于能不能把一条业务链路完整地串起来、堵住人为操作的漏洞。SSM这个组合——Spring、SpringMVC、MyBatis——在现在的技术讨论里听起来有点“老”但它做保险业务系统恰恰是合适的。原因也不复杂这类系统核心是事务、权限、状态流转和报表统计SSM的轻量程度、掌握成本、部署成本都比一上来就上微服务全家桶要务实得多。我见过太多项目业务逻辑还不到十万行先搭了十几个微服务最后光服务间调用的坑就能埋掉整个团队。所以这篇文章不是讲“SSM有多新”而是讲它如何在一个真实可落地的保险业务系统里把每一块技术用在该用的地方。如果你想做的是教学项目、毕业设计或者公司内部需要一套能快速交付的保险业务后台下面这些内容和代码思路都可以直接参照。我会按实际开发顺序来讲底层表结构怎么设计、核心业务怎么用SSM落地、权限和日志怎么补、最后怎么部署出问题又怎么排查。2. SSM三大组件在保险业务中的真实分工——别把框架用成摆设很多人学习SSM时记了一大堆XML配置但到真实项目里就迷糊了Spring到底负责什么SpringMVC和Struts有什么区别MyBatis和Hibernate哪个好在保险业务系统里这些问题的答案会被业务需求推着走抽象讨论反而没用。2.1 Spring容器不只是管理Bean而是管理“业务规则”在保险系统里业务规则往往不是写死在某个类里的。比如不同险种的退保计算规则不同、佣金比例随业绩阶梯变化、核保人员的审核权限跟角色绑定。这些规则如果直接new对象、在Service里写死改一处要牵连一大片。Spring在这套系统里的核心作用是作为容器把这些规则的对象生命周期、依赖关系统一管起来。举一个真实例子佣金计算模块需要调用费率服务、业绩服务、人员服务。如果不借助Spring你需要在每个用到佣金计算的地方手动new三个依赖一旦费率服务改了构造方法所有调用点都要跟着改。用Spring之后只需要在配置里声明Bean业务代码通过构造器注入或属性注入拿到依赖规则变化时替换实现类即可。Service public class CommissionService { private final RateService rateService; private final PerformanceService performanceService; public CommissionService(RateService rateService, PerformanceService performanceService) { this.rateService rateService; this.performanceService performanceService; } public BigDecimal calculate(String policyNo) { Policy policy policyService.getByPolicyNo(policyNo); BigDecimal rate rateService.getRate(policy.getProductId(), policy.getAgentId()); return policy.getPremium().multiply(rate); } }这段代码看似简单但它体现的是Spring的核心价值依赖注入让业务类之间的耦合降到最低。保险业务分组多、角色多、规则变动频繁如果类与类之间是硬依赖每次需求变更都要大动干戈Spring注入能让你只改该改的那一个实现。2.2 SpringMVC把“前端请求—后端处理—页面响应”这条线理清楚保险业务系统的前端交互并不复杂无非是列表查询、表单提交、详情展示、状态流转。但正因为简单很多人会把Controller写成万金油校验也放Controller、业务也放Controller、数据组装也放Controller最后Controller几百行Service层反而是空的。SpringMVC在这套系统中的定位是表现层框架它解决的是请求如何进入系统、如何被分发到对应业务处理单元、处理结果如何返回前端。需要特别注意两个细节第一个是参数绑定。保险业务里涉及大量日期和金额字段比如投保日期、生效日期、保费、保额。SpringMVC在处理表单提交时默认按参数名匹配对象属性。我曾经踩过一个坑前端传了字符串形式的日期“2024-03-15”后端用Date类型接收结果报了类型转换错误。解决方案是配置一个全局的日期转换器Configuration public class WebConfig implements WebMvcConfigurer { Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new ConverterString, Date() { Override public Date convert(String source) { try { return new SimpleDateFormat(yyyy-MM-dd HH:mm:ss).parse(source); } catch (Exception e) { return null; } } }); } }这里面的逻辑是统一处理输入格式而不是在每个Controller里各自转换。保险表单里日期是高频字段如果每个地方都写解析代码后期维护就是灾难。第二个是RESTful风格的接口规划。虽然这是一个管理后台我仍然建议在SpringMVC的Controller里尽量按资源语义写接口保单、客户、佣金、业绩分开每个资源对应一套增删改查。这样做的好处是后续如果要对接移动端或第三方渠道接口可以直接复用不需要重新拆Controller。2.3 MyBatisSQL在手统计不愁保险业务系统的数据操作有个特点查询条件组合多变统计报表复杂。比如“查询某段时间内、某代理人名下、某险种、续保成功的保单数”这种条件组合在CRM类系统中很常见用Hibernate的HQL能写但后期调优SQL时就麻烦了。MyBatis在这套系统中的定位是持久层框架它的核心价值是让你能精确控制SQL并且通过动态SQL解决条件组合问题。下面是一个真实的保单列表查询Mapperselect idselectPolicyPage resultTypecom.example.policy.PolicyVO SELECT p.policy_no, p.product_name, c.customer_name, p.premium, p.status, p.create_time FROM t_policy p LEFT JOIN t_customer c ON p.customer_id c.id where if testcustomerName ! null and customerName ! AND c.customer_name LIKE CONCAT(%, #{customerName}, %) /if if testproductId ! null AND p.product_id #{productId} /if if teststatus ! null and status ! AND p.status #{status} /if if teststartDate ! null AND p.create_time gt; #{startDate} /if if testendDate ! null AND p.create_time lt; #{endDate} /if /where ORDER BY p.create_time DESC /select这里的where和if是MyBatis动态SQL的经典用法。它能根据前端传来的查询条件动态拼接SQL语句避免写多个几乎相同的Mapper方法。在保险系统中查询条件的组合方式非常多用这种写法比在Java代码里拼SQL字符串安全得多也能有效防止SQL注入。不过要注意动态SQL的滥用也会带来性能问题。比如if条件太多时可能出现索引失效。我的经验是高频查询字段要建联合索引动态条件优先走索引前缀字段。下面建表章节会继续展开。3. 核心用例拆解保单流转、客户分层、核保审批、佣金计算SSM的配置再熟练最终还是要落到业务功能上。拿一个最小可运行的保险业务系统来说最重要的不是用户管理、菜单管理这些通用模块而是下面四个核心用例。它们既是业务方天天用的功能也是技术方案里最容易设计跑偏的地方。3.1 保单流转一张保单从录入到失效的生命周期保险业务中最核心的一张表是保单表。我见过一些项目把“保单状态”设计成字符串随便填“有效”“失效”“退保”“待核保”结果统计时发现同一状态有三种写法“已失效”“失效”“终止”。这个问题的根源不是数据脏而是没有用状态机来约束状态流转。项目里我建议把保单状态定义为一个整数枚举并在Java层做状态流转校验public enum PolicyStatus { DRAFT(0, 待提交), PENDING_UNDERWRITING(1, 待核保), UNDERWRITING_PASS(2, 核保通过), EFFECTIVE(3, 已生效), LAPSED(4, 已失效), SURRENDERED(5, 已退保); private final int code; private final String desc; // 定义合法流转路径 private static final MapInteger, SetInteger TRANSITIONS new HashMap(); static { TRANSITIONS.put(0, Set.of(1)); // 待提交 - 待核保 TRANSITIONS.put(1, Set.of(2, 4)); // 待核保 - 核保通过 / 已失效 TRANSITIONS.put(2, Set.of(3)); // 核保通过 - 已生效 TRANSITIONS.put(3, Set.of(4, 5)); // 已生效 - 已失效 / 已退保 } public static boolean canTransition(int from, int to) { SetInteger allowed TRANSITIONS.get(from); return allowed ! null allowed.contains(to); } }在设计评审时我当时强调了一个观点状态校验放在Java层而不是完全依赖数据库。原因有二。第一数据库层面可以用CHECK约束但MySQL的CHECK约束在部分版本下是“看起来存在实际不生效”的容易让人误以为有保护。第二Java层可以做更复杂的校验比如从“待核保”直接到“已生效”在某些产品规则下是允许的免核保产品这在状态机里可以按险种扩展。保单表的核心字段应该包括保单号、客户ID、代理人ID、产品ID、保费、保额、状态、生效日期、失效日期、创建时间、更新时间。其中保单号建议使用业务规则生成格式可以是“产品编码年月日流水号”避免使用数据库自增ID作为对外可见的保单号。3.2 客户分层普通客户、VIP客户与家庭保单保险业务系统的客户表和普通电商系统的用户表有很大区别。电商用户可能只有一个账号、一个收货地址但保险客户往往有家庭成员关系、多张保单、多个受益人。如果把客户信息简单铺在一张表里后面做家庭保单统计、客户生日提醒、VIP识别都会很困难。我当时的做法是拆成两张表客户主表t_customer和客户家庭成员表t_customer_family。主表存客户的基本信息、等级、来源渠道家庭表存家庭成员关系比如投保人、被保人、受益人以及他们之间的关联字段。客户等级这个字段我推荐用定时任务实时触发结合的方式维护而不是每次查询时临时计算。原因是查询时计算会拖慢列表页响应速度而保险客户等级变化并不频繁允许分钟级延迟。举个例子一个客户名下有5张生效保单、累计保费超过10万元等级自动从普通客户提升为VIP客户。这个逻辑可以写成一个定时任务每天凌晨扫描一次也可以在下单成功后立即触发一次更新。用SSM实现定时任务时最简单的方案是在Spring配置中启用Scheduled注解Component public class CustomerLevelTask { Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void updateCustomerLevel() { // 1. 查询所有客户累计保费和保单数 // 2. 根据规则重新计算等级 // 3. 批量更新客户主表 } }这里有个小坑定时任务跑批时若中途报错数据会处于“计算了一半”的状态。所以批量更新前要先开事务并且只在事务提交后记录日志。下面日志部分我会继续讲。3.3 核保审批事务和锁的正确使用场景核保是保险业务里最体现“专业性”的功能之一。在小型系统里核保流程通常是这样业务员录入保单 - 提交核保 - 核保员审核 - 通过或拒保。这个流程看似简单但在技术实现上有两个关键点并发控制和审批记录。并发控制指的是一张保单不能被两个核保员同时处理。如果系统不控制A核保员和B核保员同时打开同一张待核保保单A先点了“通过”B后点了“拒保”最终状态以谁为准如果没有锁就可能出现状态被覆盖的问题。解决这个问题的常用手段是乐观锁。在保单表加一个version字段更新时校验版本号UPDATE t_policy SET status 2, version version 1 WHERE policy_id #{policyId} AND version #{version}如果影响行数为0说明版本已被其他事务修改此时业务层提示“该保单已被其他核保员处理”。这个方案比SELECT FOR UPDATE的性能开销小也更契合保险业务这种“读多写少”的场景。审批记录则是另一个容易被忽略的地方。核保员做了“通过”还是“拒保”不能只在保单表改一个状态就完事。后续出现纠纷时需要知道是谁、在什么时间、基于什么理由做了这个决定。所以我建了一张t_underwriting_record表字段包括记录ID、保单ID、核保员ID、审核结果、审核意见、审核时间、IP地址。有了这张表系统才能算是真正“留痕”。3.4 佣金计算金额精度与阶梯费率佣金计算是保险业务系统中最容易出Bug的地方因为它涉及金额精度、时间区间、阶梯规则、团队层级四个因素。先说金额精度。Java里做金额计算绝对不能用double或float必须用BigDecimal并且要指定舍入模式。我在项目里见过一个经典错误double commission premium * 0.2;这样算出来的结果在保费为1234.56元时commission可能是246.91200000000003保存到数据库就变成246.91用户看到对不上账就会投诉。正确写法是BigDecimal premium new BigDecimal(1234.56); BigDecimal rate new BigDecimal(0.2); BigDecimal commission premium.multiply(rate).setScale(2, RoundingMode.HALF_UP);第二点是阶梯费率。比如某险种佣金规则是月业绩5万以下按10%5万到10万部分按15%10万以上部分按20%。这种规则不能简单用一个费率字段存必须在代码里做区间判断。public BigDecimal calcMonthlyCommission(BigDecimal monthlyPerformance) { BigDecimal result BigDecimal.ZERO; BigDecimal level1Limit new BigDecimal(50000); BigDecimal level2Limit new BigDecimal(100000); // 第一区间 BigDecimal level1Amount monthlyPerformance.min(level1Limit); result result.add(level1Amount.multiply(new BigDecimal(0.10))); if (monthlyPerformance.compareTo(level1Limit) 0) { return result.setScale(2, RoundingMode.HALF_UP); } // 第二区间 BigDecimal level2Amount monthlyPerformance.subtract(level1Limit).min(level2Limit.subtract(level1Limit)); result result.add(level2Amount.multiply(new BigDecimal(0.15))); if (monthlyPerformance.compareTo(level2Limit) 0) { return result.setScale(2, RoundingMode.HALF_UP); } // 第三区间 BigDecimal level3Amount monthlyPerformance.subtract(level2Limit); result result.add(level3Amount.multiply(new BigDecimal(0.20))); return result.setScale(2, RoundingMode.HALF_UP); }这个方法的思路是把费率区间拆成若干段逐段计算后累加。虽然代码看起来比直接premium * rate啰嗦但应对规则变化时更清晰。如果阶梯区间要从数据库配置读取只需把每个区间的上限和费率抽成一个List循环处理即可。佣金计算还有一个隐藏细节佣金发放和保单生效的时点不同。有些产品是客户签单即算佣金有些是保单生效后才算还有些是续保时再算。如果系统不区分“佣金计算时间”和“佣金发放状态”财务对账时就会出现已计算但未发放的差额。我的方案是佣金表加两个字段calculate_status未计算/已计算和pay_status未发放/已发放由两个不同的定时任务分别推进。这样财务报表查“待发放佣金”时只要按这两个状态过滤即可。4. 数据库表设计八个在保险系统里必须提前定好的细节SSM项目里Service和Controller的代码写错了容易改但数据库表一旦上线后再改代价就大了。保险业务系统尤其如此因为数据量大、字段多、报表复杂。下面八个细节是我在多个保险业务项目里反复踩坑后总结出来的建议在数据库设计阶段就定好。4.1 主键策略不用自增ID用业务主键保单表、客户表、代理人表的主键我建议不要直接用自增ID而是用业务主键。比如保单号按规则生成“P20240315001”客户号生成“C1000234”。这样做的原因是保险业务里数据经常需要在不同系统间同步、导入导出、甚至手工核对。如果用的是自增ID两边数据库自增值不一样合并时就会冲突。当然业务主键也不是完全没有问题。它的缺点是插入时需要先查询生成规则性能比自增ID略低。但在保险业务的数据量级下这个性能差异完全可忽略。业务主键带来的可读性和可追溯性收益远大于开销。4.2 金额字段统一用DECIMAL(18,2)保费、保额、佣金、手续费、退保金额所有涉及钱的字段一律用DECIMAL(18,2)并且Java侧统一用BigDecimal映射。不要因为某些金额可能超过百万就想着用更大的精度也不要用FLOAT或DOUBLE浮点数的二进制表示方式决定了它天生不适合做精确计算。4.3 时间字段的时区意识保险系统涉及保单生效日期、缴费日期、续保提醒日期、操作日志时间。如果系统只在国内使用DATETIME就够了。但如果后续要对接保险中介平台或者异地业务建议统一使用TIMESTAMP并明确存储UTC时间展示时按用户时区转换。这个细节看起来小但真遇到“客户在凌晨12点投保保单生效日期比投保日期早一天”这种问题时排查起来非常痛苦。4.4 状态字段统一为TINYINT不存中文所有状态字段统一用TINYINT或INT存储并配合一个状态枚举类。好处有两个一是省空间、查询快二是杜绝多人维护时中文状态不统一的问题。有人会说“存中文不是更直观吗”直观是直观但后期如果要把状态从“待回访”改为“回访中”需要全表UPDATE而用数字编码只需要改前端映射。4.5 必加四个通用字段每张表都必须有create_time创建时间、update_time更新时间、create_by创建人、update_by更新人。前两个用于追溯和按时间统计后两个用于责任追溯。保险业务对操作留痕要求高这四个字段是保底设计。4.6 统一逻辑删除不物理删除保险数据涉及财务和客户隐私任何业务表都禁止物理DELETE。统一使用is_deleted字段0未删除1已删除实现逻辑删除。好处是误删数据可以恢复对账时数据完整审计时能看到历史全貌。代价是所有查询语句都要记得加WHERE is_deleted 0。4.7 索引设计联合索引要照顾查询条件的前缀保单表最常见的查询组合是“客户ID 状态”或“代理人ID 创建时间”。我见过一个问题t_policy表上单独建了customer_id索引和status索引但查询时用了WHERE customer_id ? AND status ?MySQL只能用到其中一个索引导致另一个条件变成回表过滤。正确做法是建联合索引(customer_id, status)让查询能走索引覆盖。要注意联合索引的最左前缀原则即查询条件的顺序必须和联合索引字段顺序匹配。下面是一个常用索引建议表表名建议联合索引对应场景t_policy(customer_id, status)客户保单列表t_policy(agent_id, create_time)代理人业绩列表t_underwriting_record(policy_id, create_time)审批历史查询t_commission(agent_id, cal_status)佣金待发放统计4.8 预留扩展字段Json字段比一堆扩展列更合理保险产品的差异化字段特别多同一张保单表有的产品需要填行驶证信息有的需要填健康状况。如果每个产品都单独加列表结构很快会变成上百列的“大宽表”。我的建议是核心字段用标准列差异化字段用JSON类型统一存到一个extra_info字段里。MySQL 5.7以上版本对JSON字段的查询支持已经足够需要模糊查时也可以配合JSON_EXTRACT函数。这样既能保持表结构稳定又不会丢掉产品个性化信息。5. Shiro安全框架在保险系统里的实战配置——登录、授权、密码加密一个都不能少保险业务管理系统处理的是客户敏感信息和资金数据安全这块不能只靠“登录页面加个密码框”糊弄。在我参与的SSM项目中权限控制通常用Apache Shiro而不是Spring Security。原因有三Shiro的配置更轻量注解驱动的方式更贴近业务人员的表达习惯而且和SSM的整合资料多上手成本低。5.1 核心安全需求谁能看谁能改谁能批先想清楚权限模型。保险业务系统的用户角色一般有系统管理员管理用户、角色、菜单、数据字典业务员录入保单、查看自己名下的客户和保单核保员处理待核保任务查看保单详情财务人员查看佣金、保费、对账数据团队主管查看本人团队业绩数据这里有一个权限设计的经典误区认为角色多就等于权限细。实际上一个角色往往对应多个权限组合。如果角色和权限Map写死在代码里后续有新角色时就要改代码。正确做法是经典的“用户-角色-权限”三张关联表Shiro通过Realm查询用户角色和权限。5.2 Shiro核心配置三部曲Shiro在SSM项目里的配置主要分三块shiroConfig定义SecurityManager、Realm、过滤链realm查询数据库用户信息controller/service里用注解拦截权限。下面是我整理的一个简化配置流程Configuration public class ShiroConfig { Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); factoryBean.setLoginUrl(/login); factoryBean.setUnauthorizedUrl(/unauthorized); MapString, String filterChainDefinitionMap new LinkedHashMap(); filterChainDefinitionMap.put(/login, anon); filterChainDefinitionMap.put(/logout, logout); filterChainDefinitionMap.put(/static/**, anon); filterChainDefinitionMap.put(/policy/underwriting/**, roles[underwriter]); filterChainDefinitionMap.put(/commission/finance/**, roles[finance]); filterChainDefinitionMap.put(/customer/**, authc); filterChainDefinitionMap.put(/**, authc); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; } }这里注意LinkedHashMap的顺序Shiro按配置顺序从上往下匹配URL一旦匹配成功就不再继续。所以/login这种匿名路径必须放在最前面/**兜底放在最后。在Service层方法上推荐用注解更精细控制RequiresPermissions(policy:approve) public void approvePolicy(Long policyId, Long approverId) { // 核保通过逻辑 }这套组合的实际效果是页面级的操作靠URL过滤链控制方法级的敏感操作靠权限注解控制。前者拦得住看一眼的权限后者拦得住point-and-click的越权操作。5.3 密码存储BCrypt而不是MD5后台管理系统的密码存储我强烈建议不要用MD5。这不是说MD5不能做密码校验而是MD5已经被GPU批量碰撞攻破了加盐也挡不住彩虹表攻击。Shiro自带Sha256Hash支持更推荐直接使用BCrypt。如果项目里用的是Shiro可以这样处理密码校验Bean public CredentialsMatcher credentialsMatcher() { return new org.apache.shiro.authc.credential.HashedCredentialsMatcher(SHA-256); }但坦白说实践下来更省心的方式是在注册和校验时都用BCrypt工具类不依赖Shiro自带的Matcher。BCrypt的好处是每次加密会混入随机盐同一个密码两次加密结果不同数据库泄露后也无法反推原始密码。5.4 一个容易忽略的小问题会话超时保险业务员往往要同时处理多个客户会话页面开着不动是常态。如果会话超时时间设得太短比如3分钟业务员刚填完一张保单准备提交点一下发现跳回登录页体验极差。但如果设得太长比如2小时又存在安全隐患。我的建议是把Shiro的sessionManager的globalSessionTimeout设置为30分钟并在前端加一个“操作时重置超时时间”的机制也就是每次有Ajax请求或鼠标按键时刷新会话。这样既保证了长时间挂机安全又不会在用户正在操作时突然踢下线。6. 综合报表与数据导出SSM里最常见的两个坑保险系统上线后使用频率最高的往往不是录单而是报表。每天、每周、每月业务员看自己的业绩主管看团队的排名财务看保费和佣金管理层看全局数据。SSM项目里做报表最常用的方案是通过MyBatis写统计SQL查出来的数据在Java层组装前端用表格展示并提供导出Excel的功能。6.1 统计SQL的性能尽量在数据库层聚合先看一段典型的业绩统计SQLSELECT DATE_FORMAT(p.create_time, %Y-%m) AS month, a.agent_name, COUNT(p.policy_id) AS policy_count, SUM(p.premium) AS total_premium FROM t_policy p LEFT JOIN t_agent a ON p.agent_id a.agent_id WHERE p.create_time #{startDate} AND p.create_time #{endDate} AND p.status IN (2, 3) GROUP BY month, a.agent_id ORDER BY total_premium DESC这里要提醒的是GROUP BY的字段和SELECT的字段必须保持一致性。如果SELECT里用了DATE_FORMAT(p.create_time, %Y-%m) AS month那么GROUP BY里也必须写完整的DATE_FORMAT(...)不能简写成GROUP BY month否则在ONLY_FULL_GROUP_BY模式下会直接报错。这个错误在新手写统计SQL时非常常见。另外大表统计时要确保WHERE条件的时间字段能走索引。如果create_time上没有索引一个月几百万条保单数据的表统计一次可能就要几十秒。运营人员等不了最后也会投诉。6.2 导出Excel的内存溢出问题Excel导出是保险系统的高频功能。常见的做法是用Apache POI在内存中构建一个工作簿然后写入HttpServletResponse输出流。但问题是报表数据量一大POI把整个工作簿对象都放在JVM内存里很容易触发OutOfMemoryError。我当时处理这个问题的方案是分页查询 SXSSFWorkbook流式工作簿:public void exportPolicyReport(HttpServletResponse response, MapString, Object params) throws IOException { Workbook workbook new SXSSFWorkbook(100); // 保持100行在内存中其余写入磁盘 Sheet sheet workbook.createSheet(保单报表); // 分页查询 int pageSize 500; for (int page 1; ; page) { ListPolicyVO list policyService.pageQueryForReport(params, page, pageSize); if (list.isEmpty()) break; // 逐行写入Excel int rowIndex sheet.getLastRowNum() 1; for (PolicyVO vo : list) { Row row sheet.createRow(rowIndex); row.createCell(0).setCellValue(vo.getPolicyNo()); row.createCell(1).setCellValue(vo.getCustomerName()); row.createCell(2).setCellValue(vo.getPremium().doubleValue()); } } response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenamepolicy_report.xlsx); workbook.write(response.getOutputStream()); workbook.close(); }这段代码的关键在于数据不一次性加载到内存而是每500条一批写入ExcelPOI的SXSSF实现会将超出内存阈值的数据刷到临时文件。这样即使用户导出一年的数据也不会撑爆JVM。有一个细节要注意导出Excel时金额字段如果用vo.getPremium().doubleValue()导出Excel里显示的是小数如果后端精度是两位小数前端显示正常但Excel里出现0.30000000000000004这种值财务人员会直接打电话来问。所以导出时建议直接setCellValue(vo.getPremium().toPlainString())用字符串写入再让Excel按数字格式显示。7. 部署上线与日常排查从Tomcat到生产环境的必要条件很多SSM项目在开发环境运行得非常好一到服务器上就各种幺蛾子。这不是代码的问题而是部署方式和运行环境的问题。结合多次保险系统部署经验我把需要注意的关键点列出来。7.1 环境变量与配置分离保险项目通常有开发、测试、生产三套环境数据库地址、Redis地址、文件存储路径都不一样。如果把配置写在application.properties里每次部署都要改代码或手动改配置很容易漏改或改错。推荐使用Spring的Profile机制# application-dev.properties jdbc.urljdbc:mysql://192.168.1.100:3306/insurance_dev # application-prod.properties jdbc.urljdbc:mysql://10.0.0.5:3306/insurance_prod在Tomcat的catalina.sh或setenv.sh中指定激活哪个环境export JAVA_OPTS-Dspring.profiles.activeprod -Xms512m -Xmx1024m这样做的好处是同一份War包在不同环境部署时只需要改启动参数不用重新打包。保险业务上线后改配置的频率远高于改代码这个方案能减少很多不必要的发布风险。7.2 日志给自己留一条后路生产环境下最怕的不是功能Bug而是出了问题却找不到线索。所以日志一定要分级别、分模块写清楚。我在项目里规定了几条日志规范Controller入口和出口要打印请求参数和耗时Service层的关键业务操作提交核保、佣金计算、状态流转打印INFO日志SQL执行慢于1秒的语句打印WARN日志所有异常必须打印完整堆栈不能只打印e.getMessage()有一个经典的低级错误是只记录日志消息不记录上下文比如log.error(核保失败)结果排查时根本不知道是哪张保单。正确写法是log.error(核保失败, policyNo{}, approverId{}, policyNo, approverId, e);这样日志里就能直接看到保单号和操作人ID哪怕不查数据库也能先判断大概范围。7.3 上线前必做的三轮数据校验开发完成的体检我认为至少要做三轮第一轮是功能冒烟测试跑一遍保单录入、核保、佣金计算、报表导出的核心流程确保没有阻塞性Bug。第二轮是数据兼容性测试把老系统的Excel数据导入新系统查重复、查空值、查日期格式尤其是客户电话和身份证号这两类字段最容易出现格式不统一。第三轮是并发和压力测试用JMeter模拟20-50个用户同时提交核保观察是否有超时、死锁或内存异常。以上三轮跑完系统上线后虽然不能保证100%没有Bug但至少不会出现“第一天就被业务方打爆电话”的尴尬局面。8. 关于这套系统我最后想分享的一点心得写到这里如果你问我SSM框架值不值得继续投入我的答案仍然是肯定的。技术圈一直在追新但保险业务这类管理系统核心难点从来不是框架有多新而是业务规则是否梳理清楚、数据状态是否管得住、权限审计是否留得下痕迹。SSM恰好给了我们一个不臃肿、好维护、容易招人的组合把精力留给业务本身。以我个人的实操经验来说用SSM搭建保险业务系统时真正拉开项目质量差距的往往是那些不起眼的地方状态流转有没有做校验金额是不是全部用的BigDecimal逻辑删除有没有贯彻到每一张表导出大报表时是否做了流式处理。这些点单个看起来都不难但全部做到位系统的稳定性和可维护性就会高出一个量级。最后再分享一个小技巧如果你的时间允许建议在核心业务表上额外加一张“操作流水表”把保单的每次状态变更、每次佣金计算、每次审批动作都记录下来。这张表平时看起来没什么用但当业务方问“为什么这个客户佣金少了20块钱”的时候你能在几分钟内给出完整链路那种成就感比写十个新功能都实在。保险系统本质上是信任系统数据留痕多一分系统的可信度就高一分。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑