FineReport填报报表集成原生审批功能实战指南
发布时间:2026/10/2 14:33:07来源:尧图网络
1. 项目概述为什么一张填报报表非要加上审批功能在FineReport的实际项目里我见过太多“填完就走”的报表——销售填完客户线索采购填完入库单HR填完入职信息数据直接落库连个确认弹窗都没有。这种模式在测试环境跑得飞快一上线就出问题销售填错客户电话采购把单价少输一个零HR把试用期写成三年……没人复核没人留痕出了问题只能翻数据库日志查半天发现是某个人手抖点错了提交按钮。这就是典型的“有填报、无治理”。审批功能不是给系统加一层“官僚流程”而是给数据加一道“质量门禁”。它解决的不是技术问题而是协作信任问题谁填的、谁看的、谁批的、什么时候批的、为什么批或不批——这些信息比填报内容本身还重要。尤其在财务、法务、合规强监管场景下没有审批流的填报报表本质上就是一张不可追溯、不可担责的“白条”。你可能觉得“不就是加个按钮吗”但实际落地时会立刻撞上三堵墙第一堵是逻辑墙——审批状态怎么和填报数据绑定是填完立刻触发还是允许暂存草稿第二堵是权限墙——不同角色看到的审批界面不一样销售填表时不能看到审批意见主管审批时要能看到原始填报记录和修改痕迹第三堵是集成墙——审批通过后要不要自动触发下游流程比如审批通过后自动生成合同编号、同步到OA待办、发邮件通知相关方这已经不是报表配置问题而是业务流编排问题。所以这个标题里的“增加审批功能”本质是把FineReport从“数据录入工具”升级为“轻量级业务协同平台”的关键一步。它适合三类人重点参考一是正在做OA、CRM、ERP等系统集成的实施工程师需要快速补全审批环节二是企业IT管理员被业务部门反复要求“加个审核步骤”却苦于找不到低代码方案三是报表开发新手想搞懂FineReport里“填报工作流”到底怎么搭才不翻车。接下来我会拆解真实项目里怎么一步步把审批功能嵌进填报报表不靠插件、不改源码、不写Java纯用FineReport原生能力搞定。2. 整体设计思路与方案选型为什么不用插件而选原生工作流很多人看到“审批功能”第一反应是找插件——网上确实有各种“FineReport审批插件”号称“一键安装、三步配置”。我试过三个主流插件结果都踩了坑第一个插件依赖特定版本的Tomcat升级FineReport后直接报class not found第二个插件把审批状态硬编码进数据库字段导致后续要加多级审批时得重写SQL第三个插件UI是固定模板业务部门说“审批意见框太小要能上传附件”插件作者半年没更新。最后发现最稳的路反而是“绕远路”用FineReport原生的填报属性工作流参数传递自己搭一套。为什么坚持用原生方案核心就两点可控性和可演进性。可控性是指每个环节都掌握在自己手里——审批按钮点下去触发什么动作、审批人列表从哪张表查、驳回时数据怎么回滚、超时未处理怎么提醒全部用填报事件、SQL查询、JavaScript控制出问题能立刻定位到具体哪行配置。可演进性是指未来需求变了能快速响应比如现在是一级审批明年要改成“销售填表→主管初审→财务复核→总监终审”原生方案只需在工作流里加两个节点插件方案可能得等厂商排期。具体架构分三层第一层是填报层在原有填报报表上加两个隐藏字段——approval_status审批状态值为draft/pending/approved/rejected和approval_id审批单号用于关联工作流第二层是工作流层用FineReport内置的“工作流”模块建审批流节点类型选“用户任务”分配规则用SQL查审批人比如SELECT user_id FROM t_approver WHERE dept_id ? AND role manager第三层是交互层用JavaScript监听填报按钮点击事件先校验必填项再调用FR.doWorkflow()发起工作流并把approval_id作为流程变量传进去。这个设计最大的巧思在于“状态解耦”填报数据表和审批状态表物理分离。填报表只存业务数据如客户名称、金额审批表单独建t_approval_log字段包括approval_id、form_id关联填报主键、status、approver_id、comment、create_time。这样做的好处是即使审批流异常中断填报数据不会被锁死业务人员还能继续填新单同时审计时查审批日志表比翻填报表历史版本清晰得多。提示千万别把审批状态直接写进填报主表我见过一个项目把approval_status字段加在客户主表里结果审批驳回后业务员直接在填报页改状态为approved绕过整个审批流——因为前端没做状态只读控制。状态必须由工作流引擎写入填报页只能读取显示。3. 核心细节解析与实操要点从零配置审批流的7个关键卡点3.1 审批状态字段的初始化与生命周期管理很多新手以为“加个字段就行”结果状态值乱飞。approval_status字段必须在填报初始化时就写入默认值且后续只能由工作流引擎修改。具体操作分三步第一步在填报报表的“填报属性”→“填报前事件”里写JavaScript// 如果是新增填报初始化状态为draft如果是编辑已存在记录则读取当前状态 if (this.isNewRecord()) { this.setFieldValue(approval_status, draft); } else { // 从数据库查当前状态避免前端缓存脏数据 var status FR.remoteEvaluate(SELECT approval_status FROM t_customer WHERE id this.getCellValue(id)); this.setFieldValue(approval_status, status); }第二步在填报按钮的“点击事件”里加状态校验if (this.getCellValue(approval_status) approved || this.getCellValue(approval_status) rejected) { alert(该记录已审批完成不可重复提交); return false; }第三步最关键的——在工作流的“节点完成事件”里更新状态。比如在“审批通过”节点的“完成脚本”中写UPDATE t_customer SET approval_status approved WHERE id ${form_id}这里${form_id}是工作流变量对应填报表的主键字段。注意这个SQL必须放在“节点完成”而非“节点开始”否则审批人还没点同意状态就变成approved了。注意FR.remoteEvaluate方法在高并发时可能有性能问题生产环境建议用“填报后事件”调用后台Java接口查状态但对中小项目remoteEvaluate足够稳定。3.2 审批人动态分配的SQL写法与权限隔离审批人不能写死必须按业务规则动态查。常见误区是用SELECT user_id FROM t_user WHERE dept sales这种静态SQL结果销售总监调岗后还得手动改SQL。正确做法是建一张审批关系表t_approver_rulerule_idmodule_codedept_idrole_codepriorityis_active1customer101manager112customer101director21然后在工作流节点的“分配规则”里写SELECT u.user_id FROM t_user u JOIN t_approver_rule r ON u.dept_id r.dept_id AND u.role_code r.role_code WHERE r.module_code customer AND r.is_active 1 ORDER BY r.priority LIMIT 1这样调整审批人只需改t_approver_rule表不用动报表配置。更进一步如果要支持“会签”多人同时审批把LIMIT 1去掉工作流会自动创建多个并行任务。权限隔离的关键在于“谁能看到什么”。在填报报表的“条件属性”里给审批意见字段加显示条件对填报人approval_status pending时不显示审批意见避免提前看到领导评语对审批人approval_status pending AND user_id ${current_user_id}时才显示审批按钮防止越权审批对管理员永远显示所有字段。3.3 工作流节点跳转逻辑与驳回机制设计审批流最怕“死循环”填表→审批→驳回→填表→审批→驳回……实际项目里我们加了两道保险。第一道是驳回路径控制在工作流设计器里“驳回”连线的目标节点必须设为“填报人任务”且该节点的“任务类型”选“用户任务”分配规则写SELECT user_id FROM t_user WHERE user_id ${initiator_id}${initiator_id}是流程发起人ID。这样驳回后任务精准回到原填报人不会跑到别人头上。第二道是驳回次数限制在“驳回”节点的“完成脚本”里加计数器UPDATE t_approval_log SET reject_count COALESCE(reject_count, 0) 1 WHERE approval_id ${approval_id}然后在填报页的“填报前事件”里查reject_count如果大于3次强制隐藏“提交审批”按钮提示“已驳回3次请联系IT处理”。3.4 审批意见富文本支持与附件上传实现FineReport原生工作流的审批意见框是纯文本但业务部门要求“能加粗、能换行、能贴截图”。解决方案是放弃工作流自带意见框改用填报页的富文本控件。具体操作在填报报表里加一个textarea控件类型设为“富文本编辑器”在工作流节点的“表单配置”里把这个textarea控件拖进去设置“字段名”为approval_comment在“节点完成脚本”里把富文本内容存进审批日志表INSERT INTO t_approval_log (approval_id, form_id, approver_id, comment, create_time) VALUES (${approval_id}, ${form_id}, ${current_user_id}, ${approval_comment}, NOW())附件上传同理在填报页加“文件上传”控件字段名设为approval_attachment工作流节点里同样拖入该控件。FineReport会自动把文件存到服务器/WebReport/Upload/目录并在数据库存相对路径。实操心得富文本内容存库前一定要过滤XSS攻击。我在节点完成脚本里加了JS过滤var safeComment approval_comment.replace(/script[^]*?[\s\S]*?\/script/gi, );再把safeComment插入数据库。3.5 审批状态实时刷新与页面防重复提交用户填完表点“提交审批”如果网络慢他可能连点三次——结果发起三个审批流。防重复的核心是“按钮置灰状态锁”。在填报按钮的“点击事件”里var btn this.getWidgetByName(submitBtn); btn.setEnabled(false); // 立即置灰按钮 btn.setText(提交中...); // 发起工作流 FR.doWorkflow({ workflowId: wf_customer_approval, variables: { form_id: this.getCellValue(id), approval_id: APP_ new Date().getTime() _ Math.floor(Math.random()*1000) }, success: function(data) { alert(审批已提交等待处理); btn.setEnabled(true); btn.setText(提交审批); }, error: function(err) { alert(提交失败 err.message); btn.setEnabled(true); btn.setText(提交审批); } });这样用户点第一次后按钮就变灰直到成功或失败回调才恢复。同时approval_id用时间戳随机数生成确保每次提交的流程实例唯一避免重复提交覆盖。3.6 多级审批的节点串联与状态映射一级审批简单但“销售填→主管批→财务核→总监终审”四步流怎么串关键在状态映射。我们约定主管审批通过 → 状态变pending_finance财务审核通过 → 状态变pending_director总监终审通过 → 状态变approved。在每级节点的“完成脚本”里更新状态-- 主管节点完成脚本 UPDATE t_customer SET approval_status pending_finance WHERE id ${form_id} -- 财务节点完成脚本 UPDATE t_customer SET approval_status pending_director WHERE id ${form_id}填报页的状态显示用条件格式当approval_status pending_finance时显示“财务审核中”当approval_status pending_director时显示“总监终审中”。这样业务人员一眼就知道卡在哪一环。3.7 审批超时自动提醒与升级机制没人处理审批单我们加了超时自动提醒。在FineReport的“定时任务”里建一个每天执行的SQL任务SELECT a.approval_id, a.form_id, u.email FROM t_approval_log a JOIN t_user u ON a.approver_id u.user_id WHERE a.status pending AND a.create_time DATE_SUB(NOW(), INTERVAL 2 DAY)任务执行后用FineReport的邮件组件发提醒“您有2条审批单超时未处理请及时处理”。更狠的是升级机制在同一个定时任务里加第二段SQL把超时单自动转给上级UPDATE t_approval_log SET approver_id ( SELECT superior_id FROM t_user WHERE user_id approver_id ), status pending_upgrade WHERE status pending AND create_time DATE_SUB(NOW(), INTERVAL 3 DAY)这样3天没处理单子自动升到上级领导待办池。4. 实操过程与核心环节实现从配置到上线的完整流水线4.1 环境准备与基础表结构搭建先确认FineReport版本——审批功能在V10.0及以上原生支持低于此版本需升级。我用的是V11.0.2JDK1.8Tomcat9.0。数据库用MySQL 5.7建三张核心表填报主表t_customer精简字段字段名类型说明idbigint PK主键customer_namevarchar(100)客户名称contact_phonevarchar(20)联系电话amountdecimal(10,2)合同金额approval_statusvarchar(20)审批状态draft/pending/pending_finance/.../approved/rejectedcreate_byvarchar(50)创建人create_timedatetime创建时间审批日志表t_approval_log字段名类型说明idbigint PK日志主键approval_idvarchar(50)审批单号如APP_20240520102345_678form_idbigint关联填报主表idapprover_idvarchar(50)审批人IDstatusvarchar(20)当前节点状态pending/approved/rejectedcommenttext审批意见富文本存HTMLattachment_pathvarchar(200)附件路径create_timedatetime创建时间reject_countint驳回次数审批规则表t_approver_rule字段名类型说明idbigint PK规则主键module_codevarchar(20)模块编码customer/orderdept_idint部门IDrole_codevarchar(20)角色编码manager/directorpriorityint优先级数字越小越先is_activetinyint是否启用建完表后在FineReport的“数据库配置”里添加数据连接测试连通性。特别注意t_approval_log表的comment字段类型必须是text不能是varchar否则富文本内容超长会截断。4.2 填报报表配置从空白模板到带审批控件新建一个填报报表数据集用SQLSELECT id, customer_name, contact_phone, amount, approval_status FROM t_customer WHERE id ${id} -- 参数化查询支持编辑已有记录拖入四个控件customer_name文本控件必填校验正则^[\u4e00-\u9fa5a-zA-Z0-9\u4e00-\u9fa5\-\s]{2,50}$中文英文数字横线空格2-50字contact_phone文本控件校验正则^1[3-9]\d{9}$|^0\d{2,3}-\d{7,8}$手机号或固话amount数值控件最小值0小数位2位approval_status隐藏控件类型选“标签”勾选“隐藏”用于存储状态值。关键配置在“填报属性”“填报前事件”写前面提到的状态初始化JS“填报后事件”写提交后的状态刷新逻辑比如跳转到审批列表页“填报按钮”右键“按钮属性”→“点击事件”粘贴防重复提交的JS代码“条件属性”给审批意见控件加显示条件approval_status pending AND user_id ${current_user_id}。此时预览报表新增一条记录approval_status自动为draft编辑已存在记录状态从数据库读取。这是审批功能的地基地基不牢后面全塌。4.3 工作流创建与节点配置可视化拖拽背后的逻辑进入FineReport设计器→“工作流”→“新建工作流”命名wf_customer_approval描述写“客户信息填报审批流”。拖入四个节点Start节点类型“开始事件”无配置。FillForm节点类型“用户任务”表单选刚建的填报报表分配规则写SELECT user_id FROM t_user WHERE user_id ${initiator_id}让填报人自己确认提交ApproveManager节点类型“用户任务”表单选一个纯审批界面只放审批意见富文本和通过/驳回按钮分配规则用前面的动态SQL查主管。ApproveFinance节点同上分配规则查财务角色。End节点类型“结束事件”。连线规则Start → FillForm自动FillForm → ApproveManager条件approval_status draftApproveManager → ApproveFinance条件status approvedApproveFinance → End条件status approved。重点配置“ApproveManager节点”的“表单配置”把填报报表里的富文本意见框、附件上传控件拖进来在“完成脚本”里写状态更新SQL和日志插入SQL。测试时用两个账号一个填表一个审批看流程是否自动流转。4.4 JavaScript深度定制让审批体验像原生应用原生工作流的UI比较简陋我们用JS把它“包装”得更顺手。在填报报表的“模板web属性”→“引入JS”里加自定义脚本状态文字映射函数function getStatusText(status) { var map { draft: 草稿, pending: 审批中, pending_finance: 财务审核中, pending_director: 总监终审中, approved: 已通过, rejected: 已驳回 }; return map[status] || status; }在状态字段的“控件样式”→“文本”里写getStatusText(this.getCellValue(approval_status))这样数据库存pending_finance页面显示“财务审核中”。审批按钮动态显示// 根据状态和当前用户角色决定显示哪个按钮 var status this.getCellValue(approval_status); var currentUser FR.getCurrentUserName(); var isManager currentUser.indexOf(manager) -1; if (status draft) { this.getWidgetByName(submitBtn).setVisible(true); this.getWidgetByName(approveBtn).setVisible(false); } else if (status pending isManager) { this.getWidgetByName(submitBtn).setVisible(false); this.getWidgetByName(approveBtn).setVisible(true); } else { this.getWidgetByName(submitBtn).setVisible(false); this.getWidgetByName(approveBtn).setVisible(false); }这样填报人只看到“提交审批”主管登录后同一张表自动显示“通过/驳回”按钮无需切换页面。4.5 权限体系对接与现有OA或LDAP打通很多企业已有统一身份认证FineReport需要对接。以LDAP为例在WebReport/WEB-INF/resources/ldap.xml里配置ldap server hostldap.company.com port389/ base-dndccompany,dccom/base-dn user-dn-patternuid{0},ouusers,dccompany,dccom/user-dn-pattern /ldap然后在FineReport后台→“管理系统”→“用户管理”里启用LDAP同步。关键点是审批规则表t_approver_rule里的dept_id和role_code要和LDAP的OU、group匹配。比如LDAP里销售部是ousales,dccompany,dccom那t_approver_rule的dept_id就存sales这样动态查审批人时SQL才能准确定位。4.6 上线前压力测试与边界场景验证上线前必须测三类边界高并发提交用JMeter模拟100人同时填表提交。重点观察数据库连接池是否耗尽调整WebReport/WEB-INF/resources/frlog.properties里的maxActive50approval_id是否重复时间戳随机数方案实测10万次无重复工作流实例是否堆积监控fr_workflow_instance表记录数超500条需优化SQL。异常中断恢复手动停掉Tomcat再启动检查正在审批中的单子是否自动续跑FineReport工作流有持久化机制重启后继续填报页的approval_status是否仍显示正确状态字段独立存储不受工作流影响。数据一致性验证写校验SQL-- 查所有状态为approved但审批日志里没有终审记录的单子 SELECT c.id FROM t_customer c LEFT JOIN t_approval_log l ON c.id l.form_id AND l.status approved WHERE c.approval_status approved AND l.id IS NULL结果为空才说明数据一致。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 经典问题速查表问题现象可能原因排查步骤解决方案提交审批后流程没启动工作流未发布或填报按钮JS报错1. 查浏览器F12控制台是否有JS错误2. 进入工作流管理页确认状态为“已发布”3. 检查FR.doWorkflow参数是否拼写错误修复JS语法重新发布工作流核对workflowId是否与设计器里一致审批人收不到待办分配规则SQL返回空或用户不在系统中1. 在数据库客户端执行分配SQL看是否返回user_id2. 登录FineReport后台查该user_id是否存在修改SQL确保返回有效用户在后台手动添加缺失用户驳回后填报页数据消失驳回时误删了填报主表数据1. 查t_approval_log表看驳回记录是否正常2. 查t_customer表确认主数据是否还在驳回脚本只更新状态绝不删数据加数据库触发器备份关键操作富文本内容显示乱码数据库字段编码不是utf8mb41. 执行SHOW CREATE TABLE t_approval_log2. 看comment字段的CHARACTER SET执行ALTER TABLE t_approval_log MODIFY comment TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci审批状态不实时刷新浏览器缓存或填报页未监听状态变更1. 强制刷新页面看状态是否更新2. 在填报页JS里加console.log(status:, this.getCellValue(approval_status))在工作流“节点完成脚本”末尾加FR.refreshCurrentPage()或用WebSocket推状态高级方案5.2 我踩过的三个深坑及独家解法坑一审批意见富文本里的图片不显示现象用户粘贴截图后审批页显示红叉。查源码发现FineReport把富文本里的img srcdata:image/png;base64,...当成危险内容过滤了。解法在WebReport/WEB-INF/resources/custom.js里加白名单// 允许data:image开头的src FR.editor.config.extraAllowedContent img[src,alt,title,width,height];; FR.editor.config.allowedContent true;然后重启服务。这个配置在官方文档里根本找不到是我在源码里翻fr-editor.js发现的。坑二多级审批时上一级驳回导致下一级任务卡住现象主管驳回后财务待办池里还有一条“待审核”任务。查日志发现工作流引擎把驳回当成了“流程终止”但下一级节点没收到终止信号。解法在主管节点的“驳回连线”里加一个“执行脚本”DELETE FROM fr_workflow_task WHERE instance_id IN ( SELECT instance_id FROM fr_workflow_instance WHERE business_key ${form_id} ) AND node_name ApproveFinance强制清理下一级待办任务。虽然有点暴力但比等用户投诉再手动删库强。坑三移动端审批按钮点不动现象iPhone Safari里审批按钮点击无反应。调试发现是FR.doWorkflow在iOS上需要用户手势触发而填报页的JS是自动执行的。解法把提交逻辑改成“点击按钮后3秒内必须再点一次确认”var clickCount 0; this.getWidgetByName(submitBtn).click(function(){ clickCount; if (clickCount 1) { alert(请再次点击确认提交); } else if (clickCount 2) { // 执行FR.doWorkflow... clickCount 0; } });虽然体验稍差但100%兼容所有iOS版本。5.3 性能优化实战从3秒加载到300毫秒审批报表打开慢八成是SQL问题。我优化前首屏加载要3秒优化后压到300毫秒第一步索引优化在t_customer表加联合索引ALTER TABLE t_customer ADD INDEX idx_status_create (approval_status, create_time);第二步填报数据集精简原SQL查所有字段SELECT * FROM t_customer改为只查必要字段SELECT id, customer_name, contact_phone, amount, approval_status, create_time FROM t_customer WHERE id ${id} OR (approval_status IN (draft,pending) AND create_by ${current_user_id})第三步状态缓存在填报报表的“填报前事件”里用FR.cache缓存常用状态var cacheKey approval_status_ FR.getCurrentUserName(); var statusList FR.cache.get(cacheKey); if (!statusList) { statusList FR.remoteEvaluate(SELECT DISTINCT approval_status FROM t_customer); FR.cache.put(cacheKey, statusList, 300); // 缓存5分钟 }这三步做完填报页打开速度提升10倍用户感知明显。5.4 安全加固 checklist别让审批功能成后门审批功能涉及敏感数据必须加固[ ]SQL注入防护所有FR.remoteEvaluate的SQL参数必须用?占位符禁用字符串拼接[ ]XSS防护富文本内容入库前用StringEscapeUtils.escapeHtml4()过滤出库显示时用% StringEscapeUtils.unescapeHtml4(comment) %[ ]越权访问防护在填报报表的“填报前事件”里加校验if (this.getCellValue(id) this.getCellValue(id) 0) { var ownerId FR.remoteEvaluate(SELECT create_by FROM t_customer WHERE id this.getCellValue(id)); if (ownerId ! FR.getCurrentUserName() !FR.hasRole(admin)) { alert(无权编辑他人填报的记录); window.location.href /WebReport/ReportServer?reportletapproval_list.cpt; } }[ ]文件上传防护在WebReport/WEB-INF/web.xml里限制上传类型servlet servlet-nameUploadServlet/servlet-name servlet-classcom.fr.web.core.UploadServlet/servlet-class init-param param-nameallowedExtensions/param-name param-value.jpg,.jpeg,.png,.pdf,.doc,.docx/param-value /init-param /servlet最后再强调一遍审批功能的价值不在技术多炫酷而在于让每一次数据变更都有迹可循、有人负责、有据可查。我见过最成功的案例是一家制造企业上了这个功能后客户投诉率下降47%因为每条投诉都能精准定位到是哪个环节、哪个人、在什么时间、基于什么理由做的决策。这才是报表该有的样子——不是冷冰冰的数据堆砌而是活生生的业务脉搏。
网站建设高端定制企业官网