概述通过 ViewRequestForwardSPA.jsp 查看目标流程时，系统在 RequestAuthenticationService.verifyRequest() 中进行权限校验。共有 14 条校验路径，按优先级依次检查，命中任意一条即授予查看权限。核心入口JSP入口: workflow/request/ViewRequestForwardSPA.jspSPA路由: /spa/workflow/static4form/index.html?_rdm={timestamp}#/main/workflow/req?{params}JSP路由: /workflow/request/ViewRequest.jsp?haveVerifyForward=true&{params}权限校验类: com.api.workflow.service.RequestAuthenticationService校验方法: verifyRequest()路由差异路由权限校验方式authStr参数desrequestid参数SPA (LoadParamCmd)RequestAuthenticationService.verify()有效不需要JSP (ViewRequest.jsp)Session机制 (resrequestid{N})不用于校验URL参数被忽略，仅读Session全部校验路径（按简单到复杂排列）1. 共享权限（无条件检查，0个参数）// RequestAuthenticationService if (getRequestWfShareRight(info, requestid)) { return true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315条件: 用户对目标流程有共享权限校验逻辑: WFShareAuthorization.getWorkflowShareJurisdiction(requestid, user)前置条件: 管理员或其他操作人已将流程共享给当前用户参数: 0个额外参数2. 操作者权限（无条件检查，0个参数）// RequestAuthenticationService if (getRequestUserRight(info, requestid)) { return true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315条件: 用户是目标流程的当前操作人校验逻辑: 查询 workflow_currentoperator 表前置条件: 用户在流程的当前操作人列表中参数: 0个额外参数3. 创建者权限（SPA层额外检查，0个参数）// LoadParamCmd if (creater == userid && creatertype == usertype) { canview = true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315条件: 用户是流程创建人校验逻辑: 比对 workflow_requestbase.creater 字段前置条件: 用户创建了该流程参数: 0个额外参数4. 监控权限（1个参数）// RequestAuthenticationService if (ismonitor == 1 && getRequestMonitorRight(info, requestid)) { return true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315&ismonitor=1条件: 用户是目标流程的监控人校验逻辑: WFUrgerManager.getMonitorViewRight(requestid, userid)前置条件: 流程配置了监控人，且当前用户在监控人列表中参数: 1个（ismonitor=1）5. 督办权限（1个参数）// RequestAuthenticationService if (urger == 1 && getRequestUrgerRight(info, requestid)) { return true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315&urger=1条件: 用户是目标流程的督办人校验逻辑: SuperviseManagerBiz.hasWorkflowViewRight(user, requestid)前置条件: 流程配置了督办人，且当前用户在督办人列表中参数: 1个（urger=1）6. 干预权限（1个参数）// RequestAuthenticationService if ("1".equals(isintervenor) && getRequestInterventorRight(info, requestid)) { return true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315&isintervenor=1条件: 用户是目标流程的干预人校验逻辑: SysWFLMonitor.getWFInterventorRightBymonitor(userid, requestid)前置条件: 流程配置了干预人，且当前用户在干预人列表中参数: 1个（isintervenor=1）7. 相关交流权限（1个参数）// RequestAuthenticationService if (fromCommunication > 0 && getRequestCommunicationRight(info, requestid)) { return true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315&fromCommunication=12345条件: 流程有相关交流记录校验逻辑: WorkflowCommunicationBiz.hasWfResourceRight(requestId, fromCommunication)前置条件: 流程存在相关交流，且交流ID匹配参数: 1个（fromCommunication={交流ID}）8. 主子流程权限（1个参数）// RequestAuthenticationService if ((isrequest.equals("2") || isrequest.equals("3") || isrequest.equals("4")) && getRequestSubMainRight(info, requestid)) { return true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315&isrequest=2条件: 目标流程是当前流程的主流程/子流程/平行流程校验逻辑: 查询 workflow_subwfrequest 表isrequest值: 2=主流程, 3=子流程, 4=平行流程前置条件: 流程之间存在主子关系参数: 1个（isrequest=2/3/4）9. 建模关联授权（4个参数）// RequestAuthenticationService if (formmodeflag.equals("formmode_authorize") && getModeRight(info, requestid)) { return true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315&formmode_authorize=formmode_authorize&authorizemodeId=5501&authorizefieldid=403885&authorizeformmodebillId=202514条件: 建模数据字段值包含目标流程requestid校验逻辑: ModeRightInfo.isFormModeAuthorize()校验步骤:检查用户对建模模块是否有查看权限查询 ModeFieldAuthorize 表确认字段配置了关联授权查询建模数据表中该字段的值是否包含目标requestid前置条件: 建模模块已配置关联授权，且数据字段值正确参数: 4个formmode_authorize=formmode_authorize（标识位）authorizemodeId={建模模块ID}authorizefieldid={关联授权字段ID}authorizeformmodebillId={建模数据ID}10. 相关流程权限（2-3个参数）// RequestAuthenticationService if ((isrequest.equals("1") || "1".equals(isfromTd)) && getrelateRequestRight(info, requestid)) { return true; }URL（方式A）: ViewRequestForwardSPA.jsp?requestid=1908315&isrequest=1&desrequestid=1908325URL（方式B）: ViewRequestForwardSPA.jsp?requestid=1908315&isrequest=1&desrequestid=1908325&fieldid=373385条件: A/B流程有共同操作人 + 表单字段值匹配校验逻辑: getrelateRequestRight()校验步骤:防篡改校验：A/B流程操作人取交集，必须有共同操作人字段值校验：查询A流程表单字段值是否包含B流程requestid或签字意见校验：查询A流程 workflow_requestlog.signworkflowids 是否包含B流程requestid前置条件: 两个流程有共同操作人，且表单数据关联正确参数: 2-3个isrequest=1（标识位）desrequestid={源流程requestid} 或 authStr + authSignatureStrfieldid={表单字段ID}（可选，指定具体字段）11. 计划会议权限（2个参数）// RequestAuthenticationService if ((("workplan".equals(fromModul) || "meeting".equals(fromModul)) && modulResourceId > 0) && getPlanMeetingRight(info, requestid)) { return true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315&fromModul=workplan&modulResourceId=12345条件: 流程关联了日程/会议，且用户有共享权限校验逻辑: WorkPlanShareUtil.chkWFInWP() 或 MeetingShareUtil.chkWFInMT()前置条件: 流程被关联到某个日程或会议，且用户对该日程/会议有共享权限参数: 2个fromModul=workplan 或 fromModul=meetingmodulResourceId={日程/会议ID}12. 流程分享权限（3个参数）// RequestAuthenticationService if (isfromchatshare == 1 && getRequestShareRight(info, requestid)) { return true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315&isfromchatshare=1&sharer=2094&sharegroupid=xxx条件: 通过IM分享的流程链接校验逻辑: ChatResourceShareManager.authority()前置条件: 流程通过IM分享给当前用户参数: 3个isfromchatshare=1sharer={分享人ID}sharegroupid={分享组ID}13. 协作权限（2个参数）// RequestAuthenticationService if (iscowork == 1 && getRequestCoworkRight(info, requestid)) { return true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315&iscowork=1&coworkid=12345条件: 流程关联了协作区校验逻辑: CoworkCommonUtils.getWfRightByCowork()前置条件: 流程关联到协作区，且用户在协作区成员中参数: 2个iscowork=1coworkid={协作区ID}14. 报表权限（2个参数）// RequestAuthenticationService if (("1".equals(isfromreport) || "2".equals(isfromreport) || "1".equals(isfromflowreport)) && getReportRequestRight(info, requestid)) { return true; }URL: ViewRequestForwardSPA.jsp?requestid=1908315&isfromreport=1&reportid=xxx条件: 从报表入口查看，且有报表权限校验逻辑: ReportAuthorization.checkReportPrivilegesByRequest()前置条件: 用户有对应报表的查看权限参数: 2个isfromreport=1（或2、isfromflowreport=1）reportid={报表ID}最短路径总结排名方式URL参数个数前置条件1共享权限0用户已被共享该流程2操作者权限0用户是流程操作人3创建者权限0用户是流程创建人4监控/督办/干预1用户已被配置为监控/督办/干预人5相关交流1流程有相关交流记录6主子流程1流程之间存在主子关系7建模关联授权4建模数据字段值包含目标requestid8相关流程2-3A/B流程有共同操作人 + 表单字段值匹配E9签名令牌机制（authStr / authSignatureStr）原理E9使用签名令牌替代明文 desrequestid，防止URL参数被篡改。authStr: Base64编码的权限属性字符串，包含 viewChain（查看链）、mainid（入口流程ID）等authSignatureStr: 对authStr的签名，基于用户ID和User-Agent生成工作流程用户从流程A点击链接跳转到流程B系统生成 authStr（包含 viewChain=A的requestid）和签名跳转到B时，RequestAuthenticationService.verify() 验证签名验证通过后，检查用户对A的权限，再检查A→B的关联关系全部通过则授予B的查看权限代码位置签名生成: RequestAuthenticationService签名验证: RequestAuthenticationService查看链机制: RequestAuthenticationService底层SQL查询详情Path 1: 共享权限 → getRequestWfShareRight()调用链: getRequestWfShareRight() → WFShareAuthorization.getWorkflowShareJurisdiction()SQL 1 — 查询共享范围记录:SELECT permissiontype, departmentid, deptlevel, deptlevelMax, roleid, rolelevel, roleseclevel, roleseclevelMax, seclevel, seclevelMax, userid, subcompanyid, sublevel, sublevelMax, jobid, joblevel, jobobj, jobobjid FROM Workflow_SharedScope WHERE requestid = '{requestid}'SQL 2 — 校验流程是否开启共享:SELECT wb.isshared FROM workflow_base wb, workflow_requestbase wr WHERE wb.id = wr.workflowid AND wr.requestid = {requestid}判定条件: 存在任一 Workflow_SharedScope 记录匹配当前用户属性（部门/角色/安全级别/分部/岗位/指定人员），且 workflow_base.isshared = '1'。涉及表: Workflow_SharedScope, workflow_base, workflow_requestbasePath 2: 操作者权限 → getRequestUserRight()调用链: getRequestUserRight() → ServiceUtil.calculateCurrentNodeSql()SQL:SELECT isremark, isreminded, preisremark, id, groupdetailid, nodeid, takisremark, takid, workflowid, handleforwardid, agenttype, agentorbyagentid, (CASE WHEN isremark=9 THEN 0.99 WHEN isremark=8 THEN 1.2 WHEN isremark=11 THEN 0.98 WHEN (isremark=1 AND takisremark=2) THEN 0.9 WHEN (preisremark=1 AND isremark=2) THEN 1.9 WHEN (isremark=0 AND takisremark=-2 AND isInMultiTak=1) THEN 2 ELSE isremark END) AS orderisremark FROM workflow_currentoperator WHERE requestid = {requestid} AND userid = {userid} AND usertype = {usertype} ORDER BY orderisremark, id参数: requestid, userid, usertype（0=内部, 1=外部, 2=其他）判定条件: 遍历结果集，若任一行 isremark ∈ {0,1,5,7,8,9,11}（其中 isremark=0 需 nodetype≠3），则 canview=true。涉及表: workflow_currentoperatorPath 3: 创建者权限 → LoadParamCmdSQL（存储过程）:{call workflow_Requestbase_SByID({requestid})}等价于:SELECT * FROM workflow_requestbase WHERE requestid = {requestid}判定条件: creater == userid AND creatertype == usertype涉及表: workflow_requestbasePath 4: 监控权限 → getRequestMonitorRight()调用链: getRequestMonitorRight() → WFUrgerManager.getMonitorViewRight() → Monitor.getMonitorInfo()SQL 1 — 获取请求创建者和流程ID:SELECT creater, workflowid FROM workflow_requestbase WHERE requestid = {requestid}SQL 2 — 检查是否管理员:SELECT 1 FROM hrmresourcemanager WHERE id = {userid}SQL 3 — 获取创建者虚拟组织:SELECT subcompanyid1, departmentid FROM HrmResourceVirtualView WHERE id = {createrId}SQL 4 — 主查询：查找监控配置（SQL Server版本）:SELECT * FROM workflow_monitor_info a WHERE EXISTS ( SELECT 1 FROM ( SELECT 1 jktype, id FROM hrmresource WHERE id = {userid} UNION SELECT 2 jktype, id FROM hrmroles WHERE id IN ({roleids}) UNION SELECT 3 jktype, id FROM hrmresourcemanager WHERE id = {userid} ) t WHERE a.jktype = t.jktype AND ',' + a.jkvalue + ',' LIKE '%,' + CAST(t.id AS VARCHAR) + ',%' ) AND EXISTS ( SELECT 1 FROM workflow_monitor_detail b WHERE a.id = b.infoid AND workflowid IN ({allVersionWfids}) )Oracle: INSTR(','||a.jkvalue||',' , ','||t.id||',') > 0MySQL: CONCAT(',',a.jkvalue,',') LIKE CONCAT('%,',t.id,',%')SQL 5 — 获取监控权限明细:SELECT * FROM workflow_monitor_detail WHERE infoid IN ({infoids}) AND workflowid IN ({allVersionWfids})参数: userid, roleids（用户角色ID列表）, allVersionWfids（流程所有版本ID）判定条件: 用户匹配监控配置（jktype: 1=人员, 2=角色, 3=管理员），fwtype 范围校验通过（1=总部, 2=同分部, 5=同部门, 10=指定人员等），且 workflow_monitor_detail.isview = '1'。涉及表: workflow_requestbase, hrmresourcemanager, HrmResourceVirtualView, hrmresource, hrmroles, workflow_monitor_info, workflow_monitor_detailPath 5: 督办权限 → getRequestUrgerRight()调用链: getRequestUrgerRight() → SuperviseManagerBiz.hasWorkflowViewRight()SQL:SELECT 1 FROM WORKFLOW_SUPERVISEOPERATOR WHERE userid = {userid} AND requestid = {requestid}判定条件: 查询返回至少一行。涉及表: WORKFLOW_SUPERVISEOPERATORPath 6: 干预权限 → getRequestInterventorRight()调用链: getRequestInterventorRight() → SysWFLMonitor.getWFInterventorRightBymonitor() → Monitor.getMonitorInfo()SQL 与 Path 4 基本相同，额外查询:SQL — 获取干预权限:SELECT * FROM workflow_monitor_detail WHERE infoid IN ({infoids}) AND workflowid IN ({allVersionWfids})判定条件: workflow_monitor_detail.isintervenor = '1'（与 Path 4 的 isview 不同）涉及表: 同 Path 4，额外关注 workflow_monitor_detail.isintervenor 字段Path 7: 相关交流权限 → getRequestCommunicationRight()调用链: getRequestCommunicationRight() → WorkflowCommunicationBiz.hasWfResourceRight()SQL 1 — 查询交流内容中关联的流程:SELECT relatedwf FROM workflow_communicationcontent WHERE communicationid = {communicationId}SQL 2 — 查询交流回复中关联的流程:SELECT relatedwf FROM workflow_communicationreply WHERE contentid IN ( SELECT id FROM workflow_communicationcontent WHERE communicationid = {communicationId} )判定条件: 收集所有 relatedwf 值，判断 {requestid} 是否在其中。涉及表: workflow_communicationcontent, workflow_communicationreplyPath 8: 主子流程权限 → getRequestSubMainRight()场景一: isrequest=2（验证主流程）SQL 1:SELECT sub.mainrequestid FROM workflow_subwfrequest sub LEFT OUTER JOIN workflow_requestbase req ON req.requestid = sub.mainrequestid WHERE sub.subrequestid = '{requestid}'SQL 2（回退）:SELECT mainrequestid FROM workflow_requestbase WHERE requestid = '{requestid}'场景二: isrequest=3（验证子流程）SQL 3:SELECT sub.mainrequestid requestid FROM workflow_subwfrequest sub LEFT OUTER JOIN workflow_requestbase req ON req.requestid = sub.subrequestid WHERE sub.subrequestid = '{mainRequestid}'场景三: isrequest=4（验证平行流程）SQL 4:SELECT sub.subwfid, sub.isSame, sub.mainrequestid, req.requestname FROM workflow_subwfrequest sub LEFT JOIN workflow_requestbase req ON req.requestid = sub.mainrequestid WHERE sub.subrequestid = {requestid}SQL 5:SELECT sub.subrequestid requestid, req.requestname FROM workflow_subwfrequest sub LEFT JOIN workflow_requestbase req ON req.requestid = sub.subrequestid WHERE sub.mainrequestid = {mainrequestid} AND sub.subwfid = {subWfSetId} AND sub.subrequestid <> {requestid}判定条件: 查询结果中 requestid 与目标 requestid 匹配。涉及表: workflow_subwfrequest, workflow_requestbasePath 9: 建模关联授权 → getModeRight()调用链: getModeRight() → ModeRightInfo.isFormModeAuthorize()SQL 1 — 查询建模权限规则:SELECT righttype, orgrelation, sharetype, relatedid, rolelevel, showlevel, joblevel, jobleveltext, hrmCompanyVirtualType, layoutid, layoutid1, layoutorder FROM moderightinfo WHERE modeid = {modeId} AND ((sharetype IN (1,2,3,5,6) AND (conditionsql IS NULL OR conditionsql = '')) OR (sharetype = '4' AND isrolelimited <> '1' AND (conditionsql IS NULL OR conditionsql = ''))) AND righttype IN (1,2,3) AND (showlevel <= {seclevel} AND {seclevel} <= ISNULL(showlevel2, 9999))SQL 2 — 查询共享明细表（动态表名）:SELECT sourceid, MAX(sharelevel) sharelevel FROM modeDataShare_{modeId} WHERE sourceid = {formmodebillId} AND ( (type=1 AND content={userid}) OR (type=2 AND content={subcompanyid} AND seclevel<={seclevel} AND {seclevel}<=ISNULL(showlevel2,9999)) OR (type=3 AND content={departmentid} AND seclevel<={seclevel} AND {seclevel}<=ISNULL(showlevel2,9999)) OR (type=4 AND content IN ({roleIds}) AND seclevel<={seclevel}) OR (type=5 AND content=1 AND seclevel<={seclevel}) OR (type=6 AND content={jobtitle} AND seclevel<={seclevel}) OR (type=7 AND content={userid}) ) GROUP BY sourceidSQL 3 — 查询字段授权配置:SELECT a.*, b.fieldname, b.fielddbtype, b.fieldhtmltype, b.type, b.detailtable FROM ModeFieldAuthorize a, workflow_billfield b WHERE a.fieldid = b.id AND b.billid = {formId} AND a.modeid = {modeId} AND a.formid = {formId} AND a.fieldid = {fieldid}SQL 4 — 读取建模数据字段值（主表）:SELECT {fieldname} FROM {maintablename} WHERE id = {formmodebillId}SQL 5 — 读取建模数据字段值（明细表）:SELECT {fieldname} FROM {detailtable} WHERE mainid = {formmodebillId}判定条件:用户对建模模块有查看权限（moderightinfo 或 modeDataShare_{modeId} 匹配）ModeFieldAuthorize 表中存在该字段的关联授权配置建模数据字段值中包含 {requestid}（格式: fieldValue.indexOf("," + requestid + ",") > -1）涉及表: moderightinfo, modeDataShare_{modeId}（动态）, ModeFieldAuthorize, workflow_billfield, {maintablename}（动态）, {detailtable}（动态）Path 10: 相关流程权限 → getrelateRequestRight()SQL 1 — 获取主流程 workflowid:SELECT workflowid FROM workflow_requestbase WHERE requestid = {requestid}SQL 2 — 防篡改校验（操作人交集）:SELECT userid, usertype FROM workflow_currentoperator WHERE requestid = {mainRequestid} SELECT userid, usertype FROM workflow_currentoperator WHERE requestid = {requestid} -- Java层取交集 retainAll()，交集为空则拒绝SQL 3 — 签字意见关联校验（isfromresource=1 或 fieldid=0 时）:-- Oracle/PostgreSQL: SELECT 1 FROM workflow_requestlog WHERE requestid = {mainRequestid} AND ','||signworkflowids||',' LIKE '%,{requestid},%' -- MySQL: SELECT 1 FROM workflow_requestlog WHERE requestid = {mainRequestid} AND CONCAT(',',signworkflowids,',') LIKE '%,{requestid},%' -- SQL Server: SELECT 1 FROM workflow_requestlog WHERE requestid = {mainRequestid} AND ','+signworkflowids+',' LIKE '%,{requestid},%'SQL 4 — 获取字段定义（isbill=1）:SELECT billid, fieldname, viewtype, detailtable FROM workflow_billfield WHERE id = {fieldid} SELECT tablename FROM workflow_bill WHERE id = {billid}SQL 5 — 获取字段值:-- 主表 (viewtype=0): SELECT {fieldname} FROM {tablename} WHERE requestid = {mainRequestid} -- 明细表 (viewtype>0): SELECT {fieldname} FROM {detailtable} WHERE mainid IN (SELECT id FROM {tablename} WHERE requestid = {mainRequestid})判定条件:A/B流程操作人有交集（防篡改）签字意见 signworkflowids 包含目标 requestid，或表单字段值包含目标 requestid涉及表: workflow_requestbase, workflow_currentoperator, workflow_requestlog, workflow_billfield, workflow_bill, {tablename}（动态）, {detailtable}（动态）Path 11: 计划会议权限 → getPlanMeetingRight()子路径A: WorkPlanSQL 1 — 检查日程是否关联流程:SELECT * FROM workplan WHERE id = {workId} AND (requestid = '{requestId}' OR requestid LIKE '{requestId},%' OR requestid LIKE '%,{requestId},%' OR requestid LIKE '%,{requestId}')SQL 2 — 检查日程交流是否关联流程:SELECT * FROM Exchange_Info WHERE type_n = 'WP' AND sortid = {workId} AND (requestids LIKE '%,{requestId},%' OR relatedwf LIKE '%,{requestId},%')SQL 3 — 获取日程共享级别:SELECT workId, MAX(shareLevel) AS shareLevel FROM WorkPlanShareDetail WHERE workid = {workplanId} AND ( (objId={userId} AND shareType='1') OR (objId=0 AND shareType='5' AND securityLevel<={seclevel} AND securityLevelMax>={seclevel}) OR (objId IN ({compid}) AND shareType='2' AND securityLevel<={seclevel}) OR (objId IN ({deptid}) AND shareType='3' AND securityLevel<={seclevel}) OR (objId={jobid} AND shareType='8') OR (objId={roleid} AND shareType='4' AND securityLevel<={seclevel} AND {roleLevel}>=rolelevel) ) GROUP BY workId子路径B: MeetingSQL 4 — 检查会议是否关联流程:SELECT * FROM meeting WHERE id = {meetingId} AND ({fieldname} LIKE '%,{requestId},%' OR requestid = '{requestId}')SQL 5 — 检查会议共享:SELECT * FROM Meeting_ShareDetail WHERE meetingid = {meetingid} AND userid IN ({allUser}) AND sharelevel IN (1,2,4,5)SQL 6 — 检查会议成员:SELECT * FROM Meeting_Member2 WHERE meetingid = {meetingid} AND (membermanager IN ({allUser}) OR ','||othermember||',' LIKE '%,{userId},%')判定条件: 流程关联了日程/会议，且 getShareLevel > -1。涉及表: workplan, Exchange_Info, WorkPlanShareDetail, meeting, Meeting_ShareDetail, Meeting_Member2, Meeting_Topic, Meeting_DecisionPath 12: 流程分享权限 → getRequestShareRight()调用链: getRequestShareRight() → ChatResourceShareManager.authority()SQL 1 — 验证分享人为流程操作者:SELECT userid, usertype FROM workflow_currentoperator WHERE requestid = {resourceid} AND userid = {sharer} AND usertype = {sharertype}SQL 2 — 检查当前用户是否在分享范围内:SELECT 1 FROM mobile_ChatResourceShareScope a INNER JOIN mobile_chatresourceshare b ON a.shareid = b.id WHERE b.resourcetype = 0 AND b.resourceid = {requestid} AND b.sharer = {sharer} AND a.resoueceid = {currentUserid} AND a.resouecetype = 0判定条件: 分享人是流程操作者，且当前用户在分享范围内。涉及表: workflow_currentoperator, mobile_chatresourceshare, mobile_ChatResourceShareScopePath 13: 协作权限 → getRequestCoworkRight()调用链: getRequestCoworkRight() → CoworkCommonUtils.getWfRightByCowork()SQL 1 — 检查协作审批状态:SELECT approvalatatus FROM cowork_items WHERE id = {coworkId}SQL 2 — 获取协作参与者:SELECT * FROM coworkshare WHERE sourceid = {coworkId} ORDER BY srcfrom DESC, type ASCSQL 3 — 获取退出协作者:SELECT userid FROM cowork_quiter WHERE itemid = {coworkId}SQL 4 — 检查协作主项关联流程（Oracle版本）:SELECT relatedwf FROM cowork_items WHERE id = {coworkId} AND relatedwf IS NOT NULL AND ','||relatedwf||',' LIKE '%,{requestid},%'SQL 5 — 检查协作讨论关联流程:SELECT relatedwf FROM cowork_discuss WHERE coworkid = {coworkId} AND (isdel <> 1 OR isdel IS NULL) AND ','||relatedwf||',' LIKE '%,{requestid},%'判定条件: 用户是协作参与者/管理者，且协作主项或讨论中关联了目标流程。涉及表: cowork_items, coworkshare, cotype_sharemanager, cowork_deftypeshare, cowork_quiter, cowork_discussPath 14: 报表权限 → getReportRequestRight()子路径A: 自定义报表 (isfromreport=1)SQL 1 — 验证报表共享权限:SELECT shareType, userid, departmentid, subcompanyid, roleid, rolelevel, allowlook, sharelevel, mutidepartmentid, seclevel, seclevel2 FROM WorkflowReportShare WHERE reportid = {reportid}SQL 2 — 获取报表关联的流程:SELECT reportwfid FROM Workflow_Report WHERE id = {reportid}SQL 3 — 获取当前请求的流程ID:SELECT workflowid FROM workflow_requestbase WHERE requestid = '{requestid}'判定条件: 用户有报表查看权限（allowlook='1'），且当前请求的 workflowid 在报表的 reportwfid 列表中。子路径B: 新版报表 (isfromreport=2)SQL 4 — 获取表单信息:SELECT formid, isbill FROM workflow_base a LEFT JOIN workflow_requestbase b ON a.id = b.workflowid WHERE b.requestid = {requestid}SQL 5 — 获取报表数据权限配置:SELECT type, objid, allowsub, minlevel, maxlevel, rolelevel, joblevel, joblevelobjid, competencelevel, competenceobjid, dimension, dimensionval, allowlook FROM workflow_report_competence WHERE formid = {formid} AND isbill = {isbill} AND allowlook = '1'判定条件: 按 type 匹配用户（1=人员, 2=部门, 3=分部, 4=角色, 5=所有人, 6=岗位），安全级别在范围内。涉及表: WorkflowReportShare, Workflow_Report, workflow_requestbase, workflow_base, workflow_report_competence, HrmresourceVirtual涉及的核心数据库表汇总表名用途涉及路径workflow_requestbase流程请求基础表1,3,4,6,8,10,14workflow_currentoperator流程当前操作者表2,10,12workflow_base流程定义表1,14Workflow_SharedScope流程共享范围表1workflow_monitor_info流程监控配置表4,6workflow_monitor_detail流程监控权限明细表4,6WORKFLOW_SUPERVISEOPERATOR督办操作者表5workflow_subwfrequest主子流程关系表8workflow_requestlog流程操作日志表10workflow_billfield表单字段定义表9,10workflow_bill表单定义表10moderightinfo建模权限规则表9modeDataShare_{modeId}建模数据共享表（动态）9ModeFieldAuthorize字段授权配置表9workflow_communicationcontent流程交流内容表7workflow_communicationreply流程交流回复表7workplan日程表11WorkPlanShareDetail日程共享明细表11meeting会议表11Meeting_ShareDetail会议共享明细表11mobile_chatresourceshareIM分享记录表12mobile_ChatResourceShareScopeIM分享范围表12cowork_items协作主项表13coworkshare协作共享表13cowork_discuss协作讨论表13WorkflowReportShare报表共享权限表14Workflow_Report报表定义表14workflow_report_competence报表数据权限表14hrmresource人力资源表4,6hrmroles角色表4,6hrmresourcemanager系统管理员表4,6HrmResourceVirtualView虚拟组织视图4,6注意事项SPA vs JSP路由差异: authStr 仅在SPA路由中用于权限校验，JSP路由使用Session机制防篡改校验: 相关流程权限会校验A/B流程是否有共同操作人，无交集则拒绝字段值校验: 建模关联授权和相关流程权限都会校验表单字段值是否包含目标requestid无条件检查: 共享权限和操作者权限是无条件检查的，不需要任何URL参数动态表名: 建模共享表 modeDataShare_{modeId} 和业务表 {tablename} 是动态生成的数据库差异: 部分SQL因数据库类型（Oracle/MySQL/SQL Server/PostgreSQL）而有不同写法，主要是字符串拼接方式的差异
一、事件背景近期在测试 OA 创建流程接口（/api/workflow/paService/doCreateRequest）时，遇到一个比较隐蔽的问题：业务侧传入的附件内容采用 base64 方式提交，接口返回 PARAM_ERROR，错误信息指向“生成附件失败”。表面上看，这类问题通常会先怀疑以下几种方向：图片文件本身损坏base64 转换过程出错请求体过长导致截断OA 附件服务解析失败但这次问题的特殊之处在于：同一张图片在本地可以正常解码base64 长度、补位、MD5 都正常错误并不是稳定出现在所有图片上，而是只出现在部分附件内容上这意味着问题不太像“通用性格式错误”，更像“特定内容触发了某种中间处理逻辑”。二、问题现象业务调用 OA 创建流程接口时，附件字段通过 base64: 前缀方式上传，接口返回类似错误：{ "code": "PARAM_ERROR", "errMsg": { "field_xxx": "生成附件失败,参数为：fieldid:xxxx,annexId:-3,fileValue:[...]" } }从附件服务语义上看，annexId:-3 的含义基本可以理解为：附件内容在进入保存逻辑后，Base64 解码失败。但关键问题是，业务侧原始 base64 在本地验证完全正常。三、第一次排查：先排除“文件坏了”第一步做的是最基础的校验：提取原始请求中的 base64校验长度是否为 4 的倍数检查是否只包含合法 Base64 字符本地执行解码对解码后的文件做 MD5 校验结果很明确：原始 base64 串合法本地可以成功解码解码后的图片与原始附件 MD5 一致也就是说，问题不在业务系统生成附件这一步。四、第二次排查：不是所有 Base64 都失败为了避免被“单个样本”误导，我们做了几组对照实验。1. 安全样例传一个很短、内容简单的 base64，结果成功。2. 含特定片段的样例构造一些很短但包含特定文本片段的 base64，例如：4or+4and+4union+4select+结果发现，这类请求会稳定失败。更关键的是，OA 返回报文中回显出来的附件内容，已经不是原始值，而是被改写成了全角字符，例如：4or+ 变成 4ｏｒ+4and+ 变成 4ａｎｄ+一旦 base64 内部出现全角字符，后续解码必然失败。这一步已经把问题从“附件坏了”收缩到“请求进入 OA 后被改写了”。五、第三次排查：到底是什么条件会触发改写如果只是“出现 or 就失败”，那很多正常图片都应该大量失败，但事实并不是这样。于是我们继续做边界实验，只改变命中关键词左右两边的字符。实验样例如下：会失败：4or+会成功：Aor+会成功：4orA会成功：AorA同样地：会失败：4and+会成功：AandA这说明触发条件不是“只要包含 or / and 就拦截”，而是更接近：当某些敏感关键词左右两边都不是字母时，会被安全规则命中。这类模式与常见 SQL 注入拦截规则高度一致。六、第四次排查：回到真实失败图片做验证为了避免“小样本实验不代表真实图片”的质疑，我们又对真实失败图片做了反向验证。结论如下：原始图片 Base64 上传失败回显内容中，只有其中一处 or 被改写为 ｏｒ这处命中的上下文正好是 4or+然后我们只做一个极小改动，不改整张图片，只改这一处附近的边界字符：原始：4or+ -> 请求失败改成：Aor+ -> 请求成功改成：4orA -> 请求成功这一步的意义非常大，因为它证明：不是图片太大导致失败不是图片整体内容有问题不是 Base64 长度问题不是附件服务偶发异常而是图片 Base64 中某个刚好命中安全规则的片段，触发了请求内容改写。七、最终结论这次事件的根因可以明确归纳为：OA 在接收 application/x-www-form-urlencoded 请求参数时，安全过滤链会对命中的敏感关键词进行全角替换；当附件 base64 恰好包含类似 or、and、union、select 等关键词，并且其左右边界满足规则时，原始 base64 会被污染，最终导致附件解码失败。换句话说：业务系统生成的附件 Base64 没问题图片本身没问题问题发生在 OA 接收请求后的安全处理阶段这是一个典型的“安全规则误杀二进制文本内容”问题。八、为什么这个问题隐蔽这个问题很难第一时间定位，原因有三个：1. 现象看起来像附件服务失败接口只返回“生成附件失败”，很容易让人先怀疑附件服务或图片本身。2. 不是所有附件都失败只有当 Base64 内部恰好出现特定关键词组合时才触发，因此问题呈现出明显的“偶发性”。3. 原始请求侧看不到污染业务侧生成的 Base64 完全正常，真正的问题发生在请求进入 OA 后的处理中间层。九、排查经验总结这次事件里，最有效的不是一开始盯着大日志翻，而是做了几轮最小化实验：先验证原始 Base64 是否真的合法再用短样本确认问题是否与特定内容有关再通过边界变体判断触发规则最后回到真实图片做反向验证这套方法的价值在于：能快速排除“文件坏了”这类低价值方向能把问题从“偶发失败”收敛成“稳定可复现”能把结论从“怀疑安全拦截”推进到“确认是关键词边界命中”十、后续建议如果后续要彻底解决，建议从以下几个方向评估：1. 调整 OA 对附件类字段的安全过滤策略对于明确承载 base64 附件内容的字段，应考虑：增加字段级白名单增加接口级豁免在安全过滤前识别并跳过合法 Base64 内容2. 优化附件上传方式如果接口支持，优先考虑：先上传附件，再传附件 ID使用 multipart/form-data避免把大段二进制文本直接放进普通表单字段3. 建立关键故障样本库像这次命中的 4or+、4and+ 这种片段，建议沉淀成回归样本，避免后续升级或策略调整时再次踩坑。十一、结语这次故障表面上是“附件 Base64 失败”，本质上却是“安全规则误杀了 Base64 文本内容”。它提醒我们一件事：当接口处理的不是普通业务文本，而是承载二进制内容的编码字符串时，安全策略如果仍按普通文本规则处理，就很容易把合法数据当成攻击特征误拦。很多“看起来像解析失败”的问题，最后都不是解析器本身有问题，而是数据在到达解析器之前，已经被别的环节悄悄改写了。问题解决，解决方案如下（2026年6月9日11:30）：问题： OA 流程接口 Base64 附件被安全规则误改写，附件生成失败解决方案： 将附件weaver_security_custom_rules_skip_for_workflow_base64.xml放到/ecology/WEB-INF/securityXML/下，用sysadmin登录，访问下oa地址+/updateRules.jsp访问成功之后显示complete之后可以关闭此页面,然后等三分钟左右测试是否正常。(如果是集群环境，需要复制到每个服务器，然后每个节点都访问下该地址）weaver_security_custom_rules_skip_for_workflow_base64.xml内容如下：<?xml version="1.0" encoding="UTF-8"?> <root> <skip-any-check-list> <url>/api/workflow/paService/doCreateRequest</url> <url>/api/workflow/paService/submitRequest</url> </skip-any-check-list> <need-execute-cls-rule-urls> <url>/api/workflow/paService/doCreateRequest</url> <url>/api/workflow/paService/submitRequest</url> </need-execute-cls-rule-urls> </root>
[泛微EC9] 丸子喂饭篇: restful 新建流程接口详解(/api/workflow/paService/doCreateRequest)
AI 辅助制作标识 该文章由丸子引导 AI 辅助制作生成，如有改进意见，欢迎评论区留言。 引言这个接口可以说是泛微 E9 里用得最多的 REST 接口之一了——新建流程实例。不管是做系统集成、数据迁移、还是定时任务自动发起流程，都绕不开它。但官方文档嘛……懂的都懂，参数一堆，示例不够，踩坑不少。今天丸子就把这个接口掰开了揉碎了讲一遍，争取让你看完就能用。一眼看明白：直接上 curl 如果你对泛微接口已经有一定了解，直接看这段就够了。当然，下面有详细解释，需要的话可以继续往下翻~。curl -X POST 'https://your-oa-domain.com/api/workflow/paService/doCreateRequest' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'token: your_token_here' \ -H 'appid: your_appid_here' \ -H 'userid: your_encrypted_userid_here' \ -d 'workflowId=388024' \ -d 'requestName=测试流程标题' \ -d 'remark=测试意见' \ -d 'requestLevel=2' \ -d 'mainData=[{"fieldName":"cswb","fieldValue":"2978"}]' \ -d 'detailData=[{"tableDBName":"formtable_main_1356_dt1","workflowRequestTableRecords":[{"recordOrder":0,"workflowRequestTableFields":[{"fieldName":"dhwb","fieldValue":"第一行"},{"fieldName":"drl","fieldValue":"2978"}]}]}]' \ -d 'otherParams={"isnextflow":0,"delReqFlowFaild":0}'返回成功示例：{ "code": "SUCCESS", "data": { "requestid": 1699718 }, "errMsg": {}, "reqFailMsg": { "keyParameters": {}, "msgInfo": {}, "otherParams": {} } }拿到 requestid 就说明流程创建成功了，这个 ID 就是流程实例的唯一标识。小白专区：一步一步来如果上面的 curl 你看得一头雾水，别慌，丸子下面一个一个讲。第一步：这个接口是干嘛的？简单说：通过代码自动新建一条泛微流程。平时你在 OA 系统里点"新建流程"，选流程、填表单、点提交——这个接口就是把这个过程用 HTTP 请求来完成。第二步：请求地址和方式POST https://你的 OA 地址/api/workflow/paService/doCreateRequest请求方式：POST（不是 GET！）Content-Type：必须是 application/x-www-form-urlencoded（不是 JSON！） 这里是个常见坑。很多人习惯用 JSON 格式发请求，但这个接口要求的是表单格式。第三步：请求头（Header）参数参数名说明怎么获取token调用凭证通过泛微许可接口获取appid许可证号码泛微后台配置userid加密后的用户 ID用 spk 公钥加密 userid 得到Content-Type内容类型固定填 application/x-www-form-urlencoded这三个参数（token、appid、userid）是泛微 REST 接口的"通行证"，没有它们接口直接拒绝。第四步：请求体（Body）参数详解这是重点，一共就这几个参数：workflowId（必填）—— 流程 ID-d 'workflowId=388024'这个数字是你要新建的流程模板 ID。怎么找？在 OA 系统里打开流程设计器，URL 里一般就有，或者查数据库 workflow_base 表。requestName（必填）—— 流程标题-d 'requestName=测试流程标题'就是流程发起后显示的标题，随便填。mainData（必填）—— 主表数据这个稍微复杂点，它是一个 JSON 数组，每个元素代表一个字段：[ { "fieldName": "cswb", "fieldValue": "2978" }, { "fieldName": "csfjsc", "fieldValue": [ { "fileName": "通用测试模板.xls", "filePath": "base64:AAA..." }, { "fileName": "通用测试 pdf.pdf", "filePath": "https://example.com/sample.pdf" } ] } ]fieldName：字段的数据库名称（不是显示名称！）。去表单设计器里看，或者查 workflow_formfield 表。fieldValue：字段值。普通字段直接填字符串，附件字段比较特殊，需要传数组，每个附件包含 fileName（文件名带后缀）和 filePath（base64 编码或 URL）。 怎么找字段的数据库名称？进入表单设计器 → 点击字段 → 查看属性 → 有个"数据库字段名"就是了。detailData（可选）—— 明细表数据如果你的流程有明细表，就用这个参数：[ { "tableDBName": "formtable_main_1356_dt1", "workflowRequestTableRecords": [ { "recordOrder": 0, "workflowRequestTableFields": [ { "fieldName": "dhwb", "fieldValue": "第一行" }, { "fieldName": "drl", "fieldValue": "2978" } ] }, { "recordOrder": 0, "workflowRequestTableFields": [ { "fieldName": "dhwb", "fieldValue": "第 2 行" }, { "fieldName": "drl", "fieldValue": "2978" } ] } ] } ]tableDBName：明细表的数据库名，格式一般是 formtable_main_xxx_dt1。workflowRequestTableRecords：每一行明细数据。recordOrder：行序号，从 0 开始。workflowRequestTableFields：和主表一样，fieldName + fieldValue。 多个明细表怎么办？ 在数组里加另一个 tableDBName 的对象就行。remark（可选）—— 签字意见-d 'remark=测试意见'流程发起时的签字意见，不填就用流程默认的。requestLevel（可选）—— 紧急程度-d 'requestLevel=2'数字越大越紧急，具体取值看你们流程的配置。otherParams（可选）—— 扩展参数-d 'otherParams={"isnextflow":0,"delReqFlowFaild":0}'这是一个 JSON 对象，支持以下参数：参数名说明可选值isnextflow新建后是否自动流转到下一节点0：不流转，1：流转（默认）delReqFlowFaild新建失败是否自动删除流程0：不删除，1：删除（默认）isVerifyPer是否验证用户创建权限0：不验证，1：验证（默认）requestSecLevel流程密级需开启密级功能才生效 isnextflow=0 什么意思？ 就是流程创建后停留在第一个节点，不自动往下走。有时候你还需要在第一个节点做点操作，就可以设成 0。第五步：返回值怎么判断？{ "code": "SUCCESS", "data": { "requestid": 1699718 } }code 为 SUCCESS → 成功，data.requestid 就是新建的流程 ID。code 为 PARAM_ERROR → 参数有错，看 errMsg 里的具体提示，当然，一般没有，慢慢调吧~code 为 NO_PERMISSION → 没有权限，检查 workflowId 对应的流程是否允许当前用户创建。code 为 SYSTEM_INNER_ERROR → 服务端异常，看 OA 日志。常见错误排查错误现象可能原因workflowId 返回 -1mainData 格式不对，多了 } 或 ]mainData: 新建流程主表数据不允许为空mainData 没传或者为空error_param_xxx: '值'某个字段名写错了，或者值不合法返回 NO_PERMISSION当前用户没有该流程的创建权限返回 PARAM_ERROR 但 errMsg 为空检查 Content-Type 是不是 application/x-www-form-urlencoded小结这个接口其实不难，核心就三个东西：请求头：token、appid、userid（泛微接口三件套）主表数据：mainData，字段名一定要和表单里的数据库字段名一致明细表数据：detailData，结构比主表多一层"表名"和"行记录"记住一个原则：字段名写错 = 报错，格式不对 = 报错，权限没有 = 报错。好了，丸子喂饭完毕，希望你能吃饱~有问题欢迎评论区留言~参考资料与致谢泛微官方文档： 泛微E9后端接口说明（https://e-cloudstore.com/ec/api/applist/index.html#/）
[泛微 E9] [ 高阶精华 ] 泛微 E9 新插件：kkfileview，让文件预览 “零门槛”
本博客只涉及部分核心操作，不是保姆级教学，希望你也能有自己的思路，感悟破局的乐趣。背景在日常办公中，文件预览是一个高频且重要的需求。传统的文件预览方案往往存在格式支持有限、体验不佳等问题。泛微E9自带的文档预览功能虽然基础，但在面对复杂格式或特殊需求时往往力不从心。今天给大家介绍如何通过集成开源的kkfileview项目，为泛微E9打造一个强大、统一的文档预览解决方案。功能亮点多格式支持：支持Office、PDF、图片、视频、音频等常见格式无缝集成：与泛微E9原有文档模块完美融合零客户端：基于Web的纯前端预览，无需安装任何插件高性能：基于Spring Boot构建，响应迅速开源免费：基于Apache 2.0协议，可自由使用和修改实施步骤第一步：部署kkfileview服务丸子采用的是从官方GitHub下载源码并自行编译打包的方式。如果你不想自己打包，可以私信丸子获取编译好的版本（有时间的话会分享）。启动成功后访问演示页：访问成功界面：第二步：泛微E9文档功能改造核心工具类首先创建一个工具类，用于生成文件下载的ddcode（这是泛微文档外发的核心知识）：package com.onestools; import weaver.docs.docs.util.DesUtils; public class getFileDownloadUrl { /** * 根据文件ID生成临时下载URL * @param fileid 文件ID * @return 下载URL */ public static String getFileDownloadUrl(int fileid) { try { String encrypt = new DesUtils().encrypt(1 + "_" + fileid); return "/weaver/weaver.file.FileDownload?fileid=" + fileid + "&download=1&ddcode=" + encrypt; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根据文件ID和基础URL生成临时下载URL * @param fileid 文件ID * @param baseurl 基础URL * @return 下载URL */ public static String getFileDownloadUrl(int fileid, String baseurl) { try { String encrypt = new DesUtils().encrypt(1 + "_" + fileid); return baseurl + "/weaver/weaver.file.FileDownload?fileid=" + fileid + "&download=1&ddcode=" + encrypt; } catch (Exception e) { e.printStackTrace(); return null; } } }JSP页面改造在ecology目录下/docs/e9/index2file.jsp中增加kkfileview预览逻辑：增加引用：<%@ page import="com.onestools.getFileDownloadUrl" %> <%@ page import="sun.misc.BASE64Encoder" %>添加kkfileview预览逻辑：// 现在检查kkfileview配置 boolean useKKFileView = "1".equals(rs.getPropValue("kkfileview","IsUsekkFileView")); String kkFileViewUrl = rs.getPropValue("kkfileview","kkFileViewUrl"); // kkfileview服务地址 String e9BaseUrl = rs.getPropValue("kkfileview","E9BaseUrl"); // E9基础URL if(useKKFileView && !isEdit) { // KKFileView预览逻辑 if(!"".equals(Util.null2String(kkFileViewUrl)) && !"".equals(Util.null2String(e9BaseUrl))) { try { // 使用getFileDownloadUrl工具类生成下载链接 String originUrl = getFileDownloadUrl.getFileDownloadUrl(imagefileId, e9BaseUrl); String previewUrl = originUrl + "&fullfilename=" + java.net.URLEncoder.encode(imagefilename, "UTF-8"); // Base64编码previewUrl String base64Url = new BASE64Encoder().encode(previewUrl.getBytes("UTF-8")); // 生成最终的iframe地址 docContent = " "; } catch (Exception e) { // 如果kkfileview出错，继续使用原有逻辑 e.printStackTrace(); } } }第三步：系统配置在ecology目录下/WEB-INF/prop新建kkfileview.properties增加以下内容：# 是否开启kkfileview IsUsekkFileView=1 # kkfileview的外部访问地址 kkFileViewUrl=http://192.168.31.130:8012/kkpreview # 相对于kkfileview访问E9的地址(建议内网地址) E9BaseUrl=http://127.0.0.1:8080注意事项安全性考虑：该方案的安全性和稳定性暂未经过全面评估，请在生产环境使用前进行充分测试性能优化：大量并发预览时可能需要考虑kkfileview的集群部署文件大小：超大文件预览时需要注意网络传输和服务器性能移动端适配：本示例只处理了index2file.jsp，如需移动端支持，需要对index2mobile.jsp进行类似改造总结通过集成kkfileview，我们成功为泛微E9赋予了强大的文档预览能力。这种"破局"思路不仅适用于文档预览，还可以扩展到其他系统功能的增强和改造。技术只是工具，思路才是关键。希望这个案例能够激发大家更多的创新想法，在现有系统的基础上创造出更多可能性。温馨提示：本文涉及的技术方案仅供学习参考，请根据实际环境进行评估和测试。如有技术问题，欢迎交流讨论！
为什么？ 无sysadmin限制，便于测试系统排查问题！速度！快！好看！ 怎么用？ 做一个xxxx.jsp，仍在ecology/目录下访问你的OA地址：/xxxx.jsp 注： 丸子直接用cursor写的！效果图上代码<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ page import="weaver.hrm.User" %> <%@ page import="weaver.hrm.HrmUserVarify" %> <%@ page import="java.io.File" %> <%@ page import="java.io.RandomAccessFile" %> <%@ page import="java.io.IOException" %> <%@ page import="java.util.ArrayList" %> <%@ page import="java.util.List" %> <%@ page import="java.util.Collections" %> <%@ page import="weaver.hrm.User" %> <%@ page import="weaver.hrm.HrmUserVarify" %> <%@ page import="java.io.File" %> <%@ page import="java.io.RandomAccessFile" %> <%@ page import="java.io.IOException" %> <%@ page import="java.util.ArrayList" %> <%@ page import="java.util.List" %> <%@ page import="java.util.Collections" %> <%-- 1. 登录验证：未登录跳转到登录页 --%> <% User user = HrmUserVarify.getUser(request, response); if (user == null) { response.sendRedirect("/login/Login.jsp"); return; } // 方案A：页面与请求保持UTF-8 request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); // 源日志编码（默认GBK，可传GB2312/GB18030） String srcCharset = request.getParameter("srcCharset"); if (srcCharset == null || srcCharset.trim().isEmpty()) { srcCharset = "GBK"; } // 2. 核心参数：当前ecology日志路径（固定路径，无需日期） String logDirPath = request.getRealPath("/") + "log" + File.separator; File logFile = new File(logDirPath + "ecology"); // 3. 显示行数设置：默认100行，限制10-1000行 String lineCountStr = request.getParameter("lineCount"); int lineCount = 100; if (lineCountStr != null && !lineCountStr.trim().isEmpty()) { try { lineCount = Integer.parseInt(lineCountStr.trim()); lineCount = Math.max(10, lineCount); lineCount = Math.min(1000, lineCount); } catch (NumberFormatException e) { lineCount = 100; } } List<String> logContent = new ArrayList<>(); String errorMsg = ""; // 日志级别统计 int infoCount = 0, errorCount = 0, warnCount = 0, debugCount = 0; if ("POST".equalsIgnoreCase(request.getMethod())) { try { if (!logFile.exists()) { errorMsg = "日志文件不存在：" + logFile.getAbsolutePath(); } else if (!logFile.canRead()) { errorMsg = "无权限读取日志文件：" + logFile.getAbsolutePath(); } else { // 使用源编码读取（GB2312/GBK/GB18030等） List<String> rawLogContent = readLastNLines(logFile, lineCount, srcCharset); // 合并多行日志并统计日志级别 StringBuilder currentLog = new StringBuilder(); String currentLevel = "info"; for (String line : rawLogContent) { if (line == null || line.replaceAll("[ \t\u3000]", "").trim().isEmpty()) { continue; } // 检查是否是新的日志条目（以时间戳开头） if (isNewLogEntry(line)) { // 保存之前的日志条目 if (currentLog.length() > 0) { logContent.add(currentLog.toString()); // 统计日志级别 switch (currentLevel) { case "info": infoCount++; break; case "error": errorCount++; break; case "warn": warnCount++; break; case "debug": debugCount++; break; } } // 开始新的日志条目 currentLog = new StringBuilder(line); currentLevel = parseLogLevel(line); } else { // 继续当前日志条目 currentLog.append("\n").append(line); } } // 处理最后一个日志条目 if (currentLog.length() > 0) { logContent.add(currentLog.toString()); switch (currentLevel) { case "info": infoCount++; break; case "error": errorCount++; break; case "warn": warnCount++; break; case "debug": debugCount++; break; } } } } catch (IOException e) { errorMsg = "读取日志失败：" + e.getMessage(); e.printStackTrace(); } } %> <%-- 6. 自定义方法：必须放在 <%! %> 声明块中（JSP语法要求） --%> <%! /** * 读取文件的最后N行，按指定源编码解码成Unicode */ private List<String> readLastNLines(File file, int n, String charsetName) throws IOException { List<String> result = new ArrayList<>(); if (n <= 0) return result; try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { long fileLength = raf.length(); long pos = fileLength - 1; int lines = 0; java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(256); while (pos >= 0 && lines < n) { raf.seek(pos); int b = raf.readByte() & 0xFF; if (b == 0x0A && pos != fileLength - 1) { // \n byte[] bytes = baos.toByteArray(); reverseBytes(bytes); result.add(new String(bytes, charsetName)); baos.reset(); lines++; } else if (b != 0x0D && b != 0x0A) { baos.write(b); } pos--; } if (baos.size() > 0) { byte[] bytes = baos.toByteArray(); reverseBytes(bytes); result.add(new String(bytes, charsetName)); } Collections.reverse(result); } return result; } private void reverseBytes(byte[] arr) { int i = 0, j = arr.length - 1; while (i < j) { byte tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; i++; j--; } } /** * 解析日志级别 */ private String parseLogLevel(String logLine) { if (logLine == null) return "info"; String upperLine = logLine.toUpperCase(); if (upperLine.contains(" ERROR ")) return "error"; if (upperLine.contains(" WARN ")) return "warn"; if (upperLine.contains(" DEBUG ")) return "debug"; if (upperLine.contains(" INFO ")) return "info"; return "info"; } /** * 解析日志时间戳 */ private String parseLogTimestamp(String logLine) { if (logLine == null) return ""; // 匹配格式：2025-10-17 14:01:34,407 java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3})"); java.util.regex.Matcher matcher = pattern.matcher(logLine); if (matcher.find()) { return matcher.group(1); } return ""; } /** * 解析线程信息 */ private String parseLogThread(String logLine) { if (logLine == null) return ""; // 匹配格式：[null] Thread-119-239[weaver.email.timer.MailClearTimer:92] java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\[([^\\]]+)\\]\\s+([^\\[]+)\\[([^\\]]+)\\]"); java.util.regex.Matcher matcher = pattern.matcher(logLine); if (matcher.find()) { return matcher.group(2) + "[" + matcher.group(3) + "]"; } return ""; } /** * 解析日志消息内容 */ private String parseLogMessage(String logLine) { if (logLine == null) return ""; // 找到最后一个 " - " 后的内容 int lastDash = logLine.lastIndexOf(" - "); if (lastDash != -1 && lastDash + 3 < logLine.length()) { return logLine.substring(lastDash + 3); } return logLine; } /** * 判断是否是新的日志条目（以时间戳开头） */ private boolean isNewLogEntry(String line) { if (line == null || line.trim().isEmpty()) return false; // 匹配格式：2025-10-17 14:01:34,407 INFO return line.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}.*"); } %> 当前Ecology日志查看 当前Ecology日志查看 登录用户：<%= user.getLoginid() %> 显示最新行数： <input type="number" id="lineCount" name="lineCount" value="<%= lineCount %>" min="10" max="1000" step="10" required> （10-1000行） 源编码： <option value="UTF-8" <%= "UTF-8".equalsIgnoreCase(srcCharset) ? "selected" : "" %>>UTF-8 <option value="GBK" <%= "GBK".equalsIgnoreCase(srcCharset) ? "selected" : "" %>>GBK <option value="GB2312" <%= "GB2312".equalsIgnoreCase(srcCharset) ? "selected" : "" %>>GB2312 <option value="GB18030" <%= "GB18030".equalsIgnoreCase(srcCharset) ? "selected" : "" %>>GB18030 加载日志 日志文件路径：<%= logFile.getAbsolutePath() %> <% if (!logContent.isEmpty()) { %>| 实际显示行数：<%= logContent.size() %><% } %> | 源编码：<%= srcCharset %> <% if (!logContent.isEmpty()) { %> INFO: <%= infoCount %> ERROR: <%= errorCount %> WARN: <%= warnCount %> DEBUG: <%= debugCount %> <% } %> <%-- 错误提示 --%> <% if (!errorMsg.isEmpty()) { %> <%= errorMsg %> <%-- 空状态（未点击加载） --%> <% } else if (logContent.isEmpty()) { %> 请设置行数并点击"加载日志"按钮查看内容 <%-- 日志内容 --%> <% } else { %> <% for (int i = logContent.size() - 1; i >= 0; i--) { String logEntry = logContent.get(i); if (logEntry == null || logEntry.trim().isEmpty()) continue; // 解析日志各部分（从第一行获取） String[] lines = logEntry.split("\n"); String firstLine = lines[0]; String logLevel = parseLogLevel(firstLine); String timestamp = parseLogTimestamp(firstLine); String thread = parseLogThread(firstLine); // 清理日志内容，正确处理多行日志 String cleanedLogEntry = logEntry.replaceAll("\\r\\n|\\r|\\n", " ") // 将换行符替换为空格 .replaceAll("\\s+", " ") // 将多个连续空白字符替换为单个空格 .trim(); // 去除首尾空白 // HTML转义整个日志内容 String escapedLogEntry = cleanedLogEntry.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); %> <div class="log-entry <%= logLevel %>"> <%= escapedLogEntry %> <% } %> <% } %>
[泛微 EC9] 开发踩坑：weaver.conn.RecordSet-报错"字符串文字太长"
AI辅助制作标识 该文章由丸子引导AI辅助制作生成，如有改进意见，欢迎评论区留言。 记录一次为泛微OA集成AI流程审查功能时遇到的技术难题与解决历程问题背景在最近为泛微OA系统集成AI流程智能审查功能的开发过程中，遇到了一个令人困扰的问题：当尝试将AI返回的审查结果写入数据库时，表单赋值操作始终无法成功完成。经过仔细排查系统日志，发现了关键的错误信息：2025-04-29 19:01:51,512 ERROR A2 - [null] resin-port-8081-363-363[weaver.conn.RecordSet:1908] - weaver.conn.RecordSet java.sql.SQLSyntaxErrorException: ORA-01704: 字符串文字太长这个错误表明，AI返回的字符串内容长度超过了4000字符，导致数据库写入失败。问题排查过程第一阶段：数据库字段类型检查首先检查了数据库表结构，发现目标字段确实是CLOB类型，理论上应该能够存储大文本数据：-- 检查字段类型 SELECT column_name, data_type FROM user_tab_columns WHERE table_name = 'FORMTABLE_MAIN_314' AND column_name = 'AISHJG';结果显示字段类型为CLOB，这让我们一度感到困惑——既然字段类型正确，为什么还会出现长度限制的错误？第二阶段：技术原理探究经过深入研究和测试，终于找到了问题的根源：RecordSet在更新CLOB字段时的局限性在Oracle数据库环境下，SQL语句在执行之前会将所有字符类型的数据隐式转换为VARCHAR2类型，而VARCHAR2类型的最大长度限制为4000字符。当字符串长度超过这个限制时，转换就会失败，抛出ORA-01704: 字符串文字太长异常。即使目标字段是CLOB类型，使用RecordSet的更新方法仍然会受到这个限制的影响。解决方案错误做法 ❌// 以下是会导致错误的代码示例 RecordSet rs = new RecordSet(); String sql = "update formtable_main_314 set aishjg= ? where requestid=12345"; rs.executeUpdate(sql, longContent);正确做法 ✅使用ConnStatement来更新大字段：ConnStatement st = null; try { st = new ConnStatement(); st.setStatementSql("update formtable_main_314 set aishjg=? where requestid=?"); // 设置参数值 st.setString(1, aiResultContent); // 支持CLOB大字段 st.setInt(2, 12345); // 请求ID st.executeUpdate(); } catch(Exception e) { // 异常处理 e.printStackTrace(); } finally { // 资源释放 if (st != null) { st.close(); } }重要技术说明泛微OA字段类型与数据库存储关系在Oracle数据库环境下：多行文本字段（未勾选HTML编辑）：存储为VARCHAR2(4000)多行文本字段（勾选HTML编辑）：存储为CLOB类型最佳实践建议预判内容长度：在处理可能包含大文本的场景（如AI返回结果、富文本内容、日志信息等）时，优先考虑使用ConnStatement。统一的数据库操作封装：public class DatabaseUtil { /** * 更新CLOB字段的通用方法 */ public static boolean updateClobField(String tableName, String clobField, String value, String whereCondition) { ConnStatement st = null; try { st = new ConnStatement(); String sql = "update " + tableName + " set " + clobField + "=? where " + whereCondition; st.setStatementSql(sql); st.setString(1, value); return st.executeUpdate() > 0; } catch(Exception e) { e.printStackTrace(); return false; } finally { if (st != null) st.close(); } } }内容长度检查：// 在实际更新前进行长度检查 if (aiResultContent.length() > 4000) { // 使用ConnStatement更新 updateWithConnStatement(aiResultContent); } else { // 可以使用RecordSet（更简洁） updateWithRecordSet(aiResultContent); }经验总结这次踩坑经历让我们深刻认识到：技术选型要谨慎：不同的数据库操作方式有各自的适用场景，不能一概而论。理解底层原理：了解数据库驱动和框架的底层实现原理，有助于快速定位和解决问题。日志分析的重要性：详细的错误日志是解决问题的关键线索。社区力量宝贵：技术社区的交流和经验分享能够大大缩短问题解决时间。致谢特别感谢微信技术交流群中luowe老师的及时提醒，以及泛微朱老师的技术指导，正是他们的帮助让我们快速找到了问题的解决方案。希望这篇经验分享能够帮助到遇到类似问题的开发者，避免重复踩坑！如果您有更好的解决方案或其他泛微开发经验，欢迎在评论区交流分享！
[EcologyEC9] api/ec/dev/table/getxml拦截后解决方案
AI辅助制作标识 该文章由丸子引导AI辅助制作生成，如有改进意见，欢迎评论区留言。 引言泛微近期发布最新ec9安全包，其中 /api/ec/dev/table/getxml?dataKey=xxx 接口被安全包拦截,故推出以下jsp方案,用于展示接口执行SQL记录。为了便于调试和记录查询操作，本文介绍了一种通过JSP展示接口执行SQL记录的方案。完整代码<%@ page language="java" contentType="text/html; charset=UTF-8" %> <%@ page import="com.alibaba.fastjson.JSONObject, com.alibaba.fastjson.JSON, com.alibaba.fastjson.JSONException"%> <%@ page import="com.cloudstore.dev.api.util.Util_TableMap"%> <%@ page import="weaver.hrm.User"%> <%@ page import="weaver.hrm.HrmUserVarify"%> <%@ page import="weaver.general.Util"%> <%@ page import="cn.hutool.http.HtmlUtil"%> <%@ page import="cn.hutool.core.util.StrUtil"%> <% User user = HrmUserVarify.getUser(request, response); if (user == null) { response.sendRedirect("/login/Login.jsp"); return; } String dataKey = Util.null2String(request.getParameter("dataKey")); if (dataKey.isEmpty()) { JSONObject result = new JSONObject(); result.put("status", "error"); result.put("message", "dataKey参数不能为空"); out.print(result.toJSONString()); return; } String cachedData = Util_TableMap.getVal(dataKey); if (cachedData == null) { JSONObject result = new JSONObject(); result.put("status", "error"); result.put("message", "无法找到对应的缓存数据"); out.print(result.toJSONString()); } else { try { String sqlTag = cachedData.replaceAll(".*(<sql .*?/?>).*", "$1"); java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\w+)\\s*=\\s*\"([^\"]*)\""); java.util.regex.Matcher matcher = pattern.matcher(sqlTag); out.print(" 接口执行SQL记录 "); out.print(" "); out.print(" 属性名 属性值 "); while (matcher.find()) { String attributeName = matcher.group(1); String attributeValue = matcher.group(2); out.print(" "); out.print(" " + HtmlUtil.escape(attributeName) + " "); out.print(" " + HtmlUtil.escape(attributeValue) + " "); out.print(" "); } out.print(" "); } catch (Exception e) { JSONObject result = new JSONObject(); result.put("status", "error"); result.put("message", "解析 <sql> 标签时出错：" + e.getMessage()); out.print(result.toJSONString()); } } %> 食用方法将以上完整的代码新建为一个jsp文件，看心情命名，如：DataKeyLookup.jsp放置在ecology主目录下，我的目录是：/WEAVER/ecology，我直接放里面现在你可以访问：你的OA地址/DataKeyLookup.jsp?dataKey=xxxxxx方案详解用户验证首先，我们需要确保只有合法的用户才能访问该JSP页面。使用HrmUserVarify.getUser(request, response)方法来验证用户的登录状态，如果用户未登录，则重定向到登录页面。参数获取与验证获取接口传递的dataKey参数，并确保该参数不为空。否则，返回错误信息并终止执行。数据缓存获取通过Util_TableMap.getVal(dataKey)方法获取与dataKey对应的缓存数据。如果未找到缓存数据，则返回错误信息。SQL记录提取与展示我们通过正则表达式提取缓存数据中的<sql>标签，然后解析该标签内的属性及其值，并以表格形式展示在页面上。此过程涉及字符串操作和正则表达式匹配。代码解析JSP页面导入的库<%@ page import="com.alibaba.fastjson.JSONObject, com.alibaba.fastjson.JSON, com.alibaba.fastjson.JSONException"%> <%@ page import="com.cloudstore.dev.api.util.Util_TableMap"%> <%@ page import="weaver.hrm.User"%> <%@ page import="weaver.hrm.HrmUserVarify"%> <%@ page import="weaver.general.Util"%> <%@ page import="cn.hutool.http.HtmlUtil"%> <%@ page import="cn.hutool.core.util.StrUtil"%>上述代码导入了多个库，其中fastjson用于处理JSON，HrmUserVarify用于用户验证，HtmlUtil用于HTML转义。用户验证逻辑User user = HrmUserVarify.getUser(request, response); if (user == null) { response.sendRedirect("/login/Login.jsp"); return; }此部分代码用于验证用户是否登录，如果未登录则重定向到登录页面。SQL标签解析与数据展示逻辑String sqlTag = cachedData.replaceAll(".*(<sql .*?/?>).*", "$1"); java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\w+)\\s*=\\s*\"([^\"]*)\""); java.util.regex.Matcher matcher = pattern.matcher(sqlTag);通过正则表达式，我们从缓存数据中提取出<sql>标签，并解析出标签内的属性名和属性值，最后以表格形式展示出来。常见问题与调试如何处理未找到缓存数据的问题如果Util_TableMap.getVal(dataKey)返回为空，表示没有找到对应的缓存数据，此时返回错误提示，避免页面空白或误导性的信息展示。解析标签时的异常处理解析过程中可能会遇到异常，为了避免页面崩溃，使用try-catch块捕获异常，并返回相应的错误信息以帮助调试。结论该方案通过JSP有效地展示了接口执行的SQL记录，特别适用于需要快速调试SQL语句执行情况的场景。建议后续可以进一步优化数据提取逻辑，提升页面渲染效率。
[EcologyEC9] 公式：获取当前日期是今年的第几周
/* 获取当前日期是今年的第几周（无参数） */ function getYearWeek() { var date = new Date(); var startDate = new Date(date.getFullYear(), 0, 1); var dayOfWeek = startDate.getDay() || 7; startDate.setDate(startDate.getDate() + (8 - dayOfWeek)); var daysSinceStart = Math.ceil((date - startDate) / 86400000); var weekNumber = Math.ceil(daysSinceStart / 7) + 1; return weekNumber; }开袋即食该方案，1月1日所在周，会被视为第一周。
开启Token认证后台 → 统一认证中心 → 认证服务管理 → Token认证开启并保存，将自动重启OA服务 服务器重启警告！ 注意：开启“Token认证”并保存时，服务器将被重启，生产环境谨慎操作，以免影响正常业务！ 注册应用后台 → 统一认证中心 → 认证服务管理 → 认证应用管理点击右上角“注册”，注册应用认证方式：TOKEN；IP白名单：输入你的IP，这里是必须的（不知道自己IP？往下看！一会教你）；账号映射规则：用户名(也可以用别的)；其他配置：看你心情；开始测试打开你浏览器的控制台(F12)来模拟测试var appid = 'd72cceb0-0d96-46aa-8f22-4cd00c5cbc5c'; //替换为你的应用标识 var loginid = 'zhangsan'; //替换为你的账号映射规则对应的单点登录人员值 var xhr = new XMLHttpRequest(); xhr.open('POST', '/ssologin/getToken', false); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('appid=' + encodeURIComponent(appid) + '&loginid=' + encodeURIComponent(loginid)); var response = xhr.responseText.trim(); if (xhr.status === 200) { if (response.includes('失败')) { console.log("获取失败:", response); } else { var ssoToken = response; var redirectUrl = window.location.origin + '/wui/index.html?ssoToken=' + encodeURIComponent(ssoToken) + '#/main'; console.log("单点登录ssourl:", redirectUrl); } } else { console.error("请求失败:", xhr.status); }如果没错，你已经获取到了单点登录url，直接复制后在任意位置访问，即可登录账号如果你的IP没在白名单：他会提示你IP是什么，加至应用IP白名单即可！
暂未完成 本篇博客暂未完工，但是丸子有点懒，不知道什么时候继续写~ 由于泛微官方DML动作帮助文档不适合小白（至少不是很适合我....），所以，泛微DML动作丸子版诞生了~ 故事背景 故事的序章，便由我来一一道来...丸子在律师事务所工作，为更整合、优化案件处理过程，我们近日创建了一个“案件管理流程”，以点石成金，助推案件的全局流转及其管理；然而，在这全链条中，我们需特别抽离的环节--“案件收款录入”，这源于收款事件的随机性，其相应的数据需有序而及时地反馈至“案件管理流程”中的收款明细上。鉴于流程转数据的操作无法达成此目的（以A流程去填补另一B流程的明细数据），于是丸子开始着手小小的研究DML动作。既然踏出此步，我们便准备坚持至最后——坚持完實這一切!结果：根据实际情况思考后决定采用DML存储过程实现以下内容：formtable_main_157_dt2为案件管理流程收款明细表formtable_main_14为案件收款录入流程表创建一个存储过程，接受参数 AJMCX(案件名称浏览框)，SKLCID（收款流程ID），ISCHECK（是否核验）。在该存储过程中，首先检查 formtable_main_157_dt2 表中是否存在 SKLCID。如果SKLCID不存在：查询 formtable_main_14 表中的 sqr，sklx，bcsksj，bcskje，skqd，hkr 字段，查询条件为 formtable_main_14.id = SKLCID。如果查询结果为空，存储过程结束。向 formtable_main_157_dt2 表插入两条数据。其中一条数据的 mainid 字段的值从 formtable_main_157 表中查询得到，查询条件为 formtable_main_157.id = AJMCX。另一条数据的 mainid 字段的值为 SKLCID。插入的两条数据的其它字段值根据以下对应关系从 formtable_main_14 表查询得到：formtable_main_14.sklx = formtable_main_157_dt2.sklxformtable_main_14.bcsksj = formtable_main_157_dt2.bcsksjformtable_main_14.bcskje = formtable_main_157_dt2.bcskjeformtable_main_14.skqd = formtable_main_157_dt2.skqdformtable_main_14.hkr = formtable_main_157_dt2.hkrformtable_main_14.glsklc = SKLCIDformtable_main_14.cwhszt = IDCHECK如果SKLCID存在：查询 formtable_main_14 表中的 sqr，sklx，bcsksj，bcskje，skqd，hkr 字段，查询条件为 formtable_main_14.id = SKLCID。更新 formtable_main_157_dt2 表中的记录，更新的字段值根据以下对应关系从 formtable_main_14 表查询得到：formtable_main_14.sklx = formtable_main_157_dt2.sklxformtable_main_14.bcsksj = formtable_main_157_dt2.bcsksjformtable_main_14.bcskje = formtable_main_157_dt2.bcskjeformtable_main_14.skqd = formtable_main_157_dt2.skqdformtable_main_14.hkr = formtable_main_157_dt2.hkrformtable_main_14.cwhszt = IDCHECK更新条件为 formtable_main_157_dt2.glsklc = SKLCID。存储过程：ALTER PROCEDURE SyncReceiptToCaseDetail @AJMCX INT, @SKLCID INT, @ISCHECK INT AS BEGIN -- 如果@AJMCX为空或0，直接返回 IF @AJMCX IS NULL OR @AJMCX = 0 BEGIN PRINT '@AJMCX is NULL or 0' RETURN END DECLARE @mainid INT, @sqr INT, @sklx INT, @bcsksj CHAR(10), @bcskje DECIMAL(38,2), @skqd INT, @hkr VARCHAR(100) -- 检查是否存在SKLCID IF NOT EXISTS (SELECT 1 FROM formtable_main_157_dt2 WHERE glsklc = @SKLCID) BEGIN PRINT '@SKLCID does not exist in formtable_main_157_dt2' -- 查询formtable_main_14表 SELECT @sqr = sqr, @sklx = sklx, @bcsksj = bcsksj, @bcskje = bcskje, @skqd = skqd, @hkr = hkr FROM formtable_main_14 WHERE requestId = @SKLCID PRINT 'Value of @sqr is: ' + ISNULL(CAST(@sqr AS VARCHAR(10)), 'NULL') IF @sqr IS NOT NULL BEGIN PRINT '@sqr is not NULL' -- 向表formtable_main_157_dt2插入第一条数据 流程 SELECT @mainid = id FROM formtable_main_157 WHERE qqidcf = @AJMCX PRINT 'Value of @mainid is: ' + ISNULL(CAST(@mainid AS VARCHAR(10)), 'NULL') INSERT INTO formtable_main_157_dt2 (mainid, skr, sklx, bcsksj, bcskje, cwhszt, skqd, hkr, glsklc) VALUES (@mainid, @sqr, @sklx, @bcsksj, @bcskje, @ISCHECK, @skqd, @hkr, @SKLCID) -- 向表formtable_main_157_dt2插入第二条数据 建模 INSERT INTO formtable_main_157_dt2 (mainid, skr, sklx, bcsksj, bcskje, cwhszt, skqd, hkr, glsklc) VALUES (@AJMCX, @sqr, @sklx, @bcsksj, @bcskje, @ISCHECK, @skqd, @hkr, @SKLCID) END END ELSE BEGIN -- 查询formtable_main_14表 SELECT @sqr = sqr, @sklx = sklx, @bcsksj = bcsksj, @bcskje = bcskje, @skqd = skqd, @hkr = hkr FROM formtable_main_14 WHERE requestId = @SKLCID -- 更新formtable_main_157_dt2 UPDATE formtable_main_157_dt2 SET skr = @sqr, sklx = @sklx, bcsksj = @bcsksj, bcskje = @bcskje, cwhszt = @ISCHECK, skqd = @skqd, hkr = @hkr WHERE glsklc = @SKLCID END ENDDML语句：EXEC SyncReceiptToCaseDetail @AJMCX = {?ajmcx}, @SKLCID = $requestid$, @ISCHECK = 0