数字科技风险报告管理系统,前端时间刚带团队落地了一个类似的课题项目,从需求分析到表结构设计再到权限模型,一路踩了不少坑。今天把这套基于Spring Boot的完整设计与实现思路拆开聊聊,包括为什么选这个技术栈、核心模块怎么拆、数据库怎么建模、审批流怎么设计,以及那些写代码时容易被忽略的细节。如果你是准备做相关毕设选题或者在公司内部要做类似的管理系统,这篇文章可以直接当参考骨架用。
1. 项目选题与技术选型的底层逻辑
1.1 这个系统到底解决什么问题
数字科技这个概念这几年在金融、网络安全、数据合规领域出现频率很高,指的是利用大数据、人工智能、区块链等技术手段来辅助业务决策和风险管控。但技术手段越先进,系统本身的运行风险、数据安全风险、合规风险就越多,所以需要有专门的团队持续监控和上报风险。
风险报告管理系统,本质上就是一个面向风险管理人员、技术负责人、决策层的线上流程工具。它的核心痛点是:
- 传统风险报告靠邮件、Excel传递,版本混乱,过程不可追溯。
- 风险等级没有统一量化标准,同样的风险事件,有人报高有人报低。
- 风险处置任务下发后,责任人不明确,跟踪靠人工催。
- 报告数据分散在不同部门,无法形成有效的统计分析和趋势预警。
这个系统的目标,就是把这些线下流程搬到线上,用一套标准化的状态机来管理报告的流转,用RBAC权限模型来控制不同角色的操作范围,最后再通过统计报表让管理层能直观看到风险分布。
1.2 为什么选择Spring Boot这一套
这个问题在答辩或者项目评审时经常被问到。Spring Boot在当前Java后端开发中的地位,相当于装修时候的标准化施工流程——约定优于配置,起步快,生态全。
选择Spring Boot而非其他方案,核心考量有三个:
第一,团队技术栈匹配度高。Java在后端企业级应用中的统治地位目前还是稳的,尤其涉及金融、政企类项目,Java的技术积累、人才储备、社区方案都是最多的。
第二,Spring Security + MyBatis Plus的组合能快速搭建一个具备生产条件的基础框架。Spring Security处理认证和授权,MyBatis Plus提供CRUD和条件构造器,这个组合在中小型管理系统中非常常见,踩坑资料也丰富。
第三,与前端分离开发友好。用RESTful API配合Vue或者React做前后端分离,接口文档用Swagger自动生成,联调效率高。
技术选型并不是越新越好,而是越稳越好。这套系统如果用Node.js也能做,用Python Flask也能做,但在"不知道什么时候业务方会要求对接统一登录、对接OA审批流、对接ES索引"这类场景下,Spring Boot的适应能力是最强的。
2. 系统整体设计与功能模块拆解
2.1 核心功能模块怎么划分
这套系统我建议按业务域拆成五个核心模块加一个统计模块:
风险报告管理模块:负责报告的起草、提交、撤回、修改。报告内容包含风险标题、风险描述、影响范围、发生时间、发现方式、关联系统等字段。这个模块是整个系统的心脏,所有其他模块都是围绕它转的。
风险等级评估模块:根据风险发生概率和影响程度,自动计算风险等级。例如采用5x5风险矩阵,概率分5档,影响分5档,两者乘积落在不同区间对应低、中、高、严重四个等级。这块可以做得简单,也可以做得复杂,最简单的落地方式是前端传两个值,后端查评估规则表定等级。
报告审批流模块:审批是这类系统的核心流程。一个人提交风险报告后,可能需要技术主管初审、安全负责人复审、管理层终审。这个流程不能写死,因为不同风险等级对应的审批链路可能不同。低风险可能主管审完就行,高风险必须走完三层审批。
风险处置任务模块:报告审批通过后,会生成对应的处置任务,指派给具体的责任人和协办人,设置处置截止时间,支持处置结果的反馈和验证。这个模块本质上是一个轻量级的任务管理工具。
统计分析模块:按风险等级、发生部门、系统类型、发现时间段等维度进行统计,生成趋势图和分布图。这部分用ECharts在前端展示,后端提供聚合查询接口。
基础管理模块:用户管理、角色管理、菜单权限、数据字典、系统参数、操作日志。
2.2 数据库设计的关键决策
数据库设计上,我直接说几个关键点。
用户、角色、菜单三张基础表,关系用RBAC标准模型。用户表不要直接存角色名称字符串,而是通过中间表关联。不然将来需求从"一个用户一个角色"变成"一个用户多个角色",你的表结构就要推倒重来。
风险报告主表与扩展表分离。风险描述、处置方案这类字段内容可能非常大,建议单独建一张报告详情表,主表只存关键字段和状态。这样列表查询的时候,不用每次都把大文本字段捞出来,性能上会有明显差别。
报告状态放在主表单字段,但状态流转记录单独建表。很多人设计时只更新状态字段,不记录流转历史,后面审计复盘时完全查不到"这个报告是谁什么时候从待审核变成已通过"的。所以必须有report_log表,每次状态变更插入一条日志,包含操作人、操作时间、操作类型、备注。
文件表与业务表分离。涉及到报告附件的上传,建议文件元数据存数据库,文件二进制存本地磁盘或者对象存储。数据库只留file_id、file_name、file_size、file_url这些字段,不要用BLOB字段存文件。
2.3 核心表结构参考
这是风险报告主表的参考结构,直接贴DDL你们感受一下:
CREATE TABLE `risk_report` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `report_code` varchar(32) NOT NULL COMMENT '报告编号', `report_title` varchar(200) NOT NULL COMMENT '风险标题', `risk_type` tinyint(4) NOT NULL COMMENT '风险类型 1-技术风险 2-数据安全 3-合规风险 4-业务连续性', `risk_level` tinyint(4) DEFAULT NULL COMMENT '风险等级 1-低 2-中 3-高 4-严重', `probability_score` tinyint(4) DEFAULT NULL COMMENT '发生概率评分 1-5', `impact_score` tinyint(4) DEFAULT NULL COMMENT '影响程度评分 1-5', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态 0-草稿 1-待审核 2-审核通过 3-审核驳回 4-处置中 5-已关闭 6-已撤回', `report_user_id` bigint(20) NOT NULL COMMENT '报告提交人ID', `report_dept_id` bigint(20) DEFAULT NULL COMMENT '提交部门ID', `happen_time` datetime DEFAULT NULL COMMENT '风险发生时间', `discover_time` datetime DEFAULT NULL COMMENT '发现时间', `current_audit_node` tinyint(4) DEFAULT NULL COMMENT '当前审批节点', `close_time` datetime DEFAULT NULL COMMENT '关闭时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除', PRIMARY KEY (`id`), UNIQUE KEY `uk_report_code` (`report_code`), KEY `idx_status` (`status`), KEY `idx_report_user` (`report_user_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='风险报告主表';report_code建议做成业务流水号,格式是RISK + 日期 + 四位序号,比如RISK202606010001。流水号用Redis的自增或者数据库序列表生成,注意要加唯一索引,防止并发重复。
日志表结构参考:
CREATE TABLE `report_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `report_id` bigint(20) NOT NULL COMMENT '报告ID', `operate_type` tinyint(4) NOT NULL COMMENT '操作类型 1-提交 2-审核通过 3-审核驳回 4-撤回 5-转办 6-关闭', `operator_id` bigint(20) NOT NULL COMMENT '操作人ID', `operator_name` varchar(50) NOT NULL COMMENT '操作人姓名', `before_status` tinyint(4) DEFAULT NULL COMMENT '操作前状态', `after_status` tinyint(4) DEFAULT NULL COMMENT '操作后状态', `comment` varchar(500) DEFAULT NULL COMMENT '审批意见/备注', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', PRIMARY KEY (`id`), KEY `idx_report_id` (`report_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='风险报告流转日志表';3. 核心流程设计与状态机落地
3.1 报告流转的状态机设计
做这个系统最核心的一件事,是把风险报告的整个生命周期用状态机明确下来。不要小看这一步,很多项目做到一半发现流程乱成一锅粥,根源就是一上来就写代码,状态没有提前定义好。
我的建议是,先画出状态流转图,再写代码。一个极简版本是这样的:
草稿可以提交变成待审核;待审核可以审核通过变成已通过,也可以驳回回到草稿,驳回时一定要填原因;已通过的报告如果风险尚未解决,应该生成处置任务,变成处置中;处置完成后可以申请关闭,变成已关闭;在草稿和待审核状态下,报告的提交人可以撤回。
实现这套状态机,用Spring Boot的传统做法就够了,就是Service层写条件判断:
public void auditReport(ReportAuditRequest request) { // 1. 查询报告,校验存在性 RiskReport report = reportMapper.selectById(request.getReportId()); if (report == null) { throw new BizException("报告不存在"); } // 2. 状态机校验:只有待审核状态才能审核 if (!report.getStatus().equals(ReportStatus.PENDING_AUDIT.getCode())) { throw new BizException("当前状态不允许审核操作"); } // 3. 并发安全:乐观锁校验 // 4. 更新状态,写入流转日志 // 5. 如果通过,并且等级为高或严重,自动创建处置任务 }我的经验是,状态机校验不要散落在各个controller里,抽一个ReportStateMachine类,把所有状态流转规则集中管理。后面需求变了,比如新增一个"重新提交"的状态,只需要改这一个类,而不是满项目找。
3.2 风险等级评估规则怎么落地
风险等级评估是整个系统里最有含金量的业务逻辑,也是答辩时老师最喜欢深挖的地方。我的建议是用可配置的规则表,而不是写死在代码里。
最简单的实现如下:
public RiskLevel calculateRiskLevel(Integer probabilityScore, Integer impactScore) { RiskLevelRule rule = riskLevelRuleService.getActiveRule(); int totalScore = probabilityScore * impactScore; if (totalScore >= rule.getSevereThreshold()) { return RiskLevel.SEVERE; } else if (totalScore >= rule.getHighThreshold()) { return RiskLevel.HIGH; } else if (totalScore >= rule.getMediumThreshold()) { return RiskLevel.MEDIUM; } else { return RiskLevel.LOW; } }采用5x5风险矩阵的话,概率1-5,影响1-5,乘积最小1最大25。一般情况下建议的阈值是:1-4低风险,5-9中风险,10-15高风险,16-25严重。这个阈值不是拍脑袋定的,而是根据"概率低影响小可以接受,概率高影响大必须立即处置"的管理逻辑来的。
但更进阶的做法是把规则存到risk_level_rule表里,让系统管理员可以通过后台页面动态调整阈值。为什么这么做?因为实际业务中,不同阶段的风险容忍度不一样。比如重大保障期间,同样的评分标准可能会整体提升一个等级。代码写死的话,每次调整都要发版,这在很多公司是不可接受的。
3.3 审批流的两种实现方案
审批流的实现方式,我简单对比一下。
一种是轻量级的硬编码审批链,就是代码里按当前审核节点做分支判断。比如:
if (node == 1) { // 技术主管审核 } else if (node == 2) { // 安全负责人审核 } else if (node == 3) { // 管理层终审 }这种方案的优点是简单、直观、容易调试,适合你这种单体项目,功能期短,需求方明确。缺点是如果审批链经常变(同一个等级的发起人也可能不同),代码就要改。
另一种是使用Flowable或者Activiti这类工作流引擎,把流程定义到XML里,支持可视化建模和动态调整。优点是灵活,但引入工作流引擎的代价是学习成本陡增——你还没学会它,你的项目可能已经过了验收的时间。
我个人的建议是,除非需求方明确说审批流要经常动态调整,否则就用硬编码审批链。原因很简单:这类管理系统的审批流程虽然存在,但变化的频率远没有那么高,一个学期甚至一年都未必调一次。为一个低频变动需求引入一套工作流引擎,得不偿失。
4. 权限与安全设计的实操要点
4.1 RBAC模型的实现
权限设计上,标准做法是RBAC(基于角色的访问控制)。五张核心表:用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。
做这套权限控制的时候,有一个常见的坑:先做菜单权限,忽略按钮权限。结果用户登录后能看到菜单,但点进去后操作按钮全都显示——后端没有做接口级的权限控制,用户完全可以绕过前端直接调用API。
正确的做法是:后端每个接口都有权限标识,通过Spring Security或自定义拦截器校验。前端控制菜单和按钮的显隐只是提升用户体验,真正的安全边界必须由后端守住。
4.2 Spring Security + JWT的无状态认证实现
现在做前后端分离,Session方案已经逐渐弱化,主流是JWT。核心逻辑是:登录成功后生成一个带角色信息的Token,前端每次请求带上,后端解析校验。
jwt: secret: your-secret-key-change-in-production expire-time: 86400JWT的密钥必须放到配置文件里,不要写死在代码中。生产环境中密钥长度不能少于32字节,且要通过环境变量注入,避免提交到代码仓库被别人看到。这个细节在代码审计的时候一定会被查到。
Spring Security配置的关键点有三个:放行登录接口和Swagger文档;其他接口都要认证;@PreAuthorize控制角色权限。
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/auth/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }我的习惯是Controller的接口上不加权限注解,而是在Service层加。这样权限校验和业务逻辑在同一个事务边界内,方便测试和排查。当然了,这不是唯一答案,也有人坚持在Controller层用注解更直观,看团队习惯。
4.3 数据权限的一个实战思路
最后提一下数据权限。在这个系统里,一个普通用户应该只能看到自己提交的报告;一个部门主管应该能看到本部门所有报告;管理员才能看全部报告。
这就是典型的数据权限需求。实现方案上,MyBatis Plus即将提供了数据权限插件,可以在SQL解析层自动拼接权限条件,但配置起来比较费劲。我建议在小项目里用最简单的方式:查询接口接收一个currentUser参数,在Service层判断角色,根据角色决定是否拼接WHERE report_user_id = ?或者WHERE dept_id = ?条件。
曾经见过一个项目,为了数据权限引入了一套很重的规则引擎,搞了半个月也没完全跑通。其实你仔细想想,多数系统的数据权限场景,无非就是"本人、本部门、全部"这三种,写死成策略模式就够了。
5. 实操环节:从0到1搭建的关键步骤
5.1 项目初始化和依赖选型
工具准备:JDK 8或JDK 17都可以,Spring Boot 2.7.x或者3.x都行,我建议如果团队里没人用过Java 17的新特性,稳妥起见用Spring Boot 2.7.x + JDK 8,生态最成熟。
核心依赖清单如下,直接加进pom.xml:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency>这里特别说下为什么用MyBatis Plus而不是Spring Data JPA。JPA的托管实体和懒加载特性确实很方便,但在这种需要写复杂报表SQL、动态条件拼接、大批量更新的场景下,MyBatis Plus的灵活度明显更高。尤其它的LambdaQueryWrapper,写动态查询比XML里拼SQL舒服多了。
5.2 核心业务代码实现思路
报告提交的核心逻辑,写出来大概是这个感觉:
@Service @Transactional(rollbackFor = Exception.class) public class RiskReportServiceImpl implements RiskReportService { @Override public Long submitReport(ReportSubmitRequest request, LoginUser loginUser) { // 1. 生成报告编号 String reportCode = reportCodeGenerator.generate(); // 2. 构建报告实体 RiskReport report = new RiskReport(); report.setReportCode(reportCode); report.setReportTitle(request.getReportTitle()); report.setRiskType(request.getRiskType()); report.setProbabilityScore(request.getProbabilityScore()); report.setImpactScore(request.getImpactScore()); report.setStatus(ReportStatus.PENDING_AUDIT.getCode()); report.setReportUserId(loginUser.getUserId()); report.setReportDeptId(loginUser.getDeptId()); // 3. 计算风险等级 RiskLevel level = riskLevelCalculator.calculate( request.getProbabilityScore(), request.getImpactScore()); report.setRiskLevel(level.getCode()); // 4. 插入主表 reportMapper.insert(report); // 5. 保存详情 RiskReportDetail detail = new RiskReportDetail(); detail.setReportId(report.getId()); detail.setRiskDescription(request.getRiskDescription()); detail.setInfluenceScope(request.getInfluenceScope()); reportDetailMapper.insert(detail); // 6. 写入日志 reportLogService.record(report.getId(), OperateType.SUBMIT, null, report.getStatus(), "提交风险报告"); return report.getId(); } }注意看第3步,风险等级计算是在提交时完成的,不是在审核时完成的。这意味着提交人填完概率和影响之后,系统立刻告诉他这份报告会被划到什么等级,他会有一个预期。如果等级过高,他可以在草稿状态自行调整或者补充说明,而不是等到审批环节被驳回。
5.3 报表统计的SQL写法
统计分析模块要支持按风险等级分布、按风险类型趋势、按部门处理情况等维度查询。这里如果你的SQL能力不够扎实,用MyBatis Plus的wrapper能应付简单的统计,但复杂的还是要写XML。
举例,统计当前未关闭报告中各风险等级的数量:
<select id="countRiskLevel" resultType="map"> SELECT risk_level AS riskLevel, COUNT(*) AS cnt FROM risk_report WHERE deleted = 0 AND status IN (1, 2, 4) GROUP BY risk_level </select>这个统计口径要注意status条件,你不能把草稿也算进去。草稿是用户还没提交的半成品,代表不了实际风险情况。同理,已关闭的报告如果是正常处置完成,也不应该计入当前存量风险。
时间趋势类报表,我建议在SQL里用DATE_FORMAT配合GROUP BY,例如按月份统计新增报告数量:
<select id="countByMonth" resultType="map"> SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) AS cnt FROM risk_report WHERE deleted = 0 AND create_time BETWEEN #{startTime} AND #{endTime} GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month </select>这类统计SQL性能优化上就一条:create_time字段必须建索引,status字段区分度不高但如果查询频繁也可以加入联合索引。
6. 常见问题与排错经验
6.1 并发提交导致报告编号重复
这是一个典型的坑。多人同时提交风险报告时,如果用"查当前最大编号再加一"的方式,极大概率出现重复编号。因为两个请求同时读到同一个最大值。
解决方案有两个层面:
最简单粗暴的就是数据库唯一索引兜底:
ALTER TABLE risk_report ADD UNIQUE KEY uk_report_code (report_code);然后在代码里捕获DuplicateKeyException,重新生成编号再插入,设置重试次数上限。更优雅的做法是用Redis实现INCR原子自增,按天生成key,比如report_code:20260601,然后拼成序号。
6.2 事务注解失效的三类场景
@Transactional用着简单,但失效场景你迟早会遇到:
同类内部方法调用,比如controller调
submitReport,submitReport里调同类里的updateReportStatus,updateReportStatus上的事务注解不生效。因为Spring的事务是通过AOP代理实现的,内部调用走的是this,不是代理对象。非public方法上的事务注解不生效。这里不多解释,记住了就行。如果你在一个Service内部需要拆分事务,用
TransactionTemplate替代注解。异常被吞掉。事务回滚默认只针对RuntimeException和Error,如果catch了异常但没有抛出,事务是没法回滚的。要么不catch,要么catch后手动抛
RuntimeException,要么在注解上指定rollbackFor = Exception.class。
我在项目里要求所有Service方法统一加@Transactional(rollbackFor = Exception.class),并且禁止catch后不抛。这个规范救过很多次命。
6.3 JWT过期后前端页面不跳转
前端登录态失效后,用户操作时会收到401响应,但前端页面还停留在当前页面,不跳转到登录页。这个问题的本质是前端没有统一处理401响应。
解决思路是用axios拦截器:
service.interceptors.response.use( response => response, error => { if (error.response && error.response.status === 401) { // 清除本地token并跳转登录页 localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } )默认JWT过期时间不要设太长,24小时以内。太久意味着安全风险窗口大,太短用户频繁登录体验差。
6.4 文件上传的内存溢出
风险报告可能附带截图和附件,上传接口如果不限制大小,大文件会把应用内存打爆。
Spring Boot里配置:
spring: servlet: multipart: max-file-size: 20MB max-request-size: 100MB文件不要存数据库,存服务器本地目录或者对象存储,数据库里只存元数据。如果你存的是本地路径,注意应用部署机器的磁盘空间,定期清理过期附件。如果你的项目是用Docker部署,本地路径要挂载volume,否则容器重启文件就没了。
7. 上线部署与后续扩展建议
7.1 部署方案的参考
项目开发完之后的部署,小型项目用单机Docker是最省事的方案。
一个参考的docker-compose.yml:
version: '3.8' services: mysql: image: mysql:8.0 container_name: risk-mysql environment: MYSQL_ROOT_PASSWORD: your-password MYSQL_DATABASE: risk_management ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci app: build: . container_name: risk-app depends_on: - mysql ports: - "8080:8080" environment: SPRING_PROFILES_ACTIVE: prod这里特别提醒,很多入行没多久的同学习惯把MySQL连接信息、密码写在application.yml里一把梭提交到Git仓库。这种做法在正式项目里绝对能被安全团队打回来重改。密码和密钥要通过环境变量或配置中心注入,这个习惯要从项目第一天就养成。
7.2 后续可以扩展的方向
如果你这个项目做完了,还有余力或者想优化,可以考虑几个方向:
消息通知:审批节点变化时,通过邮件或站内信通知下一节点处理人。如果公司有钉钉或者企业微信的群机器人接口,也可以接入,提醒效果比邮件更好。
Excel导入导出:领导总是喜欢看Excel,你可以用EasyExcel实现报告的批量导入和历史报告的导出。
更细粒度的数据权限:如果部门层级复杂,就要考虑引入部门树的过滤逻辑,做一个数据权限的组件,而不是在各个Service里手写判断。
风险趋势预测:这是比较进阶的玩法,基于历史数据做简单的风险趋势分析,比如某类风险是否在上升、哪个系统的风险密度最高,用简单的统计模型就能跑出有价值的结果。
8. 最后说几句经验
整个项目做下来,最大的体会是:技术本身并不复杂,复杂的是把业务流程理清楚并且用代码正确表达。
如果你现在正准备动手做类似的系统,我建议你从数据库建表开始,而不是从写Controller开始。先把表的字段、约束、索引都定义好,把状态流转图和数据流图画清楚,你会发现后面写代码的思路就是水到渠成的事。
另外一个小技巧:实现的时候每写完一个模块,都自己用Postman或者Apifox完整跑一遍主要流程,不要等到所有模块都写完再统一调。这种系统最怕的问题是到联调阶段发现某个状态转换漏了判断条件,那时候排查成本比开发时多好几倍。
最后再说一次那个老生常谈的原则:一切以业务为主,技术永远是更准确传达业务逻辑的工具。风险报告管理系统不是一个炫技的项目,它最大的价值在于帮助团队把原本靠人肉催、靠Excel传的流程规范起来,让每一份风险报告都有迹可循、有人负责、有始有终。能把这一点做好,这个项目的意义就已经超越了代码本身。