From de87db8081b88dbfa539d13723c784994ce3005a Mon Sep 17 00:00:00 2001 From: RuicyWu <1063154311@qq.com> Date: Wed, 11 Feb 2026 11:21:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cfgHome/component/esign/pluginCfg.xml | 6 + .../component/esign/spring/spring-bean.xml | 32 ++ .../esign/spring/spring-controller.xml | 7 + .../css/contractCompareBtn.css | 36 ++ .../css/img/icon16.png | Bin 0 -> 103273 bytes .../html/contractCompare.html | 104 ++++ .../js/compareBiz.js | 99 ++++ .../js/compareinit.js | 207 +++++++ .../com/seeyon/apps/esign/EsignPluginApi.java | 44 ++ .../esign/config/EsignConfigProvider.java | 22 + .../apps/esign/constants/EsignApiUrl.java | 19 + .../esign/constants/EsignConfigConstants.java | 44 ++ .../DefaultEsignCallBackController.java | 89 +++ .../fieldCtrl/ContractCompareFieldCtrl.java | 61 +++ .../seeyon/apps/esign/job/SealDocSyncJob.java | 71 +++ .../com/seeyon/apps/esign/msg/MessageVo.java | 50 ++ .../apps/esign/msg/ThirdMessageService.java | 8 + .../esign/node/EsignMultipleSignerNode.java | 332 +++++++++++ .../apps/esign/node/EsignOneSignerNode.java | 355 ++++++++++++ .../apps/esign/node/EsignOrgAuthNode.java | 38 ++ .../apps/esign/node/EsignPersonAuthNode.java | 56 ++ .../apps/esign/po/auth/AuthPsnInfo.java | 61 +++ .../apps/esign/po/auth/PsnAuthConfig.java | 40 ++ .../apps/esign/po/auth/PsnAuthPageConfig.java | 45 ++ .../esign/po/esignapi/EsignApiHeader.java | 219 ++++++++ .../apps/esign/po/esignapi/EsignBaseResp.java | 31 ++ .../po/esignapi/EsignCallbackParams.java | 65 +++ .../esign/po/esignapi/EsignException.java | 36 ++ .../esign/po/esignapi/EsignHttpResponse.java | 27 + .../apps/esign/po/esignapi/EsignToken.java | 34 ++ .../seeyon/apps/esign/po/file/SignFile.java | 22 + .../apps/esign/po/file/TemplateComponent.java | 31 ++ .../apps/esign/po/file/TemplateInfo.java | 22 + .../apps/esign/po/flow/SignFlowInitiator.java | 39 ++ .../apps/esign/po/org/OrgInitiator.java | 22 + .../apps/esign/po/org/SignFlowConfig.java | 127 +++++ .../seeyon/apps/esign/po/org/Transactor.java | 13 + .../apps/esign/po/param/FlowParamSource.java | 197 +++++++ .../apps/esign/po/param/JsonParamSource.java | 122 +++++ .../apps/esign/po/personal/PsnInitiator.java | 13 + .../seeyon/apps/esign/po/seal/SealInfoVo.java | 50 ++ .../esign/po/signer/DefaultSignerBuilder.java | 163 ++++++ .../esign/po/signer/JsonSignerBuilder.java | 107 ++++ .../seeyon/apps/esign/po/signer/OrgInfo.java | 59 ++ .../apps/esign/po/signer/PersonInfo.java | 48 ++ .../apps/esign/po/signer/SignParty.java | 128 +++++ .../seeyon/apps/esign/po/signer/Signer.java | 65 +++ .../apps/esign/po/signer/SignerBuilder.java | 26 + .../apps/esign/po/signer/SignerType.java | 5 + .../po/signfield/NormalSignFieldConfig.java | 58 ++ .../po/signfield/RemarkSignFieldConfig.java | 32 ++ .../apps/esign/po/signfield/SignField.java | 42 ++ .../esign/po/signfield/SignFieldPosition.java | 41 ++ .../po/upload/EsignFileUploadParams.java | 49 ++ .../esign/po/upload/GetUploadUrlResp.java | 23 + .../esign/service/ContractCreateService.java | 31 ++ .../apps/esign/service/EsignAuthService.java | 33 ++ .../esign/service/EsignByTemplateService.java | 54 ++ .../service/EsignCallbackBizService.java | 7 + .../service/EsignCallbackFlowBizService.java | 89 +++ .../service/EsignFileTemplateService.java | 133 +++++ .../esign/service/EsignFlowQueryService.java | 32 ++ .../esign/service/EsignUploadFileService.java | 187 +++++++ .../FlowFormSignParamBuildFactory.java | 102 ++++ .../NormalFormSignParamBuildFactory.java | 88 +++ .../apps/esign/service/SealService.java | 122 +++++ .../apps/esign/service/SignLinkService.java | 150 +++++ .../esign/service/SignParamBuildFactory.java | 15 + .../apps/esign/service/SignParamSource.java | 51 ++ .../apps/esign/service/TokenCacheManager.java | 98 ++++ .../seeyon/apps/esign/utils/EnHancedMap.java | 17 + .../apps/esign/utils/EsignHttpCfgHelper.java | 471 ++++++++++++++++ .../apps/esign/utils/EsignHttpHelper.java | 83 +++ .../apps/esign/utils/EsignRequestType.java | 38 ++ .../com/seeyon/apps/esign/utils/FileUtil.java | 516 ++++++++++++++++++ .../apps/esign/utils/HmacSHA256Util.java | 27 + .../seeyon/apps/esign/utils/HttpClient.java | 499 +++++++++++++++++ .../seeyon/apps/esign/utils/JsonCleaner.java | 28 + .../com/seeyon/apps/esign/utils/ProtUtil.java | 84 +++ .../esign/EsignFlowQueryResource.java | 33 ++ .../esign/EsignTemplateResource.java | 50 ++ src/test/java/TestOne.java | 9 + 82 files changed, 6789 insertions(+) create mode 100644 seeyon/WEB-INF/cfgHome/component/esign/pluginCfg.xml create mode 100644 seeyon/WEB-INF/cfgHome/component/esign/spring/spring-bean.xml create mode 100644 seeyon/WEB-INF/cfgHome/component/esign/spring/spring-controller.xml create mode 100644 seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/css/contractCompareBtn.css create mode 100644 seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/css/img/icon16.png create mode 100644 seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/html/contractCompare.html create mode 100644 seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/js/compareBiz.js create mode 100644 seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/js/compareinit.js create mode 100644 src/main/java/com/seeyon/apps/esign/EsignPluginApi.java create mode 100644 src/main/java/com/seeyon/apps/esign/config/EsignConfigProvider.java create mode 100644 src/main/java/com/seeyon/apps/esign/constants/EsignApiUrl.java create mode 100644 src/main/java/com/seeyon/apps/esign/constants/EsignConfigConstants.java create mode 100644 src/main/java/com/seeyon/apps/esign/controller/DefaultEsignCallBackController.java create mode 100644 src/main/java/com/seeyon/apps/esign/fieldCtrl/ContractCompareFieldCtrl.java create mode 100644 src/main/java/com/seeyon/apps/esign/job/SealDocSyncJob.java create mode 100644 src/main/java/com/seeyon/apps/esign/msg/MessageVo.java create mode 100644 src/main/java/com/seeyon/apps/esign/msg/ThirdMessageService.java create mode 100644 src/main/java/com/seeyon/apps/esign/node/EsignMultipleSignerNode.java create mode 100644 src/main/java/com/seeyon/apps/esign/node/EsignOneSignerNode.java create mode 100644 src/main/java/com/seeyon/apps/esign/node/EsignOrgAuthNode.java create mode 100644 src/main/java/com/seeyon/apps/esign/node/EsignPersonAuthNode.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/auth/AuthPsnInfo.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/auth/PsnAuthConfig.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/auth/PsnAuthPageConfig.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/esignapi/EsignApiHeader.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/esignapi/EsignBaseResp.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/esignapi/EsignCallbackParams.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/esignapi/EsignException.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/esignapi/EsignHttpResponse.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/esignapi/EsignToken.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/file/SignFile.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/file/TemplateComponent.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/file/TemplateInfo.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/flow/SignFlowInitiator.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/org/OrgInitiator.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/org/SignFlowConfig.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/org/Transactor.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/param/FlowParamSource.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/param/JsonParamSource.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/personal/PsnInitiator.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/seal/SealInfoVo.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/signer/DefaultSignerBuilder.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/signer/JsonSignerBuilder.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/signer/OrgInfo.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/signer/PersonInfo.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/signer/SignParty.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/signer/Signer.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/signer/SignerBuilder.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/signer/SignerType.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/signfield/NormalSignFieldConfig.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/signfield/RemarkSignFieldConfig.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/signfield/SignField.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/signfield/SignFieldPosition.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/upload/EsignFileUploadParams.java create mode 100644 src/main/java/com/seeyon/apps/esign/po/upload/GetUploadUrlResp.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/ContractCreateService.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/EsignAuthService.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/EsignByTemplateService.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/EsignCallbackBizService.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/EsignCallbackFlowBizService.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/EsignFileTemplateService.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/EsignFlowQueryService.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/EsignUploadFileService.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/FlowFormSignParamBuildFactory.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/NormalFormSignParamBuildFactory.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/SealService.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/SignLinkService.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/SignParamBuildFactory.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/SignParamSource.java create mode 100644 src/main/java/com/seeyon/apps/esign/service/TokenCacheManager.java create mode 100644 src/main/java/com/seeyon/apps/esign/utils/EnHancedMap.java create mode 100644 src/main/java/com/seeyon/apps/esign/utils/EsignHttpCfgHelper.java create mode 100644 src/main/java/com/seeyon/apps/esign/utils/EsignHttpHelper.java create mode 100644 src/main/java/com/seeyon/apps/esign/utils/EsignRequestType.java create mode 100644 src/main/java/com/seeyon/apps/esign/utils/FileUtil.java create mode 100644 src/main/java/com/seeyon/apps/esign/utils/HmacSHA256Util.java create mode 100644 src/main/java/com/seeyon/apps/esign/utils/HttpClient.java create mode 100644 src/main/java/com/seeyon/apps/esign/utils/JsonCleaner.java create mode 100644 src/main/java/com/seeyon/apps/esign/utils/ProtUtil.java create mode 100644 src/main/java/com/seeyon/ctp/rest/resources/esign/EsignFlowQueryResource.java create mode 100644 src/main/java/com/seeyon/ctp/rest/resources/esign/EsignTemplateResource.java create mode 100644 src/test/java/TestOne.java diff --git a/seeyon/WEB-INF/cfgHome/component/esign/pluginCfg.xml b/seeyon/WEB-INF/cfgHome/component/esign/pluginCfg.xml new file mode 100644 index 0000000..e7b0812 --- /dev/null +++ b/seeyon/WEB-INF/cfgHome/component/esign/pluginCfg.xml @@ -0,0 +1,6 @@ + + + esign + 致远E签宝集成封装 + 20260122 + diff --git a/seeyon/WEB-INF/cfgHome/component/esign/spring/spring-bean.xml b/seeyon/WEB-INF/cfgHome/component/esign/spring/spring-bean.xml new file mode 100644 index 0000000..148f59e --- /dev/null +++ b/seeyon/WEB-INF/cfgHome/component/esign/spring/spring-bean.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/seeyon/WEB-INF/cfgHome/component/esign/spring/spring-controller.xml b/seeyon/WEB-INF/cfgHome/component/esign/spring/spring-controller.xml new file mode 100644 index 0000000..0dcbb84 --- /dev/null +++ b/seeyon/WEB-INF/cfgHome/component/esign/spring/spring-controller.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/css/contractCompareBtn.css b/seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/css/contractCompareBtn.css new file mode 100644 index 0000000..e5c4ae0 --- /dev/null +++ b/seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/css/contractCompareBtn.css @@ -0,0 +1,36 @@ +.customButton_class_box { + width: 35%; + line-height: 24px; + height:24px; + color: #1f85ec; + cursor: pointer; + font-family: "Microsoft YaHei"!important; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-break:keep-all; + box-sizing: border-box; + -webkit-box-sizing : border-box; + -moz-box-sizing : border-box; + text-align: center; + outline: none; + border: 1px solid #1f85ec; + background-color: #fff; + border-radius: 15px; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; +} +.customInputArea{ + width: 65%; + height:auto; + color: #1f85ec; + font-family: "Microsoft YaHei"!important; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-break:keep-all; +} +.customButton_box_content{ + width: 100%; + height: auto; +} diff --git a/seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/css/img/icon16.png b/seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/css/img/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..e43def3b7a2d46b32ea439d5f5e133a03204e1f6 GIT binary patch literal 103273 zcmbTdWmH>HwzvZn1sZA~R?3~~$r0D!HcEUyayAb|h#MMHTVsTgY;c>N*s zQ+Vg6=jq@VXzgPUkg@Z$v8PsXw|2DGwYRnl_8zwv2LOeWiE*0cuZR=|9M{Q&8=TFueFUZx5)p{`9I+c2*~pY@G8hC@CYgJ@F)lg^9l3F@bK{SDDv?M@QMih53Y)b zub;Jtt^NP-c7Emk-?$?GS6op!AA4&*Pal0xPq+WE0&OQxKTlsLPcLdYx&N%X05!9Q zwXL(qe+F6pvrzwQ-tzW7&H?sziawt1)c>VvQRn}I6#TaQ0`|7T4jg%i8b&m79~pRrju(=?B~})g#YytP+?keImq-Ue zhX^Q-CsTm&;YkibEIN*Mft~-T@1Dv9@o4;a<~`w@+|Y%dkrY>Dz?30PO0Bu#Tp})HJz{6xm-zE^^B^w(u=S(_}?siVfYh z<&W->JBi_oCS6>T-)z2he;RG~ih&kCw}j~756l>pcYS&m7W;7I;|F=FxdEE$->Nhi_GX3LEXudmcb3H+k*kF6fnqZ83ZWpYoz030U z9JmjfIMZ52h|H!vR48b1Z&b|pBE07)!ao0u3UeslcDD7itnsU5U#=WfweIzq^%t< zu(xMcrkI3Mm$`=FckCSMni<;Pn$IY4{gHU0WD-TiS@Xd^RFr+|n1G+k84uyy zKP3uQvK`y!*gVy%R6P!Jzy!EOGq`WF_+_a}^^%k-Fjr^4*_y4D6kD#J9c=DZa6h2p zh0t=@HEJqLexeHYIYHLHS@bZ{~`Si;mBZ%;~ycT$@H zZ=CD(baz_P>-6?tAt~w*e11712&lyGR6j#-)h?3(uwNEHtwRiI(9hIz4DPmn_v5#l zcT?93`yI|jooziA?A?s=Yb&R8m(XBm2mn z7iuNctKC9nYa#e>d|}ainy}lX5Rlw?iHsZ0dl?f16#tXlzg2CXr4Wp;W}ri}{k`4yGi}UYljT zbGG(p+}$2%u^Z;>^?R;|>)4fyhLNGWxWmRGGAwM?B^P8iq53SL zfzSby{^$ZMFc>#Ig!6Y5#)fE9J(+o9QZ#30AxqG#h5i8@Wz+r1qt=$Pb}P7gPFpyq z+NqUoO)#UP*^du9?xRLJP^l)F^Ni}3f5Aqz=pEbM%{RYyCFVV;JA&s+R~#XMKU1Lz;ErHnPr+qe<;*r0lt0yaHM(CmQPE(~`}081 zZiKQj+ET5NmWU!8U<<(Y0c;&bWl*{9ubvrmACwN$osFH#$v6k489x^>T6C@!q^hL*8;yl0UR4mlg=Aq%h@_hBr@GSo5 zRFws?VHF9%fN*kiCI3K8Zl$%CFB5W^pGV4+{jh$-_tY7iPZ+LuoUi{K%PAKBP-qo( zl@92)F%^=y$c^e-4(?O>bGN}am+GKSZ9Iz=t1c~on!k3xP`q3*B#i=N;&dGPBA?Kr z_Hq%m?XF(Zx^tVBUZ+ULUhT zn(<8$A{(DhJ3D`&TAdXS^<#>1Z8H$gY>>!^#Qa`yd)fGcpr4qM;twj_<677mPWX)l zo-o`$F-pFDKD{4%D|veW^6n%9h$OIKDjdsN`;8;2ZM zH+#n-RpC05gS-d=DKjSKX!tZS_YAmy*xvRkFnVQANn^nG+#}XxBB_3Yzt`E`xl)c- zz^90@p5bGfjelCXO9e@h8?=7~*nMchxDhOm`j`wr*tR?Zks%<$BBh^j-?83RNxI&9 zEyIr95=uv%+G3u8X4a*Okjz_dJ#$g466fUYmdUBhvED>pH1^A(kD{533EBWoRs{>y z`Ks^AuMxW*DcpLLc6V7~#60Ec`Pt2|W|+41-A?>5NTTX~EV3aYimfIgj?Y zO0?dNLT|rJ*T;Ty=Q|rCbw)7J{f7SZex1x&Fca3&++z#x6ruc@ix!vrFNys7T|x~c z^%9n%6sqtbz>w%y2(sE+vOyF-yoPu#-_6EI;=5t#kWxRm2#vc`mqC5bemaHsokk=a73!8aVg%-)cmd=jcotA= z%({Fm7^y2h5KHI;2zfBW0xUFy!DYpyXE8VLM8v{;22C*!4To%LMS~YvmX+2~E)&Sv zW-R*fW+zqKBsxAIgvnsy8@)_1Ox6MsKAwT{?{nYy2-4=Biu@#R>_I`gQ*j zZ#*4F!lGMLgnqCND=$QpoS-EoB6N|XmV(eUty;JeGX^kV`8Z5dTV)Tlz1r3@k|l$Q z#z3%hg>3omF<r!X4yxm1 z(xL@^C+LSQEPfQmx|Jt4V3N-lyMFP0LnzI3@m_egcZQ+rtxb4EW45-I9U#QdD!cZS z!v=unl|<#CkF^!?6$6iuc{*=U`ywSg(07|qh`?(8QjP%Z{r%Q&yPEvN{-g4N2-V#^ zGWv5AD!eE+V@h8E--@a8?ZRvZM$-8MOP^T5=o}Nzuc=gH9{AE*sDVcY?AJM8bW!Lh zX9SiG#5l&6g6OIGiXaOsLBp8{_;onMhc8F@8+=!XZK zk7*Nj4X?wH3isl?VWfX!<|VNy1oZ{eXG)|}aUk;K(0@ z>9B5yJZjTB5K-9Lot)?U8Hy@0^wuLaMhtk9+Hm2%QC1N?lC zGLS5KKwMVG%EC6!Mf#PTB6i6@RFa+k%johN;Q_j6N%I)uUOk#s^=xlu)KF}dagPCn z7pamBfvRWT&)}ex&V+<#x(D)HM7|e(Qvlu~m{3Ido)|4%L!FPEjN5YbjJ+DpVhoHd z2f&y4=awFxYNA_HV;Z`!9xeya9(wiO#jtXNE!9d*wS6dOIUMY{XOj%4?iD28>bq{s zZ_uP!wJwrkUnmuL?rf}5C)_AcBQuCKF7VG4pICpDMB1vR7H2Dfc{s?ufW3yd{qWxI zOl^*NXBe58nf>FnpJ1+`p4<79mxorRStL6Sa$lJRhmkbuf|!E)bQgF*R zC?i(LMyXC8GZ`<#Pfq^`ju7!7UT7)w;-gao_RgaWBsef(`73llv2=&$r8}}KDE(=b zEG3S%V}pT5!W&+rh3R9R%C!TD9bVMjYz<$1Xo<{;k}`7$2^zjB!}#v*?QFKb#jVk}W;V#=hCsG!xY0$e2Ou z3}FR5aiTnUPZ}rdVD%DinIm2d<`Y=Z)nMPw6VfM^0@CWzv5D%G1M9FoY6H?TaYPef zQkficw}9e@KOXr%?(u)RM@!zEEk^F=q53ck}h5IaV*QpaN^4veb2x2U||8xs2IZ*m`m5ga4h(XaM#T7JLGLW5N) z-z^BS8TJ1&&L%g+#`Rw3-JJ!Yi#3zk9!Zr&U|*O{6y2bs_Rh6_=6s+XQH&o_!>S+y z;Nk%u0#;BlA-Gn(jPdsCd_(aBL$oQALD*;Y9-;OrD}@UY%WyBYWKvUv{*%O@`O^=s zZbJ?L3&jn+wao@B3 zV2|iFo?6WgwbtJ_p8K2RKlm{luCqgvmeC*_MTaHHS{>j{se$n0BZ2(Im>J0cfpRkP zERg!STKKyC4cj%fE#d;p?2Dj&ooccyo6*kzJN6QOkzUJ?TjbZEf_w3(p=7Ntovjwq zts%%aM$W&Gy8m6YgaBzspRAXVT%ZfzAtIIspXzx&8C)x34MvJTwq83PhvFDi*(`|9a;T*(VwT#jJ>3ZT{qtHklMUYv!v7sYc2lIL)KFm-+PE^-75+4x+gu9EVsdQ7?+bhwm+j)sGeI0y8gb8_Ib2wv29 zGpK`LzvQKLG0`Zt^JiNB5IRi4j8B1_%C!o}#`qguZj`b4*X?F(XAb$nbo3cG4jKZr zr1t-mF2*$#9ci!~wzeB=bQ>s+XB>>_Kni&1RS#293V&u#Mi^{x~=wg7g@^`K9f29aK>k1{6&M z2rw6BKL3)GcsU8-)AOHgog7?<6C{WTLVA%NPuh?}Q5!4jNu67=)}@I5y)6*% zec3^0Ha}Pq8X8RYR}FY?9QZC=$eWR}5D3LNHaU}gp-b<#eVXK8&0YnSJd8HSyh*0D zQl#JkM4`AA&MI_j&(S{E7R}NtlmLx1g**wh@eG$P%LEc~r8}OGAnBbJ+T4|By-#YE zbKau15%E&h^AB*|uh_-<;~oW9R)t3gO4HQuiI}gEc%f#w-~rj7j|lK2dTgt;#EfkS zL!emZjZCxdJIW%*i?6fv;Y%2GOSn_^q~6zsr*Z$}W})0dFDt447BW)fue8BMsc zLFn2S+pmE#JI}>}VLVa_={a|>?bYY}F*vtw%`ctW;`)U&MwkIsnxj$jj5J@k$3^Vs z!(=XfWHF9pHLIFX_cdjZihve6{&i`)(z$Q5QsdJ_MBd;9i)dU8QnBY!wN7(>ye*jN zHJzYS3;U5$)etLj`3r@=o@nv$XcE|O8yC!zbAlQYDUs<@)N@r-t;4UC6uhBnNX>D* z#YN?rcW`tL>w1D&Cgf;K1&@@HnMPl3qV(fRQbI298Q}aE<_383Lo`dQ)uz@&k(5x% z;<^o7s+wPfTaV$SW?EgJjh@j23dm&l(>S)?WHM6AHUE4D(@%Z_-Z1~wu!2UW2Y{_^ z$i@0g&a`Ntf8`%i)A_t{g=g6Tj?X^Zu0fo=rMjb5)Q@T!F|!1&^2zVDNLkCm#;sarw~9V_=`emf~1BiYoUD_?Qv4KV^N+y z(s4VN9~Zu$&J4d8d@%m%d6n;fQhtP*05U<8bz7G*=JoW$K!O)wJ&VOmTF=%J<*}o+ zP>ZYU-ekB@5jY+5xn|%^OQy~_xFKE{VRC^>V2n8!;7Sfzs06|@xXYASWe4dDD1F&)&BhvX~7y)eId0eK>BGX zoD`cGG*j_?HhYKZ!wl`2YD@p<{mZBRgq@dhzvxUs)QlRuNLRWI(HJ}3<%3kg!c=QA~eB-He}{9>QA=@S#MFhmFZj+>eFaWFP>%d7o9 zOUr}^R=I6?nFl|jFt(ou+R4&!D=36y^)9a6iKYHx7kM}eu_rou`czb9VY9%u-e94x z!J3SY1z9Lh@P(qSlZD(0py@R|%NPQFL-|_aoDzJs2LIl3zemT7tGH3$NNG*3+$p0m zb>pm`NkR|nIsz$IarIB^XRlp+^3_lzN1oS`ufw?N3ql`7mcP=3U%fdwDF7D&*i;tdGbj(m!?-F05ZSV8_t5ma&K|_g)a0C>VXi`r@t#u^=4J?XEoJy2V!xQZv5)n{PN4Pk7ZRr!+Y@=H!HwD zs4K^a(}+BE9{6gZRNkLZ4s#{b8KR-_#TTzNkI`!o48Y`4P?+$!t1 zgj+_Q&-q^)Gd<)@Kx5X1IY?CXI*2F&^#G}JG1Ohde=$lfIpFmAT2@~~k~5KO;aPxw z-qWAU_<0k^Y{O;lKl$5J;{}JI40L9+wHyAZfc=tPH`xPA#4Xn?lo2fY9;p*yL4oZl zIm?Dn%;e0$t^qNXCsPssCYGAwg04u65u!K^COEjr5w1hJqt)|f@bwf=dnDrl=ihj9 zFA>s3amP+ljw(Q;it-lJQ?>_%!^@eA&QMm^f8j2O0~%26qjrDVfo&Iua(YBhc*-EX zL%=QeNuAVHnh*F*S2Ur*M_BHIdcqf|{}oLF|GnlQbchq}NLaCpxqDMr1(?{R#C|fC ziqQM7_=`mxq~>N1BO5xLyx=K7Z&0oUBXyiGs{3>HsL*mq*#+tAOPE9poq=BlXYz&u zd$fjg7LA66BHGn%|4cjzDD8&#&PyEU^p68Ta&=#7U(gE%$4$8jJMl6f63`A*fhS|c zC}|q5&Nj?rn2178DblA1= zQ>W010tcAYAzREGeDg7B(<8 z?q4fnS~9Y#Psoj$h52#!?CRtXv$jq35zGBt3R!AqGxg+(u^PxW+erd6Kz8NbXqo9tu?u?Q@(_2;ZR%*Se`K{m@AE zPr(~zQ+>dm^^E>(i7U;vjOi|3g~oEPSeCO;l$VoI0c%V=s*uAW6B0!VW_*mi9{$Hi z*H~r8ED2sFKK>p@QNtZ^8}Tl9k2ZGAefz_1ido^o0qD%xYYt!#Di?@t7lWKBke`Uf%IKYZf3m~S;Sug zf76S=8n+oJ?+liTPiJ*y`>~Zv-h}*R)ODtfJeM=(4pG}f*B83!fif38Mcirq-at~< z$QxT%-H)H^69ehMxz?J*dR)oiAz2PWK7n}Y`)09D! zZM9$FLs~*Jk zO1f83EjW^i$Df?#TfNOPuyEqK;q!Y+8T?>%;=!}>HJN-gK{(Ym_f5gDVU-Bw2SLX> z+x1hLw02kBlS8qxh}+y1;dkM#CPZv~o-S;Sq7E$+yza9H7Dfq1&Cl5BS6ZTbf#D4e zhQU?Ez`AAJtgqO{F)T_2rqzFn6WJOtRzr%g<>+`^kSW*g4{y-DL^`VKJ@&;Z{qPo* z&*lPV)~lR1OZ7lVh+D>wZfb#cl&Rsvnv$^&hjMlTbL7{3TWo!YrsxrQk9WfHsLfEZ zkx*1s4$F;6&~dwzGY>|6kv63)GR{AK?P0G$Ms%|+6!_8zcWyIDad1cd{GCY{o&J%C z`tOkownBNZ>)EBIc5-)q*%o{-_dF4Vu(BKWWA7!>diHmsSUVyN(Em6%y~l2h-GG&y z%iplOmx=d};zAuTj1(9(YFAi?^(l1Orj4L$r|a%ZnaY>2G&WwM8B4V|Vll3v!fd9U zV+}1x((ukV_pi619h}H1Z2K`0Ac`IlyW7BlZ35ZT{^_QJ8|=f`2)z?n&X8qQ6-wIV z_&DGfBR4YJHaBVbV5E-%_Re0Lf#`rGnweyJW85up69S^3M9B=%k2{du`rF2pmSxO+ z5gA_*?#;fFEMKTCl9o;OGj9;yY+7v#NtIzB1SADzd+*p>a}*j_Ph0a`U%DI$SBLc)L49BeWW~&q&emv72U6;wx6lNR`={S+PTA z1i#uJD=H`{oz^>){0P)U-|Zlcu-WX>P7#LhREaw5+m9a>)l03Y*S|&Jlfv~4I`a$A z)!Hp$lhBW}+r9Y3*Xo4*s`W^p!dj6&n)@;MwQfj=5I4cV0rqaQ+Xqj{CAf9ieNqe9 zLpL?4_evLI+m?ZluRUfl7{O(V8 zCtgDpP9oGPJZ`iM%h^MVe^U_{+&G4 zy7}pPPEF40aGnr++`^nMh|(Gy`2qhVB1P@gOIJXF2dWd@mkr4X%v=`Yxri_^P5Qy> zlC$_9c3N4Br^NFZ)w74@grWmbTsx7%g(nY|d}mxsaZ6N`nYAxq&&C{vow-MPWe4ay zJ`Jf7W&+n{TK`EyW+J!sy!HL1E3QE->uCoYNm3MY%!%wQZu{`a$(Hr$9bVP&QPW!a zu=!Ey^#O)a-fFbetaOx__1r)>nU zvf{In*-5VkBrG7HacO_2sw-~S5%i#w%Ly;fGubA;Po4wem)8>9dqX~Rjqc7Jdy?>x z?xwSMh~mF`g_9p)c)jai5ar|7vJ?5+;v2qgDl=dz4z0)?4`u3geE<7t%Xf?f+Y>GO zLswQcpONm)<$XtIK7!UyQY;m=g!!pb|4VX1w=07B&x(S{B|!`9hM4LA;DsmlR=(ix zSb$WQqyM|UgNy~GhwvGkh=cS&GQ0~Z-2Okp-ROu>W+%^zwmvJ?{`W-`QW`r z=;5XV@PX4NmJlJqg-3$0HQVh#NF+3bk8BY_AI_&PorEC&S~PL_Lqj;Pek|0i0`5Lb zwGA*!&CsUzB)tiPzyFHPdXFSKD-P?gT&tTHE#>XeJv2yD44!b9vSDLkD<%nEn& zLigL2M07COg;9-5qS?jH)$SijoNgX^;c@=+kBShfFQYC$iODesEb@?`9&Xwvy|l^o zL4x}?#RJ(+f8w0Lwd8xlb<4UdjEN={)x~vF_eulg3Djip-5DZFK(4` zWUAAhTAy;4p4z<&gUJc!hAg2zD}jRaks|>q=#d1B0LCE@7EsEK%`di~2=Lxj7V;L% zd{OLdQJ}7J@B@Pv1V*lxcIKQbvH!T@<+JX+)7!%l+QtG5)%r5HKP*JU$P3&$Gr(}k z`WP>DNB{ldeCBBhodii*d-&+&&(h(6@~=*VShITUvp4iJkrN;KQgbgj9*`H_Ri!k| z8NMcnNpWlw@Ict$C@Dm6CJ*}bTW%xIO1b%7{J9z#ox>@?tWDga0d%>6)vDF!$dERg zEM^Ef1T#SpJXWc?xWPJbDgFCInNk*;1_OaoJj)cF{x@6XAXaA}P_ z0Q~SJ7K++D%pQ&H(Q{i-#6r9{EI>=`*Ks0C2KPaZ^yQz6L{yxDr2kkRNPAm*801&W zdj}(je_1x4Jtnr2$|I_rp@mo$JabIF#Mao^WukoJqoPTn&Ex+)m_?#uUm=O-w;JeC z2AD)|tTWk)3YzmL>dU`!8(3jnpdusq)VjkP-65BVLx&Y`S;v zFPa1NV67{#hSJpK_ip2hQJH(a?8SG-o-&t9vk$GXtIfU-kErf4ahm??n}Gbka9;?R>rsagybxRq=OWeAnjse^l2G1m{Yeb z2+{M;!Y_c`Cj|_S6}&4gaR6Duirhpq3u*6k#r=G;7Tc^~%xRlNa?Cb?W1!LT;nD#^ z@S4M&sMhD1%*HW7K+W;mQHX+V^ETZ*@mCJOS zA|z6F7-;(LCacEfKU3c1g+AH)xb9zLG_yB1 zSz25AUaP@}7Y5Uk%3ldd8@=sY;+}61Vvq8`>h`>LkW}^6bg5RnCxp_-UqacrMSYQ* zZ-T#H!Sm6L)n8fyk6?~`{Is*hk@wvxVJ~ZJn$pltS?>MB32v(yPGPDNtYiM9?!PuW z@?jtTP>(Vf#5j6JOUT3CVhR-}tlQh{B%~ZzUE^Cii_xqV&J~EU5;@#7lyMU(#Gv8o zOi4+lI-zBffTH5yg}0R6N5&p3@M4{lQuQ*Y)o@mwc^AfjT8#Q_6VHEiD8p-$ZXfpqYI*nrx3(RS2<1aUf?bM# z&-h+e&~$Ttsa;hIpJFNEfasx@F?-?k13yx@6_)gL-th=v&O>T1GSvEs@7Ktq8XCdByB#&olAfS~l<#t< zUl~TyzUKTq6Vw}Fs$hdy(t84LddIS1Zz+d6K3r*S;yQoozee%&$CKZWo?B=O?~igq zS06V#5j_iKJMN0X<>phVbE_6 z8{LcvXp}Cb(+9)nJ`deES4&oT9s?uj zAjCn?u6PT%%>E?x=gd>Y+)72Z>z8Qld@%8a5jveC_uDJVj|inV=Q#^r4qP)A$ib|L z5mQc;(WPYllmZPSC-2C&crI~PX)1X+00s-7Y`*Z{MfkQw-&$-?b}|qhi0r|ZpZM#P z*JnLtx`pIO$$!m6qbt`BXRcSqpW!m<$sF!4-hYnYK(wI9oIJ~v&lmx+-5R4F5e%Lw z@uAZpvoppla;Mm%PBSXb#SLz=F-ag9Ox_2Iy0=@cG(KLLxchsaC`>=)k$x;n1YnNu zxdptS6-Wz2P3@YJDX$d zApM6mY2U+b1(BoD@EYNo38%RcW(IvS9vgIt&rVZP9@?NT&IV@vbah9;uc-t`b$g4ne8=x&E{%w_SD5+4KkGaY2_mcE0Z3b*D5D#}<94H*&mBDXV$4x6eLBj`=r}{0Fn`}4<@a}cHuV9ddCh6Y>tuKTO`Pfg@Tfp!Ep`fF-x{;O%?IyJ_LF0_?VX2Uz` z4(fzaWauNnL*|$AT!Ny77c{K7H=ozbyyHy*FJh+)+kivIRTa2f#o2JigRz!&)l&k+ftnl z#5BH~H|_YD;aFxkKO3wm|9EzMbrDVBi-Nmc(1)8&&$Q+6AY|jTdF>|E+;X$N{C%@~ zHhT+|USB*2)Mxy;rFZ|xLuEPoObRJq4vR+UTUQ(&)%lqkkZT+8|6WUH3rA3Q2TSGg9j5BZXYtc`vWk6pO^iIly$^S~Bq^aF61g#)hhL9WzqZ=v#(D75T{c28*24y(fNr)B z8|NkCtU-;xSFgPWJq`GTfS6luxAb(R!kU^OINcEN91!UUxc?V_65Pa#+axGV3zoVY z#?{_+fHIJEI!52KpS)vzqx#)gryVZNc3%caw!&BL%Q9Y=<0&K}?7t)waLGUc`^RT0 z>>A$>rn!QHnn_dg)$Aw-d0^C`aJw?NLV*BNdYi&s1Mm2DL;G(9&PUq#5KC3n>pU>t zb8zOz+{DLP>}DBiMtwG!i~&pyHsZM4^M+H0u5j|KxDKQ9gj*)A*VbMFU@E^3U(vlM z71nrGl$0fEkul|XuZ@Ebmg+Jnlgysd8Noe{mozCn3u#48KgSXcmGUev?EuhrfXVt0 zSVw`=82G_QtC)OMkU%k_1S^tCdmn|Us+n#4t$s{vOwHSem2$@)aN(C1!i(6SoPd?7 zv)rRkXZRI#eLtJD^W{gtAhC5#UjN&wz&@E|z`%i6ErjP8 zfacXbVq_sHL#!g}>moG&388Y)q+AM1YC2=jw_3t&FC;|#mF7F0?}to|6%~DJ89QlO zIh;Mc81_*B`0+p`GdI6L+YhFfR_7h&o_>SiFH4TW>C$<~urMfxHf)&JdH}uF_`)B# zp~9HFQ5xh%^xO>>H`CC(C!fp5I+Q~eTa-t{jCf*>4-LqC?iT|IFy4^QRUo?A=)!-r zp;EK5104dsBE693?2>^ECA}aO>ST}k;*;nnGvNRhWAwT9Jqq4<-$Z)b7QeI#C<(BM zhaj{ZzdDXUnhxnu!yOafT99;shG3a;0^AXy9#!o{5e9R^>YbC+wFevr9?Gd#5fY!c@!v5@vQ4jjAV#avtlc0a-AyI# zZj5|#))pK{jjJg~K^jCkF1w_PPdV^%i2l9bH#5D?muWqlt)R_f8Cs81!x^2Fxpk~P zrbgeQ4fLO4wjp8Wv84W&>as{Y?z#U(^Lv#iCpC*vv8&XGbV5F%q)9%8X{zJ*Y5l5Z zb7e2t$L|3;v`6w-rAR6qD@ze*R7`hh>m|(dHW9f_oU{!1EqKULf1;`9a@I9dSqak| zc^JVeZBKlP)J3{)mua>T`dj8=5EeWbM^QeXk;Dw(65{dCG&FEgde&1Z##{pj_$CgB-~GIASqA}gPHr`#KorJ1`2lI$LQ)!<=P%NWa9B=Jgsxq zgOpS(vmflo2trc$lj8jfWo*MKgd~IGTQq)f-bv`pJ;jeo6gWpRPMeJ=s_S=w4LLCF zp;8}kt%rbi%7M8YK=ZF}5^5qTquHwnE5-3pk!@)qMFLRk>G>cK3f_1=c*|C3O}2C) zIkfuf^qHCb^z5f+sVO}uTgK{i>6df z((iHe$M{Ao0t#Vn{rF`n=tCiR!`&(vryB{)2%3 zcVB&@oN_5STAoRM3IQ?;<6m`Yh5NB_@F3tG)ihnH!#G}}@6z}ZTA2?-f)5&cu_r#O zL=I5qFQl`ZUH)wMG}Ch!ZXg3;=ZWoLkVK@L_#AMgVgey-mL;3SOf5qokyCfVu*E1p zw?ejJcxU>z7UfqWA!&upt4IFYg%@S{h==vxIl)_Q_L#tA38IX*`8m?c2}4lR*ER@& z?o$oKhc{g(ofe(~3*Fm)pQ@HT6LlrIj*`X*)|G5`K;5&lIL}doVSLV18!FdY`-4&+ z=_AEb_jqP=a$e)K_ z{q^Zfr$m)WzaSoPE7QPwsi^tfM{#xJNLblnCkytBw<7M9)vumX&tad7#r~Fyc@$s5 z1JVaE6;>tAiG(OkMh^KmPO5ufaeKJnhXGC1knOUmpvt0Hvfq*)Do)V4wTRqTHFx|t z7Gj+%czK%m3Kgw_X$lf3Vuwou(l1^r{qnlSM_g{hD!xGAV?%@)KlM5_UsRT4jG`UG z?G2ukHh^FC?$au7D6nn%{Q8szTMUur_$1170d ztdP>~xNt_h%!;;aJg*^ANeB_7&ivM`(+sXL`}cZKwn&RAT()g4BYEcDt*>7ZQ*Zo+IQ&LN@j|3eqVsDAc=PQ0#2VIQLEh>WjT+AQ zxUmHHdlFd#+u46?;w6r4_6R9G?P=P3@Irndo?j<@_5_c8tI*6cQNw=12wx4-y-Y|0 zmbNnd>SLUDxJt{qz~(_KG~iInEmDT3E_w#71yi|`;!xl?e$V5YV< z0Z{%(5EHlPi&*iwjynb0HN!oQ3XWDIt}1@%ka;(M7Uga}-Ww+_3PYc1oSH|HAl zktqAW-ZbWg{{+S!2y;M6i}yV7d*z&y5HWRR2;xJum!kcAcHd5E`cDlWxn}kapsDne4%6|7|-qHblDC+3C&CDD3Z0*(5e zUYsv&HN{`eF>*lNfEC?^-&oY@d8-BskeH;E$egw767x=lbCC#qhDiK56$$`MJZiwB z6$cS^yhxZ8%Tuq~>q7qsVl;SW;yt-&@4`kbfA_=xGm8Fw#?NsP4(h^+567aq-_|2y z5($+3_OZ!%6qW+JTsfoq90aiEU)|&!i`Sd-(eQ0D?e$zh)mr zt{qmNQ5Tuyy%-oei}C_8-*$JOHT-P6Lo!&u{)+LFlU}uw)YGKA=%= zCwTmonn4>nPG6hGH^m4*K8!npguuPDo&J`A4N zePkR1t$Z&XX5h7+%eYnhWOcpzF9zoHs#gZ4{(Z;;I2i7!ZXkqbO1Qnan#XjIOpIt< zzAy}FUP((`nWka>F=HaqbvPp6w<|SDiTE*L_E|^LxfK&l6lnlx_Sga3tX|#}$$gyj z8Y-tcndNhkgv8!kD-1?-lt#ForcSGN-f-}&)vA)Zq@8R9w2@TjGlTamfzm(D_7yig zuL=ST*wqCs&|HMGsY^4!Z^Aa5vw@E2I;@P7o&{0t3wG@V1EviANJ}Q9c7K_GRq!{n zdKXVKY++%?otc?Rw2vz7t3ClG0?6e6mac~(ETC*=zzuw}0Ruggne2A5 z=T}XB+Xuz{{XGeXe7FkkFJRXeaT@q2ts9`^pOJ7z+~K>!|w;LrWe!=z!9BrLcO*CxmY!N=#j(YIFmAlp~AV^?o3tynO(;v0~~e3v8+ zJw}J?%$ER?Pd;j#2=+jgVleG*pMEir2RvTFX8<3=j*}$7AI{FF%gjE0l4@7EOj2|o zdgp5<>)s?rw1etDM&NdxI_YZ8%<(HR36us35cneFQ5_@bFv6L|?ulSXaiy3PRzDmj z{*odx-aon@VbG2pIT}!aBrP%2;05$sb#>*{c#0t?I@LGM>KE*Xa)va6w!E#ZIQ--U=oEvE&=UuQ04iR z*}&fIe}VtcfPUeJJDWcYp2(sl#29?a%1Gk$m1%MOxBQ#TwPmSqULfsXHQA)m*hOr7 zm&Uex?6CJP2cCIQ@3QwB$IjIw^Qe=w?E-Wk3o(rfS2Wg zUa|vmKv4rgPx*V2dOrE6XRJG{Y=Y#kn+gKKpMkxdq*oRESAis7Hb)pD*8hW@O|3N0 zT)~8e>f4+2ArG4XgyZ0u*tM2AM=1j5t3r8<7{GY=Kge7YLMA7^Q>s1IoeE;ouPHXR zsrO5zjwwq8@e&QXx!{>~`3ZPdY(nBwL97A!8qPeGCT22T!w29N1ZZ-XSC|*Y`mbY* zpgg@VK7UJ<`LOeySQ)+P^vGayfp5AD871=ecbSAMJ7;0RA3f>OU)5b|5@g_C6Z~5b z!z!g2T(7{ue-;SqfjC`4%~DRf1&#?Ep-EnbsUQT}K}_Y4#5^kQzf9wOM}Ve0n?z$y z;>zqK0Y-hkowO|nVJ&FF0yG_8X74wWW&xu>S&PiNIEf6S2N$yI4h@S)q*fo1oHvt( zLTf!2$8$e!T?)bnlyzu~*uePM?(6}u@(CL76Tv$KRAd5{@=w zs9bs-Jl6sfKbDp!&^)G6vj zvNpu_L1u^c6*U>04Lr%%S7@(MU4dN9y-p6F?iAjgpj+^yo8?MesK>c)8p@eyxY$2Zfpd z5TErx*h`P4s3EWS=h7PJKfgrtV$!`sVpiHC+h=8ufIJn(t6)B%nwG$!#UL{T^G~MG z|D2!(t+?LLscGYvYo4RQVlp_B3_!M?%UB-sbJ9Wk5=yMC2{hGk&@}3F2Z^H=BfmkS zq@!a!j0ngB-pS%{VC*sjXqLz~9W+KX^dwpV4T?ZEycj zfty%bOn{ew+J3m~OVCc?;Fcy=J%4|?pfdg^C}Ug`GdaCc)x{+pF*ZLd?9C0b-$7fX zS9m5XXJhBF!TPg(Y3OF-(Fw!BGfUElUfnn*c2W-fxchyu^0DVo_EWI3nFfxY^Ff33v? z1nObpC4Ol0!O}-nCh)HW`sIbu2ZA=|fkKXY6+kjU+rEl=Dn>WxCA`y%&7aK}ft@jd zS(0m3a*E6HiTlvi$@in_3Zt8Pfy;ODeEy+JPv42(h4tT=1PVsB0A#`+F#vx-^6kt} zq}lefLWLJ(SOT;7Zed>Lr-^I#J69cX<#@r_o-yGeUgeY0v)n0x+f*nFBU)B9G+zxJ zIuvta@nMjgCQqK+_)cO1Y&>EVpKOtKG<%{s7bES;$+|Zize;|t7uLng?t$h`H3R6~ z;F;C$rkTy$%McHEtjue=;E$9mEFU~oZWQd=x_SV3hp`4_J(h5LBs^YrBO#v)`9p*H_ zxeDk{!1(?JG#EsnP&k~cv2naQo5BfeZN&+3w$^$SU5q|@pX_(39n#$^nPCGN; zuQFjQ`-0eZ7AR79cE2Q1_6|YwA*kTrxKw`leO)Q(@3w1p%DM5>Ri!Lr0H!Q7;EC>H zk2`so2J@Aj#_z7CYmnyc3DAD7VpV=NHf7^h1^sdw*Yg|vK1N)X<@O&A#&^ncD!vmj zt8qZ_2ns|NhhkCXAtp30JXm57oJ!gYH!`lmfrJgZ^;JpLuf+ts%oMD2hPlc8 z3KIY>gXkS9>E|ZEdV+Nz8#{iR1OL|ST%v|_d4mAGp~YL`y*@Qr7Bzs`IshLqG3j5O zK=v(@`cY%~j-7i;r%Fq3Ifsi%+T=KC5;$4rDR70&`tWjaMHZR*JQ~1mmh5K1C|C)) zERZ2nVA8*wx$A(x@{a6UyJny;0h7e{;qML1Vti=ib_t+iUJG9MNs}>NwCSsU0oA@Q z9&_jVXVW*p5cr3<_zdtcy!jom+ggjx|b5xTuufuo8-%-aePceG_0M5@K zU618z`Z;){%rM3PTtPqg6zvKZQ;x%&LqlXt#A`i^P=T9DNoz3y8Lb?opT)QBhs#tZ za3_O*ZR+rR7}Y=2i90axzoa3>hl6jTsGtz~0dCd%GoZ)#Z8e(9}(}785W4c`0)Uv%?)p z{LE7+0C$N0nU(pKa@A{rH2(XLF@ixd2QM)R>(AzL0Gnb6CQU$k;E&?ve3I62UO^vH z<+0eA^bDu2JEFSaq+B)c@U0GEnc zwHP!nQxJwO*`=TmO2!om0-O#dM6? zQC^`;FAMiZ`|BVE`pOJj@)vk90M+LI@*@e(!{a!0L(5X;1|$3;ErVi1`HyM zbtJ>(3>jGCN?U{c0s3H25KKVWiBTLzRc;{z`V9>5(HAhCOkkr0@CT5rD-abG1+Ck( zhNPrqs9ie_cJJN;7cN}TEKe%-ZVQs>V%df85Ia0xs|4g{QvWmc%RpL|1C}4W0QXmM zxeT<-A6lt+H3sCZjy}QrRy0#^(Vnx=rea}Oe(0Q2)DRZ$MVFMhIdhCbmFDhgdKD`K zf20vDs|1Ash+v=1R2JlGj;O-T>m(fUsT|PMg+T!W$h0_%1e|yc;Ql9z{+9*M$*4(4pPtI|1ezg%IScA zA;mBza81w(Y!d0-NhS*H3^?(k=WTpu%+-T7d7S)(iR+%ylPVa{qoj(nnJ18m;vRPH z85#g6HeO@kKi|xa+qG+}FoCOA&q0-{RbcbxEr!_T2Ppc`167JD;E(h~TnXi4Y&>-Z z1})Z)xen0QAHo2WU!xWsjAglVC_TmsWvryDMhWo?Ci&y6J9U{&jaIS2!Am#c+nwG) zEA@)!h4d^-@XoXN+>cw~*wvd-u$P&DjLHPj z1cXA4UO+%3mErxD@3b!V^x*qT3}7A71}o3~@1U%K&tBVPUlD#=a+Pk2?(LY-CtsYyTWD)RnNY!zAf{A?c z!6e=U#IylidQqLlizXM$=YZ#mYgC^>9d`}^0Gcu|b0|jkpwn5)vAr6&B^)bOstC=R zH-j^0&cMtWAIRo>td{)7LxH}#D*cNM+`UdI7_%}c%L+SnfZ;9f7=gYBH^9*|SY9Y2 zTl7TJ!OOsZB6!wDzEA7h4_q7uW%ETVOR{>2w)tb=YH|jgvd#bwPo6yaq$!Tf8hhsY z!wTsCcF3jeZ8>w*hq>BqHqiUasTSZ*eVzeWUQ9rbcxN!~^O`}%zfb{SohY4k8oQnk zK!0isfgsE{+dx&b(xDj;j+zApB6Jh@R1wP;(5)zV`m6bI@@*B&+LKvv@-AmMS z<(tYT(hs}On(jmF+3cK|?l&`a3dKag%+7U{c1C)%W(mb-_{;utFu0kw_3Dk^_Q2kY z?yi)fB@Fp7OW0ph)0umb#{2+PP{N#D82uC$!mUA0`(a|g;UamY@SW+T8!Y*#F7_eA7%5Wu7KUe=BEhXiBQ*0I@IwcPC>4`N>@LI!UkV2-ugf z>v~Cl&?$Sr&1U;{$Mzis($msH4jnoSNlD(5QKF-xGjipMc{o1)(k_j6T4nP6rI{@M zJboGe)Lw-uP!kXjLKwFl4Y!uYs*}I;CXM0j{C+=faqik#(W`>aZpa=aB|BlRl#Q z;F(-yDtTXr%0=Da@4jczSlE8<8a(u+X&b)Q!aE{1ECDWvAP6QPbng00789a?cq&JA z#+rQ>5#U8o)&psyx>hs*vA)6pVk5(#eZ``YWS7I3?-1cPyw0hM3Fy(XeP`J3hP(H( znNkD1E=v15Sf{B=rVw@AS~P#)xz+4iYY7uTX-_&?gwjOt|DV^nJ!TdB*x(V3{0eBN z$7FP}Y@K9}{nOLaF*UY5JUko< z6fAJFNRc9IQd3g;T)lc#Z#B0llb^_Lg#l}n^@p^azp@AzkGSB_^Xn;cG&{nx!e;qv;{GbzZE8^ zIz3T3rfaD>zRKx zf-9ROFW59Pcw`jut!8C2QVX>5*<*@!K6UiVlDc<+_o{^$K6_5*UKq38&VmU@t@E#^ z&>;R60;Og?AOZa6{aYG{f&3C~e*~$MC}rG`Xh||cr_m?_X+as(vUq=ip4CYgF`5Hj zl(I37A7|#KkhF=B92f<(Q|reW&^zdNM&IgoW*~|nJkH}T>4w#>mxU~iL2;xd}qtZ@4Po^uwH zutk&VEI&0%f64FqIq9HtD2Hm%1BmZCe(S~tPq1g);mqAB2`adv#5SpRiZt~{WL2n0D11J;7k5m{r7e6pY z%uGEpzSm959glS&)^V`+VaVD01af+dmSfqxOcHj7ZfT_C{bKUu$-VUMow2+yI+gs% zC$2pDYb$#ii6%IRu4@$%(<19g|8v3YJbr%$zfEpNqy?_gsf;PS+pxjBTWj#vAHLa? zIJRX{1Dgi;AYsxTbnIdDRV6xy-vAw-Q`NSV0a3r-PwV;3*8FzOE z==h8nDTPr`n8d9s18k>(C>n51_uEVunvBlb0^B)_4OiEwt_Pq!myGolbiV$~jVBBx zUdGPX)L0DNh`NKzK|j{S_^_l31Zz3Z(QA~n+Bjnt{co5z3j+*r@}#=vH>Q_BUmPY( zL?0Edo9=FlF|GFV;E(B0FoF-~88l*me~g_gNA0kY?RRJANjF{L#SVzIIMRl?ZF2)c|4aA*3J@ccWP#iVOx)Vv zeB$EZN1(sPfxcNi)n=k|Bltf}hd)Vx&ur2uFERo`0)IYL_-b~pwaNstfhyR*wzPfG z_7(e-?G5dUv{M+(noDace{yWjs1+Ah^m2j|foP|}?_fv9iFRA4T^- z$NsH1oj9PJ?*0B!!io!8c)rV@v?#1h+-W>Ip&T(yvnE}fbMgMhF|o&l^HUJ~8y9E{ z{mVTEU5a;s#b+18>|-Cp#w#1~*)W))ug>p2D-otpibu!tdW(Glzgt(c3?LZ759`}F z+Zr>jV#@c2SwBBd=c7poK;Og+c7D7d1R6Y0o9x>3@0$Bw71P=HUB-bqy1O753G4Ss;Ej!rB$_}>o}OysI++`MYZAvrV-fkam zOe@?N!?Qmi=Cg&Y4=SM*qXAvVa?csQ5K8##X96r|%1CCaIR+5ugafsYf-CjF-NX%F zG|~92Ny@?}RVSUjPmO**ssBSjCxO<`p+nc2e7+ADv-m+rn%9!{RcRPGJ%)J= zUaUA6(jDor`Pydq@Q5p1e^7;i3KMv#(hy(_pcI)0wL(=(bLJM_^nwhal!fO(X&=>X zbs-R=WI~w?==t%4eR&cwAAT841cq+1A7G#kjSEM@jw@-9D&Yc=4E<{th=7gpsWPyv zKtM5B01uwfls1%vh@x8BC`$f3DaV^JC!jok5l33fg&wfj7jV+$c{f7SY(i47wMo?`)@Ug0_6coDgRB;R6rQ zcN{ADet7%}F?sWjqs9!PN-^bY{$l`%=ZqPEBx#Vpb~5CgU8oUNLdokh`mm)%GYs42 zGLTtMW}x<@{tctwcngfPCz@Sf&L@qq2@{VS{IM*%rmA}bg=*I{n1EA|lCBFKaxFdo z7&23I)_O<5*4fGrhJXYAbGo0A$pXK@J_3IP7}ci!LpBIuI7b1R6llU4%c+3>AD27> zwPTBpP&mTKp$Y22TgGALvplU{T zvvNic8O-%nB~lR&{va@c0UAs|OEpf=VqO^DY0;U5%tB-UvD9wM!8heWXYedL`IJNx zvys^cCI7DMyL?OME6{z4Q|BI}^*{lbfD~cjO8OOu4fC~PWI8h;Dbo#pdU=%Ua{uS* zoYN%+`eR7yilO7Nj0xy5ok-3E^d2j}g)}8TP#yd6n*A zq7|TNs9)7sn6Trjk(VzQcqU{XQcVRL$HemAWPwh7j6PX|Hh)#}SA78VXs_wrob{J8 ztgpNe-l-K{HsJuCPj+$4zg2?99G@2>f&=5LBmis-<{)R!c!)haj*Y%#!3Y)ZuYSkx zu&hu$nao6o$>fuV&NXAekMH$1X?>+QA>eKBo=oas;Evxk+= zr}Gy?@CT2>7tKEVxXUQb6eI8-$OeED`Z}!wyeazMiT}ay%EMtmdCw}PxF2Q@gN0`o z8acZ&Iq6e2Gx!D4+RXI(QIcal29U-;Uj?ao1cJ>pw2Q{9>_{35KGd;P4D7j@22YmH z4STPpLG^qQ$}yh*?wDba-FmV!M+288q4DU1@5!8+Oyg)JV+>->9AxsJZoIOwuVV9e znSE5kC_gjl58k2s;&<533wNxKp-`8}&Rv$&+ei$0DcSs`Rvr>uu4I$2w}Hk;Ju(}r zf+x#?4e3XTuy=(RaL~b@Jr8OfW9gZYT9QfX3DN*R;e@S;tbBD62+oi+ zuddS@Nz4OHtiKV^yTRYBX1N$aZPK`gt1wGrwE>x%rhq35{S_J?sJ*@#%y&1%00MLl zZ=;T==G#EDP1lhAvEiS@i6iW|6Q_RlAwL1%VMJr-bhrh?YXTBL*OKbYPcaqz%}@X)ARI{VoK8LsZ|)fda~u2*BJ3`K z8Y6*5?0yXn-#pCQn(s6UQ%yhw@Fh5~7r`Gu;*c&783K92?F#rKA?Q!vz4BrxmE zoyPjjNeJ9a$3Kv{Z#H-`?TYu8Y5Ms!1gW4{fH#*D{ zX8-}_qek`V0glt*u}n8XH}xZG>@E9$jyhH(HE_HjE&eK)c!v1f2o{!%(<`ed&0o)k zPAf9J%94BbB9jo>EREJPZbNGClhod=IGev|3cv(3jqY9R`!nA|oxHVSXyu`<{&#o= z%sc7MA9xsalA;BX@IDoVV_g!$J-5y#5p#yk0qPI5L0? z0sQ9+;O|Aq1i%%Ko@~ODbC9Ia8KjjTG5~)p`?bcn-a%%PNo0aj6YssQ@pBzi4VTQA zX#QwT8ax8zfo%BJBe;96An6<5TlGVrFXtO#WN!=Bgyj-5qR>T&Gc`{A`&BrURH}O)JJCkjv!L z8ayGBK;S>{7D>=4Bq%J&aAeeabMGj~9hN)G;dF$&zGtKo8cINfWp3Vr=eY;G!w&DZ zP`3nWG-EWt9|$mcia&Tgy||elXXQW;h=vd_87T6I2>zuSH|qO~Ic7E(Bnu|d zGhAS0JsApo{a_>LJA@GKodoYlNKpguT0ntG_gM_^4^vz6QC!$ohw-`z9CDGq{on0N z3(7>WBjbB1w#*76=RPVi?~EpUDB7kmBns{N{;Z~i%weI114$jQ{*ak`;(EkS{tD_u zFda$|?Ek?U2r3g;MXLHBbokny4UYN81ffk80|$nrW~FxPzjZ(*8cYWWFyFsL;E$kv zUNGNWuu#1l0oA>1YbpW%Z6M5U{iH?V68=8x#xb!wz!RctM33rR!&J+vREpt-KXTWa zk{LwC0Qwt@+oc4AW5`4_oHQ2MxI0em#*cH9zHvo5w8a+|n4grwgzdG3Fc8PWJO=jTd8uFBCnn(41`PaP1NkCOXbb%oO+kzaOvnxmASj+a#t2l8 zzi6N48?=%iKaCQ@Y6eD`Iud;_y_4s3+Js*l7Vmpq!_;Htd9deVjK8WLw=`gAEe$0A zb7YeYg5AJR^6GnQvMqACeNYpaK$|}dq=bnwX6YZ2hBsMhOWryHE=M0qk7da0gJ3ou zBq5-80NqO(ANIZSB&gx{Ku6?S@Lfp9M}pQc5p^eR@^vyV;)JMIBt@_4FsQS{y#^rK zj`O+1aho!u*nqD6+;)GFoL0=CTS1#QZ&udxXwaa6Ujp2M1q+n=-MV%2tNtUOZB*(n z?9$dle(_4dk__5D7O!icb}V_PfOxQg8=d)~A+``DbkszK=KO&^`^!HSFe-u$ z(67k6f_?B;tJ#j*?l1f5N6YbR(=;IjG$NS(!HLY|8Xq)71 zahONGJR6)1^n4H;fMXnU9)o6zr1F!5x@JhMD^iqB-6$gv;0LwroOC1&H+1W$0}O?M zqh1I+=}<6%Te*+xH<-%T_JF7%Xgyc3C>N>s7)6HBF5PHfozsH(dKS?0dj(5a1%Kh& zp@7Bv-QLv@ghU1s@Cyg9p2pR%4)Fzo(B_|jagsW%K7+gL2`AjHHiO9R}*Yq;5Ej6ei@**QN6^P*sSg!Q)yxS6J)`&*gK8Y#aO zx^9zAc5IQ**{eq5;KVzO^w}iQ2*3pRJHR_vj7qsB-z~31m}(7jM>B>}KSu!V!wmRH z8k6VzT~8yw0b{{;98C2G)42)+*qzzAGaB!1h5U)mv&;kt{PA0?1y`h#Kj80oeFA@J z{h%@c2JGljRr8BH#!32Uw{Oa>qpFW(I)f)ERY8*KrQofIt>r60zQzq8Gw}vo`h7JN zYS&uT1ique@Twp6UnQ-9x1ZF~F`u+BU;sXY#VYNa*2=IG*qLGBITu45=Q>&NXMC|2O-O^LCc8{|(r& zl5CQnXTeXrK zWXG23GvIgEU)ZPdKz8g?KiC=opQ`jPjgD7Xu-7SJUuVBqAOUY1M3ycGQ57n{jlKIJ zceN@IRk{?MnL8g!^y;qGEfPR|{DAz7#8jPkb7XhN*8q1be$IfNU(aI#1OG|v95$Sd?XNWf z{9g4L_)lWzl=^Jnus#ENulfx9C$V!%eYU?=$^e42rsiqzER29N<|c94L2XitK~O)H z8J5@Ads_u;At6xU{!S2Ew>BgnKM6N>?}N*~t%T%#Z2Yim1mG=5n)0{)g8~D?fz6ymj*%E?Mn6*R z&wrAa*MUdxy-P=wrU(iG7O;yAv498)qM}&n7DP}H6bmXUc0~PQ0TmSxkfxv%snUCS zJmB$aa{rynY_hl6Ye^{jXZRd1xx2l)?CpMM=DV}A>2U9tD`3mvV9Kl1#02=D$N6wD zJ1<&FtftWKFtxxR5T`K^yJU(Ek}ZA@*2x-Au=8;oWhMKIS%oJOgJ2j3lviYvax*zr zf^HQA1NXULT+ahPKVvUJyLX#;T+Ym7?eP?7HB^^1rSf*J_Q~CDm0u8ib>9JjB5t9 z_icg#u+3$IK|z%=QenrDFoj>eL>g?#%#MYc64MC3L5A0vG8xUb{UF`Lz#j)OJCk8q zOSbl91t0>y9sOg~N7IvnFqxR(g#y2l2xw0rKN10+{Z;-Rq=A6OD%p82L3lln_5|`H z5#ZThWqG85czJeSM0pJFM1W_1mF1BJ;^odRDCoc7d$VH@QGDTsT)i1e1 zB`DkdN*%nl!a~SiyAHDdUM*Pnp~o(Wv3$Ow_!HED#Q~xRm~zPHVV|s_q_XchZh<}{ zB@u>qYyf}l&4TxS-|8CZ6TBYq2Q*9Y=fIBNaT9^wY1pUe4RWAY?TgU^0J2g}32>hR z@W)i#Hxsbpz%PzZVKzIQ0MtM@4-E;N&w^@Kgg?C+@c?4SyM~&Cq zBjL-f#+Ck!l>&;F$V$Mi2Vf@I`Gs+PP6qxswsR)|xNBkHA5(b-{-N?d8%ie{d5>IQ zvk~^u(sx9{x)`dNw|hGjX?!lE*RKbe3x9*+ty{vOAAf@VU(En55YB^?Sh}2OnWIJ3 zG|KY-$PdSWJ(GC>m_d_I)Bc^+%?v3YP8K`o6YAWK1i^pY73z8UCS9W(RZxj`o%i*YbqpHsm+f~~QXXC6UJ z<(WsYHSQh&v+lLlBeJ?s!0ZID@~+19%Or2ZFB$by6|Mz%62furEn2kMxlR*By`c zk3cTIIOL|^w>Tdo;w~p8Fyf*Am3oSDuI+!SbHlRe<)Qq#<&X~i_qPo*9h(kCO#z{| ziW4FKr0;{_%*sWCcwsjPjK;z&rc73_?|f}zAjO{&<$d#=o*3uoC#?J8XF~atF%b;o zW94I1w9gpJqz9kTj$2pgE0e=#8(0d_rL5;WB%a_4F!)d+rX<3uG? zxSVRT)71Y?@?!xjo*3j=zbtfNl^qkRi#A{f@yWqb|* z{g$cZL16#~k4W)8GnGVK$KscipC*AaSSbj1X zX92$qCR6Yk`1NKTlt%zrfxprf#a8$8rhTwv$6*=#bM4BOE|vt}uMG014^CYzf`6{c z^5<5+iUK;Bz)$HM>^Mg&K``KBMu}F&Bpr8>o4+=JPHA#?tJ8jb3E%N&VwS$qwQxTM zpc=VfD_Pr^uOA13mkU6SK&D#MC&A%9H}U`&Gs<2fKoU81s)|pC-p}?5l(o|Bo^M81`v$OfPT6|6|NQ zhJBhG)5|0OA8!SKlY6=`aK~($zc9ex#kTiR39!G@3HVFV3q%fhnN)r(ah10tPzzu! z3y%Yq1zkc6y?fNAJcf`Bk$|orSaa5B!TrY0vkDaEx%>@4{=)qAJ4m^ek^p~%UD@4- zbG45b>^jP}bm3jeWl?ZyXmKZiFPw8SmZ^+NPEtnB+7Mn8oy$rvPb7G<#7~|Gf-Sl> z2y7Tq@J_sL;2@{Z714m7lYJ)X(8cvMPG!P4OvQoxHedU=;1u0gU(bVqJMYJ%*k>Gb z!EuXsxiVn;$`a0WP?cxk9xBg1hstwh!1h^|XW$+x&pwCBb7jExMU;n$ky3TgujXx*P!Af{KXnYQg^Uo)@{9E!k zcEefu&q((<=V$`ZV0cF>+Rmdkmq(WOmqZ8TOUfrU^Y?#_^O@l+`I!Vv)A>%!26|Sa z7a^ht^X}t-{BSqajy!iDUP7~hmFsypNe4}{ftBT*q=Tl}z{>JsI_UrR3&Nb0vvO9> zO0+Uz<({5oaJF$#Y!Mlw9}H?%;j})#PAl~IAry{nr47(>>QAn#_G#zvI8VXsvaa%J zGq=%jJ}d7gA3mo+hO_I0#v5YIsed}9&-{Icx=&8e%EXBi(E~+K7*iKi zr`=o0`hR)gz=2AhZs3w;;y8$vAjPU@bez~~<$nT1wM#FRJ@`T7!He~W3&i7?K&bva zKmHh(`wv*SUu^$((S3*8wf{*M=owG@^{7*-O@;I%$M@Tlo!|D;jk@ReL0o=7mr#~> zcPYic&+(|n0IXuYk$Xq(ct(_udM^RKtOVSEKhU|z^Nmwoqj$%uP%JGG-uh`9EPl2f zw0`1OIC`Q`E+5LNKX~w9beHtm$ZFO>`_J`c>@ z9WmJ$2cs`>K!drJ8nKnp7|tf(_bHm@?`YD5I5pFmI@xyVcvUhkI-1E3C=gwA8rOFkMl++Un3N~BaKPM-L zT`IuzM!Mnoy?B47v)QpN3h=*^I-uN^F(nE{|J=vU>Bp47yNcgeB7t?s@~Pp}`9}*5 z!Lfn}ipbPt-0(zSTSKhwvu`M04lYVV(QrtYR-0s;1dk@eUjDPY!lpey`F?mVfkw&mOsYZ z2O!9!YGe&L6K`Vh`2zW7N+6nv94Ths8&)X_vsWH~Hg!wFnjObv@K=@3qL;pv?_f&7 zwlfLJVz7(fkVxkb6D@i20)j|WHWgA5lnS7c6D9Db6@?c876`RgwR z@9%*2+iz{4YSm96FRvSfmj^YJN8rEt)?4)hGr#%@3JVLF-tk)9pM(E94%+NgcCLph zfd~-GNGbzE8h;l0K5OUeFl*QAu3m&0z&lhB7-Ns?)?W2v@Afn0gO|QFuRHboJ0Qrn zn*kSwd}E*m=3fyfRlZ1nvF?%-;BTluZs7|aLW*#XN(uBL<#iPU_?Zd?!Jq$DFwaDr z4=yH%$+9^!8^-;%8P*>P4sR~oeFU=d3SdER7J4-P!Qr5i>?tI4tds^l>lD+s z1|^c9Rz@P+Ubh(BRI4a-shkczuSZk@`z^p9*Eqo0sOOWi!3~k{Lpo+A2xc!k+@zcY zp4&Ao(2i{;4rFlsnL;fO3jDdK1Q2M&G#a-%0)M1)(CQ2X+Of~Z@_|5mw&|H(Q2}VL zyuwT|O1Y2wnl}e{>M8KxEbv{sS`gSN?~e)~^kTfmE`rAbcz@yfiT0VyE$P;+TZ^89 z{<$SrMnRcOWzHC(MQPB={+wC&Ggar%el#K;W#{{Bz(3el2Y6PQ7(p`aGug%CPNp&< zxclh7AF99~h4y10$;0e?v7z2g?Rp$t`x#PJR(;@Z`TCf;Y3Z&bA@C0=-b2~&$DS;= zsaLNMz#ru?2KqRG@MAEpSP0q`y#vvJzvuyg$mvTJtHZcp`Fp@&8d(nmYj4Bx>@Gz> zJGxp$2Ps1@J;~)Ep~)-YDRmKO$JT&n``Z9T8-ah_li+Ll72CHOwCv513gEN#@ZvX{ z;fJAZblL|Mz;7Ee;j=|i1^#3@`HA8e4*ph3z$6cefBAu2=v_Yp>K6|iM(xU{!aqj~ z;D?=AhQwetmVbYXQ~I-W7MgehyH*XqWsoo~P?Bg1M8NUv*dgH8v%|TyGfuBZ;PURh|fQWdw^}*wuKfgT0q^pbz#PgAX9qX zb=R?57uUUr#fuknOLh>|uN$$YC!NqhCW2wq4bc%{;J#870?PJt!wBiJfx_=sfj=0O zz$8ud1a?a242psRmg^6n2lHkmGR(3X94SR!4*uR(XdiwrPPmpq=Tx_$bh9EAfCk4+ z631ob73zr`1w+nGB8>b3+93+g=k7&F~<**&|nOEtwBujmT1LZW7xpf0=eL;H3*QQ)ol z0Qgr8R9**ZpB;yCLZ9FIX?wJF<9fL`t5maA!s(xvR%OD77NwwKTJW{(%kjhWe;x+E z^DaHe!8s3*6=x@?@Gqy&O~5*qyjh6mA=`lYZ~Aw?mJgNFo5N+*o`Do!2BasJ_TWA+ zW-@H%2>kguK_w6_pOw{2=lnZ$0`1jTg8+{f`1b9fjT;A^hZ#<^p!q{hn}VlRE6~3F zx_QL?Rhl_y@rT=Y*dq|jbmJdZu3QPVYSq#KAAvupcZ$Q2BS&D>DuXfHO;!L|`3GR^ zsvE-^`GjGDT}{F8Qs(oQ=^s=ER+Nw=lgQ{n4hkm=6;Yg%M;0vf zAqGLlk8V-)Pr3eHAkTOP89~X4GRKL!x|4n{R{-KFS%-+xr^&Fn>38wj=u-SKEpVE!1kX9tOb-9YwzudB+6wwKnkoAGN z{W036wW*0xq+V2L>#cgX`N90dI^ZXq^E4DLzZpFBpN=@CN~u)%epvXicw)8b51*&c zrhV|;HFkXd5z)e;@26BS@~3P{0vyc`=z}e#+2^DL59Aa&)&Ne@!`kJHK?}c_0RFnw z!SgT83-o{D__ zOQ?Uh0d-j8COVFm+KmGEkFfxMQPBOtj=6}H@i4DIGph^LSU~9g2gd|dW#-cJbtZ2{ zmQ{4T0a?;!MsZX#udzWiDq9rlluU<>w2)7-FAL7AJO?%$n+Ic-9)h&QQqZLIjm(be z&b)c2L8+Uy~kYOB#an}WD(uWM+pSf zo`LSGGy%^ekAU_9GYGeBW6+tR|J6N##~;@Kus~10j|()Ct#rBUvWR^jPMc=n_)mpx zVCB7AQ9r8v7G42@qE&)81Xb1S+Iwt&!2l12asEIG;0FQx<0&^96qAA=AnL~L%i9XU z1+7*P&UXNlwRx*$q`@tXD#1%j{$YLM5VWc`0g9)t0Un2PG=C4w+xa{s`ScZYF^+qm z0{p)L^ISg*NQuQoloZhQwd&)lUhi`^@cOU<0NRIx5Lcec9^gr@%^c7@!TR@@D`=#B zMtruOfqylf{};iZXVq+0qZlfH0u#PJYOsx#0^+jeSOKg!6khW0jlZ*?c#=1avi8r1 z4YZ}rZC2Pl2eb48%;Lw1Do^r9!F?8QM`KpKXXSy9p-5sGXwJNVuGOD`^rSM7%#^@_ zT@&Gt{nH>ZA(cIRXb8y|dB!YyJ)Ymm7(m5}#`1}Y8tmTf3DHB$()T>{P*@41r3IA$ z`|i2#K7fMS_ougVpOvWPb};qX|H@Vr}6-&IVRt0i1jtCf0yr)WcGMja0Hnv4NJQb|Z? zJW(G5@RV*3TGk(?*TKPGlo!9=tOS2t(rwT?5&8H_Pz7%lyaG32pmC8S`2k1!EaYXP z?Cj2QB4f#|_kgtaszO$JJqgWmoO=GQmvm%ADhTD*ofF`X10OKUKY|-wDSdA4vbw_M zixgR*Ena-CP9ZfT7OX{!peOMB^FbwmgT$x=MvMr01sNHp_xJgH@cCz-slngth18(+ zFP2B^R!@r7jNPAuzdQ)suwg@3x^$^n#;I85^XH&00>0CITz>4`UXPup6yWkPF$a)- z+>n20l9o>cl>EC#05y&Fan$Lo_gu=|1(fso=5?A>!TMjeU-SkdJ@LKa_oD5154`oC0#6nDg|qx? z!zsN3LKKj$NEmqdH&4)`e=rKjzp}UE`cQ%YFKZ6Ur>P=-Tn_@rN&&fH#8ln@M)$n( zXWI+FJQ8TKleqxQ`Y%cipSO?4*uJNk7m#C81ZOlh_mXG!>#hmt2padM>KR81b z>pW%AqD4WCvT|j}+DAnofqyQjGl5#kNz3Q25;?&f{~l8we69?FA;*;&u;t11=kQ9= z)L*oMnO)x?j6e^gIC&2U+&_sYe!FQue7|`=tlED}j|NKg?T4a?b>YHF55dAcFF?7} zhHy#MCzwY(0WxzpFvZ{#qj^N||B7g`uVQ|B8;`&&3WyK*c2gYF+k-Q+J|6;Whk)ns zLe^A|J4d&?y->L92JqJC2cE=IpylpnuY9*Y4kJ8^PC4`yX2FA40@yzGX zfgQm(Da5{513ezkVM-w_%G>c2-JJj40pT<=PNY8wDIo}>&|*&IR=!nXI-bvr+GK~u z)-JOAi_^1X-!z05ezz|uW$>P8V$ z0AhT)X!#@fzvjADWuUkFZb1Pd#e@0sF9PFY7bsa^%%yK%gL`K>(da31y$7}!=Z5%K z-7lB1xkm8qeY1Qo%wKz02LD_W!1qMq0r)wky%X~DuY)R8 z-qn^YX@_$FFw_$&R0w($2M>C5+UNN3lXL%|62KY0x|d~P$>tLHPj#vAzg5t)p?vo{z=+JHqU^-Zdre_ zIlM5J6oUW$VXgL@*5~(Vol{hEsXPd!UiB`tGutui4?BsQ#0rc~SJlybK{rj_f zCB)SK(%CX({huHMRe=_+kQoNj5yu7hOh7`hz)X za_T<&hHabJ%xE3-7nj+`S-SI0po91yyVTuznStN&g7oMDhv&Zmxa*Itzi-RQAk zh^dDnZ#qqxi=kN0;>KM5C&i1hV@Pq06OWxH?~%97{w|b;rQifY@mi@#36PTL71v#Ww49||TcvR%_)^=-qu>PXzM~}6xTI#7m7uJPRY39^= zR&GI=mlkXozHsY7yvFb|sC-CX{qsW0^$6~3x?l&KVKV-uiH_d?;0FgDuu=dqfj>zt zx+YFWdX6cCflLW3byyZ-Yu|>*!B^xZ+tG&O4;ULePtkF(63o(3%Al?~2NTp#-s*^i zcOKyy*`Uso5)VP^1xL*J;8hwaK5U3P9h8sAr_Mkge-nmvKExD7PpZSI61ZPO_u{@enG!J6N0ZW!p6r897VeS{nWo}) z3;resMy&nMN>0{V5-9x!w5?+az9<CcqF)H6&6 zIGM=!GgB0o$V$McT@i|@55$sHP9X>;iE8I)m4fKOo;8}S$9x&0;pJ~nus4+d9E`bju8nBMPuv=0ZiMK=To4&_XMIC-R= z^v)7uXkQ#C`^DN&(UK=GK$mVU%0pUmf=U4>*JFqS_=Bpvw_zvnRyv2(QX*&vwt#=- zcL0TX(O%!tS`sH){6Vj=@JuUD;O}HThj%F4d#cd?Gl{(jF`M8ZQj+)o!Gi~5V!FO; zdt?0g@dKimgsQ|B)%%x~fG^IVud-?|WbS&V2oQKCF-1_I0QI*K_?Z&WHyb5@K>aBM zNAvet(K}H}vJ-8 zj1d(LNfA6qU@qnyuSp8yK?3$`q|3^I{`DNxp*IfiN>3>s5OPCpda^&}SHXDU8w7>3 zo;;~nYCkhsZ!=zFI+28)?JGmAa=};Drd|nl+ykR;st>vOe%N~?4?dW`6Y>iKyd&+G z+FU%qJ3@IR^?aF?_d#ICYDl{;4Ye_20YoZOh?os9CZIO#?BOC)&r=dIYS@9DxohE*&32idL1ZuYkb*A7Ni# z!nM8({2u`KlTg#6egBbRpi!h0bsz{Dn69-CQv?q%B_K{j)rUk(*t+XjK$ zW8h88fWYy?koWfjIR5=)DA=(^ydGMij|dtt)n%pS2o|~{SBinY$3kg{(%b<6swSbB z@r&zhuH`D9I(Y(OK6QLn41???oF;p0q6e~5pobiZO!ug0BR-(Z2Zr&x{8=3I6$X$w z_}>N|4|e3D>Gc~QDbk>s?zJ6XyRuyNx>Yzx_y9YnA5#Kyx(xrU75iaa&j!${PPp$~ z-?{?)x%DVa{bdaY|5FFvNvYr|TN4VV4~4>6FF?}0)4*HfJU3PT1<@R0CE;9-h{SEg z`xh|qPja!ox5m!rO8mLS+Ycr7_~ zQ!=F{G%M2s=I$JaIenb0=WxEovCR_Bydii10j31pPC_kIV7dPOnYAB!|IGT2c>n5o z04`ICx{&&)LYNYGn<)VfD8YDJj~6m-dJ25y>p)(99_-kC04{IZ5*jvc0u$eu0#%;5 z6|z1Z3CF$-%>chfPylHT0M{eYKWQdbsdCX92L87B*ir6pTN|!wPzj!!yAFQZw9jq* zpl9=(@Lrr+W3lE_KRTyRhw5$$cbYPHtDEyp#t9;XgS<)!m;pb5Kf2swFz&znE%$)e z=PQ2n_eFXYX0&XVRJ3_(*!$6&I41D{JGTmJf6wu{qvu2I@wy!{ zbJ8DB=yy5H5IcB-w*u=bUgo8o$b^I@mw~r&XYkK`%MJMBVg%Pxn`%hliNOVzy z!n+scDcbA|Dg<9l3Wu{`Ka8ThnaJf48=e>G7J;foMX?gX%UX7`1OKaQO@yR`bXa@% zYuKB;TJAqFF%U%iJb>VD!vvVyrbaCPf&u?^4DhdIo8|lW!KoFbN+#+ZH7xo9)I~2J zpl>M8ju;L%+;S^?G3$G{y33{T_8YIk$Y-Bp3Su;91vzm1hc`myqz0);K<^QF^hLsN zHk*lS2L6uou_O4SJl3IpGztJM>z4$@hSEczGWfd8oYwf1K39+8U6ebOFW!jAu zDjYhNudm)zxnwG&CMLi!f8nVF?_guD5?n(i@y^#lJ9Ys4zrG6vUyO8GoP$4Rqr(i6 z7~O@7`aD6NTQ3!j$gKa|C|+$kfl5DWpRbV-BQINu`*6@ciTc+L;As!6%VXbrlq~KN z)c$d-s%!Df4R>&F+MFL3kC3x zCMCVWd*R?u?;ot*zfS_^ln~A1BBiJkcagOHP}b?TDUKOCP(p$Z{9k?lV{rR?y5Ow) zomRR~&E9(;??7-sGr7z?paovDc~)70aKhgY;ekt~^4yh`1C=sTVb=QK1dsD7mxM80 zjg)KOFMb!@QCXfPS}Z}b)b~l?{eqwXOhpP26-9i3MVLiD*JYzCZ?Yd_tTAl$CgMU9 z+yK68@yoQ2iG(b@oviw?8=Tk8SwQ9}vzN)46W>Aw`pXFnhyZ$PVVq!N?jMi|WhDcd;ms zV^tSOUOIk9i=wW{{jqP>;FiDW1)z{85V3Mq`SeE6r1T9?tN6w6)usWE;7vAw|JI`m zV={uoXNf7PUZmtHDCN-mX9^*oC?Ng)v%gQXeH%H5@h!zMx9<-8(Z7C#0srr2)EvHy zW8Y2GylEMpqTL>XKK<{5-FNo}}WqF!TWnO)YEeLcx=e_k|0yQj->K)ItCI4MC53`Od=;FWYOK@$ypP zKH{_N6F{PzT&6dj;WYzX#@YAYril2wx zTLWb?lEJN;?-IQE?@txmoR=vPZO5uuK%pU7e)+<_k4OPDQYiohxPwAT$o0_guJdbz z*1El=Sv6n)+)n^-yxvg>{J9`V*R&~rfeDyaV*Tn#ll$YC45q7T!26y=0m%lO-hcLF z{{?e)JO!Pq41=p{PtsF4TY&#ow=X2lpf6L`%JC3LM>k*qfVGwD8Ej#N)Fkivaw& z!{>M$`~~4zUct|ggF64Xk^=HUK~5ngn&xBIwM=?4OuFjaP%QHGjVr^dy;*t$!oUlR z*gW+6)v#t?M2hb^1WI>F`~6@(FM&(U`P3DHos%bh!`vwuU`zs+Bq;-Yb{upZNevR+ z{mV&_;Nn38%nVBYn=H6~jek}th}zFg{&>@q{gDzJgBE_Vb6Z2j+xx@Om*6e-ztDB_#$66BCnAu5zH&n!4KJ#S6b2prngz#AHDoZR=P~+SE2s1Av2ih7?nVP zbv+Bq|7*7|#LNSTLl|hH$@?EebeQS;_u0`g4EQ|?1@SGItcXLh3~$CdnG^C9sf>I1 zz=uC`ciF6uhvF8(R{T#25k>QQoP%p4_i`UZuKtRHP#D&@%7Oxlps^*3*^rODYrXOj z6HywM&wv%XGvS)|ZC%G}iH=Az(e9dJj6;-I^Qq^9F*L3-Sqa=4K;94|2s)VXj)Tct zkYoRmC&j$^Z7dXkUEO;Wzd54)F(vREQvy3N3TV$qQ;Mda)8t5zbDHX-xmn8=WHazD z&W_<)oBi3jA4F+sX-<@Yf5o?;9o`9#q%O{fCHW1Sv?P*Cs6L`UjzGw zw!<6d0g*_gerzM9d z>kP$!1ql9^*RKGduiD8CIji#P8EDq!ps%2Dwp%Cxet%pe_|v2WFC_cCP~g`fx6m&d zpATX@KhE$K`wv$FX8lJ>34!x|Y*)0i7z2nk4Ak}eXNqC`;0Fi#trP&d{Xar@@rLTO zKR{p|&&gLmlGXV!SVX5VC4lj>7)-s=#`3^t>q;AVl1qg01>t9Iw9_`*L4|%RapZ=4 z>=29)y1?acUg+>;I+a-Rsb@-Io%qf#XZt1y)_yVRoJ5HN!bZo(W!4cLgsCll0reb> zMK-_3towi{)1M!Hk813;igeR{pCh&ibyyM%P zTKO*K2gnk7_bI+0_}|c^3ReJjg5yY|Ou!!qFNWA$1K0Na^z+-=f{!VIH6EcAtcj`p#UmT2oD9yP7vSN^;IexqLpMl0RLuj5E+9W z1P%z^M1YL6&nHAX9*^PHBhgZp6~O;D@r--%<(MwOw8&W$J1b}9G|Fus{1#!ge{fqH zeoXdakUqj$^m%xWaNI0?x@y1S8fWF-D^_Cwzd|?#Fmk-@&TvcOFgP1M#~E4`r2!cX zVRXGDCA-G+4y58;7k z{k&=SL}t@yJt8ZC&cQWyTQ44!vE)qe1TDvA$rQ;aM66X-uW2Cm;ekt< zxh^j*!x>F^`5G}W7~%OJ<-0>by@tBs-kj1#>G9r0cF&F-ynyzgF1 zWKnLwpZ|U=69iyy@>mFj`;sTM3M4do5eiq{!>TQHWmI(achv0%hI>xbmu9N@+W6cC`Plc<_pZ|K=Fk0+6w@xt_ zGpS-)2Bab}Iar~|Z{7-#sZH!;8>^G*7G8msCx8I{6<9~XX*QmKc#VD&+LS4=GrO?; z3x8#{*MbVvPGp|mQBHd>PiK94%Mb8eO?|Qn^$`rc8CMy06Z@THrPHk;DO~|r_VUB>qJ$? zRK8SNGQ4qZi_rJ?|Jdp@G}8e7Eh?3Op_ep(zF#hf{m1j{%MfsTQY(We<6KB+IG(BK z;^6TW1y7Zp5cp>zB-D8v{Od<5?-#dgI0Jgk#?9AEEPtMdXd0dKg$?*av`Rn*do!gV zS>HGq@IL}l3>Fvw#&sg@hG5tqUuErQ$pXiFVsytXx3)}%oB|ELSdottX)xO9DTj|< z64c4ouq|W6nc1YsYUokm#O2jH{V-!$9&~D$2p7~%1hYxwtWMl23_ArM%QZ@*`8-fJ zBhk%_wSJNUaGcZL1PD)G`2n75r2bHxep>_j-a$ClME9Nsc&=qiK)2XDrP{H6UkyA( z>M@g}q~6c9tQDZ;9|Y~-TqnZ;V5l=S}Er@dfaE5gTZD^rZ6fAPSpi9qd zny@nG>w_2Z97%Vnh(;(f7jv2V5*~u(F+N4+qi{CpM_)@PqJHUI+GfV;0 z50U}Z`ha$P1Nc|;QYoPQajf%@0vHRC(}{VUJ5oa(!I*CXKaV&0fjCtcLpIkEXrSz+ zeb-Bh1WrQvnB~xkJgA(J3SacR015*d;Oz3zbJoJLorg{Ap#;-+q8(lc-a11-J8F{} zARgC~D*{8izK?^=Mrs03OtCGdv@{gXt0kAOOQ%S?(%9kLA3RM!eL z3|h=NGSoIu3qE$9onAoyR|CBe-f1h^}2rv!3c0BNKX$j&&P8$1o$ z-RW6!*%a7aJq_pfRdUa-N<*fQfxOe~#En$t8A!*wywf!m8bwjRp)%Tutx&k6J0xE6 zcMu0OKl|QIr+%ZQd7Pfle=z38!R(GL={uUfII)R$*6!q9bHV+p#SktgOKdAyVSLK{ zZDjr6qzp{mYZiAGQ+Wh#%(TgcGH$x69L(wS@J02YcuFGR?7<}VKCb^@5j@+qF>E+^ z%(+*I3yD4tSB$)V9KUy)F#xB+gDNuJ7Bm5QD^V?KayrYqSyKUjY+0B0+i&dS zOdn{45ZLuGcq`tZ(?MG1;;1ZrqI`-A2DE(aAX5TmL#(?WlZJKYBEW)tU2LAs}xUzo0?RCPgZV+Et%PX zvjvZLyQ~-rXLvspvNx?Y6@o%cujCoutB1Am<5umhSWEy)`fMk2)_1!P*@A%)|p zJUc(Mrb3nU2CyT0t<{WZqtdFmQz_z!TLTHEzK!sf+!F*94 zk?%i(mcA-Nh=V^!8m|DtVPUFHlq%3MX0yL=Qowowaapz#Cu=qHHrDc{;v^UMjw2^v z-26z8ty{W?J|^Hq0-MHxc4!_XGT=EBqOMO@V1NePEB5=^p}qjJQ?9BeQ^S=Xq>2|fcSTZf_2mbj3LPnI8mD0 z4hGKZNvdJPm1QwDgC)!=eLk<%>4AUHELMd&E+3!bH^`;8)ke|DZ`u>Oex zas!%}nj{yR8K_V@Da99X~C4@FWu@b_-N{{6+<(xdVKnUivj;=VHQWTiJ%O2O8eB4yxuK@r)?cWU*=DA+#lgkZXW z(zFl90jklnVJ@V7V)(op)dxF5EmpQ~(1%%gzBs)>2dgcTGEvgUrR8G##R%ZjtsO z|382XDwX8(2&j09JP_1UdFEqV>`p z0KF@vCzaQ8>ND_vg6JB{H5Et+oS+F|KPrr79ACX!KgXqr9)L5HRiF@pjSs>!MYW1ygGv$58#~}&JU8C%+mjJ&yiRgrmI}? zv0TFqPVK-S`w6CvxEG8w=(%76 zLP{0K*;Hm~vJDuYj)Y}y)+2ar%*xDut;?p!6i^2;6!o<11pfT@q5_@;(u6`6%E?I| zilyG7TT@|@c9bcB&SX$?g9)S0I_t-x5GNi$wBnO~aJEpqKb_n>nE*fsEx#V44Lyq)`QZcx0{ALtTtl;l54Me61m}^l2gp+e# zhRDbtE;xca#KrStJIsm4^T(K|&1^k_A__&?f6K9gcefocSRypim&r@$YI1%2jsJ_m z2=cf%KbrOz{hA7sv@E6sI#K=UkfQH91>$y2fX@PK#T*C)68)phY0|A*H+felHxHx8 zPw%+A6@TcU0gQkslEDezhk*NhLbAl;8ZmnU();%~Z2UMLV7Vy0iUQ(_!3yA2RFHT~ zkx;F~Nw5a(z_(8<*N_Hzb4{Mr)$_*~{lgVNZE4?1djG2DSsKvv$)o|=T9D_HN#*ts zi%^TcBTe>q6v|kONBy`gzY&aMUgN3#n|p?nG)&jfEDrZ}EJXJlr8>fQk!E}2v+1B? z7l$sI!FsXd^`f~(oai4r2`wlw?Vhyma8~}+BBYXzYCVFqYPHNRU6UAu8G^xx<25)r z=}B&23fVwiE71S%x%}fH9_=*GH$N+9<*b~QvvPXG7mtg$ST7W3zxC(ZJYE*8?*9Dn zmFs-opLV`FF86jA?ii6voM;ek{e-T8Yp3D@){0rj%6#b2Zcj z@$;`9<8iZ|f=-_`dR%)x%C?5#SU7mthP9v2;;yu&!&n!W8p79eSUabf6-~p%upUB# zPDjs}PAt5CR~E5In||`=Htp?YTOF0d6e&H=tqM0at>%%ecgOW7-uu(Pc4+Vaz7_KQ z0mt{hy+w5l)zI#T7qKy=AY$ zMT{jMcr2oMJ>a>qbu!%1E*Thp!{2_#llbfiITjyQw=jV;Ac65bS{;pAVybFnXjYL?$mB z>++qAHF<`p)q!T=Q9NW@u)vH;Lq=mWpJ(zN~bAvG0peGv_5e)FwatKCcOPR z3HLQ?zetU~1i`GzVjbZ1deVkU0Ovv!oGQQ_Jb3WCpqRPO7oI=43LvR8Yw;NX*_)jx zfnE;4ALyK3iVh!ZK5$*``K=D}o*1)@A#kXo4^bIxyciAf^P6opmXYtBoajlv@6wb| z{+2r~N`|jj)aSqmi)s=KaD zl9(3&Dg|J5Zyo%ZRgd0)QzhUO_z~zax&Q~sTe`Ju9Y%D;ydHuI{2Nv&s{dWH<5>9j zuygQSrU2w;a0327=R9SiyrPbJF#}*!SogH70o@u@fQ<)^!_S-d!*}cULZ0#bnCj2$ z-lfx%ArR2uaO9#+a+!AQJs%{C`>r{CI&6a8nw4jq#Lg)Y=x@C}GONCr5uYNT`qTRcy?pEcC16B?8*8ti*Gfhey)sen}Z zyc4czR0*olYG&tFECF8TQaseIF60&l;9zzhe7R;92N>lwzFfTvwjBxPtodVirrvR0 z{B;AQ_`G^Yx}{lFt^^{=_Gq1)UZYHK%EkFLeR}%qC%UHsGEn=E2H?{rc~DSj`TRQg zi+{N|R0t~IM=Pr-1lNWr?vfITP^z(*F<`SE0ta?kraye1{#Raf-o#5ta`{YV*0%=H zPd1FW*o#**fx2aaIT`UUZe8k@gTddeifjGJQn6SnjK88OO#f?#-u7)2eoOYQ(S`Y$ z0sB-3&r}#^kZsSwzeA%GxUhZ-Jp08F)(05|vTilFdj_oBk`F)psYmzrq2E5+qW#u| zVEU5Z*l&A0mIq^&K1S?47nK5F;EyvVF&3O5IsC=GW2YR?6oDfb##`}veW8=U_IJ(b zpnh?D355*gqs|A;z#sidoW_kYc^IWIky-l3*?w%mI4SC3OACrMsh4Kuz{u;;^sJ7P z=j6h{V~*Wg|Gdoy;PFdJL$eBDth=mU1^9daQFwaQ4qIxWeXTMu=I4X3a!)t|?5y>B z^qfXR8&}aSIrIdW0*EL(XLUY|zCKOQK*^r{=g`v+aB-kPg+7bh7Iaz)Nbflf_QbqsiRMixALRR&zvJQdPYyj%gG^i?SU zR#)il;%s$Xg8=7R?PB{XvER1KA>DI5QBY14LkFb-U?7hVaUWesjPXh)AiSBKb2n20 zk<;3}N#Lz=2Y8Cq4c30Ci}VE!aBW-g=jiL%2DVLMj$r=DcnPuQ14mB>eZB`7@J|r- z*8`L6m$Gcw>({*iY`1^!(&tE_NsdcxehBIerv{qhu~~n?>({h^>Lr4n0Ip2-&^Igf zG}m@gGBpXFyQ~S^`{nPjH8YgGGQbqbuv*^-a}ZLMTt`W+J7-jv-}>V1awXMV_fqxmY zf~~k#e89!*+zCtx7_DLk?18;sfv@T7;3;`wST9t*4YVV_Ltx8mR^Wfc#dE99O>sy8 znF%JiJ4hAHy#lOIj9MsdRj^0vWa!i=5h|7NLdhb*#`tp2bd4EHTRT2`bRu~axEKmr zDZ2FY+si`zqZmzd508`T}s|FPAb%Y5lU3<4VgRNf<5aVvYc3h zoIj_149aCfI5*f0FDy5V2`DUCTYAfZl8w9N%fKIlE-+97b|1}!nQQmJfVOo_e*=Hc zffv-#$D(jNYvRIqAqSn|$ zFLVi=8*yg_^2(OO6oZo&jq4Bp#;pGk+Op3$75sd=f}bwV(>H~Mib=`lfHA2a(rOs- z(V4V$hHS;Q^uhe~ z1u*WL99{X`*eYrIi9BtE)ABYH(8-Xu?Lbrpn~ao17~Zh~{IxerkBo9c0denuA4g^2 zz*{)g;vvIql|v8A5k*oY$p`nftp#J}>T6Myx1nIB2w(fqK1^!gfRD*gP=bc-GtRP! z-6KH1vBR7`9gav;kRzFVai*z9P$aPw^l9`SY&^0MzS%NHE{Yvg4-C;c|En^fTt)&M zIu?LZMZNIYH5s}R2m}lUtUrq1iH7<2?6McYcJ?COlWjRqVLkib`Gw*1uIQ z5q$gAnAPuL!0)Q{4C$5%8EGCUWUn-@AXxOG`iU*Nj6M3O2%fAz!Bs7i;MpjyaTd5^B$M)WXnT;a!!dofJZaQ`*C z{4j4_`2D;!H*aV}d7I~VNdp-qvFm=r?<<&H7HKP_6&jiR_NU!g$q4@AE^n&y*A2>L zpaLkcqJcQz^L>I6Fr^mIvKI$@x#cep0>@zCTgj6|83ZhpgzbCa(xgHL3~66i4-Y!2egL3eKq@_z!A66Dp=RfEvZl zM+G3a9gOqG1(e_s2L4@}r^0-seHiE?_#+*J6+Zg25OFb2Ql1#X`iX)9Qb}{dYP+!K z6%LFBg95mltn66?^4myL;b7Yx5S+U0Xb{+2`XUJIpAG)Ck0LP#2@RjuBZ2~ZXF9HQ z0(>_-abk=V5Dv=Vdc!@)ayAH44CtNUM2_W5K^VM(v0q2#?t12bIe48v`S+qJ9(el3 zG-y@J2Uj*vf=50&fe|)0i2#ZnK-DQsI<80MLJP7JgtWwH%?)MNFM1SR>XwHQKdy!Y z*?FP+D+a6$0`Ux7h^L?ji#ci@26ZHmBcmq4>w#Y9R)ZUxR0g8f;rqKU#I^$8Z`cRZ|Jnt) zg?_&Na@>{K?xQ&dB`|sMCOyK<#`{2Q3Ubc9fn#G4_f~CM89t~^yAkRzhN1xFU+4L4g$}hYhz?cHBt_j5mwtN07p4~L*`&_7VkHYd(qGZ`3$uO+rIr)koCL@8@`v{a^iU4!0 zS7(16DNlUTpEvC@1%DKb@R7mZVo+GMLYAK1K?Zgl6Obc*cq#~m3G|Hmc|;?DwPgb)VI%LgTV}33E*E+5)t=KQY%D#0%T^6 zx=N%nBWTtyQpUH>>C+)#@|(>hSKW3rShjb%3j9^)qxFvA^A}M4BX3nmf6<`CP|TSEk(NE{9x_lq z_!pIetO#%)(nb7yQ82fUBE5rSq1-;P=J(+#7I3>2TnvIDpkf$tZlND0EZ79gcOHR3 z7uJTN$v)`v_5%H!Hq}Z&b>;z_U%3Qytyj*}M0_lJnL z4b)91_lT~xO?k`vQ#R0;p8wx22>KN7j}bmd@w~>szCWjjLw!nx*Mo~|l{=e{|6eYrT>(howGYf!bW(cn48Y38 z^Z)}4Z5aK^n+fbN1Q7?P5gjr5=OY5Xu$eOHtnxf#1mEN6Z2-fosc5qBOXlaCeRpT& zti*=`Vjvzb;4bO%nM%g7E^q~-&mg#zohbObE~gD?0}Su{JDxA$p5N+>M7Q$IHXDH= zkideX;^&NFpGW?O0NnkW2G$_1sl2AM^4}JJpb+~ndT$-cW5oe1Ok;wU=HvvgZ?Q>H z96!J@KO9HFkZ(ETFSa`@iv!F!Ir3N>9~;9wN0IS~19ljK{WKY!{*zf>I@p>lM~Ia- zpFUqi4`mQxL6>*|w+&=22&F%X zru-GG`~d}E$8$XgSAdUUft{wz#WI+x7YFkiKyVNb3Gip<^yy%jh^X?MfBNYs)AAiV zcKq4tvt$mb$%OchD#uOT>&V>x4VS>HPFUl0cT zde+hGf`fhkI`|oEJ+=_C@^`WK6o6t$6{}V+*0yJ}(zo#}Vc#=gj2^-q8#uE% zm)`$r@c=M~O%37mwN}t1@NxhLZIPE@KbD4%39m@qc~9afK;q(pI$p`pv$uA%mT z;E(tHWZSXdu9>rAH01lU4JRJW-wl5q{2bP2&W0{kpT4(Jk#keX(g$jDGpIjf{k%kN ztWbfK*zO)AMS&BW(0b!Ha{~Un@Ad~FgD3}A0^>rT(+1S?R9Jx&H^SwY!gyTLgH zsQB`oDuAwZAuiX@zS*Y(xMRufK5nBww4T8ubrMt)*=~g30@&b?T0m(^9a9Xq>rNIJ zhMnB5ppv*eNiXHYvktpK(oO5#sEAG1b8z(TGEI}3-b3Jxxj1@JTTi0SiaDJoF@VEV z18bu#&y>JirUceTq2QeYi@a_Ej?iP`n#<_>9jL272>%tAQpDuF+O ze02zpONtXQ>QQ?`gLD{YCkWr$Q~N`&|28KI05#7fk>VkWXcBe4ypsvsW@Y64POi{wm`>v<5V2VJrfNnPhkjvTW8yMDXQr_nl0mRX&{X%+?=z_mbFZ&)+LR=oCMs)Zj z0;*^Hnpivibn!ZWcI6q!8_F}_cT!%Izo|oWqsz&dlWKB7RiYwA zNpk@99-^=wpth@Wg%^s{C95L@2}~j1kky}|U6UgCi=BhU)*AMA3fm_y2)a|+hB^8(VZLUpNIdjR+aJvQ^s7Nc-&IdZhj`KemvnB0{vJBhdmD^ z4;T|^$|2IIv=)&kkwf=FtlDj+wH_t+O_thVk@(9j!IQi{mUOpHmil8LpUAdBY(p)G z;}(4d#fEX~zq4Z^jnUZDdc15n>jHDxYkyyLs}@q-h~Ph;6vPs8!?8h&G48e8SPV$r z*bNTLg5VF~oG|is%0yW3UL)`~dj`?vMkAvWS+BPcMSxb%TC!qau)VRY2<*MO21MJa zyxbX*rFt^Thor>VjRCe@#x@WAbwU9gP#6?Qlhm|Q6lDFYCQPj*$^ezn@6vtlCQzJ1 z;9rT9!3-JvB}H(U>S8#R5oHrusc2*_mpnY&w~YR_6f9gW=k7ed%yFCCc1#b`Z!K8Q zK-cGTn`AA^-TXDX;^T>&k7k#?}}5(1>~jmAw`3kcVDJ9 zhKdjaQ-%oTZy@Dx$mAy@4TuBSm$K~~`rbk10n{K(XeLk4gKcpO+^VdAQG$t;#qW4M z19t8N<_TcptkO!*5Tzwk{OcKxFP$dt$J_IG}u-{vw!RM$Jsgnwdft#c{Eq z9gpvT<98yc{VSSq^ifNU?*)Riu^u$ujsinmleRvV7*{>3Z2tEp1|#d00zX0H2d0dYufn zL;{-SCSY$SNIgs?%4Z$*F}VzS2}pb4jNE7Fyxjs=7bSxO6-aSXPM4e9?-+6Mj|-H~ zaPkJt+_-;=C0DGQmABJT;YheN;e1f z*Nx8oHXguD&*0-$lhc7?A+HD*SL+!t>wlTRzg1S!2iukgHw2AK9#VC95CB}G7tosk zK7;-ylJ#^Ky*!MB-~;U4Pd2JrD3xkXuOU!3PC+9Y5h<~qdZ(<%@U#?jO3OC{D8d}I zZp`r9#$Ma8{S0D$xJ{`5L=SGrA54`nLo=7@>RvQl6EIy}?O6iwam4x0AS-bYy{Al2 zr14diC(hq(`B5Pi-k;7uhi*uVriPzo2A@YL9+7YSJ^2MhFyENBGb z7zi$HXq7HOqx$?q0BI}vw0`l+hAr7Q#aTs_(gPYfulqdpVSMh5#E%~|DT1I-*l?Z#<(eWiz z9x-SXd2%5IfR_Elh$oqC8Wum1-1A%qAZ-WS1wvgV$>$R9Z>IU9TF*cl1b&YcS|o?e zWiE4iYF#Lw!Iz!GbgdWDGk#8*dOA@{*kEuF4i~oBPdwtgf&y3>4gF)~DKsqJ4jwq? zozOWZ9)PKwTd4$$ytGl&9-Lg>McG7Y@U}7>Vou6+6PUk3*K5zPV=wDSQ?6FILE?lmdELOW1GWUQRS3M$F{0zB-M)?>nt%Dmv!{ zhu3$Sd3Su1(l_4y&(U8i9>`f|d18%X7eeDw*JHoq1E{NsYsb2Ig8;q@c|Pq(VJ48* z+=aOFz2s>bBD#>@FPRpZ&=vRNK(K1P4--bBknUTX!2cJbfk3_|$sgo}EhnXf<=P6Y zL?L+>=v7@y_jo`AP}FEOBYLDU3P_ZiRAzLPw2#k1^UK#!W%B!$HKfa>d+6&KFmMk@ z;{u`e3{1j65ww@uj2kcq_)YN}YTr)<#q%|Fz-QDrJ z0E?L6Q-<}4b66i~#q5D~Kbk3l$r$Ph!N`zCWeI)GwgB*yt;sHa_d zc1{O&t`%RgQQ~{8ECag)a_IyjEr~`MO*}AecWavVjs<7zM#?w{9lhA%9HfUnb9k@% zh5-Pt53TX;ovV(T`{DW?MN|cn6N*8LvON*}fAxA2u484yahlIaF)k83r``&WCyQv? zw+K@VGJnfKnE&>ISh{Zs@s*Q1&l@kr+otL4d&DOWx>ZbdR?m6uRnR~D zyT?iSD5WxWv{6v08M9%S!?u_iv*$B?S)pOjnh#F8vKBwX-z;Wq-^`SE$?9Z_Tk8#q z^N$GCM9Uw7lZP>KMRbbuk0_6pzmxJL?ZKD@^`ba5i|SwuL;#}D-icGQ!#43X z>)%&doy7X>XVl+BSwjZY4KD7tMp6R5Cwfzm+rHfAO@>PUb z%uVnlqPPDcJ6AQJ^_%c{DzOP@B_F2WmQ6~3B}7gV1HuERk$q+ZYUpu1A@qR_4#6J? z;HQ!&V>Z`hEm2?TvL*f-S*cDbAi2U0vlm+;E|5{NEsSHYWDoPy=T8Et)D#uKA4>1DfK_!t4nu>aAn%XTmBv#~WCp#6 zf}E%x2x%X({C|!HG?Df}%il@)5@R>+$sAHg{Ju9%^(x08mWkI3W5Dlqc;1aFqAniK zkmybGqov=j;-H^uWpwC5zZdal9|L`-IdyUgMBh>Lb1Okh-Ek@ilViydv}llia`Nox zo!sT%yaOQ2qW2Ni(>cbMU+yG;Ky8SekFb(B){`M<8nz~Y??DQo1X)eXi1L{sYk$h+ z0|MVBT14`MC9@I8BM>=@(@u`W5s+hAOJU|6?L5+VtLuAQJVPf~{vl3sxk)zwcKx-T zZJ!X;CWnA7fT!x65B8yYKx zrqO2gT{$cBt_I(Hpi?;CT;>;n&I&;jx`!x19IG2fma?lPY?Rw)K#V_$PuRMg*n#)q zBqxLOjC?M7JRLFQ{+J&?-~_QD<49{O+Rh^Y#=+a=M4e0nDa^@kVwsineqd-!&;HiS zPsimYj2ipOX{r0Oa#sFJ3Lsh$6u|#GIqfTg)m;0t$6&=dkClcuUK|xOfXAu?&PvP% zK&;fmzxw`XCGLW=WpMx(#}E0eXSAkqOz|x+=f}@z`S|0HC!p7V^UXJpjPeo_h&31) zjRHWBMluySE(}k?0k{Xj`Ap1M%b!ZAPp(`CdR7!107C8-qdERQiRCe(2*(bJnjAky z_4f8jDjz)%?tVx6ddEaZ=O%iSplhXj;nIqCS zwn8vc3p>FMhv4n_{%7Tk5RA6N2s@k*CG+s-(1wG$Q>YK*#LM!3Zgp%7>|>1_(T{gP zFt8+A1)wbNQ~_YcVI&JCKRQYs3B%9sk5hS!j(C|q7drG>L?DZ`yzU7?Fj@#L2MnWn z62d$?UlitJvLx`B%+1AG9_0lK@CG_I-U7g*E>HRJ6G?etTr`eCBHf1bJx0yx)4?!< zROZZQ?{YrJsUVQApU)J4xEqxv4!~(beR7dQ`%*x?FgLb?$q+f49e>M1WPso{RZ%t` z_g6~quXo@*$Ix{yd8=EMd2m)#iX3TQ95=)8%i0hggTr(PI1~9JrnHxtfIrT4=wx#X zFX2!f>Zd$R_kt)Nc@YDDY$vUvERQp@+c>cP--Y13qkd7|$F#gt1<-@Q?SJ%r09E+? zaVn1kQxnPJx*CiFoTA`B=~wj9h6z9xYk8DcsnfTF$m!JB2r(=XCwRR==iV!w6Ki=C z9Ph9rHm%Y~M_rzQKR!E-vta!3Qc@Jy0WoT05j%bjQv%;wfw~(}oc|1U22_q1f+yy$ zX-pP40`H?_(f3h+|2ryrM(_|)gmb3G`Tl0$-;CZjm$_`;MNA2VVr5H=-i{t3kDfC` z(Pa*=>Y;P^0zesT-+yXBV)lN@5A?|F}YFW1aEb zLzj@Za6f_nHZq=^W;(`b9gNp+D|nbMfPFq5QDI?PhSmY3mus@>dXF*cGg^W#*FwF^eqv z@8VRROD;XXmE_tSi?;j#`W;U&pdTYB2BhwACgKO|a}9Rv$VsBz?kDj3NpT#N(|qZD znG%@4dGluZRE+Pw zj@~1#S;6xX>>}zBX|PYkzPEJg(wNHMeDlpQm1lsCp{N*AUen>b!?YR=S-Cc|%j!`+ z4I!NI7iAQA;Om6@V>~^s@%?ykN##C*j^>6#$6s=Le!@I!e5*mpWJn`aTG(lNf@6!WjhN zA1B>$F zWTo0CvHVyU;LoKNT?fa!dP{WOI#Mt@1O>32tow};AomvfB#?sQW%fy4Voy?JKT2i9 z&)<+@05U+|6VB;H&-9~U461;?I4KcqpJN#KKhO4|E9+sO=Q^kxLwTaw_41&YgTxzF zGXl*l>Kw-jJiCx~yqaEMe>#pGM@*6*Yk5(SqB%&F$<2$8$+~wEArwt{v=08DhWHYV z58(6jS$WS&$CiOH7kI4Y&jhJY4MrsksKM5c=E5bDE5M1uW031VZg$G!_?^}}EdINg*NK2kef7gM!_;r{(e`Fl@7`OvK3mGAthE5K%VU8;$n1?4L$l^aL_bd~C3BvC6Oy>c;8KG#U+ z%4Nj{nk^X&qseQt8aWorivcdF^jr(%^3_&-H233GI>3)29L3MLu5|#LF%v0G%O#FUm|(n#cpMAU^(thZci1AiY` ze2D@*fF1o9DT>z^=m*$&E7;$e4xbMJr;se#UG%(s@sjqw*>BE=TklLD<0c}jEP{VR zLPF^C)~#FRQ+jBcmS&+q5xm90YB_xa0)83>19&{1Xv?R1Jst>XQLRywFL?K&)D!AS zwDuoVTthB<87U|uY}sg zTIgQD{q?87oIUTr>|JlLP9IPy;XJ|Ok|kAet#eMo?~mpFubFW8A2Gle2Y^eucpl`H zJ2IfpB9DL%0+(X@4#R&$ydIbl(DV>RZta8o8PvIRREo5>4MkZ%I5kDUDlvU0YDsx5 zh7#l<@-}c2y>FG+l~1uLZz~M^(HOfEj6n*Hg@M1JyjO5(Z=%oGkZ~Z#O&*L`%bUrz zD8b)t`S&GC!A>5K_`z7qp9wOG8aSp!ZqGI^+e)!*gwUuBQUVR+t9Y` zwNN&t8g+IrbgtMN2A=Z{lu9C3_Z-MQpa;ETjO|tvm7jq z$U6|hz>zSJAIHI;zK>#?M*@F8(VP)kG?Di4`~6T@Sg3zi$Y;!Qzlr*-Dhp?SvM$>T z#+{)&mZfV%TmI(4fc9t!6!Ujy__n7c~h@`mhuz5DQ z{A1F4zaz$es;6XO9>LaF$}^8(tEd3_5udh+ItI^)^O4ZdFD1(!2QF`yE*fk3(=C(f znQwJ)E>ErdY8=a-c41aOQXAJ2MKzb`hl6Bw@^#QuIOnsZSSv$hX4jTrp1e*he?8GY z0~F;O$It|@zKh4JJbx)j%wlNT69nYA_yG<=H6;!bI{*%XZKh7~qCgA8qx|WXeBx2> z2V;1sC{@UHyPvqc!Z?;cQgH9ECncidL(Pc1p zbr0BZ)R5y0L-S-J7%k^EpkAk;P_nXcVqrvhE6xKjneCfPW#V;zRXTNy_`v9)G%zmw zbNBzTcO7t26idH$Z}%c_# z`J(Vv(=92+M!{jqI;2KOnL;nIVncD8F@i$smE1*j+3n_HEt!tZ7hHcLX-#?~^HT=U zPdfauSOJvX^LD)WU6T1EnsW14LiXVoM=w*YB)F9?@^+l~6Id!)%`q=4HNaT;755kf za)E2Je{2Ld5?UJaN1ebDT?@QpV8vo96=v*eqEs|kNyDHSK_hN zTH3u;4-x%$<^5COqU5ld6;Ctpd>rNHx6S{=_`l+He=#sh?W`vPy_HxGeTNSOH`54K zTHa^Hld$13Ry;+&`3DJI=Wi6;B0WI6RQm>veMt~X{80JtwaK3tKlvwDS~#b; z_>Ba^H!K7`h48r`6r%b*G`e^B(Qn}Qj9FpF_GWK_H~)G7_UHZ`cD_V?SnE?ka2lj>3to^;MF6-bFS)M-lu# zBLw?`7QL>^W!LC4xNRpjh%$W0&8Ch9?g++ZAr?Y-R#11gc&%nBsw06_y4xndIuba7 z#`V9jEeh2oI>)FDl{51HAN}LSwv}2xzeMMl!ms-Otq1^>$h3K$%{tPu$ANwxe>D7) zA-0)`V&xaj&MEAoDm)APg-o!LGM@07{@XwCY zg?l6D7lRWOKeZT~+EA&+&{d!@C%OgtgE91Sh;E*eW!%o%BDhns1tnsP#C39=(|D$- zsxZKPmHD@|ldeI>+akCJ`EfnSuL=X)r^#=1J^;0v=i@B>lxm*CpLyJFHPA4AILp;Q zOW=F}53{>WwPcb7%~kdPl4UVTXFdSS{3WHw|I^DsBVbuN53K+KrLqYAaZTZrTtH`> zHsrTHYxh3Zpi)1+Rj!}0!z2`QoNEKAK zUaRv})ZajT?qU}J2>#9l7dYLClbi|eUIh9+PvKynr|31GH}+hBW+_X-?RG;-O3Kr? zj3HrAP>??*H#fIeK|vwj&R6ULph2fbC?ZA&ydChj=4G*lX_D#U?TlUGyM}zC0 z4z}HtLnUkX(t$H#Q-Q{Wgdjrs-g5M3Z79A zVXDddcmpDbUAO0jh9D0N`vKt<4NdETgD{_Kg9hQ{bY{$$Vak8cJ@>3Icy?R&7s!wO z+;Q85HK9j~+Te0JV9ut4@X6}k;PnOHI|ZIPXnjNtfkOGm6bz$UN^ZXQla#A(qS007 zhNcc&A(W%aqm*}S1qou|SHWK_yQ@dZeEt}5rwHhJgn~lvkmh{FPh8aqZa%*{e7pG| zjQf3?={LA_bpN`n>~v&>=drJ!9lyjL<~#A9lfGYT+;|GXe~~+-)VHqWG(`Y-@^hXp z$~`>}!MVWiiw&Tu+yerCQpJiDpDtIfykc#fIB`N9W%_l+ePXV?Rz{24!ZAIf0O?DTTW1XlhC0{>BBZ7yJSZSb zh5D$h3Hf+>A&=(wj<*m&@Hd)|f_~Fucurpw0#o(cZrk%+kcvWJ_KJvDnq~YDl<+Hr z^{=f604IOo2JAbdbPg&gqz-DJjzI$|Ht5{+b^-h;>xX>JNerr$_!yPa_eaW2N=Qh13U5R~38^_0;=16ciawVnANlY!h z1y5+=a3)kj3!oEP0CQp$04|pcN|h?rrBbEJm5=~IehV&N7Zw(zxLodOPN$;=f`9Y8 zyu1(c^Yhoy=wORNU@j{llomVy!M_=T{(AgNsoUS;-z#vt5aLYO#`Vf1lX5~i{xb}C zuVN)l4FXeHc$!DNlhHjp{OcF1k4G;qhdm^)@Plh)@Skj4#N+Lj~ zW{GG8fR+GgYEuZvqmeBM0O}dX_-j_xNtxn|^LHX2x!joI%g#wRP>mz1> zdAh3BNW*jnrVR#sdZh*$TND7X^2Z!hwQ3O<2#5&qSD#A~0vG9MO${`()%)2EYD5)K ziYGXnu8f4ll&$X6(g&RhuIdQ#HT*vB-MD`$3Wez?97ciPM^?b~-opGjey=A2kf*G8 zm7am>g{qVLP!`d14_)@mHu#K~q1~ts%dV&DK{kVoAFah_^N6;UEe>*V-jDz?SRn|>4=zB8^ zcpres44~_&qzPI9$K&*TX&Rb;Xg&&303LrFd^2aF>deJBt5gB!op*xo-QeK=_mF1+ zl)qDP2i<*TQd+%AfychAzKiN3!GF4@0Ky4mobtiUa);H05lf>TpXHxf;HdAg>$s1CI9_Y0Ds_hnu+QO;T)A>tp|R#`4uYk`rzHA+u^zP z=R*Saut_U+!tvaw6(qmTM>ZQ6qnF~i#o~Qq}!UdPDiE>Flqh z@9uG+QlHQQJN|FD=0bqKEg>;!t~;gFn+b`@ulYQM2==+RA<9qn`82aw2=b<3D0j-ibcta0T8(h}Xx$}qYCU`L` zQ9(73HX!)ZfVsCA@QOnK7)^L&)}ge1g9Gf%6%h>u=c8~A90<Crigi&{Im1f zk~5|?)cZ%&Vz!&`(Rd5d?8srEZM7Ju-y zj3j!cN$=j}Yd36&4?G|IUwi>I4jqE?TDB|;8uK+IB_%>yT3UU#J5eeAhCsi`=kwBh z0L{vkD@(!wg+%hV$;nAjrcCJE!z2tiv!f}w=f;B{`1E)1j@JHGWM6#<;Q2RGJ{1dzNM2(+%<(IdP z?(h8i+3`MEwM6Y7+{tO5xl_x$Ltu^|-<-hS@AE3z4W#8eU5V95)AxIdGU*uc;J&g> zrwamcf^e5b)jD;c;Kv`qvwl7JPX?-+luuW5s)+fX3B*y$Pp4pTVPPQ&07Z$zeN~f^ zk`?z7S_Ru(_#71~RJ<64fg&i59?kfy2vy6XB3s>n+C(Y}120+vLB<{Pw&^N0M#ps8 zegU@qX&P>35(;MJ$Y-#>Tb#IP)EUrI>IT;kqe$})YW*ZZ5d0NuARNEyhPm=w4yaW= z0Wwh-kUP0TDTg2T-Qe;1*IK|2dSCr)KJzp!0YK^znRxd>U?9OQVt|4zgsBD>eUk-1 z3Tur&vJaJfQc1!&vQ$@!E1`)B`}glx{wk&dg1@?t8m&a%$o%4rInwMoM)kyl{GdI} zn;AcRwef(mNJNtMD;BEa#4uC`nNlXNXH!jYg75bQ0Wb|ahSlN!1lE*kMHx2hqPV{< zf6q(mQf8AOzY)-}inp#QMKD8{5}VIL?o5gvsoJgC-|I>L(* zPCkZz09{_+1;6}V1h0LU2b4J2?TW-Natr)xEcV~W*+cQD#oN8lJep~s6!7g1p{jOb z(BpMItyR#VC|KB<0bi~f`3IjP64l#o2Z2IdLGTyXQG=21Bh~{C2HQd>>?YV>|w9H{yK1O<6$0r;9J89oWh@xV9= zzrRKR&>f`tX9mDG0waIkzh=!HWlx`0a*Kfh`gkB)XmO+$N^zIcwQBgV{h^Slf$RGa z@b}>U#w-+?6q}g@78pLjrp(KqtXS>7_q2_*kAB+OB}0 z_KofRHC9H54w$rgk>mg}0izeMN7teN>NvcX&%osaV7;c8O4KO=4X{d;Ft?9#F?I(@ zRzR2r^w$9|EZGw35+5xtc;0INr5yi*!zQX0z@rwxzn?lTKuZ8@wk1gVh=lnRoDx!`48 zm0(DXiCRLzVdVdptr2?HYAnFS{~vV0$U6}H{o-Gh<~;X~GLRC1em!dZ_xPR8rhc#Q zEd}Hqj=O_6;`ggX!kS6_OpAu>+q(7IyriVErBG|9E1X8__n6O@rT?z|TOl+VIo)F) z?)w>^tucoxdZ0D3-0Su9L1k(aS^yi1iahH{IOzEM9Vi3{sVWP*S2N%|%x>JLidiWZ zv$RYn(xEW|5Xew`LQ>Q&#A6*Gl)ZNcZa<7dU~3$o^;@=@!HFQRg1;ODmNJ3D=CC++ z+Nb5twcP($2)toI0K6d%3ezhF_afh1JU-S$`w!w9n_g|C{Sgx)3#8TqKP375$oeoH zh($U5CkI^o>HY>(QviDz{1vlSa-XGGxTcHUOQHa{o&BHkpqrY>7vBuY7vB_d zk^D8^;qq-QGk$XWkmfJ@_@}MjL)Jj8XuyBjUh|AR9q`xhfe7we@0tgIncfe^=EA&P+bQUPM8vvWYBr+wa~fl<2Q zmw)K8Xa1&apJN3DV~zy|Pm%^pvy~0TW1Hk>K#iZ=qj)WUdneuxHUG}I{k_NIorU}Q zI-Lo)&(+K0$$Kg*>(p`FKa?!%yu4hr47|G4C{PGgK_T!pgzD#$JCl+_HZt&3gUG^U zF%yC_wJ9oWc8{pl<9@O@Mj~+U)8OgOu4yq-WFT*^l#~s^rS-`3{MyO$$7+01GwomN zjlIP__+;N3Zv?&zg1)NllOT}5U!IRb7e9KpPln#@B8%Umg#aHd%9s0JOAFVebGDfY zfNhc$K-#XIf;Ge0RFwA_QA9m(us8Q>1V4g(O#Bf0+_QTSkeaB#Bm)!(Q_|tF$}!yM zJ-LM?%786atqa_%HE!Jqz(b8VZ2!6ew1#2Cw%!auI3e;&ZBJ zS1Z4prhG(={yV(Re-P*?9&mSIVZmSo`Gt6F5ekI%iHYvbMMZ_aW5;r7lrZuX0SW^S z0P82Ypb)qfg@78RrYxi^cJKZI+Np-I06(=qRfE;aN+QeSK|GIK7y}W!3njOPh>|35 zbNPB&MCaDk*zb%8@)&OiKy=HNeV!&w0`pPeH}#E`w06sGl0BS5iwF2lxX509l3%QY8BWV8%O?V3UqrJ=ibz4=e5bO)k3JCJ!w%saN zG2LH-NNuJ_`%60D52CLbk5TZP((EdkT$hA#PrRNu6`o#=f93uL*ZMS0{Hj)ytfek3 z+@5_|IU$<4unLbtpgD3>7fzJvtu7UpH+!QXr+ z7|1Q;NN{CPN+>CjK9A=$Z(+XQr;GxQO#l=WG;E zMoFFy-D;pJQM%WyTBejqHBgn*i?)UT$7GV@p2Ysho~(f!WPdx2K+Y?MYM{36uTc&3 zj6eU`qBE5qfhY8o3J8ps^n zSgt33fB7*MGOqeNb_x%yDRd;Fc~$FAyy$~<5T0Hq`oL%evhMLdefk(4H{%E6x=3e) zCQh74ahm_~=9O%oTPA!5G4Y!`8=0L{*PSv+D8uSyyq}%f$RhZ`=4@v845RpW4zv6G zNB?+n6blLKynz6(vM9m;0+m}y82FX6ONXNMdkgbEN)56lnf}z_9wLJ`+sN4ZbWq*U-h)qStj+Zcq276je&eqdH>q z*G``Am5%F8X?Z>`RBTh7(lSru`nLI9Hs9ew6coDUND=(C>dDW;{THDSFjGD1*?boD z{AN1Go1LNKXPf>jtS_@DJoP9Veuob5lFy&Cgj*4Ox8ODvVEY9BrsB4h@gzZh2o$hV z_#qHa7_V_ZKLfo(0{@}Bt}6r(==}`z4hj5+^17iAcIEq(;h-4FAS?OD|CDz@RSC(dNt1_= z>9>@DW+NW!#TvYk0MO*;#%#*33IYupipPdZ;OEd11X{d!Y^WCSoud#T_>({5OU8g- zA&^i(iXTmyPN}w(#@Pdfz;_ZWfj?!{th9Km7G?qi0lo!m`zg3yUjTnU98CGir@b~k&gHm|;SQi#1^cz(-Rs_EeMew`qnDrWG`yliU}c%MFq zvi>TBEoIEeD1J_@>HXFVzoR&hpKbb8Sl4A}%UK9qYhnrxWV2*5g8wWO3KR$_h3mU) zy~?I|+ZxTV%cB)_&j$eKb)=Ct0K1j~WNf8jmqRT8P_tq_evSmJ)U!MxP~597Ye2=t zZxjR;NeKws2m*)kQb4!_27&!&nV(9$Hl<>2YNn5xc1m@AxK=*}KQ9LJT=wE1(!k4} zf%}l#=RXi$fR5%)__s|h-Au?36-FscgZxGUfPySl*$rgI@{2?%z#TRAQ+aTzrb@%m7vp;xjPK`d(8?O6k3DujS z$ycT!(BCw_cU#dKpjp1CLEfhYTS2^RelJh2?qHgP@n!DI}aC2Kv;GkNOTIXoh~W@8revG zX^cWx5E#Pe(bh@}miO(A)h!oi{%MQ{^!>y=rO* zXH^-i$n8W1^hylo#SsK1WvwSG2gwrn4`St6kLEFelZ$;BW&7v_ayb)-=w1Pr{ju;n z4IYdxNEP~avsSj8P4H*9Q<6KV3xhv(N=mV!sI(ISo^rUUs;2ls%d&Mao8tubXw0Q3 zl3oq04_>J{7)z=v35^&vEQb-5_+6)V3AXk;x?@wmp)A!-Yw@pwf9KtEd>l&;o6 zI?s@wJGK#jn5M4=e50UIV1FLj|M?*Bf=AlVhA8TYcnq{*+fXz?3a0JC@@z@Nwx?n4Igm)R{uLV^YsXw{Ra$>T55(Iy6< z5LkhNV!GjL-HSlq0KeyEo(uY$X9Iu95+JwGA0Kg3D;XqfeP-JhG%qV-BgCLmCfEz& zo{&I48vgPKwq*L!&`zzAy41#Kz{>r29#sI{$J(@*uWX4Q0u@BH_+>XwRKZ>b{-GAN z{*p{*BM7Vtf~lyl(m<}Y4A}B-ronAAh%SjS zR~_ja!LM_#2>xI(I2cq)jx>`*p-JIsUVt^M^^*lauAbuz7W-gqc5S_1h9KXbO+K2g zj%Ly#e^)JjkR2!hLT;4)j>vHU&G7bDphEEp_WiLMbEhBM2@_{8Fl(HC6TtRI%ddVQ z1RDBUWnvH*QSsCLHG-bDvUsFY0Jiycoj@W`g(*wPg#tW5$t#1 zx>yQ=TvV6h{z3y@!x(@htSCO!T5ze3WiU3Z^hcF+7c5wyx{$O=Ic(rlFM?1hheWn! ztdxQ<YiWfJ|rQBo}Y^kKzfZgzdRJ!0es zZVOiCGT2>A{9%A*XY{WIn1uyYLog20-h2Dac&UVGs_w;lTYoeJ zZkodOG_MyTsNcy}!8{}Iw*vYS(b$K@5Ev&#*-)o!y66fSW7T4APeAzG#-iJgwEd?< z7g#L(q6HwIOuj4f?e}LUdU?~?1w>)**@{1DY<9!AplE-c_kH^tGzg5!0Z=8o%lOZh+~XP9rx#t z2VFTli%Lv{&5gWlWV!?QQ^)U(>$$pKiEsDWdVe3?Tr2GS0jsjDrZP-ni(JWYd8=iq zxU@ex;A+ONX7R{=eA?Cu-r~MTk>`D@E~99u;*=k(C?bXY`zO?%72D((K@qoe4TT@S%@~&e z{_R=9Rgq4nR(1_91Ms{%Q3%LU=Dz(~H!=i&qV!}pJcU961wdbj%&@Rh64|H}bu9dK z&k)YU3;YbP3JiO2gxfguZlU_bQoUZ*3xG4uXM5*$ikk#fs(&%*ap_&a{M zxSyQLiTnHrLVEpg0#^$qzqCgy6G%g-j_3Qmi0^8o&F9p64tnNs5U48qr3C&UYC#^aWlpZCAkkw0@$<-`5#P zEu@@iV}YK4dO!m9^}@_z5dt>(={wUH50cy`Ou(P|@?+empU3SN@Lib&RsA~aqUP^p z7tS<3XEyJf83F#}-qDUr(l~-yY$<4_cs}#YGa*cQ^;N5YY?qRwxJIF@IJgkZEAtDm zIyV5NXU#&s#wY~TmNjAgAKP9b?Y_LR3kb&jzD)(BXh{X}i_Q{%JS~B~akx%j27emw z{v7C}NkU*C9t}zVH=g?n3W4WK((|+CuYx~mV>IAr9v>qGg)$e&Z%65FSAa0kM-miY zYgn-~2#JR1KMaBS1(rqQWgw3R3W0$r1jgxs|Fc(BgxcwWFxIrTLaeeykLO7h~f1H8rNuvjLR_THdRSJB2r&_uH zG5(Fk`TePe*fJCzlwPh|BAx^zfaIraQHwV1}^ZkdnM=#rZ+OOs`7Kq24KMvY`wBQG#PnP|<5!+hl2y}ouOdr;gof;Yv zS$UL_Te~=(UyKQ;!C~4buFJ;-`j-IyH?o$fy7kCEw15RfYyOS$tYDOqJeLI>oL~M% zxU9;9@X6*dS5Xx{hujhSEdI_Vl8bO43W4${1V+dwlIE|1KT-R-X?g-c^Zm81=z@l& zop2hp|B5IKZmQviWc-SYGd$qNzg1J5(5|u@KHH;s!JaS`0M+kA3m{EP0BGI6qG&}U zm>9Wq{94a%08G)Oxl+M6N~dg(!k`)o0V`#|jh;7E(EP=*4?T6kzspE~t*9$**Q{04 zLr(yR7C^Lufxura=`i=HEK5Q_uBpWF7g-QaX8~}tDge5Mze0!A5(~Er0)XfRUgQ^V z`QvKykuK~=0B7xo!BHwuO~X;)8f9NX(>GA>%L9M@LGbPQ5N(ITXk96``UHcLKT4rM zgPLAt#ik!v8<$}QTq%O)uhK=jnt$hEf#pwRnbuRjR!dzRiO~WO!JmlMtqK7RfuR1V zBnJOGc$=!St|0*stSMb*cuR0sOolz9Rzc~@Dnq3^>Osbg?U23pxN@xUU@q)^B{EIL3pB5}`h*9rs+^#>fQ~WJE4mmeWu;vX z$j4nPjvEXv^?Sj0_-k-Ae;*v>+k`BEn)iYK=>ozK48V0&a=Ed>J_vzce;!m+C1 z$I5S!RMX#5W`YgyrqnFWK#rD0VIo8vY}s{1zMGa{Q24;D>r? zPMCMF2u>lyq+m~4e#`@1Yq%iS1U0f5gY1w&Rwz+^h?FuaO4STex11JFc3Iae6mB}AV zfylun#RX);H8jGi0I&ewE?$z&TuW!0bimv2`EAf@Rt!jJeWBxSP|;j#G-Oygi9%m4 zoGv^B%`0^ao^Uw#FI51Xk(XMdv>WcdC>v~t~ra7QZWf#;;bt*sl zG=hC53XY124%n6LgA}v?%AjS?yI~^ye$)$latwSy&LnW8*29-@6z}K7_1cIXDcI#L z34pk|fNX016a)I0wg0WS-4MZ^(k)wX`=W(P)XcQw0wSq7JvzPyHrn1s9VaZ0Q=gLUex&A zP&L^BZZxaA*Kxz5BOa)OmO;~W7YYI=yuYnL7tNg&uK{1iJn(KBteEf#7mmSw?Z9{V zo08tuC_Z%|uakG+OB4bNCFQ~rwy^de)bs_7Ul9hvsU&c2jJSZ90!s6bH)tcDF}*k} zfLKU%0o^|BcbbRu5ciYYHBP0{K08zaKr{JJM8A}cdr`2_tfs%x>3d#M7w*5LPQ*d9 z65x~7yKBYMg;QKEAbEU%oUJI0Mv7f|UW7vvRr zNoZEk(EKNXFi(r?yeaJt|NYQn$`<*|hQ@a<@|o4srcS>ZLB0ttdgttkrwb=mem!76 z!|9U%9g#d zuir>%p+c7{4BAZ~9W>->yc=|?ed>(#J>!Hzfb#s?L7;%<<=kgJWGgwC0HFkd5zJ?z z5GZ8nU{qfD0E55PNjxcP@{)!bT6ZBh?xDI%F}B0x-C6&gg}{J-q}p<4FADL<_Oze-Op-Ke^I)EIzxnA%Z4 zNqM-Ot%1ulFX&TJd{?uuFjb)Bk`N|>AtHey5cCBz9?ffp+ z96x&;|)2<|t z14wJC(0Y`_8X1kGplJI+_UlokB=1}~=wN`Cx_ukgfN8mu z8z8BY)G@wdiz-Y5E&=7BV|xB`<)F{eIXa7)Z>$0!bFeb%P#p+nWx{2(9K^8lW4504 z7r@Ui(|QK9W|H(=&wrb%`_J}o&DYM|q01thrt4dv~J>t*WIZT4p$uQ`mauHYF4i@Kf}6L{*QT+5lJ0G~*ZFNjNW3 z_Gc+keg9J3^NbWDx}TaA(hB>F=EvH9e^?o`vS8u@hrry#3e}tz8VlBE{%+fSXySEs z$CM}m;Gg6s4gLVWxvSveq7yLL{SNNyjY8lfqXwwkv72{+Qun?U)Urui7Bud@K>_fR zFd_U9zawnhi0E8_E*-o=;afT8GKadZMRZ(aH~IbEg-E}(dYy6_mJF4*n8}Sr%(qp? zNH~T4PMvd#h19YXdVeJ{Y`BRDPAK#SlOD8ckq2qepg|{DK;AZX?AWh)UpolQo0^6D zjM6lz-1B=H&(a72Zp41POtH#OAm4fNFO57tKY_}lTKow9RMw;n7$-o`b;sbjWl#uw zXk=Ae_&gD1$+ad`7oLv>LXBq^o2qn;!rw-b$uRJz#=8E85@M3&5x4J|h;O+&KyD^L=iv@mItf*#{buba*6%OUEf5Nn z()!mX=idg}NidWie>E>O!i(nM*7AEtRgYg#!faWNvJw-U0GQ+^#3i3Rl?$fy<3ZS1 z-a-ZEConIY98p3xp-MwYYSjahTZbay68IC9w8Ufr{|fwE8@MA^lb&~j+K2eucUb_a zxf#^RZWXqj8vEq!Sb*EqHKg}fqd{s?lS-vwMCj2!tIca^SV4JK*o*fhwdc4xWUpf+85%_B?1&5L^?e$kIEH(Q>pgtD7n~!m@z6qXP1Dz^sJ?Tz>;K z|8MYbK5kpC#hu!92qYu$Lqg1(X@ z{U=^m*Yx50%-^lGJC|Kfwk*fExPbih0sIkWY;^?xRM4MbBvPd5fWK-1=n4YW?W4MM zB-f5zQe8ksZXg39n`*O-{fg1UK4zJ|htM!{y>ns)?pV$f-8`>$ZlOqJ$93w&+sUL4eBVp#^XpFIt3XfDkb0 zPJm7Gw9j`YM1sFc+ZSUG7==*1rr5qewslefDD#ZQjWmJ4^tIx*eFc|K@Vs9jFcA&) z$T^ zD9y;7BgDCkEXUv=K8?8mwA|X-vw(4ItE_{3Zw|-yc(md(X#t!Cy3s%|P_OA^a869X z9WOybK7IoE?#*k%>o>K4YNeC&5&Xx_*>qsuMb*+b;GZS|gt><5h}-5e#4DgbV> z%0!mH)RnuSa;fA2HUz*f_Sv4te&D#9M0(kH*onhRKClqbMEL{+_iClIakOe$l5*UX z%GSiMI|=QK%H-S6N30zK-wj;^cTZRbt3P)`VXgz_jS7L7|R9F1`B;;HugP3JMEg-@&8M zrTOJ>;YH10+y@^*t(WeFteGR=^!IP6*St+j0K`BZ{MIra&t^;SFJT|OzdrH0sf)$N zNxAuEK)C+KDZkPH7|L## z%4WLy?MK_Zbdt&Opxvxy_E2V!-nz_#RL{=C%~_#V*lO|C&}V4UNjD!M4o6pzm^d+0b{}Qn>!}m%y8i-L*^^ zaNc|~q+HYi9v+B4WyOln1*#x6UWfASC;Yrew)o+tgB_v2W0ledethvm-8PVy6)K+A zc{r*&&liBx)e)9XTE*>Wl1ck9^qa`pJ{%Jec|PUqzk&UyP@_*#pZ=Q`>=%hF5Iz+x ztkPZjD+|%f!-v6LJ?@3sbAN#k6_WWZP082RO7fY} zN4?%Z$=z1$d%54dL;}D;K+i58zi0uN3IVIxKgH_;qOV0YihkvePgS+v0pntM3GA0s zeT?e*LTSIa-Jv8|0HP2mo#g)Ft>3m%fh_WqUnLx-Y?+}fxUL0hf@!9stXb94=-|aQ zYRv~a#nJT4U`wu$K#?%2o$?uLkL@8)Jn>G59Eu;43kaXn1I7j4sf@)NC>HH%9T$a z3aq!d^LGUGmA2c!T*RjQtOc;>t(8%>pJ<3mo={w17Pjjj4q&L5v;L2sNnx)ql%?r+p*I$-#7VaX%m++!~S#ew3 z9}94+_4xDSB}fqbUtsh0DlLryw`s_{UbzMGPg5#wzd?fr&0}y@D>yDP5W0;d4PSR% z;ECY3Y;XJg+e}Vb#QxzqlD7G87I5Diz5gi4)_i~D3WDB|U4-OXtD5EDi_Iy}_Ln&d z_&d9F0jOMA0e)xCo+yShpvDhBkl*P^2%pw9M73KEg|)MkHo&?5p<{t<{Wh@Zt$}L$ zvB<0W{iN{$y5FBPd~EOiSP0By_ly>O#^B#i7#E<-hjRLdaeilNx})^X1L2W=kHO02 z%i)eLonYRQU*XxI!@=(@f~?PmLiW$WjQE{VerJb{@F>83C0ojr?z;-XpHg;Jp|+ic zDB;pJy%+!(XB35E0I0=Nz1~T`yU%okTXV4S^ zmMMlnaFi|}^3fCUQGIYVvD(Q#xa-*TC#n0Kh7b66jq6>-0ROvi&Q-#;D^3k-(Sl?R>vUJ=mz5&Rvu-wp{EU4$^(2>i2W2ljhB z_v;FP23M2&CQwUCoghX0YupBc?Z+Z-%-@fN09i>!>7GgqN9qr=l|JqFJF>nQ1=$Ne z$|&8b--)k2omkFMszL?dsfbQS zMa`eyCux^dGJ|YpatDmVV|sP9$-hn-oSl)VZ^e;crdmu|q8%BcyRVs%J5^TJOk)6kjh9YyP$(LX12v(^iA{pQ zPc9(-8Tbqaj~%d2%aMf3i14X z3;>fY{qs8yF z(ta%P#^d{`0$?S#`RiF(qxC^31b*UUEg^jF6Y9fv^EYwn-B2ATQ02v6eMg?Zn#P1= zMG5}8xF{)DlX}>yeZ8M%fx8MbMXofIKbZaGTF33k-E+=X(a4U_R)V0*Hmb6r0(X zvQzK;6_ zvU{kdmf{S7vi0;#RSbjx%_~6(0Bd!g&8!xZ-vTbG)CU4F$(ro)QR}V!#Y*cS7I0ox z(xOs^T_(pv_54-sKLq!G_yhI)H$fmhNz|b8_+@ChhITSAjWyKf&s-3g;uzc6r%0)v zM_3?C;+H#u{n|vMA(X<4*r@<`H} zhH%0c+Z97JV)$xmvuojoMk@mWUl(@u>}>E&nWC7mIhCqxPRly+rq1^dYXb<_VQqi` zTxfKykZ|Ro4%KKe;3y;Cue5!@G#T7rBFOXKh<821n0JsX!A! ze?lU=HT}(KIJXIU?iX1p$zaWY5aSRH2D>koo0yk_p5#vQb3K0)05$RSU_F1d9`Z5= zHzuukDujYzMsA-lDk0^dd+_fOUjzkTzTt+vtR(-2c+4kYU?x%;#`7#B>$2HS<}(Fz zv;WuUa$xsPOz%Gq<)C$srJgzoz5d;^GMcCZaOnvN36;R>J&Ky#C%6!pX1Hh^K!3xo zYS;!eeEarz|DTY>^v^I0KpZn*VO|xHNQP6gN*U~fU1Jsi!p+x%adE1@*)-m-`i9TZ zIXXw@=p03@Kty0NyTKl{!jB3>OqRFo0y3-oNoiDzAu!pnA_R=e*apQOTNx;5=-1x1 z;E{F<<2(-@ZSXK8>?-Eg!4756i_ygYWI_Wb}IiR7e_yc9OatIotxSbm6xC)w`zHD z+u$$jSg40NZo9B1^k`9A8Ra}@(?R%T^=_pA9FtiU$#`5tb^F*o7IYc6sCW}3yGp?? z2O<)J%YQRem~{)rbm7gB{)si0R?c!^3IPS+@E(a zvGdX0cAmd*ZY5Dl6DZ8Ez-CmBJRbBa3nn58h^ixa$3Z9`eiI9#c=LZ7#q-`ACw>y} zYQXkd?&Lp9=XT^{z~>HRK48JEW|vUVZZAaE3wj7b!}nXVBGDoa;IC-bNeKyQk6d0q zSmJ^}{Ku__;ozyD!Dv=WIF@VpAT-{=D}bupnZ#p$2n~J;mO{R~YBxwqNYfGkX3w82 zTPAwMc$6$zCd&QYWokj~+rNb5Di=fcjvrv(oO{4mlxyYw0~Xr+UmS!%-CK)SYK5=QBKE~SN9p3NM=K8XQ< zQVeIJuvjmF`Xn3A8&;288aF_V_zt#6Xb_f~RdRrz_gmIbbAlHOA9!|wuwDg1DREVG z&e5`JV&OlV7Jx{UzkjJ-hJ@Y+an{|SP>E7=M4udWEk zcjmzH9f8kOsc|VNTPqc|FFdN}GEJjW(5LbdBvJ!|*lA9O0*_Y-B_+6t{Y*B zON*y{%}3L!r3GF`lX7jr;Yfgco4gH$-aI&2a1a*l{{TF`0<-6r0bY}I1Vpe7f6bdd zhLmcTDeI*hb%jc8hQX0V&suqZAc4ORB1iXL%;#vOfGgj>QB@}l=#&KO_j(}#xrg1B z49BwliU8<(i5t4Na>F;PiXs$x0sIexK`N`!2RR$`tRMhl3T6lzz1{QrK={mGL10wD zRho^f8uQqxB|iy5C2;?Z0ztI628w>sOY0>1dtF)DaYfxkD3g)^nb|&Ax~l*_`1KTQ z&L|RHX>vVs0v$>htEh4R#=P9ZrXpo#d{Dwb_~x_7|GT6yQK}}5*1nUsX%N`u5I*cY0gM-%}-F}7B{>qEwl)3EJl z{ZZmn0^;4J+u^zP=PNThOrN#Ac)H3yKv65!_=sw` zXgdPn8p)STV4~LezJ;Zqe|VR&j(*paaqQUkH$2v@H*EWR3tZ5&8NB`eWavJi4!ro| z33z8oumZPQkDP=D*~@a-YN}%Tupods^EwF#n%#_AdN0XWeMHb6C{T7>SWgzt(W6-T z&lXX&*IlgDQ`+w;jdLh}d;|nyK9mamouovpVnA$XDct?96;f zEgu+6Ui*r2xSj|b-#lpEp)#`-4L-%409%hL?SxE-mssoEYvn~~T*Fb^FaK0w28>&G zI}Ew(SE!a2@G58IZihEkcggYki)=l=&2&nQ_n(8vohEGOp3#PQS|}-a)5R`WyvYN% zU*?93Y9&DRa!&a2&muESrMCDudjf72+TNGk=2D8q0zmMu#Guns6#(++G$GS~z^~JS zN80TdhSS>((|1h58j30FO7j=oy$Rfp&ve4s!aJ;2cL~%6Ej#K$wc6PNLZH< zgak$kUgLIvmsLBzzu*eX=0){itL3nbhQPP(PMPNQ@IIv*Q5)P!IzF~oX9$1Lo zoX*IH&kHK1Mn$~1`&luHTljkvC<|C>z*{xyB=Uiw`_ zKw~}+Zi~9t7~b(hxM#*Pv2Qx0bxl&Udt{Qbg~BavXA)FOsiz17zuyOC607)e-wj@G z(ON6dZ>GFj6%41gdK5tV%ZOE!DpNR}Z? zHe}^u7Q3rfvJyb)`?HpfOkZrt0GV)t5d4qAKY)aE&sZ))Ss{y9 z!~80-^U*zpb1Rk86MPP5cx7OhEA*MXeoN-I9n<_Al`zUVQUIl!B}3&4OGBAz$*^Rc zanZ-GHXcwGi6~Y@gU@v5^MF9L+EWJCh>Ap@SA~HQ_!BiL*ACVnn+u<99RxHDdAk~Y zzR34xud(|4M))NP1)3F2&1#za)?)y@H-?+AONK!D`Q5toc*QID!3T2>PTLH4)rIeu@{UF@u;b z00jSr4E_x?G<}dP0UCNf62U*qszz^@ti}QaBGa;--u*Rz-*04&BCwOY=O}`FFi4E+ z6cC!J!=lt9pSmo&LbY@kJkhoc^zQpOtom~qZ2NmNT-@?9==0bBC_>~P@%>4dvMe~^ z_f8Z}UVOfLATWf!n{?feSo0@oqScB?wRwoH0o#JP@Z%*HPA^`cEV*#>lHX(DKbtXt z5=B{u^Pz65ig4bwRbcxsM(`KuCaXYarkZbu`}|Ucb{Gb>yCx8qQBq$s`_lLw9bF8{fQR+7L^M=zmXN-Y1(wc z+&~I%#AR4H3-$NY`=Q|Y=0FzE?w`T$Eflpeubtl%jnbIIpSp3?1Q;|u9~xG1LdDV! z*mBSZ>-Ji@eK!R6@d)(f=B1Jr-wLDERUzP^F#x&(z-A2KXR9s|1pa{~u)k4YZh-i~ zOkw~I2oL6o;D0ZJaW1akhQPmw_mfS1J#K%e!KTzB$1mh~`Nxy+^|~B?pR39{-kSb1 zn&N&qo{ODfXFg1rdI)x9hD`;l)+5hlcg|uqi_7!=E}&hXC=&v~%n2HnftWo&fL%dY z*E$E{%wN)p+?!Xj8{&HiFMnc%DCTt=o8<38L7udc7kHEW;6LKN z-huX~fb})F{TvDbc`CoKdgPl97dZxedED%`Ytw7b{*4S&l!i!Y9OVC3(?Dl{ zkOo?U=xoLStPte}4imO%fZ!W8_-~S)kx=G-trL&!$bnVUcSG*+LdeN1jOCry)Kl&l zC~K8)fm>4mdl~%W`uydZJqXo1y$_Cr;G|rRgYzC0amjDNW3dKIvV8a)%NV$7m)Fst zO2qu7-?M_Jhl?K~z?1K(wcd>kT|jnX0I1bZ==kAc^25=TiB7%imEgKTRQ!B6NCUwF zfFFo?3E-c_=Wda%K|ml&9ZKV%f=$N-P52cQ0w4(iOZCVFqER`w@UzH)>(hMCkE47S z6nR$j^fMrou_I>pY%WIrl1K#HG?SY=4xo)i0YDiUcWQvuU3vnb@U#cE&X3FV$uwfQ zi9x^KQ(Q02j`nZ#{0R0cg?0NE`Rd~l1|KURf_JhIm=iJHC5IDnsp z7yXq=LHO0Tp|q$>5w%OR(S#KEDq5%g=9AkKbJLdA+or zHf>o5@Ce^{&GVr*pM=(Y9uYuqrupj$fM(G?Eee4-C=m{G)LK9f z7xS}|bukh`*)lTGK(?3Tbgz>j6(F;q`Jc4&{2JgdKYzUI@vCEl#Cd$rB-xqI!!HMV zRgVP%zF}cFm!QpCm2?|KK_LxI{~4r`3PIOT?~<8`D7a9(?4B41_9m#NMCoX%fl`2L znN-n}zmTGLjMS>0F_01e4cwk7xt(GH!TCHus4QzT zqm;wwUgNGKLiT9jnTQrdmQkUNvT56^f}Y1WzQ7&(@=f9H<@1xlEdqSz;+NI;Hy$X2hcQ(2D}VH>3J6!p}0Wg+&o3(u?1ymo5I z6DjrNk$+4)p1u3@rB?4x{@>Z3^=NgM@Vl+HUwYeAHY7*xLwEo5wH5wC2u<9soHz6O zTgCTpyB@#z{+?Lg!ZwEJjX3_NQLOJyq>ZmQ)KV{$yg3IS=X9s%8UU0 zi5#@d;o*-r|KnAm;lxx=+b;N9vJP6@0oxC6hHZy8LDjM~;o`biz?$95;qb}*asP;R z!Cxl3;2$Na*X{?}X>GCGpW4p2PW2LcvlS3&ZKXQk{PIpnb%*v18Z^jE7?T;S>n|QV zHs~#@XY+Z*^X>YM<$ChElsUG`t?V|UQW%6-50oGDgFNKc>ix;ozcWe~OGU6&%idw| zJ;w33_)_Dz`Wq-CuuK&1uO!suKU);bTm~Ze)BB^O12rjy{LAm*{u3w!J~OfaC{w^~ z!-nLcqc9 zAR3f*9RvcP^;%YO8i73l{Rsi|?P#{KlG6F84!*}1ek-kgB9969Q_3F&DsMv2-+@SHPdN|Chgh+R_ipIGzo=_h%}gPXeHBt-#<~+NSm0 z`!Z#gFz`%qr$W1iong<39kA)}TBu&87BsJMIjq|E2V`VxPU0QpZ`ZhU*nyqLwt_ps z4YkUJ6xKg?{|dQ<+3^qpeYKw7CQ%yC+k*8-V-d)w^b-mK%2=3*f`GpH3W%(`4+7w4 z3BaGFaoo%uXt-^K#@^T*-u!hd>^T{#06DJf#qh$BBf#Ez+^&i-bGE+DndvVX4=geJK7l+Wp| zS{V??U#A)`xzE9(b9Ff#{spU8nnd3(yr z-IfFZxqg1#GGB28k?V)nt;hfLytBHF&)3kgx-9FB%C~)@uh>)NRYX+z-ol-4HKJ zps&{R-+E~xJbQEa?0x+|&Vdy>JzCcxu$B*yBqu*LrPJPyg5U=f0{@0k`3jj%3Rb{L zo#R%t%3x=*(sATYM^!N2x%Cnl|NA!hXGU`b2c4-_m}O^ za|y}dPf8Pw$bP^4pda4F{gls6BZu!GR}k(q97oW7`cl6v6twom>$eMmU{Pyy6r^GE$lJb5pg-C6lXke=)Yx6=XFHmt0GKT*ZBMCeeak}^xtM}O{ue2=xU zSa)6d5HzZK0Zd-}wh{}V?Jt&1vFrvSpeMJ_uKfX*Pd5aAat+ZjGx~MseCU2jAGo7g zPk4F$(-#8H!m?e9;aGMC4C(Y5R7k4~uir5h@{96d=y#97)+0K~BW3g4kP^9V zb${$cqgttIBV`|u@1I^6jgMG{+CRmKDzg9BtYPcL0Ln=gN3qc90uPK{yjjuopSrp+ zyi=((Jo4pA7%~4Z=+K}tJl?tyG^$t{p8RHw9{7`G{EPtp6AX&@rS{tAo1m60sCfL=puU?pS4Rl+&s0-!>M*Rj7l*NPCJh1SoM z5YRZ^)MAp~o6h@k7+ntm{TPBg^}#2F-Gv~OEK$cisKP)L02Dk}Eo@WP!uPT}4nh-Q zhhY~!uq<_+gFvPQHTF_HFR2TktlmxgNvP0hN~)z2R;_Ty@f^6MMp>v#RIs!q@Q~4)s^|}gafwD7sg*lLtn5M)OF00)}UjT@pZUp)^>B|A7?N|dO74MVA z0p#HJa`uJmnQdb5w=M&`5y+pxs-=_lz`rZEdD2#CpwinK;7^7B7UBM9;|S6&h(kKJ>;giI;0{*YR`Ks5i{L-( zXMP|WR|Y9}WJ#?*oCP1R+8u!*2>L39Ok>Z%Ra)0UL2)IXOTN+}M!!h~djfuO+a}o+ z0`mE$6X-o+t)xDBc<`tb<*e_M$G#6nu>9Shklp!A~qFR7yn09Ck9=22KS#CFuI zNdR<2?Vl#3{(SubxcA~((7t{p#q6H8dXG^D2#eAY$oHSxBMA26=Dh%)qae@#e@%Xw z(s-im?3@Dd`F!9h%!i~D?WBeBsTJX|HvfVDu6iHtZ2llzcit`V(K7S8VRgzkfLogP zfNL7w04w+Y4&xWU2>shV50}=y3T|t0A2^*(_-?~YIFWnYSO}b17m(p=W6Q6-j^d#D z_f;W~rQ_Z?p(8k~xPqwB{Q`k_7R9)3W34CxTqxtGM~gbpy?HGK@cVwX3X)t-Y`b;g z&ZaeC%h6M44d@ng9L*hXiUj`S;sE}I2K$O^xMloSTtFxc=vo^Y{96n6+Rhp{l^x$^ z=LZn@zt*KW)GMC`7gR|n0Z?RsA^Sn6P##$c92CCKNC3!SA0_yMRRA{-0`YX2nJPR8 z_T=v;MMpI$eio0*ZXi_vP;{%3&4&?Zig0V0ekj#C81P>!?;s06?T9ni?SoyJIg0C# ztbotf?S&J0n&sEU4&ac5fcWxwI}5_gRp|1IjFV5@D&T_dD(~JDtz(|AIp15Wh zWSl+(b2ra|1{Iq?>w4G1rzPHP?jL3KU26IF zE%^NXCjNR$ZV46!7qaFr3ZLmL0Giqn03!I4A~$k@;{BB~58YA)(V7C-%ivE`yM%(j zECej^N@co@uBZDvJPzPV2(SG#2KR49!LXa$KdJywT|ueE*2p=ExCpi zix;F)xH=E+*ZPs9$qUv%kk%bK3GXl4sjPp$`!efrI6Q#a04=l-(a>5D{HK zEC7x{Wcr}GZMjyp7WgmQs~r!M(mnU%c4HOvWnlmoCy9@Pwrw|iNKg1ej4mMY+f(c# zRRVvS8IY0>n#e{0E|W;%7PWs`4+eoBZ~n(GY$j92&on^*&@_ev@%X_gB_Np}H|zYs zh@4E)@W1*ibSujG5x4DPlR*ctlFAmwPv9@|`+Q!%J24VyDbIiI=FniXy(e}mitq8Y{6C}YgnDK63s0wb7HQq8Ldd>sTAk!}!} zz@JcqQ~?Sa^h4lZ7{kMoi--K3*GMiG)qH=pm|vDz2XL5xi!Oi8>El1ZnJ*7(?B>zh zHvU_nCj`D;S*R>=XsAY73`s?rit|^YO+-Bq%zz$)Kxu;N2>!RRTZUXfqN=9Wl26p& z*8~5sm=S{fJE-~7D$V*u(+ASa9fAA*Yr)-fM=`a3t#ZQSE%J|KARfvKF!MEN{oA@f!&K zg>mTcM6LK^sy=@bDwHa>=?n+}y$+BBI`gqw9m&t{%Grb{A;Z1A2iix%XMCB+w#_{6*m$yS3!=nFnMcKzh_!{H9Tr6V{;u{su3y z8zF-=|3UozXe91^X7BLzU;f_5?urT~?@#M{(^vq?-s;W@7`c5uV~om$|375Z{tlU0 z!l>0-t>G3C1a^!VJwE!Ue&+GB{^mN5ZXx>W+Su&?l`;m~=Jyl@E|HR!4wFXT2LH=6 z@9s3ya|H1JmbLyJ!Gohu!8tm+q%jE0i0EU|7>6=5ufMhHxwrSvrhmKuINNk;c*Hyt zls!SSicv|8`YdpE}>c`OA!)P$GVZP!gI7F7;(IoLm6=S#zcbRU+}rMy8jT z_doW{akE%cd=zvFUK!EoA9G{kKQ=FRPo^-1shKY~OoQI^R{Jr~KTL6^Kr#gbY4Q~E z*;Dys`mjqO;KwJwc_@8td`KCrcf6H&9m@Lra9vY3co8ZRoX~Lo#-ib${F*zUn`p z!NTgdGt4h0IM}MEFQ!f>xs@cKR||?Pjl0k)2~5fcl>%<5(dWAGLatXDld$;=^eW0m zd5_nr6v!5ncWA2*i0_Uqn5qyMxN0^hFU1dNQuV_c^^c;aXlnY?_lu@zVLaNOdS6ul z5Fk;)c^~fJdP8LixPM|0`&|I=Soz~)&V1oie-mkQH0P>XF>n+AaPrT3{CDYgvWK|Q z${&$RVGQIKNQ+!gB9R*(Mcc`#{W3%bGAWQuX*@D* zq_R@cU)5C=_uF{CmfF~zHTU@p-t?ZuSV9BP_Ipz!I1mT^h-Rqsn~mz}4@Tg(D-4q* z@L#F`q%#4ZOgA-``U9Z_4er`rjNngM29!alngt}7u7L0<9Ch1plzrd5Ksr=oLYA0o`VSg3hz4*~gYb9>xu9t-@;RVPe=*i<{EMyj=Mv1jKKye3 z3;Gocn{tP7KzJT*to-pJ`glLyUgP$<_tfs5a!sXc6p8T5(O)Xh*!$U&vk%O^m0dIW zI^aJ!Y>%qphb)9-;T%y2&>*L48MIUlnKUs9mQmvVv;5)*^7b_5d5q7EwHA7izf-eB zv;sg&0GQQM?|WaHB>463BKT{+7yggE>j03VSfVw18IB|8jFOXrA_yWXASj*+iXdW! zZvaF^R19FmfQqQ7L@}UZCJ0}VBp5+KkeqYAs1dkzT$=fVbjwi;(C@yk*+12522EUjc`^;GtljF~l(tnuA>iuc) zhcok@F)?1Ek<;HSQ*r{oKU(^ul4yrY;J696M^yh}CGew4G==|K$zNP)h|cJ10MDY7 zkCr@$;NRfkCL?P$O=%iaCRWd7b9uGPtJX@=GL3pr35?T_ta%*V^3F-Pzegg>Uy~0GBWN?OVqJE6pYr~oXz1UHn9%e+6GS;@6!54b#gqql(lsQR$#pHKj#Q4TObI|&`L zpUXg$%FAvLv5YMe|bAv{t2zhwu(uNNz^DIqI*aYoKx-` zpna$SZZ=T>!%a2vsCYZ=JzN7LooXU2`>f%Z?d)cn^@mUx+Yq*N={#oXw7=y0l(VK* zGP6b(uFHp~uPX&@>ldCPwQW4CLrZ?-G+hErZ76ta~Xa@T-0s9f`88Q*zAtkc~$95v3241dnfOk%HV%Bds*eU zP8eO?$=tAKnt-@b%0{!iO~a@2j^y9f`LAv0kdXqCJpuwsU+{bc!Itp;wE)+?g7zZn z$Ui={Qvz*^fqwne@h{YwO8K5BMZsPMHUeC^Gk4~)jo!y@XrBP{ z*A!^cL^&@BRk77J$5;!#_ngp zze%DGmN>Zag5&(y0N(VM`h31}_#Qr3!yWq*-}8|OUJ?VA?HGV zfe-q=x=Qv63xzaN{CD|#5#Wgjr*ODe_4UN3U=y0|($)+5v1dJo_$g><~a|^X3+IE5ofYY;$k!W5cRk#-lJBC#& zfWN9~ji7bS)y8cEpyC}{C;`HTe$qM_SKsjOt1W1tsEZTO%O2eP@%N&SYdJ$`Z4(9E z2C?xg#KC;0`X}9oZQsWkrvUH)%drPgRoYyYDWPka7qgNvAME;Y(b>so`c_dXvi-B{?{Oj(lm zI5qbiufI#RGI4NG!%|Qw$p?qCOT)4)*>L1!K~J>qsS%5u>if{*zvJ#{SyOMhC_$r> zcpLyM-QWe>PRfA#JFK(wdGo+Oe;g>=egRJ}y!BFL!M|)8YTuKfq*OthtO+PhE-tRB z<6{eZDqBk*v*rE@gZqsVphvyDL&N`%)QE))a;pH0;D6hjf68rlO$gY2a5I`hQ}u|T zo>F&Lum@lq?;s{%akAq7;A8D57_&HQKUj|s3Ici~sEKW|5^y1lOq90k30k5l2=rt$ z-EBd8-edJT_?=zAin$z9iDW1acUk|e`kRz9nF5-}RT(nG!N|i02VJexPY|& z)%|MUkl(XqSOhb@g z@b04AeeZ^*0jy~Y{<4X+c{%td-Hb}$5m5jBL5l(k)WY&LimTp`MSRTAu)ku?v^QJa z9jmKDTe&S2WqKrk-V7oh+``v8o}j0}w%*z2_7 z@yToGdnzdvBd}R00qWb^4EFsGMlj4aqeP-j0rCCc9lI{n()YG#akq^ESkA`ow`llo zg~HL)cbX{xtEOmpcE!pAsKA^V0gTfq!B%{11<55WYI#}c0gX{Wdzd%8)5Sv%N1*SH zOOkf&UW|Lovmq3($ADn)JdY6K0R0QE_3cp@5zBD2QI2My_=Co+9yPB6Y6NXvz} zD-Xe}pY{j1V^S8VC`L>7L>>IU8)#E2@ITC!zLh6Kis2FnjpctA?8o>NNsRQ59pyuP z00sqcOB8M)|M$Je<1_QUwg>G<%Yswn0kuTKmw^9V2p)H{FpTo^?Gq(~pn2M!e1UHmS4 zxbTR=9SNChq6Bt;KG5q)ezqZ8S>FWfR1ikX&h1o@C!$>5N}h(o306i*)_+hgeFpvz zw5BO2Pv&}^uCa$b0P+UTw>j<)Ru)bQl=)P+yBIWYwEl`(aS3MOETYY&e|p z3@)xxK}4wy*vT@JZM$tKBdcdMDgjdufBiK0Xc#^3kYJy#)SWDLSQOYLD|h5=ixO@vWkgPhV05S!{8>8|F7BYO%SE z0b(>psztfHnZz@d_pZbHGqco)b1yBmQ|-N3(py4g{9^iGSCoG{!rU?${|S78Rq`iuQSjX{q&2{GEgNOe}xJrpDi%7^@6XRbPL9fwxA2N7<_=3R(UH z{((_hhOD5&4A&cKcSD@j^CdXFa)4>VFI`eS3OA}~ly?(RLcmbKjXNlkO-t(>>HB*l z=vJu#%;Ke}vQYxsji=}v-Xj6N4D|QFnJ;$Va3Xj38YL*C8o@o%2#JUzniLux`cIaJ zz7I;?B=VsTWcRfyyXxefP{&qKKJ=HnWkQLRNKqw>j=u!1qd5^M{t_vX=(}A)0Ys9M zUar6o&08II`IoOeCz=YtDj&KmYeC9~ex@jZ*|TSdol875H8o&uEL|D|K(6OAxcFlI z`>EZ!sRsAo*vVCffeV!HzmIa3aGdGPr|$G2atUnpyy~s8`zY-*Yf!lPH@z9@{-a_VbQ>?#=LtI z=%KU|77ps7btKz(zC`^!C#};MfCi`-&POF+%b$-JF@gp#UhVL--=M2+Nm{?T=0c(Y zMzR8@dQFvi8w0>=Lt6i|zZij(!1H~6cN)gvwy=}{9V|opUHDGw8SM|QyVba!i+4tf;6GfqCO~MPoS1Jto)w5tB70Cd z(6dZNMc{W*07cD7??xQUTYx{X7N-41B@mPk{p<=C!{%d)?Cv4z`OvF>uwGc+#H(M- zmV?OklFdrzUNCeZ9~jt726ZRN2m6VzL+3LvR~}~b8OXBi)SGY&{uS!#*)03Li*K+z zKvG#fz45{q0j_)o;OmEM-xq>E&Adh-#*Iws9Yx{XipN}uN?@yn^`t|wW6({oU?9gTXY?7B6$ z?$8k8g;+k{1^7dxO27#AR!YHOeN*TAHw>f{B}M&CGH4N?y~58yePD|W7P!K7`bJMZ zv~he{;NLTYLTzb~rQ4Jnk$(TW`n{;2rmq73?rES@yBa)Ix?9@R_}q0(z@NTxO%2DY z#z5=3F%Tc4&>S7g-sAoxRsVxS|E+L%*Qr?M@1S#5Nx~T5^rJ68?F1Yq;IVUog1;CF4LeY7ywGG)p@y?XUv!-fsu_38#oY-}tvZQ2xe z?b-zy8CnV$c|L>FFA2#2FK0kVnob557F`@0tSs17Cs%6%bVHgzDSXR39x_;c4K1RhxKiJ`d3x?&+Ec zkA0R6%k)$)HTl?u0Xk@0=6{*05|mIq_82HAg*b8w>HP(_UyjGFwNe0HY$$G1Lzv~` zE^Wo>KQwQ(V|k&?8QkefAIEJk@;1_rFN1l?grI!rb;`Ddi|X71KknAU{)y)|bQB8- z$%l@>Pv?J#Ehz%peYmYAHtcff4vr=;jnqr6R?&!EV%a^*@o*I3CiS3%E^dM$fs z6j{?#GKa{Dd+5CL&eJ~(NPlCJD1oV!d=65UPEeZUmaD%_p^dVFqHL$Ny3G_lGFkhnhZgjXVDS@`w zCjYRp=HvNqI)oRSpJFg5$xHu&=@*hd>>1?n^V5Es6GG(DqIz_f2L3hsfp?`o*Hr9z zvxE{5jDnLAu>1Zlj8q?}-VC-~y!s#xcXB%$aasb#;XM*z#pVJS^JR{fjd$9h(lE4p zqEG%!k; z`O`i_88-?bpDhi#*HUV?Zta!tzc;3$ATXs-*6(sdY06Z@d91W+GG|XCnNRRLegNzT z=jB4%#8~LaFfgXrT6}a4fT^Rl)UijU2^{! zP?(!1SK{6Sb{+qvETipM8OvZ21pb~{12qbWejAj4lLD~&{!1L6!jk}=@)v;b$U;y` zp%Lzh52CqjI*n7A>eG@hPF$D^t9E!HGgsA?gC|rttBS|i*O3}f03(to^ZFrX7~V&a z{|%SDxUak9-Bs7TrxD4WM4x~RT!}6zAWH>cra)){lB=87)c#)EA_=})x7Udaq!%>~ z*H1`R#LKuX#iX|$z>p7p&A~6AX{8>}x$aPH5n`eQDof!13R}ch(-EGq_osQ z{TF59W8iq6tL}tc2FRP)l5yLynrB#V!utfGTRoF|Fa#bn{eIc;gc*7>Zgsm=U4pvlKg=kCzy;$HQ~ z!K^K}L&e0i;Ii6}!F%hkRWl0?wTj>#AFwkT%&1&>n}aeSq`npG`@p=Wr8XFWJ`;Zv-c(~xfV{&&Y^((Vb{;kqt3*St z2D(^LKr410fiDsGU&Q@u4y42OW0`J3Kno03sFeieLtk~^V`y0MQfOPff5?FbC6Ez8 z10d_;zo-N{pb`jx^2L?oa~2%PP0I0EyL@#@#j3lrz4uC+`npg62`1nV;4qpqN@F{I zVUHsf0iO^wo$r|vZ@r1zE<`2plXSd1gq|X@4)T3c1Nxi+lq|4;d~WinE)2Qqe8ke+ z6ANMS_?6t~R*EsGFzxR`Ae+7Ye!0CG6SC1DqaRr@(EGmNqmBLiX?eP*|Iwx!AuV?&9Ld=X2eQ@z zg1-X6(_w}HGy%79-OWQ-fxL_IqnYy;3Sc{bZ;+_Nvn{R9KhXoIaf2xZB-UNZD8u*9 z1ebZp)U2)G+cH+O@%%ep2PL^7D5cL9#%4|@F)IO55*xTr;4xgEMj@iH2Cd@8`nL)f&3kCx>VN`Ci1omgO=>`%&kyr> zVuuBr4#4FNtHFsn8n&Z~49Q6@D$6tM%D%~(Z76PQ2Ie_9Lh_-1vThLkwr_?O5!9_g zVIQu+16VFK;0F=ZKQWw%gB1#ZOq|hZ`L|%uKaYXCBYFit;dV+cR*oy^MNA`DTL6p4KL8>> zy>TWheZ4&cwVcX&6S$u%c9SJ1)4lO=1bCtvMJV7Z-1db8{^V+Zj(=Y5mixH`iQv3tQ1dUmQa44o@Yf9kKcwdc=-;r?Y!o$& z(?$7{47Nf6jFH-k6$c+UXAt;%PX>TLuIo04Y5p={irf3AYz5Tc_&l#~cKeLJjZcD_ zu@*H?3aBU6XzQ_UXNvo@yZ;^Rkq2S$b?xHe=Ph1%<=Y%H4A?1g{xbtuF(R3aDhgm} z&P>51jsDT;pf}kFBX9Z&^6+H^5a)sdQf-t#(9L(KTM6>K2>u%j! zR4@M{O;2lMg*}IrrV=ZJ`i90Xi)lXe?P<%kMJNE1K7b%_#YXu%HsH1p+zI>zH_UR} z2}O;&CwIWe6U(Txc4v2xfQ5Ljy?H+M(C#eXCS0ClkK{CPyG zt3e9DiRZUXD@xC4q;<9*&xE)b4;1)AzLUnf!G zJ7uju70eUc1?DrLP)`jFq%cqdeGvvKH87_HZ&eWFj~Hkgs!$bhd%8@aA0UZ8AY}70 zivjM0P5DF8?|cT)0Ijrs%^Xsy3m}62uVVB7ZsSFkY!fhdx>l0Kb=_F+cHck21%M~G z0DQaqr^ZPccmo5$NLts4g=@}>H!6UAe5^c|h!__kJ7KqKy>ml#p{b z?AMgQEp6**N}xtc66{XTf*a4R3v<@*iyFTO(a(n-cKd+P8ME>kG|%EnmCH&+Oa_w* zg&}WpE^&GnuOs;1gkVq4^t%)es)dFc#|W^5&SwxRrt=vn-+x~@sOEO-=Hc_yWOsNE z9^=F9i&zBa!dtbkk=tJ@<-nHI{(gKx^cC^ zE8H2$L!!c{X946?pO%3%XprLe*_ep9;~USr*RG$$jsiLbeE>2CpA@DA?Z$Yu6T9!9 zALg~F?d3#p|H2~=q=I;9ZjOSBC`)bkAeg6ukmo*5G0=hy4TkX%N$CYCfMUtjK}sOC zSxp$&rXKvX>nO}d;3oypph9xAFBQ#v=wXSJ(!osR%CHd3XY?`4C4RmD{1C#>UI|1p zp8*&Y0&)EJu=Plw_Y1BQ@O0#gGcwAr@L|LAQ@lC#v0RQ|9~>#9gFh+qr-NEoVdPU5 zQ!!&ruXLwr*TWzO@Sr+Fkxh^BvrjQ_uyvuB1;Klf2`v~c5td>^9)KOV%T$orK9dm1 z@)3e~r1Xo^_g9_HVR(jIi+go*|I|KzU^YP{gHxa|8Qe6s;Eghj6$5p8jFPs0%2cIX zSHEG(O$%%N$ESlXk>V8uelij+WZ?e;mBaC2PPW8;<$Oc*> zCGu|)<=10*>MOq=18;#JDwa!tNu4U$78v&HQP_7RA7bM?@Zk9spk0OdB6$vb_wJ=u zERu6kobO^NjQu7TxGV2hm`+(NS)x#@RxMCwM3$CSy(O~v+!85~5;?Qvp2fkAL#^Z% zFzWmt6W>`oAos7M-`bX)cu883uP@T+yBsrh#z^eA-;P1$vhgtXyz-EOpuQu^2QRKk zgJUQ1!RJ-AG`pS{1uB(|g^{hx!-NYe0mZqGTA2n^c2lZuzo| z9aSNOxafPB22f}8J{h!g-B=~;IVD;=s=gGB@`gYQYR8kn8AC~QEcL%V3ewi3{@{%50Q7I-|aPTy6>8*u>x&$)TNz{|b zbh$#Z#^ovKwwSyG>fE5vCoL380J&QwV0A3s6NMI15Br_?Oh*j+D)aS*;J)1verv6x z>CQQO9F;)Vu^IF(=)+wPdgtGb3m;9fEHh%n2qeGO>ac~rAv7DlIHL0F$6s<0TYnLiN6YUbw%+9J#GyW5 z@*e5EQKU#9Bv~FeLu~--K8+)xzJmU^?+b&4NTaH$Ny_^{JBYAZ+q&2#e3hOx@?sfD9PqBNmiD>)w)zy6x>Jk>J<|&!OdJOLhRD% zTg_2TVa?4@5nPQ*AShkAr&9R@4g6n4@Lz%8f7`hg;Ju5hK)<@B;BWK-{?5pUesxM| z+ip3hJp8yVLsJx`Q29_<3Hal2`H(;ORZ!TOQ=Zos(0)43aC2lOtOM)Oa$xbY2&@A! z-?B9y3HY-RfTm*OT(V=wj%o(*C+g`cpU+3pF) zD|n_uJ6yKovKp7Od0PxFcj1zQ%X_$3GUgy`MoFw z{z_B=mjrnL#QAT_EPxf;PHN!at485>oxLj)a#2@yt*URks%Db*ecxMs9ImNT+Sp$} zuIT`Wn-Kfu?1L1eN|w_mNeL(p;7>w>xt4(x5ppDdZ+ZY~QF8W{8hE7oMm^7Bh|vp=r) zw@HZ-fbz@H%Eo~LSLx$Qq zUf^%?AS)YEu4H`uW>l1GrvjNWCF&6e_u7x|APlLo*jc!Y^0bsk( z_>ibcVa?n10pLEGtc4=SzQ_H2qwt=s=45da0_^YQ=g@w0k2k6+dlW~|x;n~rF_K%RY#TWbBci(;ABqSs>ZP>8kBDB{0=-xN; zdc7Nm4e5B9;zEu!a{8dBK5oVR1H$rG?L1`y zdIEn7z%P;(!1KB(Zxv#^(M8#OiDxfBr9i0>PVjp;+hBpqa`%+KZ-Y%~OSLSjRZBI5 z1fGQDSne)B<)G0)%`0|;ueObW>&|}8iCu_fUl0`-MCDFi(uJu%O5t&>P!>_>EN&5D zxft{U#{BYB*)ahHa3nn+ZfsfJjdDEC*wIwm>DY5y8I+ zG^wn2a*bLL7`%3`qyQ34tXrcJu*yATO2rtJKp6c4{)ZoacpplCR0N+SCMIgM3xodv z1b+g3FT3StE}(HFGF_BL{ZgFx{j9O)JMfH}EPOP@59a9x+{#R*ET|9W^>7pb=+ny` zVm_Pd2zB~Vj+>9{3Qg*uch?O5xuL6dqpTmVd#BBYFLt$V#sh1Wkt0H!)=fZNxa_Xh zjc$HXTC>ydchasxL>nRgXVN@(ZMg%*@S9v-#N22kjV4tWBq>E^A>ExeS=$k z0J~5A35)hjbW#9O6&{pGarfKU7>l#&BM=?BRYrS5wu$Y=b+t!`lK$qAl0rA_VkcE-mym|9G zy}Zp<0UXZC4{QBQD_<0`YS?zXa0Hhr$Kg1D_qPUJzP283jB*A_>n5$G8><-QC?o&7Qf%`NlHp;ICbjO>22Dy z>Gj})4`zY=XyS)>p_BpVvI3}tz`u_HxSh*edD3LvtpfOrm8|H9rUo>NtuZJ1rwS{M zhoNxFJ!-^{THqqf_DzM(HSd5||Lh6J zbAu^@K@F2(w9F>zPzC?I;NyCF0yWC-o! zmI6ZHC-7S&IRCruy6d*AtgKQqX3UsGN?_QqVc+!Z+4It7&6*u>`F?540##Uwj-oLR zrNOx~HyZLWyWt{p=m}N|54$K!VW$N0<2msiDTVpmsA=CsC=u6EJtYyw_GkeWN+rU{ ze6MQ?=$3O{gG(FSsck3Fr{BVGEvk)El)@I4e~>f8b#Eq!IUP*o0^|9Up5@e*#*zEUjsvzRtf4Hezt>_ zO@ZdPpfLgOnB5;){gEgE1pi8G`BMt{E|j0DDDT@y4Vh6zMG*~*DwK_bc{|#UX9Us|W_X2&y?;=sF z|K5A=o%rmt&-M-(GGt(GZtm9D*w`iw8Z?*&&PnvB$A1ZfKc$9l!hGwg0Dp?6+RXIW zVqO+u@r>Asd_AtR#z{(B(Du>c)& zA|nrmw=5e(r->i$t=g%s+Lx|b{716#w7;}Y+s%##eX{MpZNT432?UJ;umb;Zl)#}w zhlb_n=P!^td}%p4ZQ8U=4?p~Hzx4F<#R!JaFc)h#7GUL+t7?-*C9c0xgx_Y1!@3>) za|Hy4HB!uQS1^x3Hp)E~RMSY1I{OW>B7)~zaR!wSkq76k2>?F*4o7mmK4@FBBD`^R z8*TlO`D-*o$WF-07mE!3VOr;bzw%;X{bxsTepUJ{m#1+F6yb5F!N{p)9O-MAmetIA zr!gvHnI0m;Fk}wmf@eA?Y+;8;lk#0)Y{!l0Z5@Fr>u%LLg!?vpN#pyU@3;Ye+&3M5 zJ@^S6$=Lz%dbChhw7?(h3WZqGn1p@^{!1e{ z;0u)fQlsITxFBvBPkp^lT1$tCB z9}}|RKrT$dW7^g$qm>bY4@-l*sAFfq<|CPk6z!wYKD;h!;zxA6l$Eg?6pjOM8WAT) z2Zf9SaH4vg#tBrbR*kF{iZ@PS3nWcckO>neY{lhverHx;ph8}~bIzV?xe`Yt3m;8s z)OBozDz7^fImXru-S2-)_TMXk-ZA6O5hF&V8`e!PBDSLYBXB!WnNOhQKM{XZFvfFC1qBI(fBvWChcJ2F zEwK8CK5XTi?N4coQF_#U0OC9e&fhHutYWUAf?|S;|9#Yzql!& z0F^PSMT*vYGx?8WFsn>Fq&6rGohcOr-sZBSc`$ohCY(5#4{hs~(T+8d+xf2@P4JO) zoJ?8l0{6qAg%Y5_BJJIRze^<`XeBWMGaCv!e6x7Fn5f-uBw#6U5$O|UxmU8D%%m4D z$M{&Il#SxO6tE)w_#avxa-^VzoC06i<=3DRa6=cFdIN?IUqc7++ZLXfJf}n?Qk3%5 zY})%%7T^!Y??Y6OiM3CjHcf(bDgJ()F>4wgy9x^FAJVYpd};y2C~=S+XU^*A;{?uz zOKaT?7uL`w^&1<-I!}1?AOiY~a3jL30KKRYKw&8W%~frl0s{_YBhVMXhku=b5B%s6 z;G?cE-&X86(TDELllq|4QDo|gpdOkG*H;(U33BmRZBL6E;IrEkU1U91c81G|NO${ zFzQ5}==JAUerZ)#jq~r{=vA1tQ_M7$V&7r>0P9oQA!TspXo*l z$n3XCJuA+vp@+a^Rz!>AbSzY*#3X~4ABE#5@?m8AO37-ksQ<+{8W7-r}Q zbX`6gX@E<_blO!}Z0B?+N;Xil4&YkJEj0zusm86ZZ%zMs*x{UQaAEbEXdKKm$_J#J z=IrlhTH5H631P&GC{X|B-aZmujh9g-g)7S$z?Ft~;a^9qEh(G=ShUq-I+76%e z6ZF+etN4$iD%q+6{c!uQl->c?LF>su+=?}^;H+Fl#ma&!s-UZ7mW<06*W<9OIg8tm zK5-nZ6Hysm*+5dXU!E!(1mc@+TSBG`4MC@h8j;Y6Tbw=!sj#Xgj3VV;NVxks6rHpA? zQNDKA`RMWbFGcbFyV2r-TJ;3!L@qmd^5lrG zJ7|zT6Jq6eBP)O8KSjTXyRrvGT-JA=qX+jRFAQMC2mUVK0R<%x)wUDgebC(Hv9D`* z41R=N%AP=&?pO76JT&IYujeEk5~Jwlz4z%;#%kbCpia-GgFS6dKxI%16<(Y|L0JkE z;J*o41t;()Xy+WKu@bn86sLBXI2~!9fs?Ww8`1}ZOBu_THqI-GN+BbR_g_91F{OUx z!pO{HnSMB)8FDL@v)3T_6QA(035P~Xp))VT@wn?y39K%LI^-7o=$j1_3P6@O*IY>N zkSYfC8&GErb*>ZVNu6gM8N4VwVl+<>9x?dKBEY|h$44U{eNn_s5Q1qg_HL6+`v=5` zS4xF0qO*w3q*#kUpU(G#w8)|>5ZJFpu&22B8<`#nU?kHg^xY|{Ig%Gt?{Sv8b=|^>61T6_4+TN?>zQ!9R+USj@*eNj5mA zgl+-$6LQFw-W&XzQgF~fJj%yT(JZN~kcUgh??CXUIzv$~-x$tC`EZ7^A@Vzv$Uj*G zdk_Ll9A^`3jtg-1Qv{Q?IiDdnP+lQ#LDi`(&9YZ?Jo5VmEu<+9-0V>V1*S+49_wV$ zpDHo&0P^`SjoWU4e_zC79w~~aZ2|t|)_>{({Dn}b*5_ER5h{V83DOb#DP6*K+_?Wh zu=nzIIya398jrs#;P%(7PVPi293R=p4E&qm2VQ~TZ_*p5Qg1%JH@YVEwqC+k2<4a; z6TN8QU$3$U+MN{x-~HuYZao~4 zX*nCJ%69*6zx_s>CT0KX%9{v)A8FUFogHUc+V0)w{X7%0z8WJd0jpf>_cHhsv2r)J zG#Um!nH2*KY#PV)NQ~Eb7WBv^ijunsmBD882t@vT8ihtValXNA@1Mcorkv*TXPT6S z!nq8_gPz&c)}$8pphtfjW&J9aS0ORR1LJYuek~$X33ioq@b_pZ(C8mRe7_p#4^Xx9 zDS|dV54Sw1>Iwu;rKH75)=yHH4@lvVr6Q$)oIC(h2m<_h)5DyfBN8P*Ic&Bv5Pojh zHv&Q@h;WjMTyB&E{?rFSJa%gY_(Sr+tO$O?W2kT8FZ}x+ZcB3u@Gj&975vE#$K220 zBogRdW}q3MVKom7?~wq1?e+myI(YKhL^ztMY6>8=O&s*=5C^kYh0Y8qbN*I+0JW22 zeF+|qx;-O5)^DREoWR{l5d6vWTE;YrAebrTz;fKz8I^!}It))t$h$9n?HEYtaHF=J zlmjaO0P9@r6iWCeQ;EWxB9Ch-D_MH~udv7cDl3JR2<*p&$AI8Z`R{MSzvKiYY)6`lcV+Qc9}cN$nx)UDf!WR=EBI=s^A#Zu*cm`&0wrH~D1cHCaB!vE&_ARM>?k2CZ@>Wl;G_hOp7g`O zw+lncwj2!b%$>PG)?`gg;QXUhG&R@`Q3_SDz4R6e00Et-1LEZ88o{6UpTq8bbyl{+ zaofwFcQD&Y76Ta#sF6oa)-)VH#bUI+pK*2Lwhp4 z|NUr72E5*}N>#Rq<0Y3kn({m^6Qpeu*ubIf=DFqlK1=X7dS~Pf7(IBVaYyp{jna0J zYCw*~CvWU&_7d6#8G=*cX9WL?xxUaef}|0j$8BS9-Nk6rj=*#!O`2ypm1hO0_w zR!oI~n~hfP3s6)jjC$gTPf)n$*H4N;3k6kO^WLt+iuy6zZtw~!v%6c-0sLhpprR6Z z)aIB5%ETiASlJ8}YvT zz`4WmCFXyLzwThL{>jqpj?2Yt$*)4tzhM5(03!wR?*%&_BObDv1pbS`WE5r=djQR9 z#%Ry{_dY+QqJ2u-|I!U!ICR2&8%?d7y#IKfkKXb278T;_H!2$s`S^YhW_#i7_32Fq zHY!`LdHMK9qD=u6MP!gC4}gGPY?DD>cmU)fu#l1Q02C$&6iO(U-#LLo1vW^^NEB)) z5KKjn{SK{z*{E$(aQpXMpH{E|u@Xw;Op(RhzNdrUx1)`*bvwQYt^dV%Xd}g=O!xb- zyw20;p`dD^m*9gysHe~zm*R1WijMn)=f5LvdG%-@;Th;^LGuO#yP@dnAvW4?!3_mu zR0s&Bt}K7z+TV(dJYtb-+>heVpm`TU`}fIjJorX+Sf^G6{V%-^FeLbeVK zNglaf`aP2^>Q~sxYs1^=9u_f(r!nP2AV3jE*Eu}jOa}T67W>a(i)uqj2;qh7F<;-ebF=5)Ctb(h0)G1xRk{16>M~d-4EyU(H)dzJw9z9+MxKBWs{TlB35X9y1P{lI~E&7U5f{y+Q)kEqi&_hDC zPGFUJ8#;#v|I?`>C3*@#fVvF$Znqh@|73~Uu#*Hl-i|}=;80%dpJU=BxREan9?U_| z`vhu89Prx=&h*K?TsWx9V5Wwslk!Dw)K;=rI**kQg)kDq{F^t|T=;?+rNTm<(Cuu6 z(%in=OhJ!TLZfOiFk)sdoL$uemC7ivalaqd><%y1KRHfWyg9w#jrTTXz{u7Ws1w+6 zfLNF`gJR>2E#+fH#z;~K53CW|Pj)VJ8nPRCgsLtHM z3ZXI6L%IAt2~;jX(C@&V*3ql# zz%9BZ>4ke@3W*AxtnmBfz)nSs^HO#GAGmW_8xbrziNy!SV`9&=kBFe2DJNl)Eqpmr z$N~ICKZPA2N3|z~G;J%CCbLOisenB1_JE_76)YH3<`L*?Rd;H;7 zXcdwUW_z_+fL>a2oIC<;J4OFI{Ob|O@fA<{jCSqXZKP-)&^vse<;_H$SVsVvP(ViO z-*_DEyW2zo?BT^uu3+PVypeBt+vEKGXwcX}1pCJs?8~s1em(xC@dOib-#Gjohkpy8 zMEd3dkP-Jnks~iS7|fKydz&qR{rm{#)Lyl_k@7dY6Nn zZd*dG_9rgSMGbENmNOd&w4SLOA%`XY_j9!T2e6e`9zruhdQB*~T?q0`2<$Cnmq95} z|H)+t_(vjrQL}z2#FZl%YS4N(l9uQ1b78f_4cku^H0xMRk1!U0i3<8;+sR`4*bMZ2 zih3U6`i<7VI)u7cZV zoq=ua-`)vaw3nw*Jc8%p_o zDYb6$KEo!veDr4CoLbxYx6Ux4^aiXoxHjrW9cDvCOzPInrJM)MRHWBR{9V4uL$ z=bJ$feuw5!wnosu9e)!IM1#p@L5bK>JEx%!Ai?lgo-t71Y`o#GWc@>e)>VstxuyXz z4$kq%Xc(=Bg2oc{gSZ_}(aLyipu9&5_Y_>eCom>}y@Pr(Gzyn=!x7$|Jd@8kdcK>iP!Bt=FgJ-?~06Z}5R^ zU&Em-%EjjS)P1F572~8dcKPVdD7|Qdztj09GVK#GAq(g8v6Hc2Gx-RD`zHwQ%kb}h zlYLpZZy_!(GPt}I;r*ZK;{=LS-k94bY|8S!BH#ZS_VfhZdVt$HeKQelNc8McgXgY4 zya(1GHaA6_UhUH?e)Rx?_HH!TNg?cD_GcZgA62CCel6jf{`&OTe#-fvxdd^V)vqO% z^T+9a01qJ(G?Pb(D@g}^hKS7wV5&eAVn|~LDAIsHf(Et`Aml_d8|dkjFv33rmcN^{ z5hYS0|Ic#XyIcQK6}1Y2z90W0Rw%b2sBcpg&m8m$N@-q&s%GP#RFx*HSJn6Mcb={c zltU#7phQZfL`vjeDzPQ6phSu)?M7$l9aehgsSEIL8CGvV6e`*Iq6&e#mgV!n*ekpCh;*Gpc6sXMcc{Cd14Ygs( z7bsBxCGsy940OW}~rsyH+hq?nnTMnV3 za9_$xMI#j}x;*7Eeix#r^j6UM>WTb0>B~_hJOwsT`tk`7+6f=XB~^dy@lRD%?JMt8 z6xax==P7-~KN`P%li>Zzr#l|4(66NRlqi5RPTqe%5TsF!D?bg$(c<`TzWHXy@{=Y_ z5{(!-Q2ZwVjA=eL6Latvdq6Zk%P*ivhf49NakF+pm6*x1AKA^b3=f+Eu(|2RbDco~Z;N>52{UO7jxE5k#fAbXN?~9AcgB&!~y}lGk zQu1pcqF?al_-DQ{DOL}c=h3q7$@<_jLIsbOfe>P75F&zqF7DrnyO*OrIBwPTv~b}< z_3H&IYMEXCsw+G1>)N$zxT@nue*R(X1<-)Mb*u!;6u>16{uE+6khf7d?oGUJvArtP zA~a}l;Y$eghb6^8QBM@rbrjbh!hP8W@F$D>NB-Rb0sa^K+nkmEcwDEUt%GnIh0m^G z<3L=~?wycFauF-AV}^Sbij8YPXD0S!!}DJ%U_*+)=0{2c%ikCL`=REm85@9I5& zR8s&u9;@`de9}haG8A8a3Ba6D3ZU{k@EtbHH?UpKQ6M-Yav(#p&hz%d^^u4Fqor8} zsk||tC=tA0vIW#1MDi=%FXj`Ktmf~yfzENbXL8F&q9xtg!s-@ceti}POS}@_M|)mB z7xba`1aSMqc)x;T@J3*@o|fzTeBLcNnc3Y_N+(hdn}fm=$vm0aJuWf9Jo%Uk4}+ri z7jeh<{}qV+S7_EBk6huQ>@-;Izj(k4xR_3>`NcOswCr>BWqW7OSzKMdK7s$|U;oe& z6+pOkT;ygEDZr^dESI)h9WSR^Si}mTj)mn<&JB&Q_8_nm=miZb#BKEcU&rH!qIrT9 zyHSzI;hhT*=&Lb(L`vr-T%UzXfCiEg-@nh{nal9~tYKwDV+SsWFmfTW2*9V*6>UuJ z_iua8K*>~;k|>iiSv-cbHAIRWOyrBXUas4|BAP|3(aUmWhPP0^0m_{?( zINZxJ<{&%T9Mg%9ajM8<vAZY1IX6$Vi`WQ3Zb*{@$ii{IaU<#AQ1wh;;M@ zh=#cVx4(h)$TTGdg8#Y~Uw-SH)T=HzAg zw6e5mZj32R{n!c30YO~*KvV_^hJB3WfO?SzO=m!U(vKE&g>3 z{+~tw{_^wgHQAS40-yg+3P6Qtarp=ra(9~t)d0O3v?gv>JEkzliq$zMYnGG>y>aP- zMqq1P-U2bpy?Hr0=SS>rH#gben)kcEzRunD;j(Xq@m}6$WiyZ0m-70!Q}kU<=k=R3 zZPsc{e0=<$d0yzP`aHc8<6~FXt>2WaNz?pAem}ge>U2tuZs~hIRVW>@q7rQ}7(ew>8Z_SNjAO&)&e-Zh_n@$ET1ujp~SchBx=U(H*3&ak0_)_gVh z$0iXx&sh?+BcfI|@;fz?Hio}e+hn;F^ucn>nd^arIsn+^MDtfD35;)s@2Y_>aPri6Y0nlAtelY2me-4=o_kN z zX72LN*Y>(-sf`uxb$Cy&@}jV%{({3fhC*;^4zcYX2<iCqI=~=B9GG9<8N5TU4}5*y3ev8B3f?QF44_7Q zC9hLMT&}9>h*R`kO|K97JjPD)o!93h{=JhIV*XTPRf+-c=u>?)EolTjdtp8+-${P7 z@@ZabVvqx@pzcZ@#J|63VW28hG4}wLq@-SX(bBKyEa`OhRhRwQ=bFxc;hCEJsPY2M-f7hn$A*Qv6qdzY1Ku~{=DVw%vB8QC zk*YGYsc2jPr5n$*p}2~bpI1D}pZL$}5}5r5cmR?mPaQvlGo!2X| zg*fvReb=H~|0G)O^Zly-N;K`y<1bhjRCOM%o2T$BjQ3ANlVX&IawzEu302MM@q-8h zt%g5a@ZqK$^0ld5PJT6KY1wP9>T=?XIZMvR6DZfEc3J-A>?P%TWnXI=dBp* z!s;gblAJEG^U(?)mq6@4MF9k-+sZ`1z8TlQ#7$|q>2`&}_(I%;AnAkx8&^rIFkX46aa;y(PSo0Q&M$i_k4xTG0XWFr|TQ7y>P>2iN~c*RO0YE zvUM0q80YD_eg;}@vk|}hB6!o(LZ8}Lk|46!a`ssQm-e{w3_-u&4?aav{dhn;Zfl6g zwo~-F@x6ryKuRD3t^MRaS9QtcZDkPrGtbmUL{UBn?B9T%%-t!}NY}TJ6YQ!VkKo->SsJ_} z267#X@EoV~`fp7L=tTmk>8^=HRUh!hDak1W^fobzk#>j_QHIBml77za`dzzr%@P0> zLFnh{kgfboV^A585*&q!K}*ko>&dJbXpF#TxZM&w5!`=9&~M4cPjeHX{TBD1kH?9S z+#^gW$x%gdZY*>z3tZN$e&`;>8|uR)Q2B46fJ#Ij?Q52z*^M$1KPisAZxL=KqU%di z)a$zv9q4%iYY)v*hhUwpXv;PdOiX@MQKsL z6?GQ(!Sx?liP7j|>hqusu3tEv3&EZGuUa6`Q@U>v^l(!03gvd&4)ie50#?P8!-qP6 zKQNq@2DK5b+L$f*I7zvvnz1lc$&|cKI@b=|R*5|tpTVQZL&cz{l2DWt`FZ(!>OOx| z9zC=T7gii9KDR2Ou>g-%P?nb<&FPntY@j9b{~|OMV=aech|20$#B)$gd~K_p2>Jx9 z6ta0gTkr&^GzT+{U->YSTtIe$O2TY2DWV5WwmoE`Y-ptK*ZA{N_9W!zyoTu{vyR*Y zr~qmR9BKH{@tX2ZpI5nBRa9-bCwhkO#Cg=9`LDN(tgsJ?xf8lXN)$kel*s?Jm?!{J zIy4B96piRWrh#fi36X32cZdQgk$ + + + + e签宝合同比对 + + + + + +
+
+ +
+ + + + + + \ No newline at end of file diff --git a/seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/js/compareBiz.js b/seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/js/compareBiz.js new file mode 100644 index 0000000..d65faaa --- /dev/null +++ b/seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/js/compareBiz.js @@ -0,0 +1,99 @@ +$(document).ready(async function() { + const apiBaseUrl = window.location.origin; + let currentFilePage = 1; + let totalFilePages = 1; + let searchKeyword; + const filePageSize = 10; + + const dataList = await pageQueryTemplates(); + renderTemplates(dataList); + + + function renderTemplates(list) { + const container = $('#file-list'); + container.empty(); + list.forEach(file => { + const fileItem = $(` +
+ ${file.signTemplateName} +
+ `); + container.append(fileItem); + }); + filePagination() + } + + async function pageQueryTemplates() { + const pageIndex = currentFilePage; + const response = await fetch( + `${apiBaseUrl}/seeyon/rest/cap4/etemplate/querypage?pageNum=${pageIndex}`, {} + ); + const res = await response.json(); + // 校验返回值 + + console.log(res) + let respData = res.data; + totalFilePages = Math.ceil(respData.total / filePageSize); + return res.data.templateList; + } + + async function getContractCompareUrl(templateId,templateFileId) { + const response = await fetch( + `${apiBaseUrl}/seeyon/rest/cap4/etemplate/compareurl?templateId=${templateId}&templateFileId=${templateFileId}`, {} + ); + const res = await response.json(); + // 校验返回值 + } + + function filePagination() { + // 分页逻辑 + const pagination = $('#file-pagination'); + pagination.empty(); + + const firstBtn = $(''); + const prevBtn = $(''); + const nextBtn = $(''); + const pageInfo = $(`第 ${currentFilePage} 页`); + + // 判断是否禁用按钮 + prevBtn.prop('disabled', currentFilePage === 1); + + // 首页按钮点击事件 + firstBtn.click(async () => { + currentFilePage = 1; // 跳转到第一页 + await loadTemplates(); // 加载文件 + }); + + // 上一页按钮点击事件 + prevBtn.click(async () => { + if (currentFilePage <= 1) return; // 防止重复请求 + currentFilePage--; // 页码减一 + await loadTemplates(); // 加载文件 + }); + + // 下一页按钮点击事件 + nextBtn.click(async () => { + currentFilePage++; // 页码加一 + await loadTemplates(); // 加载文件 + }); + + // 加载文件的逻辑 + async function loadTemplates() { + let tempTemplateList; + + tempTemplateList = await pageQueryTemplates(); + + renderTemplates(tempFileList); // 渲染文件列表 + pageInfo.text(`第 ${currentFilePage} 页`); // 更新页数信息 + } + + // 添加按钮到分页容器 + pagination.append(firstBtn, prevBtn, pageInfo, nextBtn); + } + + function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + } +}) \ No newline at end of file diff --git a/seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/js/compareinit.js b/seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/js/compareinit.js new file mode 100644 index 0000000..388ffea --- /dev/null +++ b/seeyon/apps_res/cap/customerCtrlResources/esignContractCompareBtnResources/js/compareinit.js @@ -0,0 +1,207 @@ +(function(factory) { + var nameSpace = 'field_8899554679928334458'; + if (!window[nameSpace]) { + console.log("开始实例化控件") + var Builder = factory(); + window[nameSpace] = { + instance: {} + }; + window[nameSpace].init = function(options) { + window[nameSpace].instance[options.privateId] = new Builder(options); + }; + } +})(function() { + function App(options) { + var self = this; + self.initParams(options); + //初始化dom + self.initDom(); + //事件 + self.events(); + } + + App.prototype = { + initParams: function(options) { + console.log("开始初始化参数") + var self = this; + self.adaptation = options.adaptation; + self.privateId = options.privateId; + self.preUrl = options.url_prefix; + self.adaptation.formMessage = options.formMessage; + self.messageObj = options.getData; + console.log(self.messageObj) + }, + initDom: function() { + var self = this; + console.log("开始渲染dom") + dynamicLoading.css(self.preUrl + '/css/contractCompareBtn.css'); + self.appendChildDom(); + }, + events: function() { + var self = this; + // 监听是否数据刷新 + console.log("设置事件监听") + self.adaptation.ObserverEvent.listen('Event' + self.privateId, function() { + self.messageObj = self.adaptation.childrenGetData(self.privateId); + self.appendChildDom(); + }); + }, + openCompareUrl: function(privateId, messageObj, adaptation) { + // 实际的业务代码方法 + messageObj = adaptation.childrenGetData(privateId); + const targetObj = messageObj.formdata.formmains[adaptation.formMessage.tableName] + //backFill(ids, messageObj.id, messageObj.display, privateId,messageObj, adaptation); + //dialog.close() + console.log(targetObj) + if (targetObj) { + let contractRefId; + let templateRefId; + for (const key in targetObj) { + if (targetObj.hasOwnProperty(key) && !/^auxiliary/.test(key)) { + if (targetObj[key].display === "合同审批附件") { + console.log(targetObj[key]) + contractRefId = targetObj[key].showValue; + console.log("合同附件refId: " + contractRefId) + + + } + if (targetObj[key].display === "原始合同模板文件") { + console.log(targetObj[key]) + templateRefId = targetObj[key].showValue; + console.log("模板文件refId: " + templateRefId) + } + } + } + + $.ajax({ + type: "POST", + url: '/seeyon/rest/cap4/etemplate/compareurl?templateRefId=' + + templateRefId + '&contractRefId=' + + contractRefId, + data: {}, + dataType: "json", + contentType: 'application/json;charset=UTF-8', + success: function(res) { + // 后台解析数据后 将数据填写到表单中 + if (res.code == 0) { + window.open(res.data, "_blank"); + } else { + // 报错 + $.alert(res.message); + } + }, + complete: function() {}, + error: function(e) { + top.$.error(e.responseText); + } + }); + } + }, + appendChildDom: function() { + var self = this; + const params = new URLSearchParams(window.location.search); + console.log(params) + const openFrom = params.get('openFrom'); // 返回"2025" + const type = params.get('type'); + var edit = !((openFrom != null && (openFrom == 'listPending' || openFrom == 'listDone' || + openFrom == 'listSent')) || (type != null && type == 'browse')); + console.log(edit) + var domStructure = '
' + + '
' + + '' + + '
' + + '
'; + document.querySelector('#' + self.privateId).innerHTML = domStructure; + var compare = function() { + self.openCompareUrl(self.privateId, self.messageObj, self.adaptation) + } + + var content = self.messageObj.formdata.content; + const picker = document.querySelector('.customButton_class_box' + '.' + self.privateId); + if (picker) { + document.querySelector('.customButton_class_box' + '.' + self.privateId) + .removeEventListener( + 'click', compare); + document.querySelector('.customButton_class_box' + '.' + self.privateId).addEventListener( + 'click', compare); + } + const goNewPages = document.querySelectorAll('.goToNewPage'); + if (goNewPages.length > 0) { + goNewPages.forEach(element => { + element.addEventListener('click', function(e) { + window.open(e.currentTarget.dataset.href, '_blank'); // + }); + }); + } + //渲染隐藏权限 + if (self.messageObj.auth === 'hide') { + document.querySelector('#' + self.privateId).innerHTML = + '
***
'; + } + } + + }; + + function backFill(ids, fieldName, fieldDisplay, privateId, messageObj, adaptation) { + var param = new Object(); + param.masterId = messageObj.formdata.content.contentDataId; + param.ids = ids; + param.formId = messageObj.formdata.content.contentTemplateId; + param.fieldName = fieldName; + param.fieldDisplay = fieldDisplay; + param.masterId = messageObj.formdata.content.contentDataId; + $.ajax({ + type: "POST", + url: '/seeyon/rest/cap4/hywp/fileref', + data: JSON.stringify(param), + dataType: "json", + contentType: 'application/json;charset=UTF-8', + success: function(res) { + // 后台解析数据后 将数据填写到表单中 + if (res.code == 0) { + var backfill = {}; + backfill.tableName = adaptation.formMessage.tableName; + // 回填主表 + backfill.tableCategory = 'formmain'; + // 后台组装的data数据 + console.log(res.data) + backfill.updateData = res.data; + adaptation.backfillFormControlData(backfill, privateId); + } else { + // 报错 + $.alert(res.message); + } + }, + complete: function() {}, + error: function(e) { + top.$.error(e.responseText); + } + }); + } + var dynamicLoading = { + css: function(path) { + if (!path || path.length === 0) { + throw new Error('argument "path" is required !'); + } + var head = document.getElementsByTagName('head')[0]; + var link = document.createElement('link'); + link.href = path; + link.rel = 'stylesheet'; + link.type = 'text/css'; + head.appendChild(link); + }, + js: function(path) { + if (!path || path.length === 0) { + throw new Error('argument "path" is required !'); + } + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + script.src = path; + script.type = 'text/javascript'; + head.appendChild(script); + } + } + return App; +}); \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/EsignPluginApi.java b/src/main/java/com/seeyon/apps/esign/EsignPluginApi.java new file mode 100644 index 0000000..7e6c7b3 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/EsignPluginApi.java @@ -0,0 +1,44 @@ +package com.seeyon.apps.esign; + +import com.seeyon.apps.common.plugin.api.APluginInfoApi; +import com.seeyon.apps.common.plugin.vo.ConfigVo; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import org.springframework.stereotype.Component; + +@Component +public class EsignPluginApi extends APluginInfoApi { + public EsignPluginApi() { + } + + public String getPluginId() { + System.out.println(EsignConfigConstants.getPluginId()); + return EsignConfigConstants.getPluginId(); + } + + public String getCreateUser() { + return "橙阳科技"; + } + + public String getDescription() { + return "E签宝对接封装"; + } + + public ConfigVo getDefaultConfig() { + ConfigVo configVo = new ConfigVo(); + EsignConfigConstants[] var2 = EsignConfigConstants.values(); + int var3 = var2.length; + + for(int var4 = 0; var4 < var3; ++var4) { + EsignConfigConstants value = var2[var4]; + if (value != EsignConfigConstants.plugin) { + configVo.getDevParams().put(value.name(), value.getDefaultValue()); + configVo.getProdParams().put(value.name(), value.getDefaultValue()); + configVo.getParamMap().put(value.name(), value.getDescription()); + } + } + + return configVo; + } + +} + diff --git a/src/main/java/com/seeyon/apps/esign/config/EsignConfigProvider.java b/src/main/java/com/seeyon/apps/esign/config/EsignConfigProvider.java new file mode 100644 index 0000000..40b3c23 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/config/EsignConfigProvider.java @@ -0,0 +1,22 @@ +package com.seeyon.apps.esign.config; + +import com.seeyon.apps.common.config.ICstConfigApi; +import com.seeyon.apps.common.plugin.vo.ConfigVo; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.ctp.common.AppContext; + +import static com.seeyon.apps.esign.constants.EsignConfigConstants.getPluginId; + +public class EsignConfigProvider { + + protected ICstConfigApi cstConfigApi = (ICstConfigApi) AppContext.getBean("cstConfigApi"); + + public String getBizConfigByKey(EsignConfigConstants key) { + ConfigVo config = cstConfigApi.getConfig(getPluginId()); + return config.getParamVal(key.name()); + } + + public static EsignConfigProvider getInstance() { + return (EsignConfigProvider) AppContext.getBean("esignConfigProvider"); + } +} diff --git a/src/main/java/com/seeyon/apps/esign/constants/EsignApiUrl.java b/src/main/java/com/seeyon/apps/esign/constants/EsignApiUrl.java new file mode 100644 index 0000000..5c201d8 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/constants/EsignApiUrl.java @@ -0,0 +1,19 @@ +package com.seeyon.apps.esign.constants; + +public class EsignApiUrl { + public static final String SEAL_GRANT_URL = "/v3/seals/org-seals/internal-auth"; + public static final String SIGN_START_URL = "/v3/sign-flow/{signFlowId}/start"; + public static final String SIGN_BY_FILE_URL = "/v3/sign-flow/create-by-file"; + public static final String SIGN_FLOW_QUERY = "/v3/sign-flow/{signFlowId}/detail"; + public static final String GET_UPLOAD_FILE_URL = "/v3/files/file-upload-url"; + public static final String CONTRACT_DOWNLOAD_URL= "/v3/sign-flow/{signFlowId}/file-download-url"; + public static final String PERSON_AUTH_URL = "/v3/psn-auth-url"; + public static final String SIGN_POSITION_URL = "/v3/files/{fileId}/keyword-positions"; + public static final String QUERY_TEMPLATES_URL = "/v3/sign-templates"; + public static final String QUERY_TEMPLATE_DETAIL_URL = "/v3/sign-templates/detail"; + public static final String QUERY_ORGINFO = "/v3/organizations/identity-info"; + public static final String CONTRACT_COMPARE_GETURL = "/v3/contract-compare-url"; + public static final String TOKEN_GET_URL = "/v1/oauth2/access_token"; + public static final String SEAL_QUERY_URL = "/v3/seals/org-own-seal-list"; + public static final String SIGN_LINK_GET_URL ="/v3/sign-flow/{signFlowId}/sign-url"; +} diff --git a/src/main/java/com/seeyon/apps/esign/constants/EsignConfigConstants.java b/src/main/java/com/seeyon/apps/esign/constants/EsignConfigConstants.java new file mode 100644 index 0000000..638f621 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/constants/EsignConfigConstants.java @@ -0,0 +1,44 @@ +package com.seeyon.apps.esign.constants; + +public enum EsignConfigConstants { + plugin("esign",""), + APP_ID("7438886882",""), + APP_SECRET("325c8cc6c2a303d6cf1fb5657a16e591",""), + OA_HOST("http://cd-2.frp.one:58336",""), + ESIGN_HOST("",""), + SIGN_SERVICE_PROVIDER("ESIGN",""), + UNITNAME("","平台方组织名称"), + FORMEDITLOGINNAME("","表单修改登录名"), + updateAccountName("表单",""), + getTokenUrl("/seeyon/rest/token/","调用获取TOKEN地址"), + nodeTokenUrl("/seeyon/rest/flow/notification/","超级节点回调URL"), + restName("",""), + restPwd("",""), + signAutoDate("","是否自动加盖签署日期"), + eSignOrgId("61f735dd368c45a191f43a6711c3c88a","e签宝平台方组织id"), + formLoginName("2019","表单数据录入登录名"), + sealInfoFormCode("","印章档案编码"), + aSignPositionKeyword("甲方盖章/签字","甲方签署位置关键字"), + bSignPositionKeyword("乙方盖章/签字","乙方签署位置关键字"), + lpSignPositionKeyword("法定代表人","法人签署位置关键字"), + ; + + private String defaultValue; + private String description; + + EsignConfigConstants(String defaultValue,String description) { + this.defaultValue = defaultValue; + this.description = description; + } + + public String getDescription() { + return description; + } + + public String getDefaultValue() { + return defaultValue; + } + public static String getPluginId() { + return plugin.defaultValue; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/controller/DefaultEsignCallBackController.java b/src/main/java/com/seeyon/apps/esign/controller/DefaultEsignCallBackController.java new file mode 100644 index 0000000..35c5871 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/controller/DefaultEsignCallBackController.java @@ -0,0 +1,89 @@ +package com.seeyon.apps.esign.controller; + +import cn.hutool.log.Log; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.seeyon.apps.esign.po.esignapi.EsignCallbackParams; +import com.seeyon.apps.esign.service.EsignUploadFileService; +import com.seeyon.apps.esign.service.EsignCallbackFlowBizService; +import com.seeyon.ctp.common.AppContext; +import com.seeyon.ctp.common.controller.BaseController; + + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.BufferedReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +public class DefaultEsignCallBackController extends BaseController { + + private static final Log log = Log.get(DefaultEsignCallBackController.class); + + private EsignUploadFileService esignUploadFileService = (EsignUploadFileService) AppContext.getBean("esignByUploadFileService"); + private EsignCallbackFlowBizService esignCallbackFlowBizService = (EsignCallbackFlowBizService) AppContext.getBean("esignCallbackFlowBizService"); + + + public void callback(HttpServletRequest request, HttpServletResponse response) { + try { + log.info("签署回调触发"); + // 签署回调处理,改变超级节点状态 + String formId = request.getParameter("formId"); + String tableName = request.getParameter("tablename"); + String updatefield = request.getParameter("updatefield"); + String statusfield = request.getParameter("statusfield");; + BufferedReader reader = request.getReader(); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + String body = sb.toString(); + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + EsignCallbackParams callbackParams = mapper.readValue(body, new TypeReference() { + }); + String flowId = callbackParams.getSignFlowId(); + String action = callbackParams.getAction(); + log.info("签署回调当前流程: " + flowId); + if(action.equals("SIGN_FLOW_COMPLETE")) { + if(callbackParams.getSignFlowStatus() == 2) { + Object[] fileInfos = esignUploadFileService.getDownloadFileInfo(flowId); + esignCallbackFlowBizService.handleSuccessSignCallbackBiz(tableName,updatefield,statusfield,formId,fileInfos); + }else if(callbackParams.getSignFlowStatus() == 5){ + //合同过期 + //esignCallbackBizService.handleExpiredSignCallbackBiz(tableName,statusfield,formId); + }else { + esignCallbackFlowBizService.handleFailSignCallbackBiz(tableName,statusfield,formId,callbackParams.getResultDescription()); + } + //signLinkService.del(flowId); + response.setStatus(HttpServletResponse.SC_OK); + return; + }else if(action.equals("SIGN_MISSON_COMPLETE") && callbackParams.getSignResult() == 2 && callbackParams.getSignOrder() == 2){ + //发送消息 +// MessageVo messageVo = new MessageVo(); +// messageVo.setBizId(flowId); +// messageVo.setMessageType("SIGN"); + +// thirdMessageService.sendMessage(); + } + //下载合同 + response.setContentType("application/json;charset=UTF-8"); + response.setCharacterEncoding("UTF-8"); + response.setStatus(HttpServletResponse.SC_OK); + try (OutputStream out = response.getOutputStream()) { + out.write("{\"code\":\"200\",\"msg\":\"success\"}".getBytes(StandardCharsets.UTF_8)); + out.flush(); + } + log.info("回调处理完成: " + flowId); + return; + } catch (Exception e) { + log.error("回调处理失败", e); + } +// response.setStatus(HttpServletResponse.SC_OK); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + +} diff --git a/src/main/java/com/seeyon/apps/esign/fieldCtrl/ContractCompareFieldCtrl.java b/src/main/java/com/seeyon/apps/esign/fieldCtrl/ContractCompareFieldCtrl.java new file mode 100644 index 0000000..3de3357 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/fieldCtrl/ContractCompareFieldCtrl.java @@ -0,0 +1,61 @@ +package com.seeyon.apps.esign.fieldCtrl; + +import com.seeyon.cap4.form.bean.ParamDefinition; +import com.seeyon.cap4.form.bean.fieldCtrl.FormFieldCustomCtrl; +import com.seeyon.cap4.form.util.Enums; + + +public class ContractCompareFieldCtrl extends FormFieldCustomCtrl { + @Override + public String getPCInjectionInfo() { + return "{path:'apps_res/cap/customCtrlResources/esignContractCompareBtnResources',jsUri:'/js/compareinit.js',initMethod:'init',nameSpace:'" + getNameSpace() + "'}"; + } + + @Override + public String getMBInjectionInfo() { + return "{path:'http://mapResource.v5.cmp/v1.0.0/',weixinpath:'invoice',jsUri:'js/location.js',initMethod:'init',nameSpace:'"+getNameSpace()+"'}"; + } + + @Override + public String getKey() { + return "8899554679928334458"; + } + + @Override + public boolean canUse(Enums.FormType formType) { + return true; + } + + @Override + public String[] getDefaultVal(String s) { + return new String[0]; + } + + public String getNameSpace() { + return "field_" + this.getKey(); + } + + public String getFieldLength() { + return "20"; + } + @Override + public String getText() { + return "e签宝合同比对按钮"; + } + + /** + * 控件初始化接口,此接口在控件初始化的时候,会调用,主要用于定义控件所属插件id、在表单编辑器中的图标、表单编辑器中有哪些属性可以设置。 + * 使用举例:在接口中定义自定义控件在在表单编辑器中有哪些控件属性需要配置 + */ + @Override + public void init() { + //设置图标和插件ID + setPluginId("src_esign"); + setIcon("cap-icon-custom-button"); + // 自定义参数 + + ParamDefinition esignContractCompareBtnParam = new ParamDefinition(); + esignContractCompareBtnParam.setParamType(Enums.ParamType.button); + addDefinition(esignContractCompareBtnParam); + } +} diff --git a/src/main/java/com/seeyon/apps/esign/job/SealDocSyncJob.java b/src/main/java/com/seeyon/apps/esign/job/SealDocSyncJob.java new file mode 100644 index 0000000..922fdd5 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/job/SealDocSyncJob.java @@ -0,0 +1,71 @@ +package com.seeyon.apps.esign.job; + +import com.seeyon.aicloud.common.JsonUtils; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignApiUrl; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.esignapi.EsignApiHeader; +import com.seeyon.apps.esign.po.esignapi.EsignBaseResp; +import com.seeyon.apps.esign.po.seal.SealInfoVo; +import com.seeyon.apps.esign.service.SealService; +import com.seeyon.apps.esign.service.TokenCacheManager; +import com.seeyon.apps.esign.utils.HttpClient; +import com.seeyon.apps.ext.quartz.AbstractQuartzTask; +import com.seeyon.ctp.common.AppContext; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +import java.util.Map; + + +public class SealDocSyncJob extends AbstractQuartzTask { + + private static final Log log = LogFactory.getLog(SealDocSyncJob.class); + + private EsignConfigProvider configProvider = EsignConfigProvider.getInstance(); + private TokenCacheManager tokenCacheManager = (TokenCacheManager) AppContext.getBean("tokenCacheManager"); + private SealService sealService = (SealService) AppContext.getBean("sealService"); + + @Override + public String taskRun(String s) throws Exception { + log.info("开始执行印章同步任务"); + syncSeals(); + log.info("印章同步任务完成"); + return ""; + } + + private void syncSeals() { + String orgId = configProvider.getBizConfigByKey(EsignConfigConstants.eSignOrgId); + String host = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST); + Integer pageNum = 1; + Integer pageSize = 20; + EsignApiHeader esignApiHeader = EsignApiHeader.build().appId(configProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)); + esignApiHeader.token(tokenCacheManager.getToken()); + EsignBaseResp esignBaseResp = null; + Map dataMap = null; + do { + String url = host + EsignApiUrl.SEAL_QUERY_URL + "?" + "orgId=" + orgId + "&pageNum=" + (pageNum++) + "&pageSize=" + pageSize; + String respStr = HttpClient.httpGet(url, esignApiHeader.convert2Headers(),"UTF-8"); + esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); + if(esignBaseResp.getCode() != 0) { + break; + } + dataMap = (Map)esignBaseResp.getData(); + Object[] sealArray = (Object[]) dataMap.get("seals"); + if(sealArray == null || sealArray.length == 0) { + break; + } + for (Object o : sealArray) { + SealInfoVo sealInfoVo = JsonUtils.parseObject(JsonUtils.toJSONString(o), SealInfoVo.class); + sealService.upsertOaSealDoc(sealInfoVo); + } + }while (true); + + } + + @Override + public String getName() { + return "E签宝印章信息同步任务"; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/msg/MessageVo.java b/src/main/java/com/seeyon/apps/esign/msg/MessageVo.java new file mode 100644 index 0000000..1675d50 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/msg/MessageVo.java @@ -0,0 +1,50 @@ +package com.seeyon.apps.esign.msg; + +public class MessageVo { + + private String messageType; + private String messageContent; + private String messageTime; + private String messageReceiver; + private String bizId; + + public String getMessageType() { + return messageType; + } + + public void setMessageType(String messageType) { + this.messageType = messageType; + } + + public String getMessageContent() { + return messageContent; + } + + public void setMessageContent(String messageContent) { + this.messageContent = messageContent; + } + + public String getMessageTime() { + return messageTime; + } + + public void setMessageTime(String messageTime) { + this.messageTime = messageTime; + } + + public String getMessageReceiver() { + return messageReceiver; + } + + public void setMessageReceiver(String messageReceiver) { + this.messageReceiver = messageReceiver; + } + + public String getBizId() { + return bizId; + } + + public void setBizId(String bizId) { + this.bizId = bizId; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/msg/ThirdMessageService.java b/src/main/java/com/seeyon/apps/esign/msg/ThirdMessageService.java new file mode 100644 index 0000000..40ff08b --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/msg/ThirdMessageService.java @@ -0,0 +1,8 @@ +package com.seeyon.apps.esign.msg; + +public interface ThirdMessageService { + + void sendMessage(MessageVo messageVo); + + void deletByBizId(String bizId); +} diff --git a/src/main/java/com/seeyon/apps/esign/node/EsignMultipleSignerNode.java b/src/main/java/com/seeyon/apps/esign/node/EsignMultipleSignerNode.java new file mode 100644 index 0000000..b0ef62f --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/node/EsignMultipleSignerNode.java @@ -0,0 +1,332 @@ +package com.seeyon.apps.esign.node; + +import com.seeyon.apps.common.workflow.node.ACommonSuperNode; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.param.FlowParamSource; +import com.seeyon.apps.esign.po.signfield.NormalSignFieldConfig; +import com.seeyon.apps.esign.po.signfield.SignField; +import com.seeyon.apps.esign.po.signfield.SignFieldPosition; +import com.seeyon.apps.esign.po.signer.Signer; +import com.seeyon.apps.esign.service.ContractCreateService; +import com.seeyon.apps.esign.service.EsignByTemplateService; +import com.seeyon.apps.esign.service.EsignUploadFileService; +import com.seeyon.apps.esign.service.FlowFormSignParamBuildFactory; +import com.seeyon.apps.ext.workflow.vo.FieldDataVo; +import com.seeyon.apps.ext.workflow.vo.FormDataVo; +import com.seeyon.apps.ext.workflow.vo.SuperNodeContext; +import com.seeyon.cap4.form.bean.FormDataMasterBean; +import com.seeyon.ctp.common.AppContext; +import com.seeyon.ctp.common.exceptions.BusinessException; +import com.seeyon.utils.form.FormTableExecutor; +import com.seeyon.utils.form.FormUpdateField; +import com.seeyon.utils.form.FormWhereCondition; +import com.seeyon.utils.form.TableContext; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class EsignMultipleSignerNode extends ACommonSuperNode { + + private static final Log log = LogFactory.getLog(EsignMultipleSignerNode.class); + + private static final String CONTRACT_NAME_FIELD = "合同名称"; + private static final String CONTRACT_ATTACHMENT_FIELD = "合同审批附件"; + private static final String B_UNIT_NAME_FIELD = "乙方单位名称"; + private EsignConfigProvider configProvider = EsignConfigProvider.getInstance(); +// private EsignUploadFileService uploadFileService = (EsignUploadFileService) AppContext.getBean("esignByUploadFileService"); +// private EsignByTemplateService templateService = (EsignByTemplateService) AppContext.getBean("esignByTemplateService"); +// private FlowFormSignParamBuildFactory flowFormSignParamBuildFactory = new FlowFormSignParamBuildFactory(); + private ContractCreateService contractCreateService = (ContractCreateService) AppContext.getBean("contractCreateService"); + + @Override + public String getPluginId() { + return EsignConfigConstants.getPluginId(); + } + + @Override + public String getFormParse() { + return ""; + } + + @Override + public SuperNodeContext proceed(String s, FormDataVo formDataVo, FormDataMasterBean formDataMasterBean) { + SuperNodeContext context = new SuperNodeContext(); + context.setNeedSave(true); + log.info("进入E签宝多方签署集成超级节点"); + try { +// Map signParams = buildSignParams(formDataVo, formDataMasterBean, true); + FlowParamSource source = new FlowParamSource(); + source.setConfigProvider(configProvider); + source.setFormDataVo(formDataVo); + source.setDoAutoSign(true); + source.setMasterBean(formDataMasterBean); + String esignFlowId = contractCreateService.startSign(source); + if(esignFlowId == null) { + return context.back("E签宝签署发起失败"); + } + log.info("E签宝签署流程ID: " + esignFlowId + " , OA超级节点ID:" + formDataVo.getToken()); + log.info("E签宝多方签署合同已发送: " + esignFlowId); + return context.wait("等待签署结束"); + } catch (Exception e) { + log.error("E签宝签署失败", e); + context.setErrMsg("E签宝签署失败: " + e.getMessage()); + return context.back("E签宝签署失败: " + e.getMessage()); + } + } + +// private Map buildSignFlowConfig(FormDataVo formDataVo,FormDataMasterBean masterBean) throws Exception { +// String contractName = getStringField(formDataVo, CONTRACT_NAME_FIELD); +// if (contractName == null) throw new RuntimeException("合同名称不能为空"); +// String formId = formDataVo.getToken() +"_" + formDataVo.getId(); +// Long enumId = masterBean.getFormTable().getFieldBeanByDisplay("E签宝签署状态").getEnumId(); +// String signCallBackUrl = configProvider.getBizConfigByKey(EsignConfigConstants.OA_HOST) + "/seeyon/esigncallback.do?method=callback&formId=" + formId + "&tablename=" + masterBean.getFormTable().getTableName() + "&updatefield=" + masterBean.getFormTable().getFieldBeanByDisplay("盖章后合同附件").getColumnName() + "&statusfield=" + masterBean.getFormTable().getFieldBeanByDisplay("E签宝签署状态").getColumnName() + "_" + enumId; +// Map config = new HashMap<>(); +// config.put("signFlowTitle", contractName); +// config.put("autoFinish", true); +// config.put("notifyUrl", signCallBackUrl); +// Map noticeConfig = new HashMap<>(); +// noticeConfig.put("noticeTypes", "1"); +// config.put("noticeConfig", noticeConfig); +// Map signConfig = new HashMap<>(); +// signConfig.put("showBatchDropSealButton", false); +// config.put("signConfig",signConfig); +// return config; +// } + +// private List buildSigners(FormDataVo formDataVo, String fileId) throws Exception { +// List keywords = new ArrayList<>(); +// keywords.add("甲方盖章/签字"); +// keywords.add("乙方盖章/签字"); +// List positions = uploadFileService.getSignPosition(fileId,keywords); +// List bPositions = extractPosition(positions, "乙方盖章/签字"); +// List aPositions = extractPosition(positions, "甲方盖章/签字"); +// String bSignerType = formDataVo.getFieldData("乙方签署类型").getStringValue(); +// +// SignFieldPosition aQiFengposition = new SignFieldPosition(); +// aQiFengposition.setAcrossPageMode("ALL"); +// aQiFengposition.setPositionY(520f); +// SignFieldPosition bQiFengposition = new SignFieldPosition(); +// bQiFengposition.setAcrossPageMode("ALL"); +// bQiFengposition.setPositionY(720f); +// +// aPositions.add(aQiFengposition); +// bPositions.add(bQiFengposition); +// String sealId = getStringField(formDataVo,"甲方印章ID"); +// Signer bSigner = !"组织".equals(bSignerType) ? createPsnSigner(fileId,formDataVo.getFieldData("乙方姓名").getStringValue(),bPositions,1,formDataVo.getFieldData("乙方电话").getStringValue(),false) : createOrgSigner( +// fileId, +// getStringField(formDataVo, B_UNIT_NAME_FIELD), +// bPositions, 1, formDataVo.getFieldData("乙方法人姓名").getStringValue(), +// formDataVo.getFieldData("乙方法人身份证号").getStringValue(), +// formDataVo.getFieldData("乙方企业社会信用代码").getStringValue(), +// formDataVo.getFieldData("乙方经办人手机号").getStringValue(), +// formDataVo.getFieldData("乙方经办人姓名").getStringValue(), +// formDataVo.getFieldData("乙方经办人身份证号").getStringValue(),false +// ,null); +// +// Signer aSigner = createOrgSigner( +// fileId,null, aPositions, 2, null, null, null, null, null, null,true +// ,sealId); +// List signers = new ArrayList<>(); +// signers.add(bSigner); +// signers.add(aSigner); +// return signers; +// } + +// private Map buildSignParams(FormDataVo formDataVo, FormDataMasterBean formDataMasterBean, boolean byFile) throws Exception { +// +// String fileId = null; +// if (byFile) { +// String attachmentId = getStringField(formDataVo, CONTRACT_ATTACHMENT_FIELD); +// if (attachmentId == null) throw new RuntimeException("合同附件不能为空"); +// fileId = uploadFileService.uploadFileToEsign(attachmentId).get(0); +// } +// Map docMap = new HashMap<>(); +// docMap.put("fileId", fileId); +// Thread.sleep(1000); +// List docs = new ArrayList<>(); +// docs.add(docMap); +// Map signFlowConfig = buildSignFlowConfig(formDataVo, formDataMasterBean); +// List signers = buildSigners(formDataVo, fileId); +// Map signParams = new HashMap<>(); +// signParams.put("signFlowConfig", signFlowConfig); +// signParams.put("signers", signers); +// signParams.put("docs", docs); +// return signParams; +// +// } +// +// private void fillFileId(String fileId,FormDataVo formDataVo,FormDataMasterBean formDataMasterBean) throws NoSuchFieldException, BusinessException { +// TableContext tableContext = new TableContext(formDataMasterBean.getFormTable()); +// List updateFields = new ArrayList<>(); +// updateFields.add(FormUpdateField.build().display("合同文件ID").value(fileId)); +// List whereConditions = new ArrayList<>(); +// whereConditions.add(FormWhereCondition.build().display("ID").value(formDataVo.getId())); +// FormTableExecutor.update(tableContext,updateFields,whereConditions); +// } +// +// private Map mapOf(Object... keyValues) { +// if (keyValues.length % 2 != 0) { +// throw new IllegalArgumentException("参数个数必须为偶数:key 和 value 必须成对出现"); +// } +// Map map = new HashMap<>(); +// for (int i = 0; i < keyValues.length; i += 2) { +// Object key = keyValues[i]; +// Object value = keyValues[i + 1]; +// if (!(key instanceof String)) { +// throw new IllegalArgumentException("key 必须是 String 类型,当前为:" + key.getClass()); +// } +// map.put((String) key, value); +// } +// return map; +// } +// +// private Signer createPsnSigner( String fileId, String psnName,List pos, int order, String phone,Boolean autoSign) { +// Signer signer = new Signer(); +// signer.setSignerType(0); +// signer.setSignConfig(mapOf("signOrder", order)); +// if(Boolean.FALSE.equals(autoSign)){ +// signer.setNoticeConfig(mapOf("noticeTypes", "1")); +// +// Map psnInfo = mapOf("psnAccount", phone, "psnInfo", mapOf("psnName", psnName)); +// signer.setPsnSignerInfo(psnInfo); +// } +// +// List signFields = new ArrayList<>(); +// for (SignFieldPosition po : pos) { +// NormalSignFieldConfig fieldConfig = new NormalSignFieldConfig(); +// if("ALL".equals(po.getAcrossPageMode())){ +// fieldConfig.setSignFieldPosition(po); +// fieldConfig.setAutoSign(autoSign); +// fieldConfig.setSignFieldStyle(2); +// SignField qiFengfield = new SignField(); +// qiFengfield.setSignFieldType(0); +// qiFengfield.setNormalSignFieldConfig(fieldConfig); +// qiFengfield.setFileId(fileId); +// signFields.add(qiFengfield); +// }else { +// fieldConfig.setSignFieldPosition(po); +// fieldConfig.setAutoSign(autoSign); +// fieldConfig.setSignFieldStyle(1); +// SignField field = new SignField(); +// field.setSignFieldType(0); +// field.setNormalSignFieldConfig(fieldConfig); +// field.setFileId(fileId); +// signFields.add(field); +// } +// } +// +// signer.setSignFields(signFields); +// return signer; +// } +// +// private Signer createOrgSigner(String fileId ,String orgName, List pos, int order, +// String legalName, String legalId, String orgCode, +// String transPhone, String psnName, String psnId,Boolean autoSign,String sealId) { +// Signer signer = new Signer(); +// signer.setSignerType(1); +// signer.setSignConfig(mapOf("signOrder", order)); +// if(Boolean.FALSE.equals(autoSign)){ +// signer.setNoticeConfig(mapOf("noticeTypes", "1")); +// +// Map orgInfo = mapOf("orgName", orgName, "orgInfo", mapOf( +// "legalRepName", legalName, +// "legalRepIDCardNum", legalId, +// "orgIDCardNum", orgCode, +// "orgIDCardType", "CRED_ORG_USCC" +// ), +// "transactorInfo", mapOf( +// "psnAccount", transPhone, +// "psnInfo", mapOf( +// "psnName", psnName, +// "psnIDCardNum", psnId +// ) +// ) +// ); +// +// signer.setOrgSignerInfo(orgInfo); +// } +// +// List signFields = new ArrayList<>(); +// for (SignFieldPosition po : pos) { +// NormalSignFieldConfig fieldConfig = new NormalSignFieldConfig(); +// fieldConfig.setAssignedSealId(sealId); +// if("ALL".equals(po.getAcrossPageMode())){ +// fieldConfig.setSignFieldPosition(po); +// fieldConfig.setAutoSign(autoSign); +// fieldConfig.setSignFieldStyle(2); +// SignField qiFengfield = new SignField(); +// qiFengfield.setSignFieldType(0); +// qiFengfield.setNormalSignFieldConfig(fieldConfig); +// qiFengfield.setFileId(fileId); +// signFields.add(qiFengfield); +// }else { +// fieldConfig.setSignFieldPosition(po); +// fieldConfig.setAutoSign(autoSign); +// fieldConfig.setSignFieldStyle(1); +// SignField field = new SignField(); +// field.setSignFieldType(0); +// field.setNormalSignFieldConfig(fieldConfig); +// field.setFileId(fileId); +// signFields.add(field); +// } +// } +// +// signer.setSignFields(signFields); +// return signer; +// } +// +// private List extractPosition(List positions, String keyword) { +// List posList = new ArrayList<>(); +// for (Object obj : positions) { +// Map position = (Map) obj; +// if (Boolean.TRUE.equals(position.get("searchResult")) && keyword.equals(position.get("keyword"))) { +// Object[] posArray = (Object[]) position.get("positions"); +// for (Object o : posArray) { +// Map posMap = (Map)o; +// Object[] coords = (Object[]) posMap.get("coordinates"); +// for (Object coord : coords) { +// Map tempMap = (Map) coord; +// SignFieldPosition sfp = new SignFieldPosition(); +// sfp.setPositionPage(posMap.get("pageNum") + ""); +// sfp.setPositionX(toFloat(tempMap.get("positionX")) + 150f); +// sfp.setPositionY(toFloat(tempMap.get("positionY"))); +// posList.add(sfp); +// } +// } +// return posList; +// } +// } +// throw new RuntimeException("未找到关键字位置: " + keyword); +// } +// +// private String getStringField(FormDataVo vo, String field) { +// try { +// FieldDataVo fieldData = vo.getFieldData(field); +// return fieldData.getStringValue(); +// }catch (Exception e) { +// log.error(e.getMessage()); +// return null; +// } +// } +// +// private Float toFloat(Object val) { +// return val instanceof Number ? ((Number) val).floatValue() : Float.parseFloat(val.toString()); +// } + + @Override + public String getNodeId() { + return "nd_20250703"; + } + + @Override + public String getNodeName() { + return "E签宝甲乙双方签署节点"; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/node/EsignOneSignerNode.java b/src/main/java/com/seeyon/apps/esign/node/EsignOneSignerNode.java new file mode 100644 index 0000000..fea81c2 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/node/EsignOneSignerNode.java @@ -0,0 +1,355 @@ +package com.seeyon.apps.esign.node; + +import com.seeyon.apps.common.workflow.node.ACommonSuperNode; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.param.FlowParamSource; +import com.seeyon.apps.esign.po.signer.Signer; +import com.seeyon.apps.esign.po.signfield.NormalSignFieldConfig; +import com.seeyon.apps.esign.po.signfield.SignField; +import com.seeyon.apps.esign.po.signfield.SignFieldPosition; +import com.seeyon.apps.esign.service.ContractCreateService; +import com.seeyon.apps.esign.service.EsignByTemplateService; +import com.seeyon.apps.esign.service.EsignUploadFileService; +import com.seeyon.apps.ext.workflow.vo.FieldDataVo; +import com.seeyon.apps.ext.workflow.vo.FormDataVo; +import com.seeyon.apps.ext.workflow.vo.SuperNodeContext; +import com.seeyon.cap4.form.bean.FormDataMasterBean; +import com.seeyon.cap4.form.service.CAP4FormManager; +import com.seeyon.ctp.common.AppContext; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.*; + + +public class EsignOneSignerNode extends ACommonSuperNode { + + private static final Log log = LogFactory.getLog(EsignOneSignerNode.class); + + private static final String CONTRACT_NAME_FIELD = "合同名称"; + private static final String CONTRACT_ATTACHMENT_FIELD = "合同审批附件"; + private static final String B_UNIT_NAME_FIELD = "乙方单位名称"; +// @Autowired +// private EsignUploadFileService uploadFileService; +// @Autowired +// private EsignByTemplateService templateService; + private EsignConfigProvider configProvider = EsignConfigProvider.getInstance(); + private ContractCreateService contractCreateService = (ContractCreateService) AppContext.getBean("contractCreateService"); + + @Override + public String getPluginId() { + return EsignConfigConstants.getPluginId(); + } + + @Override + public String getFormParse() { + return ""; + } + + @Override + public SuperNodeContext proceed(String s, FormDataVo formDataVo, FormDataMasterBean formDataMasterBean) { + SuperNodeContext context = new SuperNodeContext(); + context.setNeedSave(true); + log.info("进入 E签宝单方签署集成超级节点"); + + try { + FlowParamSource source = new FlowParamSource(); + source.setConfigProvider(configProvider); + source.setFormDataVo(formDataVo); + source.setDoAutoSign(false); + source.setMasterBean(formDataMasterBean); + String esignFlowId = contractCreateService.startSign(source); + if(esignFlowId == null) { + return context.back("E签宝签署发起失败"); + } + log.info("E签宝签署流程ID: " + esignFlowId + " , OA超级节点ID:" + formDataVo.getToken()); + log.info("E签宝单方签署合同已发送: " + esignFlowId); + return context.wait("等待签署结束"); + } catch (Exception e) { + log.error("E签宝签署失败", e); + context.setErrMsg("E签宝签署失败: " + e.getMessage()); + return context.back("E签宝签署失败: " + e.getMessage()); + } + } + +// private Map buildSignFlowConfig(FormDataVo formDataVo,FormDataMasterBean masterBean) throws Exception { +// String contractName = getStringField(formDataVo, CONTRACT_NAME_FIELD); +// if (contractName == null) throw new RuntimeException("合同名称不能为空"); +// String formId = formDataVo.getToken() +"_" + formDataVo.getId(); +// Long enumId = masterBean.getFormTable().getFieldBeanByDisplay("E签宝签署状态").getEnumId(); +// String signCallBackUrl = configProvider.getBizConfigByKey(EsignConfigConstants.OA_HOST) + "/seeyon/esigncallback.do?method=callback&formId=" + formId + "&tablename=" + masterBean.getFormTable().getTableName() + "&updatefield=" + masterBean.getFormTable().getFieldBeanByDisplay("盖章后合同附件").getColumnName() + "&statusfield=" + masterBean.getFormTable().getFieldBeanByDisplay("E签宝签署状态").getColumnName() + "_" + enumId; +// Map config = new HashMap<>(); +// config.put("signFlowTitle", contractName); +// config.put("autoFinish", true); +// config.put("notifyUrl", signCallBackUrl); +// Map noticeConfig = new HashMap<>(); +// noticeConfig.put("noticeTypes", "1"); +// config.put("noticeConfig", noticeConfig); +// Map signConfig = new HashMap<>(); +// signConfig.put("showBatchDropSealButton", false); +// config.put("signConfig",signConfig); +// return config; +// } + +// private List buildOnlyASigner(FormDataVo formDataVo, String fileId) throws Exception { +// +// SignFieldPosition aQiFengposition = new SignFieldPosition(); +// aQiFengposition.setAcrossPageMode("ALL"); +// aQiFengposition.setPositionY(520f); +// String sealId = getStringField(formDataVo,"甲方印章ID"); +// String psnName = getStringField(formDataVo,"甲方签署经办人"); +// String psnMobile = getStringField(formDataVo,"甲方签署经办人联系方式"); +// String orgName = configProvider.getBizConfigByKey(EsignConfigConstants.UNITNAME); +// List pos = new ArrayList<>(); +// Map> subFormMap = formDataVo.getSubFormMap(); +// List subDataVos = subFormMap.get("甲方签署位置"); +// Boolean autoSign = true; +// if(subDataVos!= null && subDataVos.size() > 0 && !subDataVos.get(0).isEmpty()){ +// for (FormDataVo subDataVo : subDataVos) { +// Map fieldDataVoMap = subDataVo.getFieldDataVoMap(); +// String pageNo = fieldDataVoMap.get("页码").getStringValue(); +// String positionX = fieldDataVoMap.get("X坐标").getStringValue(); +// String positionY = fieldDataVoMap.get("Y坐标").getStringValue(); +// SignFieldPosition position = new SignFieldPosition(); +// position.setPositionPage(pageNo); +// position.setPositionX(Float.parseFloat(positionX)); +// position.setPositionY(Float.parseFloat(positionY)); +// pos.add(position); +// } +// pos.add(aQiFengposition); +// }else { +// autoSign = false; +// } +// Signer aSigner = createOrgSigner( +// fileId,orgName, pos, 1, null, null, null, psnMobile, psnName, null,autoSign +// ,sealId); +// List signers = new ArrayList<>(); +// signers.add(aSigner); +// return signers; +// } + +// private Map buildSignParams(FormDataVo formDataVo, FormDataMasterBean formDataMasterBean, boolean byFile) throws Exception { +// +// String fileId = null; +// if (byFile) { +// String attachmentId = getStringField(formDataVo, CONTRACT_ATTACHMENT_FIELD); +// if (attachmentId == null) throw new RuntimeException("合同附件不能为空"); +// fileId = uploadFileService.uploadFileToEsign(attachmentId).get(0); +// } +// Map docMap = new HashMap<>(); +// Thread.sleep(1000); +// docMap.put("fileId", fileId); +// List docs = new ArrayList<>(); +// docs.add(docMap); +// Map signFlowConfig = buildSignFlowConfig(formDataVo, formDataMasterBean); +// List signers = buildOnlyASigner(formDataVo, fileId); +// Map signParams = new HashMap<>(); +// signParams.put("signFlowConfig", signFlowConfig); +// signParams.put("signers", signers); +// signParams.put("docs", docs); +// return signParams; +// +// } + +// private Map mapOf(Object... keyValues) { +// if (keyValues.length % 2 != 0) { +// throw new IllegalArgumentException("参数个数必须为偶数:key 和 value 必须成对出现"); +// } +// Map map = new HashMap<>(); +// for (int i = 0; i < keyValues.length; i += 2) { +// Object key = keyValues[i]; +// Object value = keyValues[i + 1]; +// if (!(key instanceof String)) { +// throw new IllegalArgumentException("key 必须是 String 类型,当前为:" + key.getClass()); +// } +// map.put((String) key, value); +// } +// return map; +// } +// +// private Signer createPsnSigner( String fileId, String psnName,List pos, int order, String phone,Boolean autoSign) { +// Signer signer = new Signer(); +// signer.setSignerType(0); +// signer.setSignConfig(mapOf("signOrder", order)); +// if(Boolean.FALSE.equals(autoSign)){ +// signer.setNoticeConfig(mapOf("noticeTypes", "1")); +// +// Map psnInfo = mapOf("psnAccount", phone, "psnInfo", mapOf("psnName", psnName)); +// signer.setPsnSignerInfo(psnInfo); +// } +// +// List signFields = new ArrayList<>(); +// for (SignFieldPosition po : pos) { +// NormalSignFieldConfig fieldConfig = new NormalSignFieldConfig(); +// if("ALL".equals(po.getAcrossPageMode())){ +// fieldConfig.setSignFieldPosition(po); +// fieldConfig.setAutoSign(autoSign); +// fieldConfig.setSignFieldStyle(2); +// SignField qiFengfield = new SignField(); +// qiFengfield.setSignFieldType(0); +// qiFengfield.setNormalSignFieldConfig(fieldConfig); +// qiFengfield.setFileId(fileId); +// signFields.add(qiFengfield); +// }else { +// fieldConfig.setSignFieldPosition(po); +// fieldConfig.setAutoSign(autoSign); +// fieldConfig.setSignFieldStyle(1); +// SignField field = new SignField(); +// field.setSignFieldType(0); +// field.setNormalSignFieldConfig(fieldConfig); +// field.setFileId(fileId); +// signFields.add(field); +// } +// } +// +// signer.setSignFields(signFields); +// return signer; +// } +// +// private Signer createOrgSigner(String fileId ,String orgName, List pos, int order, +// String legalName, String legalId, String orgCode, +// String transPhone, String psnName, String psnId,Boolean autoSign,String sealId) { +// Signer signer = new Signer(); +// signer.setSignerType(1); +// signer.setSignConfig(mapOf("signOrder", order)); +// List signFields = new ArrayList<>(); +// signer.setSignFields(signFields); +// if(Boolean.FALSE.equals(autoSign)){ +// signer.setNoticeConfig(mapOf("noticeTypes", "1")); +// +// Map orgInfo = mapOf("orgName", orgName, +// "transactorInfo", mapOf( +// "psnAccount", transPhone, +// "psnInfo", mapOf( +// "psnName", psnName +// ) +// ) +// ); +// signer.setOrgSignerInfo(orgInfo); +// NormalSignFieldConfig normalSignFieldConfig = new NormalSignFieldConfig(); +// normalSignFieldConfig.setAssignedSealId(sealId); +// normalSignFieldConfig.setFreeMode(true); +// normalSignFieldConfig.setAutoSign(false); +// normalSignFieldConfig.setAdaptableSignFieldSize(true); +// SignField field = new SignField(); +// field.setSignFieldType(0); +// field.setNormalSignFieldConfig(normalSignFieldConfig); +// field.setFileId(fileId); +// signFields.add(field); +// }else { +// for (SignFieldPosition po : pos) { +// NormalSignFieldConfig fieldConfig = new NormalSignFieldConfig(); +// fieldConfig.setAssignedSealId(sealId); +// fieldConfig.setAutoSign(autoSign); +// if("ALL".equals(po.getAcrossPageMode())){ +// fieldConfig.setSignFieldPosition(po); +// fieldConfig.setSignFieldStyle(2); +// SignField qiFengfield = new SignField(); +// qiFengfield.setSignFieldType(0); +// qiFengfield.setNormalSignFieldConfig(fieldConfig); +// qiFengfield.setFileId(fileId); +// signFields.add(qiFengfield); +// }else { +// fieldConfig.setSignFieldPosition(po); +// fieldConfig.setSignFieldStyle(1); +// SignField field = new SignField(); +// field.setSignFieldType(0); +// field.setNormalSignFieldConfig(fieldConfig); +// field.setFileId(fileId); +// signFields.add(field); +// } +// } +// } +// return signer; +// } +// +// private Signer createOrgQiFengSigner(String fileId ,String orgName, SignFieldPosition po, int order, +// String legalName, String legalId, String orgCode, +// String transPhone, String psnName, String psnId,Boolean autoSign,String sealId) { +// Signer signer = new Signer(); +// signer.setSignerType(1); +// signer.setSignConfig(mapOf("signOrder", order)); +// if(Boolean.FALSE.equals(autoSign)){ +// signer.setNoticeConfig(mapOf("noticeTypes", "1")); +// +// Map orgInfo = mapOf("orgName", orgName, +// "transactorInfo", mapOf( +// "psnAccount", transPhone, +// "psnInfo", mapOf( +// "psnName", psnName +// ) +// ) +// ); +// +// signer.setOrgSignerInfo(orgInfo); +// } +// +// List signFields = new ArrayList<>(); +// +// NormalSignFieldConfig normalSignFieldConfig = new NormalSignFieldConfig(); +// normalSignFieldConfig.setAssignedSealId(sealId); +// +// normalSignFieldConfig.setSignFieldPosition(po); +// normalSignFieldConfig.setAutoSign(autoSign); +// normalSignFieldConfig.setSignFieldStyle(2); +// SignField qiFengfield = new SignField(); +// qiFengfield.setSignFieldType(0); +// qiFengfield.setNormalSignFieldConfig(normalSignFieldConfig); +// qiFengfield.setFileId(fileId); +// signFields.add(qiFengfield); +// +// signer.setSignFields(signFields); +// return signer; +// } +// +// +// private List extractPosition(List positions, String keyword) { +// List posList = new ArrayList<>(); +// for (Object obj : positions) { +// Map position = (Map) obj; +// if (Boolean.TRUE.equals(position.get("searchResult")) && keyword.equals(position.get("keyword"))) { +// Object[] posArray = (Object[]) position.get("positions"); +// for (Object o : posArray) { +// Map posMap = (Map)o; +// Object[] coords = (Object[]) posMap.get("coordinates"); +// for (Object coord : coords) { +// Map tempMap = (Map) coord; +// SignFieldPosition sfp = new SignFieldPosition(); +// sfp.setPositionPage(posMap.get("pageNum") + ""); +// sfp.setPositionX(toFloat(tempMap.get("positionX")) + 150f); +// sfp.setPositionY(toFloat(tempMap.get("positionY"))); +// posList.add(sfp); +// } +// } +// return posList; +// } +// } +// throw new RuntimeException("未找到关键字位置: " + keyword); +// } +// +// private String getStringField(FormDataVo vo, String field) { +// try { +// FieldDataVo fieldData = vo.getFieldData(field); +// return fieldData.getStringValue(); +// }catch (Exception e) { +// log.error(e.getMessage()); +// return null; +// } +// } +// +// private Float toFloat(Object val) { +// return val instanceof Number ? ((Number) val).floatValue() : Float.parseFloat(val.toString()); +// } + + @Override + public String getNodeId() { + return "nd_20250702"; + } + + @Override + public String getNodeName() { + return "E签宝甲方单方签署节点"; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/node/EsignOrgAuthNode.java b/src/main/java/com/seeyon/apps/esign/node/EsignOrgAuthNode.java new file mode 100644 index 0000000..3733b9f --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/node/EsignOrgAuthNode.java @@ -0,0 +1,38 @@ +package com.seeyon.apps.esign.node; + +import com.seeyon.apps.common.workflow.node.ACommonSuperNode; +import com.seeyon.apps.ext.workflow.vo.FormDataVo; +import com.seeyon.apps.ext.workflow.vo.SuperNodeContext; +import com.seeyon.cap4.form.bean.FormDataMasterBean; + +public class EsignOrgAuthNode extends ACommonSuperNode { + @Override + public String getPluginId() { + return ""; + } + + @Override + public String getFormParse() { + return ""; + } + + @Override + public SuperNodeContext proceed(String s, FormDataVo formDataVo, FormDataMasterBean formDataMasterBean) throws Exception { + //个人实名认证、企业认证处理 + return null; + } + + public void personAuth() { + // /v3/psn-auth-url + } + + @Override + public String getNodeId() { + return ""; + } + + @Override + public String getNodeName() { + return ""; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/node/EsignPersonAuthNode.java b/src/main/java/com/seeyon/apps/esign/node/EsignPersonAuthNode.java new file mode 100644 index 0000000..c26ffa8 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/node/EsignPersonAuthNode.java @@ -0,0 +1,56 @@ +package com.seeyon.apps.esign.node; + +import com.seeyon.apps.common.workflow.node.ACommonSuperNode; +import com.seeyon.apps.esign.po.auth.AuthPsnInfo; +import com.seeyon.apps.esign.po.auth.PsnAuthConfig; +import com.seeyon.apps.ext.workflow.vo.FormDataVo; +import com.seeyon.apps.ext.workflow.vo.SuperNodeContext; +import com.seeyon.cap4.form.bean.FormDataMasterBean; + +import java.util.HashMap; +import java.util.Map; + +public class EsignPersonAuthNode extends ACommonSuperNode { + @Override + public String getPluginId() { + return ""; + } + + @Override + public String getFormParse() { + return ""; + } + + @Override + public SuperNodeContext proceed(String s, FormDataVo formDataVo, FormDataMasterBean formDataMasterBean) throws Exception { + Map authParams = new HashMap<>(); + PsnAuthConfig psnAuthConfig = buildPsnAuthConfig(formDataVo,formDataMasterBean); + String notifyUrl= ""; + authParams.put("psnAuthConfig",psnAuthConfig); + authParams.put("notifyUrl",notifyUrl); + + return null; + } + + private PsnAuthConfig buildPsnAuthConfig(FormDataVo formDataVo, FormDataMasterBean formDataMasterBean) throws Exception { + PsnAuthConfig psnAuthConfig = new PsnAuthConfig(); + psnAuthConfig.setPsnAccount(formDataVo.getFieldData("客户联系电话").getStringValue()); + AuthPsnInfo authPsnInfo = new AuthPsnInfo(); + authPsnInfo.setPsnName(formDataVo.getFieldData("客户联系人姓名").getStringValue()); + authPsnInfo.setPsnMobile(formDataVo.getFieldData("客户联系电话").getStringValue()); + authPsnInfo.setPsnIDCardType("CRED_PSN_CH_IDCARD"); + authPsnInfo.setPsnIDCardNum(formDataVo.getFieldData("客户身份证号").getStringValue()); + authPsnInfo.setPsnIdentityVerify(true); + return psnAuthConfig; + } + + @Override + public String getNodeId() { + return ""; + } + + @Override + public String getNodeName() { + return ""; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/auth/AuthPsnInfo.java b/src/main/java/com/seeyon/apps/esign/po/auth/AuthPsnInfo.java new file mode 100644 index 0000000..bbca527 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/auth/AuthPsnInfo.java @@ -0,0 +1,61 @@ +package com.seeyon.apps.esign.po.auth; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class AuthPsnInfo { + private String psnName; + private String psnIDCardNum; + private String psnIDCardType; + private String psnMobile; + private String bankCardNum; + private Boolean psnIdentityVerify; + + public String getPsnName() { + return psnName; + } + + public void setPsnName(String psnName) { + this.psnName = psnName; + } + + public String getPsnIDCardNum() { + return psnIDCardNum; + } + + public void setPsnIDCardNum(String psnIDCardNum) { + this.psnIDCardNum = psnIDCardNum; + } + + public String getPsnIDCardType() { + return psnIDCardType; + } + + public void setPsnIDCardType(String psnIDCardType) { + this.psnIDCardType = psnIDCardType; + } + + public String getPsnMobile() { + return psnMobile; + } + + public void setPsnMobile(String psnMobile) { + this.psnMobile = psnMobile; + } + + public String getBankCardNum() { + return bankCardNum; + } + + public void setBankCardNum(String bankCardNum) { + this.bankCardNum = bankCardNum; + } + + public Boolean getPsnIdentityVerify() { + return psnIdentityVerify; + } + + public void setPsnIdentityVerify(Boolean psnIdentityVerify) { + this.psnIdentityVerify = psnIdentityVerify; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/auth/PsnAuthConfig.java b/src/main/java/com/seeyon/apps/esign/po/auth/PsnAuthConfig.java new file mode 100644 index 0000000..58809ed --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/auth/PsnAuthConfig.java @@ -0,0 +1,40 @@ +package com.seeyon.apps.esign.po.auth; + +public class PsnAuthConfig { + private String psnAccount; + private String psnId; + private AuthPsnInfo psnInfo; + private PsnAuthPageConfig psnAuthPageConfig; + + public String getPsnAccount() { + return psnAccount; + } + + public void setPsnAccount(String psnAccount) { + this.psnAccount = psnAccount; + } + + public String getPsnId() { + return psnId; + } + + public void setPsnId(String psnId) { + this.psnId = psnId; + } + + public AuthPsnInfo getPsnInfo() { + return psnInfo; + } + + public void setPsnInfo(AuthPsnInfo psnInfo) { + this.psnInfo = psnInfo; + } + + public PsnAuthPageConfig getPsnAuthPageConfig() { + return psnAuthPageConfig; + } + + public void setPsnAuthPageConfig(PsnAuthPageConfig psnAuthPageConfig) { + this.psnAuthPageConfig = psnAuthPageConfig; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/auth/PsnAuthPageConfig.java b/src/main/java/com/seeyon/apps/esign/po/auth/PsnAuthPageConfig.java new file mode 100644 index 0000000..2496cbc --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/auth/PsnAuthPageConfig.java @@ -0,0 +1,45 @@ +package com.seeyon.apps.esign.po.auth; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class PsnAuthPageConfig { + private String psnDefaultAuthMode; + private List psnAvailableAuthModes; + private List advancedVersion; + private List psnEditableFields; + + public String getPsnDefaultAuthMode() { + return psnDefaultAuthMode; + } + + public void setPsnDefaultAuthMode(String psnDefaultAuthMode) { + this.psnDefaultAuthMode = psnDefaultAuthMode; + } + + public List getPsnAvailableAuthModes() { + return psnAvailableAuthModes; + } + + public void setPsnAvailableAuthModes(List psnAvailableAuthModes) { + this.psnAvailableAuthModes = psnAvailableAuthModes; + } + + public List getAdvancedVersion() { + return advancedVersion; + } + + public void setAdvancedVersion(List advancedVersion) { + this.advancedVersion = advancedVersion; + } + + public List getPsnEditableFields() { + return psnEditableFields; + } + + public void setPsnEditableFields(List psnEditableFields) { + this.psnEditableFields = psnEditableFields; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignApiHeader.java b/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignApiHeader.java new file mode 100644 index 0000000..59f8a68 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignApiHeader.java @@ -0,0 +1,219 @@ +package com.seeyon.apps.esign.po.esignapi; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; + +public class EsignApiHeader { + private String xTsignOpenAppId; + private String xTsignOpenAuthMode; + private String xTsignOpenCaSignature; + private String xTsignOpenCaTimestamp; + private String accept; + private String contentType = "application/json; charset=UTF-8"; + private String contentMd5; + private String httpMethod; + private String date; + private String headers; + private String pathAndParameters; + private String token; + + public String getxTsignOpenAppId() { + return xTsignOpenAppId; + } + + public void setxTsignOpenAppId(String xTsignOpenAppId) { + this.xTsignOpenAppId = xTsignOpenAppId; + } + + public String getX_Tsign_Open_Auth_Mode() { + return xTsignOpenAuthMode; + } + + public void setX_Tsign_Open_Auth_Mode(String x_Tsign_Open_Auth_Mode) { + this.xTsignOpenAuthMode = x_Tsign_Open_Auth_Mode; + } + + public String getxTsignOpenCaSignature() { + return xTsignOpenCaSignature; + } + + public void setxTsignOpenCaSignature(String xTsignOpenCaSignature) { + this.xTsignOpenCaSignature = xTsignOpenCaSignature; + } + + public String getxTsignOpenCaTimestamp() { + return xTsignOpenCaTimestamp; + } + + public void setxTsignOpenCaTimestamp(String xTsignOpenCaTimestamp) { + this.xTsignOpenCaTimestamp = xTsignOpenCaTimestamp; + } + + public String getAccept() { + return accept; + } + + public void setAccept(String accept) { + this.accept = accept; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getContentMd5() { + return contentMd5; + } + + public void setContentMd5(String contentMd5) { + this.contentMd5 = contentMd5; + } + + public static EsignApiHeader build() { + EsignApiHeader header = new EsignApiHeader(); + header.setAccept("*/*"); + header.setX_Tsign_Open_Auth_Mode("Signature"); + return header; + } + + public EsignApiHeader pathAndParameters(String pathAndParameters) { + this.pathAndParameters = pathAndParameters; + return this; + } + + public EsignApiHeader appId(String appId) { + this.xTsignOpenAppId = appId; + return this; + } + + public EsignApiHeader heads(String headers) { + this.headers = headers; + return this; + } + + public EsignApiHeader timeStamp() { + this.xTsignOpenCaTimestamp = System.currentTimeMillis() + ""; + return this; + } + + public EsignApiHeader token(String token) { + this.token = token; + return this; + } + + public EsignApiHeader contentMD5(String body) { + this.contentMd5 = getBodyContentMD5(body); + return this; + } + + public EsignApiHeader httpMethod(String httpMethod) { + this.httpMethod = httpMethod; + return this; + } + + public EsignApiHeader signature(String secret) throws Exception { + StringBuffer strBuff = new StringBuffer(); + strBuff.append(httpMethod).append("\n").append(accept).append("\n").append(contentMd5).append("\n") + .append(contentType).append("\n").append("").append("\n"); + if ("".equals(headers)) { + strBuff.append(headers).append(pathAndParameters); + } else { + strBuff.append(headers).append("\n").append(pathAndParameters); + } + String StringToSign = strBuff.toString(); + this.setxTsignOpenCaSignature(doSignatureBase64(StringToSign, secret)); + return this; + } + + + public String doSignatureBase64(String message, String secret) throws Exception { + String algorithm = "HmacSHA256"; + Mac hmacSha256; + String digestBase64 = null; + try { + hmacSha256 = Mac.getInstance(algorithm); + byte[] keyBytes = secret.getBytes("UTF-8"); + byte[] messageBytes = message.getBytes("UTF-8"); + hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, algorithm)); + // 使用HmacSHA256对二进制数据消息Bytes计算摘要 + byte[] digestBytes = hmacSha256.doFinal(messageBytes); + // 把摘要后的结果digestBytes使用Base64进行编码 + digestBase64 = new String(Base64.encodeBase64(digestBytes), "UTF-8"); + } catch (NoSuchAlgorithmException e) { + String msg = MessageFormat.format("不支持此算法: {0}", e.getMessage()); + Exception ex = new Exception(msg); + ex.initCause(e); + throw ex; + } catch (UnsupportedEncodingException e) { + String msg = MessageFormat.format("不支持的字符编码: {0}", e.getMessage()); + Exception ex = new Exception(msg); + ex.initCause(e); + throw ex; + } catch (InvalidKeyException e) { + String msg = MessageFormat.format("无效的密钥规范: {0}", e.getMessage()); + Exception ex = new Exception(msg); + ex.initCause(e); + throw ex; + } + return digestBase64; + } + + /*** + * 计算请求Body体的Content-MD5 + * @param bodyData 请求Body体数据 + * @return + */ + public String getBodyContentMD5(String bodyData) { + if("".equals(bodyData)) { + return bodyData; + } + // 获取Body体的MD5的二进制数组(128位) + byte[] bytes = getBodyMD5Bytes128(bodyData); + // 对Body体MD5的二进制数组进行Base64编码 + return new String(Base64.encodeBase64(bytes)); + } + + public byte[] getBodyMD5Bytes128(String bodyData) { + byte[] md5Bytes = null; + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(bodyData.getBytes(StandardCharsets.UTF_8)); + md5Bytes = md5.digest(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return md5Bytes; + } + + public Map convert2Headers(){ + Map headers = new HashMap<>(); + if(xTsignOpenAppId != null) { + headers.put("X-Tsign-Open-App-Id", xTsignOpenAppId); + } + if(token != null) { + headers.put("X-Tsign-Open-Token", token); + } + if(contentType != null) { + headers.put("Content-Type", contentType); + } + if(!StringUtils.isBlank(contentMd5)) { + headers.put("Content-MD5", contentMd5); + } + return headers; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignBaseResp.java b/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignBaseResp.java new file mode 100644 index 0000000..9c7745c --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignBaseResp.java @@ -0,0 +1,31 @@ +package com.seeyon.apps.esign.po.esignapi; + +public class EsignBaseResp { + private int code; + private String message; + private Object data; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignCallbackParams.java b/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignCallbackParams.java new file mode 100644 index 0000000..7a22bff --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignCallbackParams.java @@ -0,0 +1,65 @@ +package com.seeyon.apps.esign.po.esignapi; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.io.Serializable; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class EsignCallbackParams implements Serializable { + + private String signFlowId; + private String action; + private String resultDescription; //签署结果描述 + private Integer signResult; //签署结果 + private Integer signFlowStatus; //签署流程状态 + private Integer signOrder; //签署顺序 + + + public String getSignFlowId() { + return signFlowId; + } + + public void setSignFlowId(String signFlowId) { + this.signFlowId = signFlowId; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public String getResultDescription() { + return resultDescription; + } + + public void setResultDescription(String resultDescription) { + this.resultDescription = resultDescription; + } + + public Integer getSignResult() { + return signResult; + } + + public void setSignResult(Integer signResult) { + this.signResult = signResult; + } + + public Integer getSignFlowStatus() { + return signFlowStatus; + } + + public void setSignFlowStatus(Integer signFlowStatus) { + this.signFlowStatus = signFlowStatus; + } + + public Integer getSignOrder() { + return signOrder; + } + + public void setSignOrder(Integer signOrder) { + this.signOrder = signOrder; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignException.java b/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignException.java new file mode 100644 index 0000000..5896be9 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignException.java @@ -0,0 +1,36 @@ +package com.seeyon.apps.esign.po.esignapi; + +/** + * description 自定义全局异常 + * @author 澄泓 + * datetime 2019年7月1日上午10:43:24 + */ +public class EsignException extends Exception { + + private static final long serialVersionUID = 4359180081622082792L; + private Exception e; + + public EsignException(String msg) { + super(msg); + } + + public EsignException(String msg, Throwable cause) { + super(msg,cause); + } + + public EsignException(){ + + } + + public Exception getE() { + return e; + } + + public void setE(Exception e) { + this.e = e; + } + + + + +} diff --git a/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignHttpResponse.java b/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignHttpResponse.java new file mode 100644 index 0000000..43ebd51 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignHttpResponse.java @@ -0,0 +1,27 @@ +package com.seeyon.apps.esign.po.esignapi; +/** + * 网络请求的response类 + * @author 澄泓 + * @date 2022/2/21 17:28 + * @version + */ +public class EsignHttpResponse { + private int status; + private String body; + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignToken.java b/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignToken.java new file mode 100644 index 0000000..887a5ed --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/esignapi/EsignToken.java @@ -0,0 +1,34 @@ +package com.seeyon.apps.esign.po.esignapi; + +import java.io.Serializable; + +public class EsignToken implements Serializable { + + private String tokenStr; + private Long ttl; + private String refreshToken; + + public String getTokenStr() { + return tokenStr; + } + + public void setTokenStr(String tokenStr) { + this.tokenStr = tokenStr; + } + + public Long getTtl() { + return ttl; + } + + public void setTtl(Long ttl) { + this.ttl = ttl; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/file/SignFile.java b/src/main/java/com/seeyon/apps/esign/po/file/SignFile.java new file mode 100644 index 0000000..741ecf6 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/file/SignFile.java @@ -0,0 +1,22 @@ +package com.seeyon.apps.esign.po.file; + +public class SignFile { + private String fileName; + private String fileId; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getFileId() { + return fileId; + } + + public void setFileId(String fileId) { + this.fileId = fileId; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/file/TemplateComponent.java b/src/main/java/com/seeyon/apps/esign/po/file/TemplateComponent.java new file mode 100644 index 0000000..a257565 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/file/TemplateComponent.java @@ -0,0 +1,31 @@ +package com.seeyon.apps.esign.po.file; + +public class TemplateComponent { + private String componentId; + private String componentKey; + private String componentValue; + + public String getComponentId() { + return componentId; + } + + public void setComponentId(String componentId) { + this.componentId = componentId; + } + + public String getComponentKey() { + return componentKey; + } + + public void setComponentKey(String componentKey) { + this.componentKey = componentKey; + } + + public String getComponentValue() { + return componentValue; + } + + public void setComponentValue(String componentValue) { + this.componentValue = componentValue; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/file/TemplateInfo.java b/src/main/java/com/seeyon/apps/esign/po/file/TemplateInfo.java new file mode 100644 index 0000000..e12aeee --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/file/TemplateInfo.java @@ -0,0 +1,22 @@ +package com.seeyon.apps.esign.po.file; + +public class TemplateInfo { + private String signTemplateName; + private String signTemplateId; + + public String getSignTemplateName() { + return signTemplateName; + } + + public void setSignTemplateName(String signTemplateName) { + this.signTemplateName = signTemplateName; + } + + public String getSignTemplateId() { + return signTemplateId; + } + + public void setSignTemplateId(String signTemplateId) { + this.signTemplateId = signTemplateId; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/flow/SignFlowInitiator.java b/src/main/java/com/seeyon/apps/esign/po/flow/SignFlowInitiator.java new file mode 100644 index 0000000..56acb53 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/flow/SignFlowInitiator.java @@ -0,0 +1,39 @@ +package com.seeyon.apps.esign.po.flow; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.seeyon.apps.esign.po.org.OrgInitiator; +import com.seeyon.apps.esign.po.personal.PsnInitiator; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class SignFlowInitiator { + + private OrgInitiator orgInitiator; + private PsnInitiator psnInitiator; + private List initialRemarks; + + public PsnInitiator getPsnInitiator() { + return psnInitiator; + } + + public void setPsnInitiator(PsnInitiator psnInitiator) { + this.psnInitiator = psnInitiator; + } + + public List getInitialRemarks() { + return initialRemarks; + } + + public void setInitialRemarks(List initialRemarks) { + this.initialRemarks = initialRemarks; + } + + public OrgInitiator getOrgInitiator() { + return orgInitiator; + } + + public void setOrgInitiator(OrgInitiator orgInitiator) { + this.orgInitiator = orgInitiator; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/org/OrgInitiator.java b/src/main/java/com/seeyon/apps/esign/po/org/OrgInitiator.java new file mode 100644 index 0000000..8c49d55 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/org/OrgInitiator.java @@ -0,0 +1,22 @@ +package com.seeyon.apps.esign.po.org; + +public class OrgInitiator { + private String orgId; + private Transactor transactor; + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + public Transactor getTransactor() { + return transactor; + } + + public void setTransactor(Transactor transactor) { + this.transactor = transactor; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/org/SignFlowConfig.java b/src/main/java/com/seeyon/apps/esign/po/org/SignFlowConfig.java new file mode 100644 index 0000000..678c1f2 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/org/SignFlowConfig.java @@ -0,0 +1,127 @@ +package com.seeyon.apps.esign.po.org; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class SignFlowConfig { + private String signFlowTitle; + private Long signFlowExpireTime; + private Boolean autoStart; + private Boolean autoFinish; + private Boolean identifyVerify; + private String notifyUrl; + private Map redirectConfig; + private Map signConfig; + private Map noticeConfig; + private Map authConfig; + private Map contractConfig; + private List contractGroupIds; + private Boolean docsViewLimited; + + public String getSignFlowTitle() { + return signFlowTitle; + } + + public void setSignFlowTitle(String signFlowTitle) { + this.signFlowTitle = signFlowTitle; + } + + public Long getSignFlowExpireTime() { + return signFlowExpireTime; + } + + public void setSignFlowExpireTime(Long signFlowExpireTime) { + this.signFlowExpireTime = signFlowExpireTime; + } + + public Boolean getAutoStart() { + return autoStart; + } + + public void setAutoStart(Boolean autoStart) { + this.autoStart = autoStart; + } + + public Boolean getAutoFinish() { + return autoFinish; + } + + public void setAutoFinish(Boolean autoFinish) { + this.autoFinish = autoFinish; + } + + public Boolean getIdentifyVerify() { + return identifyVerify; + } + + public void setIdentifyVerify(Boolean identifyVerify) { + this.identifyVerify = identifyVerify; + } + + public String getNotifyUrl() { + return notifyUrl; + } + + public void setNotifyUrl(String notifyUrl) { + this.notifyUrl = notifyUrl; + } + + public Map getRedirectConfig() { + return redirectConfig; + } + + public void setRedirectConfig(Map redirectConfig) { + this.redirectConfig = redirectConfig; + } + + public Map getSignConfig() { + return signConfig; + } + + public void setSignConfig(Map signConfig) { + this.signConfig = signConfig; + } + + public Map getNoticeConfig() { + return noticeConfig; + } + + public void setNoticeConfig(Map noticeConfig) { + this.noticeConfig = noticeConfig; + } + + public Map getAuthConfig() { + return authConfig; + } + + public void setAuthConfig(Map authConfig) { + this.authConfig = authConfig; + } + + public Map getContractConfig() { + return contractConfig; + } + + public void setContractConfig(Map contractConfig) { + this.contractConfig = contractConfig; + } + + public List getContractGroupIds() { + return contractGroupIds; + } + + public void setContractGroupIds(List contractGroupIds) { + this.contractGroupIds = contractGroupIds; + } + + public Boolean getDocsViewLimited() { + return docsViewLimited; + } + + public void setDocsViewLimited(Boolean docsViewLimited) { + this.docsViewLimited = docsViewLimited; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/org/Transactor.java b/src/main/java/com/seeyon/apps/esign/po/org/Transactor.java new file mode 100644 index 0000000..829de7e --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/org/Transactor.java @@ -0,0 +1,13 @@ +package com.seeyon.apps.esign.po.org; + +public class Transactor { + private String psnId; + + public String getPsnId() { + return psnId; + } + + public void setPsnId(String psnId) { + this.psnId = psnId; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/param/FlowParamSource.java b/src/main/java/com/seeyon/apps/esign/po/param/FlowParamSource.java new file mode 100644 index 0000000..e9e32fc --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/param/FlowParamSource.java @@ -0,0 +1,197 @@ +package com.seeyon.apps.esign.po.param; + +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.signer.*; +import com.seeyon.apps.esign.service.SignParamSource; +import com.seeyon.apps.ext.workflow.vo.FieldDataVo; +import com.seeyon.apps.ext.workflow.vo.FormDataVo; +import com.seeyon.cap4.form.bean.FormDataMasterBean; +import com.seeyon.utils.form.EnumMapUtils; + +import java.util.ArrayList; +import java.util.List; + +public class FlowParamSource implements SignParamSource { + + private FormDataVo formDataVo; + private FormDataMasterBean masterBean; + private Boolean doAutoSign = false; + private EsignConfigProvider configProvider; + private String callbackBaseUrl; + + public FormDataVo getFormDataVo() { + return formDataVo; + } + + public void setFormDataVo(FormDataVo formDataVo) { + this.formDataVo = formDataVo; + } + + public FormDataMasterBean getMasterBean() { + return masterBean; + } + + public void setMasterBean(FormDataMasterBean masterBean) { + this.masterBean = masterBean; + } + + public FlowParamSource(){} + + public FlowParamSource(FormDataVo formDataVo, FormDataMasterBean masterBean){ + this.formDataVo = formDataVo; + this.masterBean = masterBean; + } + + public Boolean getDoAutoSign() { + return doAutoSign; + } + + public void setDoAutoSign(Boolean doAutoSign) { + this.doAutoSign = doAutoSign; + } + + @Override + public String getContractTitle() { + return getStringField(formDataVo,"合同名称"); + } + + @Override + public boolean isByFile() { + return true; + } + + @Override + public String getBizFormId() { + return formDataVo.getToken() +"_" + formDataVo.getId(); + } + + @Override + public String getTableName() { + return masterBean.getFormTable().getTableName(); + } + + @Override + public String getSignFileRefId() { + return getStringField(formDataVo,"合同审批附件"); + } + + @Override + public String getSignedFileField() { + return masterBean.getFormTable().getFieldBeanByDisplay("盖章后合同附件").getColumnName(); + } + + @Override + public String getSignStatusField() { + return masterBean.getFormTable().getFieldBeanByDisplay("签署状态").getColumnName(); + } + + @Override + public Long getSignStatusEnumId() { + return masterBean.getFormTable().getFieldBeanByDisplay("签署状态").getEnumId(); + } + + @Override + public boolean autoSign() { + return doAutoSign; + } + + @Override + public String getCallbackUrl() { + return getCallbackBaseUrl() + "&formId=" + getBizFormId() + + "&tablename=" + getTableName() + + "&updatefield=" + getSignedFileField() + + "&statusfield=" + getSignStatusField() + "_" + getSignStatusEnumId() + + "&bizType=FLOW"; + } + + @Override + public List getSignerParties() { + List parties = new ArrayList<>(); + SignParty aParty = new SignParty(); + aParty.setPartyId("甲方"); + aParty.setAutoSign(autoSign()); + aParty.setSignOrder(2); + aParty.setSignerType(SignerType.ORG); + aParty.setSealId(getStringField(formDataVo,"甲方印章ID")); + if(!doAutoSign){ + OrgInfo orgInfo = new OrgInfo(); + aParty.setOrgInfo(orgInfo); + String psnName = getStringField(formDataVo,"甲方签署经办人"); + String psnMobile = getStringField(formDataVo,"甲方签署经办人联系方式"); + String orgName = configProvider.getBizConfigByKey(EsignConfigConstants.UNITNAME); + PersonInfo personInfo = new PersonInfo(); + orgInfo.setOrgName(orgName); + personInfo.setName(psnName); + personInfo.setPhone(psnMobile); + orgInfo.setTransactor(personInfo); + } + parties.add(aParty); + String signWay = getStringField(formDataVo, "签署方式"); + String signWayText = null; + try { + signWayText = EnumMapUtils.getEnumShowValue(signWay); + }catch (Exception e) {} + if(!"甲方线上,乙方线下".equals(signWayText)){ + SignParty bParty = new SignParty(); + bParty.setPartyId("乙方"); + bParty.setSignOrder(1); + try { + String bSignerType = formDataVo.getFieldData("乙方签署类型").getStringValue(); + if(!"组织".equals(bSignerType)) { + PersonInfo bPersonInfo = new PersonInfo(); + bPersonInfo.setPhone(getStringField(formDataVo,"乙方电话")); + bPersonInfo.setName(getStringField(formDataVo,"乙方姓名")); + bParty.setPersonInfo(bPersonInfo); + bParty.setSignerType(SignerType.PERSON); + }else { + OrgInfo bOrgInfo = new OrgInfo(); + bOrgInfo.setLegalName(getStringField(formDataVo,"乙方法人姓名")); + bOrgInfo.setOrgName(getStringField(formDataVo,"乙方单位名称")); + bOrgInfo.setOrgCode(getStringField(formDataVo,"乙方统一社会信用代码")); + bOrgInfo.setLegalIdCardNo(getStringField(formDataVo,"乙方法人身份证号")); + PersonInfo transactor = new PersonInfo(); + transactor.setName(getStringField(formDataVo,"乙方经办人姓名")); + transactor.setIdCardNo(getStringField(formDataVo,"乙方经办人身份证号")); + transactor.setPhone(getStringField(formDataVo,"乙方经办人手机号")); + bOrgInfo.setTransactor(transactor); + bParty.setOrgInfo(bOrgInfo); + bParty.setSignerType(SignerType.ORG); + } + }catch (Exception e){} + parties.add(bParty); + } + return parties; + } + + + @Override + public String noticeTypes() { + return SignParamSource.super.noticeTypes(); + } + + private String getStringField(FormDataVo vo, String field) { + try { + FieldDataVo fieldData = vo.getFieldData(field); + return fieldData.getStringValue(); + }catch (Exception e) { + return null; + } + } + + public EsignConfigProvider getConfigProvider() { + return configProvider; + } + + public void setConfigProvider(EsignConfigProvider configProvider) { + this.configProvider = configProvider; + } + + public String getCallbackBaseUrl() { + return callbackBaseUrl == null ? "/seeyon/esigncallback.do?method=callback": "/seeyon" + callbackBaseUrl; + } + + public void setCallbackBaseUrl(String callbackBaseUrl) { + this.callbackBaseUrl = callbackBaseUrl; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/param/JsonParamSource.java b/src/main/java/com/seeyon/apps/esign/po/param/JsonParamSource.java new file mode 100644 index 0000000..c6ec99c --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/param/JsonParamSource.java @@ -0,0 +1,122 @@ +package com.seeyon.apps.esign.po.param; + +import com.seeyon.aicloud.common.JsonUtils; +import com.seeyon.apps.esign.po.signer.OrgInfo; +import com.seeyon.apps.esign.po.signer.PersonInfo; +import com.seeyon.apps.esign.po.signer.SignParty; +import com.seeyon.apps.esign.po.signer.SignerType; +import com.seeyon.apps.esign.service.SignParamSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class JsonParamSource implements SignParamSource { + + private Map oriParams; + private String callbackBaseUrl; + private List parties; + public JsonParamSource(){} + public JsonParamSource(String jsonStr){ + this.oriParams = JsonUtils.parseObject(jsonStr,Map.class); + } + public JsonParamSource(Map params){ + this.oriParams = params; + } + + @Override + public String getContractTitle() { + return (String)oriParams.get("contractTitle"); + } + + @Override + public boolean isByFile() { + return (Boolean) oriParams.get("byFile"); + } + + @Override + public String getBizFormId() { + return (String)oriParams.get("formId"); + } + + @Override + public String getTableName() { + return (String)oriParams.get("tableName"); + } + + @Override + public String getSignFileRefId() { + return (String)oriParams.get("signFileRefId"); + } + + @Override + public String getSignedFileField() { + return (String)oriParams.get("signedFileField"); + } + + @Override + public String getSignStatusField() { + return (String)oriParams.get("signStatusField"); + } + + @Override + public Long getSignStatusEnumId() { + return oriParams.get("signStatusEnumId") == null ? null : Long.parseLong((String)oriParams.get("signStatusEnumId")); + } + + @Override + public boolean autoSign() { + return false; + } + + private String getCallbackBaseUrl() { + return callbackBaseUrl == null ? "/seeyon/esigncallback.do?method=callback": "/seeyon" + callbackBaseUrl; + } + + @Override + public String getCallbackUrl() { + return getCallbackBaseUrl() + "&formId=" + getBizFormId() + + "&tablename=" + getTableName() + + "&updatefield=" + getSignedFileField() + + "&statusfield=" + getSignStatusField() + "_" + getSignStatusEnumId() + + "&bizType=FORM"; + } + + @Override + public List getSignerParties() { + String tempStr = (String)oriParams.get("signers"); + List signers = JsonUtils.parseObject(tempStr, List.class); + List parties = new ArrayList<>(); + for (Object signer : signers) { + Map signerMap = (Map) signer; + SignParty signParty = new SignParty(); + SignerType signerType = SignerType.valueOf((String) signerMap.get("signerType")); + signParty.setSignerType(signerType); + signParty.setSignOrder((Integer) signerMap.get("signeOrder")); + signParty.setAutoSign(false); + signParty.setFreeSign(true); + signParty.setPartyId((String)signerMap.get("name")); + if(signerType == SignerType.PERSON) { + PersonInfo personInfo = new PersonInfo(); + personInfo.setName((String)signerMap.get("name")); + personInfo.setPhone((String)signerMap.get("phone")); + signParty.setPersonInfo(personInfo); + }else { + OrgInfo orgInfo = new OrgInfo(); + PersonInfo transactor = new PersonInfo(); + orgInfo.setTransactor(transactor); + orgInfo.setOrgName((String)signerMap.get("orgName")); + orgInfo.setOrgCode((String)signerMap.get("orgCode")); + transactor.setName((String)signerMap.get("name")); + transactor.setPhone((String)signerMap.get("phone")); + signParty.setOrgInfo(orgInfo); + } + parties.add(signParty); + } + return parties; + } + + public void setCallbackBaseUrl(String callbackBaseUrl) { + this.callbackBaseUrl = callbackBaseUrl; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/personal/PsnInitiator.java b/src/main/java/com/seeyon/apps/esign/po/personal/PsnInitiator.java new file mode 100644 index 0000000..1551bcd --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/personal/PsnInitiator.java @@ -0,0 +1,13 @@ +package com.seeyon.apps.esign.po.personal; + +public class PsnInitiator { + private String psnId; + + public String getPsnId() { + return psnId; + } + + public void setPsnId(String psnId) { + this.psnId = psnId; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/seal/SealInfoVo.java b/src/main/java/com/seeyon/apps/esign/po/seal/SealInfoVo.java new file mode 100644 index 0000000..6486fa9 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/seal/SealInfoVo.java @@ -0,0 +1,50 @@ +package com.seeyon.apps.esign.po.seal; + +public class SealInfoVo { + + private String sealId; //印章ID + private String sealName; //印章名称 + private String sealBizTypeDescription; //印章描述 + private String statusDescription; //印章状态描述 + private String sealImageDownloadUrl; //印章图片下载地址 + + public String getSealId() { + return sealId; + } + + public void setSealId(String sealId) { + this.sealId = sealId; + } + + public String getSealName() { + return sealName; + } + + public void setSealName(String sealName) { + this.sealName = sealName; + } + + public String getSealBizTypeDescription() { + return sealBizTypeDescription; + } + + public void setSealBizTypeDescription(String sealBizTypeDescription) { + this.sealBizTypeDescription = sealBizTypeDescription; + } + + public String getStatusDescription() { + return statusDescription; + } + + public void setStatusDescription(String statusDescription) { + this.statusDescription = statusDescription; + } + + public String getSealImageDownloadUrl() { + return sealImageDownloadUrl; + } + + public void setSealImageDownloadUrl(String sealImageDownloadUrl) { + this.sealImageDownloadUrl = sealImageDownloadUrl; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/signer/DefaultSignerBuilder.java b/src/main/java/com/seeyon/apps/esign/po/signer/DefaultSignerBuilder.java new file mode 100644 index 0000000..74548e5 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/signer/DefaultSignerBuilder.java @@ -0,0 +1,163 @@ +package com.seeyon.apps.esign.po.signer; + +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.signfield.NormalSignFieldConfig; +import com.seeyon.apps.esign.po.signfield.SignField; +import com.seeyon.apps.esign.po.signfield.SignFieldPosition; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DefaultSignerBuilder implements SignerBuilder { + + private EsignConfigProvider esignConfigProvider = EsignConfigProvider.getInstance(); + + @Override + public Signer build(List fileIds,List keywordPos, SignParty party) { + if (party.getSignerType() == SignerType.PERSON) { + return buildPersonSigner(fileIds, keywordPos,party); + } else if (party.getSignerType() == SignerType.ORG) { + return buildOrgSigner(fileIds,keywordPos, party); + } else { + throw new IllegalArgumentException("不支持的签署方类型"); + } + } + + private Signer buildPersonSigner(List fileIds,List keywordPos, SignParty party) { + PersonInfo psn = party.getPersonInfo(); + if (psn == null) throw new IllegalStateException("个人签署方缺少 PersonInfo"); + Signer signer = new Signer(); + signer.setSignerType(0); + signer.setSignConfig(mapOf("signOrder", party.getSignOrder())); + if (!party.getAutoSign()) { + signer.setNoticeConfig(mapOf("noticeTypes", "1")); + signer.setPsnSignerInfo(mapOf( + "psnAccount", psn.getPhone(), + "psnInfo", mapOf( + "psnName", psn.getName(), + "psnIDCardNum", psn.getIdCardNo(), + "psnIDCardType", psn.getIdCardType() + ) + )); + } + signer.setSignFields(buildSignFields(fileIds, keywordPos, party)); + return signer; + } + + private Signer buildOrgSigner(List fileIds, List keywordPos,SignParty party) { + OrgInfo org = party.getOrgInfo(); + Signer signer = new Signer(); + signer.setSignerType(1); + signer.setSignConfig(mapOf("signOrder", party.getSignOrder())); + if (!party.getAutoSign() && org != null) { + signer.setNoticeConfig(mapOf("noticeTypes", "1")); + signer.setOrgSignerInfo(mapOf( + "orgName", org.getOrgName(), + "orgInfo", mapOf( + "legalRepName", org.getLegalName(), + "legalRepIDCardNum", org.getLegalIdCardNo(), + "orgIDCardNum", org.getOrgCode(), + "orgIDCardType", "CRED_ORG_USCC" + ), + "transactorInfo", mapOf( + "psnAccount", org.getTransactor().getPhone(), + "psnInfo", mapOf( + "psnName", org.getTransactor().getName(), + "psnIDCardNum", org.getTransactor().getIdCardNo() + ) + ) + )); + } + signer.setSignFields(buildSignFields(fileIds,keywordPos,party)); + return signer; + } + + private List extractPosition(List positions, String keyword) { + List posList = new ArrayList<>(); + for (Object obj : positions) { + Map position = (Map) obj; + if (Boolean.TRUE.equals(position.get("searchResult")) && keyword.equals(position.get("keyword"))) { + Object[] posArray = (Object[]) position.get("positions"); + for (Object o : posArray) { + Map posMap = (Map)o; + Object[] coords = (Object[]) posMap.get("coordinates"); + for (Object coord : coords) { + Map tempMap = (Map) coord; + SignFieldPosition sfp = new SignFieldPosition(); + sfp.setPositionPage(posMap.get("pageNum") + ""); + sfp.setPositionX(toFloat(tempMap.get("positionX")) + 150f); + sfp.setPositionY(toFloat(tempMap.get("positionY"))); + posList.add(sfp); + } + } + return posList; + } + } + return new ArrayList<>(); + } + + private Float toFloat(Object val) { + return val instanceof Number ? ((Number) val).floatValue() : Float.parseFloat(val.toString()); + } + + private List buildSignFields(List fileIds,List keywordPos, SignParty party) { + List signFields = new ArrayList<>(); + for (String fileId : fileIds) { + if(!party.getFreeSign()) { + String searchKey = null; + SignFieldPosition qiFengposition = new SignFieldPosition(); + qiFengposition.setAcrossPageMode("ALL"); + if("甲方".equals(party.getPartyId())) { + searchKey = esignConfigProvider.getBizConfigByKey(EsignConfigConstants.aSignPositionKeyword); + qiFengposition.setPositionY(520f); + }else { + searchKey = esignConfigProvider.getBizConfigByKey(EsignConfigConstants.bSignPositionKeyword); + qiFengposition.setPositionY(720f); + } + List signFieldPositions = extractPosition(keywordPos, searchKey); + List lpSignFieldPositions = extractPosition(keywordPos, esignConfigProvider.getBizConfigByKey(EsignConfigConstants.lpSignPositionKeyword)); + if(lpSignFieldPositions != null && lpSignFieldPositions.size() > 0) { + signFieldPositions.addAll(lpSignFieldPositions); + } + if(signFieldPositions.size() > 0) { + signFieldPositions.add(qiFengposition); + } + for (SignFieldPosition position : signFieldPositions) { + NormalSignFieldConfig fieldConfig = new NormalSignFieldConfig(); + fieldConfig.setSignFieldPosition(position); + fieldConfig.setAutoSign(party.getAutoSign()); + fieldConfig.setSignFieldStyle("ALL".equals(position.getAcrossPageMode()) ? 2 : 1); + SignField field = new SignField(); + field.setSignFieldType(0); + field.setNormalSignFieldConfig(fieldConfig); + field.setFileId(fileId); + signFields.add(field); + } + }else { + NormalSignFieldConfig normalSignFieldConfig = new NormalSignFieldConfig(); + normalSignFieldConfig.setAssignedSealId(party.getSealId()); + normalSignFieldConfig.setFreeMode(true); + normalSignFieldConfig.setAutoSign(false); + normalSignFieldConfig.setAdaptableSignFieldSize(true); + SignField field = new SignField(); + field.setSignFieldType(0); + field.setNormalSignFieldConfig(normalSignFieldConfig); + field.setFileId(fileId); + signFields.add(field); + } + } + return signFields; + } + + private Map mapOf(Object... keyValues) { + if (keyValues.length % 2 != 0) throw new IllegalArgumentException("key-value 必须成对出现"); + Map map = new HashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + map.put((String) keyValues[i], keyValues[i + 1]); + } + return map; + } +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/po/signer/JsonSignerBuilder.java b/src/main/java/com/seeyon/apps/esign/po/signer/JsonSignerBuilder.java new file mode 100644 index 0000000..005201c --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/signer/JsonSignerBuilder.java @@ -0,0 +1,107 @@ +package com.seeyon.apps.esign.po.signer; + +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.signfield.NormalSignFieldConfig; +import com.seeyon.apps.esign.po.signfield.SignField; +import com.seeyon.apps.esign.po.signfield.SignFieldPosition; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JsonSignerBuilder implements SignerBuilder{ + + private EsignConfigProvider esignConfigProvider = EsignConfigProvider.getInstance(); + + @Override + public Signer build(List fileIds, List keywordPos, SignParty party) { + if (party.getSignerType() == SignerType.PERSON) { + return buildPersonSigner(fileIds,party); + } else if (party.getSignerType() == SignerType.ORG) { + return buildOrgSigner(fileIds, party); + } else { + throw new IllegalArgumentException("不支持的签署方类型"); + } + } + + private Float toFloat(Object val) { + return val instanceof Number ? ((Number) val).floatValue() : Float.parseFloat(val.toString()); + } + + private List buildSignFields(List fileIds, SignParty party) { + List signFields = new ArrayList<>(); + for (String fileId : fileIds) { + NormalSignFieldConfig normalSignFieldConfig = new NormalSignFieldConfig(); + normalSignFieldConfig.setAssignedSealId(party.getSealId()); + normalSignFieldConfig.setFreeMode(true); + normalSignFieldConfig.setAutoSign(false); + normalSignFieldConfig.setAdaptableSignFieldSize(true); + SignField field = new SignField(); + field.setSignFieldType(0); + field.setNormalSignFieldConfig(normalSignFieldConfig); + field.setFileId(fileId); + signFields.add(field); + } + return signFields; + } + + private Map mapOf(Object... keyValues) { + if (keyValues.length % 2 != 0) throw new IllegalArgumentException("key-value 必须成对出现"); + Map map = new HashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + map.put((String) keyValues[i], keyValues[i + 1]); + } + return map; + } + + private Signer buildPersonSigner(List fileIds, SignParty party) { + PersonInfo psn = party.getPersonInfo(); + if (psn == null) throw new IllegalStateException("个人签署方缺少 PersonInfo"); + Signer signer = new Signer(); + signer.setSignerType(0); + signer.setSignConfig(mapOf("signOrder", party.getSignOrder())); + if (!party.getAutoSign()) { + signer.setNoticeConfig(mapOf("noticeTypes", "1")); + signer.setPsnSignerInfo(mapOf( + "psnAccount", psn.getPhone(), + "psnInfo", mapOf( + "psnName", psn.getName(), + "psnIDCardNum", psn.getIdCardNo(), + "psnIDCardType", psn.getIdCardType() + ) + )); + } + signer.setSignFields(buildSignFields(fileIds, party)); + return signer; + } + + private Signer buildOrgSigner(List fileIds,SignParty party) { + OrgInfo org = party.getOrgInfo(); + Signer signer = new Signer(); + signer.setSignerType(1); + signer.setSignConfig(mapOf("signOrder", party.getSignOrder())); + if (!party.getAutoSign() && org != null) { + signer.setNoticeConfig(mapOf("noticeTypes", "1")); + signer.setOrgSignerInfo(mapOf( + "orgName", org.getOrgName(), + "orgInfo", mapOf( + "legalRepName", org.getLegalName(), + "legalRepIDCardNum", org.getLegalIdCardNo(), + "orgIDCardNum", org.getOrgCode(), + "orgIDCardType", "CRED_ORG_USCC" + ), + "transactorInfo", mapOf( + "psnAccount", org.getTransactor().getPhone(), + "psnInfo", mapOf( + "psnName", org.getTransactor().getName(), + "psnIDCardNum", org.getTransactor().getIdCardNo() + ) + ) + )); + } + signer.setSignFields(buildSignFields(fileIds,party)); + return signer; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/signer/OrgInfo.java b/src/main/java/com/seeyon/apps/esign/po/signer/OrgInfo.java new file mode 100644 index 0000000..20b9ad4 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/signer/OrgInfo.java @@ -0,0 +1,59 @@ +package com.seeyon.apps.esign.po.signer; + +public class OrgInfo { + + /** 企业名称 */ + private String orgName; + + /** 统一社会信用代码 */ + private String orgCode; + + /** 法人姓名 */ + private String legalName; + + /** 法人身份证号 */ + private String legalIdCardNo; + + /** 经办人信息(企业签一定有) */ + private PersonInfo transactor; + + public String getOrgName() { + return orgName; + } + + public void setOrgName(String orgName) { + this.orgName = orgName; + } + + public String getOrgCode() { + return orgCode; + } + + public void setOrgCode(String orgCode) { + this.orgCode = orgCode; + } + + public String getLegalName() { + return legalName; + } + + public void setLegalName(String legalName) { + this.legalName = legalName; + } + + public String getLegalIdCardNo() { + return legalIdCardNo; + } + + public void setLegalIdCardNo(String legalIdCardNo) { + this.legalIdCardNo = legalIdCardNo; + } + + public PersonInfo getTransactor() { + return transactor; + } + + public void setTransactor(PersonInfo transactor) { + this.transactor = transactor; + } +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/po/signer/PersonInfo.java b/src/main/java/com/seeyon/apps/esign/po/signer/PersonInfo.java new file mode 100644 index 0000000..349ec90 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/signer/PersonInfo.java @@ -0,0 +1,48 @@ +package com.seeyon.apps.esign.po.signer; + +public class PersonInfo { + + /** 姓名 */ + private String name; + + /** 手机号(用于通知 / 登录) */ + private String phone; + + /** 身份证号(可选,企业经办人 / 强认证场景) */ + private String idCardNo; + + /** 身份证类型(可选) */ + private String idCardType; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getIdCardNo() { + return idCardNo; + } + + public void setIdCardNo(String idCardNo) { + this.idCardNo = idCardNo; + } + + public String getIdCardType() { + return idCardType; + } + + public void setIdCardType(String idCardType) { + this.idCardType = idCardType; + } +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/po/signer/SignParty.java b/src/main/java/com/seeyon/apps/esign/po/signer/SignParty.java new file mode 100644 index 0000000..6f93672 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/signer/SignParty.java @@ -0,0 +1,128 @@ +package com.seeyon.apps.esign.po.signer; + +import com.seeyon.apps.esign.po.signfield.SignFieldPosition; + +import java.util.List; + +public class SignParty { + + /** 唯一标识(非必需,但强烈建议) */ + private String partyId; + + /** 签署顺序 */ + private int signOrder; + + /** 签署类型:PERSON / ORG */ + private SignerType signerType; + + /** 是否自动签 */ + private Boolean autoSign = false; + + /** 印章ID(企业签) */ + private String sealId; + + /** 通知手机号 */ + private String notifyPhone; + + /** 个人信息 */ + private PersonInfo personInfo; + + /** 企业信息 */ + private OrgInfo orgInfo; + + /** 签署位置 */ + private List positions; + + /** 签署关键字(可为空) */ + private String signKeyword; + + private Boolean freeSign = false; + + public Boolean getFreeSign() { + return freeSign; + } + + public void setFreeSign(Boolean freeSign) { + this.freeSign = freeSign; + } + + public String getPartyId() { + return partyId; + } + + public void setPartyId(String partyId) { + this.partyId = partyId; + } + + public int getSignOrder() { + return signOrder; + } + + public void setSignOrder(int signOrder) { + this.signOrder = signOrder; + } + + public SignerType getSignerType() { + return signerType; + } + + public void setSignerType(SignerType signerType) { + this.signerType = signerType; + } + + public Boolean getAutoSign() { + return autoSign; + } + + public void setAutoSign(Boolean autoSign) { + this.autoSign = autoSign; + } + + public String getSealId() { + return sealId; + } + + public void setSealId(String sealId) { + this.sealId = sealId; + } + + public String getNotifyPhone() { + return notifyPhone; + } + + public void setNotifyPhone(String notifyPhone) { + this.notifyPhone = notifyPhone; + } + + public PersonInfo getPersonInfo() { + return personInfo; + } + + public void setPersonInfo(PersonInfo personInfo) { + this.personInfo = personInfo; + } + + public OrgInfo getOrgInfo() { + return orgInfo; + } + + public void setOrgInfo(OrgInfo orgInfo) { + this.orgInfo = orgInfo; + } + + public List getPositions() { + return positions; + } + + public void setPositions(List positions) { + this.positions = positions; + } + + public String getSignKeyword() { + return signKeyword; + } + + public void setSignKeyword(String signKeyword) { + this.signKeyword = signKeyword; + } +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/po/signer/Signer.java b/src/main/java/com/seeyon/apps/esign/po/signer/Signer.java new file mode 100644 index 0000000..bd6f3f5 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/signer/Signer.java @@ -0,0 +1,65 @@ +package com.seeyon.apps.esign.po.signer; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.seeyon.apps.esign.po.signfield.SignField; + +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class Signer { + private int signerType; + private Map signConfig; + private Map noticeConfig; + private Map orgSignerInfo; + private Map psnSignerInfo; + private List signFields; + + public int getSignerType() { + return signerType; + } + + public void setSignerType(int signerType) { + this.signerType = signerType; + } + + public Map getNoticeConfig() { + return noticeConfig; + } + + public void setNoticeConfig(Map noticeConfig) { + this.noticeConfig = noticeConfig; + } + + public Map getOrgSignerInfo() { + return orgSignerInfo; + } + + public void setOrgSignerInfo(Map orgSignerInfo) { + this.orgSignerInfo = orgSignerInfo; + } + + public Map getPsnSignerInfo() { + return psnSignerInfo; + } + + public void setPsnSignerInfo(Map psnSignerInfo) { + this.psnSignerInfo = psnSignerInfo; + } + + public List getSignFields() { + return signFields; + } + + public void setSignFields(List signFields) { + this.signFields = signFields; + } + + public Map getSignConfig() { + return signConfig; + } + + public void setSignConfig(Map signConfig) { + this.signConfig = signConfig; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/signer/SignerBuilder.java b/src/main/java/com/seeyon/apps/esign/po/signer/SignerBuilder.java new file mode 100644 index 0000000..dcc8287 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/signer/SignerBuilder.java @@ -0,0 +1,26 @@ +package com.seeyon.apps.esign.po.signer; + +import java.util.ArrayList; +import java.util.List; + +public interface SignerBuilder { + + /** + * 根据签署方信息构建 Signer + */ + Signer build(List fileIds, List keywordPos,SignParty party); + + /** + * 批量构建(默认能力) + */ + default List buildAll(List fileIds,List keywordPos,List parties) { + List signers = new ArrayList<>(); + for (SignParty party : parties) { + Signer signer = build(fileIds,keywordPos, party); + if (signer != null) { + signers.add(signer); + } + } + return signers; + } +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/po/signer/SignerType.java b/src/main/java/com/seeyon/apps/esign/po/signer/SignerType.java new file mode 100644 index 0000000..7c4708a --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/signer/SignerType.java @@ -0,0 +1,5 @@ +package com.seeyon.apps.esign.po.signer; + +public enum SignerType { + PERSON, ORG +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/po/signfield/NormalSignFieldConfig.java b/src/main/java/com/seeyon/apps/esign/po/signfield/NormalSignFieldConfig.java new file mode 100644 index 0000000..36c86b5 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/signfield/NormalSignFieldConfig.java @@ -0,0 +1,58 @@ +package com.seeyon.apps.esign.po.signfield; + +public class NormalSignFieldConfig { + private Boolean autoSign; + private Integer signFieldStyle; + private SignFieldPosition signFieldPosition; + private String assignedSealId; //指定签章id + private Boolean freeMode = false; + private Boolean adaptableSignFieldSize;//是否自适应签章大小 + + public Boolean getAutoSign() { + return autoSign; + } + + public void setAutoSign(Boolean autoSign) { + this.autoSign = autoSign; + } + + public Integer getSignFieldStyle() { + return signFieldStyle; + } + + public void setSignFieldStyle(Integer signFieldStyle) { + this.signFieldStyle = signFieldStyle; + } + + public SignFieldPosition getSignFieldPosition() { + return signFieldPosition; + } + + public void setSignFieldPosition(SignFieldPosition signFieldPosition) { + this.signFieldPosition = signFieldPosition; + } + + public String getAssignedSealId() { + return assignedSealId; + } + + public void setAssignedSealId(String assignedSealId) { + this.assignedSealId = assignedSealId; + } + + public Boolean getFreeMode() { + return freeMode; + } + + public void setFreeMode(Boolean freeMode) { + this.freeMode = freeMode; + } + + public Boolean getAdaptableSignFieldSize() { + return adaptableSignFieldSize; + } + + public void setAdaptableSignFieldSize(Boolean adaptableSignFieldSize) { + this.adaptableSignFieldSize = adaptableSignFieldSize; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/signfield/RemarkSignFieldConfig.java b/src/main/java/com/seeyon/apps/esign/po/signfield/RemarkSignFieldConfig.java new file mode 100644 index 0000000..7c33ee2 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/signfield/RemarkSignFieldConfig.java @@ -0,0 +1,32 @@ +package com.seeyon.apps.esign.po.signfield; + +public class RemarkSignFieldConfig { + + private boolean freeMode = false; + private int inputType = 1; + private String remarkContent = ""; + + public boolean isFreeMode() { + return freeMode; + } + + public void setFreeMode(boolean freeMode) { + this.freeMode = freeMode; + } + + public int getInputType() { + return inputType; + } + + public void setInputType(int inputType) { + this.inputType = inputType; + } + + public String getRemarkContent() { + return remarkContent; + } + + public void setRemarkContent(String remarkContent) { + this.remarkContent = remarkContent; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/signfield/SignField.java b/src/main/java/com/seeyon/apps/esign/po/signfield/SignField.java new file mode 100644 index 0000000..51876af --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/signfield/SignField.java @@ -0,0 +1,42 @@ +package com.seeyon.apps.esign.po.signfield; + +public class SignField { + + private String fileId; + private Integer signFieldType; + private NormalSignFieldConfig normalSignFieldConfig; + private RemarkSignFieldConfig remarkSignFieldConfig; + + public String getFileId() { + return fileId; + } + + public void setFileId(String fileId) { + this.fileId = fileId; + } + + public Integer getSignFieldType() { + return signFieldType; + } + + public void setSignFieldType(Integer signFieldType) { + this.signFieldType = signFieldType; + } + + public NormalSignFieldConfig getNormalSignFieldConfig() { + return normalSignFieldConfig; + } + + public void setNormalSignFieldConfig(NormalSignFieldConfig normalSignFieldConfig) { + this.normalSignFieldConfig = normalSignFieldConfig; + } + + public RemarkSignFieldConfig getRemarkSignFieldConfig() { + return remarkSignFieldConfig; + } + + public void setRemarkSignFieldConfig(RemarkSignFieldConfig remarkSignFieldConfig) { + this.remarkSignFieldConfig = remarkSignFieldConfig; + } + +} diff --git a/src/main/java/com/seeyon/apps/esign/po/signfield/SignFieldPosition.java b/src/main/java/com/seeyon/apps/esign/po/signfield/SignFieldPosition.java new file mode 100644 index 0000000..a06b264 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/signfield/SignFieldPosition.java @@ -0,0 +1,41 @@ +package com.seeyon.apps.esign.po.signfield; + +public class SignFieldPosition { + + private String acrossPageMode; + private String positionPage; + private Float positionX; + private Float positionY; + + public String getAcrossPageMode() { + return acrossPageMode; + } + + public void setAcrossPageMode(String acrossPageMode) { + this.acrossPageMode = acrossPageMode; + } + + public String getPositionPage() { + return positionPage; + } + + public void setPositionPage(String positionPage) { + this.positionPage = positionPage; + } + + public Float getPositionX() { + return positionX; + } + + public void setPositionX(Float positionX) { + this.positionX = positionX; + } + + public Float getPositionY() { + return positionY; + } + + public void setPositionY(Float positionY) { + this.positionY = positionY; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/upload/EsignFileUploadParams.java b/src/main/java/com/seeyon/apps/esign/po/upload/EsignFileUploadParams.java new file mode 100644 index 0000000..473cb92 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/upload/EsignFileUploadParams.java @@ -0,0 +1,49 @@ +package com.seeyon.apps.esign.po.upload; + +public class EsignFileUploadParams { + private String contentMd5; + private String contentType; + private String fileName; + private Long fileSize; + private Boolean convertToPDF = false; + + public String getContentMd5() { + return contentMd5; + } + + public void setContentMd5(String contentMd5) { + this.contentMd5 = contentMd5; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public Long getFileSize() { + return fileSize; + } + + public void setFileSize(Long fileSize) { + this.fileSize = fileSize; + } + + public Boolean getConvertToPDF() { + return convertToPDF; + } + + public void setConvertToPDF(Boolean convertToPDF) { + this.convertToPDF = convertToPDF; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/po/upload/GetUploadUrlResp.java b/src/main/java/com/seeyon/apps/esign/po/upload/GetUploadUrlResp.java new file mode 100644 index 0000000..5ce149a --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/po/upload/GetUploadUrlResp.java @@ -0,0 +1,23 @@ +package com.seeyon.apps.esign.po.upload; + +public class GetUploadUrlResp { + + private String fileId; + private String fileUploadUrl; + + public String getFileId() { + return fileId; + } + + public void setFileId(String fileId) { + this.fileId = fileId; + } + + public String getFileUploadUrl() { + return fileUploadUrl; + } + + public void setFileUploadUrl(String fileUploadUrl) { + this.fileUploadUrl = fileUploadUrl; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/service/ContractCreateService.java b/src/main/java/com/seeyon/apps/esign/service/ContractCreateService.java new file mode 100644 index 0000000..19a48e6 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/ContractCreateService.java @@ -0,0 +1,31 @@ +package com.seeyon.apps.esign.service; + +import com.seeyon.ctp.common.AppContext; + +import java.util.List; +import java.util.Map; + +public class ContractCreateService { + + private EsignByTemplateService templateService = (EsignByTemplateService) AppContext.getBean("esignByTemplateService"); + private List signParamBuildFactories; + + public String startSign(SignParamSource source) throws Exception { + for (SignParamBuildFactory factory : signParamBuildFactories) { + if(factory.support(source)){ + Map buildParam = factory.buildParam(source); + if (buildParam == null) throw new RuntimeException("签署参数构建失败"); + return templateService.createBySignTemplate(buildParam); + } + } + return null; + } + + public List getSignParamBuildFactories() { + return signParamBuildFactories; + } + + public void setSignParamBuildFactories(List signParamBuildFactories) { + this.signParamBuildFactories = signParamBuildFactories; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/service/EsignAuthService.java b/src/main/java/com/seeyon/apps/esign/service/EsignAuthService.java new file mode 100644 index 0000000..049cb15 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/EsignAuthService.java @@ -0,0 +1,33 @@ +package com.seeyon.apps.esign.service; + +import com.seeyon.aicloud.common.JsonUtils; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignApiUrl; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.esignapi.EsignApiHeader; +import com.seeyon.apps.esign.po.esignapi.EsignBaseResp; +import com.seeyon.apps.esign.utils.HttpClient; +import com.seeyon.ctp.common.AppContext; + +import java.util.Map; + + +public class EsignAuthService { + + private EsignConfigProvider configProvider = (EsignConfigProvider) AppContext.getBean("esignConfigProvider"); + + public void personAuth(Map authParams) throws Exception { + EsignApiHeader esignApiHeader = EsignApiHeader.build(); + String jsonBody = JsonUtils.toJSONString(authParams); + esignApiHeader.appId(configProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)) + .httpMethod("POST") + .heads("") + .pathAndParameters(EsignApiUrl.PERSON_AUTH_URL) + .contentMD5(jsonBody) + .signature(configProvider.getBizConfigByKey(EsignConfigConstants.APP_SECRET)); + String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.PERSON_AUTH_URL; + String respStr = HttpClient.httpPostRaw(url, jsonBody, esignApiHeader.convert2Headers(), "UTF-8"); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); + Map dataMap = (Map) esignBaseResp.getData(); + } +} diff --git a/src/main/java/com/seeyon/apps/esign/service/EsignByTemplateService.java b/src/main/java/com/seeyon/apps/esign/service/EsignByTemplateService.java new file mode 100644 index 0000000..4586930 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/EsignByTemplateService.java @@ -0,0 +1,54 @@ +package com.seeyon.apps.esign.service; + +import com.seeyon.aicloud.common.JsonUtils; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignApiUrl; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.esignapi.EsignApiHeader; +import com.seeyon.apps.esign.po.esignapi.EsignBaseResp; +import com.seeyon.apps.esign.utils.HttpClient; +import com.seeyon.apps.esign.utils.JsonCleaner; +import com.seeyon.ctp.common.AppContext; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +import java.util.Map; + + +public class EsignByTemplateService { + + private EsignConfigProvider configProvider = EsignConfigProvider.getInstance(); + private TokenCacheManager tokenCacheManager = (TokenCacheManager) AppContext.getBean("tokenCacheManager"); + private static final Log log = LogFactory.getLog(EsignByTemplateService.class); + + public void signByTemplate(Map params) { + + } + + public void startSignFlow(String flowId) throws Exception { + String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.SIGN_START_URL; + url.replace("{signFlowId}", flowId); + String respStr = HttpClient.httpPostRaw(url, "", null, null); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); + if (esignBaseResp.getCode() != 0) { + throw new Exception("签署发起失败"); + } + } + + public String createBySignTemplate(Map params) throws Exception { + EsignApiHeader esignApiHeader = EsignApiHeader.build(); + String jsonBody = JsonCleaner.removeEmptyObjects(JsonUtils.toJSONString(params)); + esignApiHeader.token(tokenCacheManager.getToken()).appId(configProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)); + String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.SIGN_BY_FILE_URL; + log.info("合同发起入参: " + jsonBody); + String respStr = HttpClient.httpPostRaw(url, jsonBody, esignApiHeader.convert2Headers(), "UTF-8"); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); + Map dataMap = (Map) esignBaseResp.getData(); + if(!"0".equals(esignBaseResp.getCode() + "")) { + throw new RuntimeException("签署发起失败," + esignBaseResp.getMessage()); + } + String flowId = (String) dataMap.get("signFlowId"); + return flowId; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/service/EsignCallbackBizService.java b/src/main/java/com/seeyon/apps/esign/service/EsignCallbackBizService.java new file mode 100644 index 0000000..e8f8a4c --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/EsignCallbackBizService.java @@ -0,0 +1,7 @@ +package com.seeyon.apps.esign.service; + +public interface EsignCallbackBizService { + + public void handleSuccessSignCallbackBiz(String tablename,String updatefield,String statusField,String formId,Object[] fileInfos) throws Exception; + public void handleFailSignCallbackBiz(String tableName,String statusField,String formId,String failMsg) throws Exception; +} diff --git a/src/main/java/com/seeyon/apps/esign/service/EsignCallbackFlowBizService.java b/src/main/java/com/seeyon/apps/esign/service/EsignCallbackFlowBizService.java new file mode 100644 index 0000000..d1f63e4 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/EsignCallbackFlowBizService.java @@ -0,0 +1,89 @@ +package com.seeyon.apps.esign.service; + +import com.alibaba.fastjson.JSONObject; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.utils.FileUtil; +import com.seeyon.apps.esign.utils.ProtUtil; +import com.seeyon.ctp.common.AppContext; +import com.seeyon.ctp.util.JDBCAgent; +import com.seeyon.utils.form.EnumMapUtils; +import com.seeyon.utils.form.FormTableExecutor; +import com.seeyon.utils.form.FormUpdateField; +import com.seeyon.utils.form.FormWhereCondition; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + + +public class EsignCallbackFlowBizService implements EsignCallbackBizService{ + + private static final Log log = LogFactory.getLog(EsignCallbackFlowBizService.class); + private ProtUtil protUtil = (ProtUtil) AppContext.getBean("protUtil"); + private EsignConfigProvider configProvider = EsignConfigProvider.getInstance(); + + public void handleSuccessSignCallbackBiz(String tablename,String updatefield,String statusField,String formId,Object[] fileInfos) throws Exception { + String[] strings = formId.split("_"); + String oaFlowId = strings[0]; + String tempFormId = strings[1]; + String[] statusParams = statusField.split("_"); + String status = statusParams[0]; + String enumId = statusParams[1]; + String enumItemId = EnumMapUtils.getEnumItemValueByEnumId("已签署",Long.parseLong(enumId)); + //下载文件到本地 + List conditions = new ArrayList<>(); + String refId = FileUtil.uploadContractToOA(fileInfos, getModuleId(tempFormId), + configProvider.getBizConfigByKey(EsignConfigConstants.FORMEDITLOGINNAME), + configProvider.getBizConfigByKey(EsignConfigConstants.updateAccountName)); + List updateFields = new ArrayList<>(); + updateFields.add(FormUpdateField.build().fieldName(updatefield).value(refId)); + updateFields.add(FormUpdateField.build().fieldName(status).value(enumItemId)); + conditions.add(FormWhereCondition.build().value(tempFormId).fieldName("ID")); + JSONObject params = new JSONObject(); + params.put("message", "签署完成!!!"); + params.put("returnCode", 1); + protUtil.sendPostNotification(params.toString(), configProvider.getBizConfigByKey(EsignConfigConstants.nodeTokenUrl), oaFlowId); + FormTableExecutor.update(tablename,updateFields,conditions); + } + + public void handleFailSignCallbackBiz(String tableName,String statusField,String formId,String failMsg) throws Exception { + String[] strings = formId.split("_"); + String oaFlowId = strings[0]; + String tempFormId = strings[1]; + String[] statusParams = statusField.split("_"); + String status = statusParams[0]; + String enumId = statusParams[1]; + String enumItemId = EnumMapUtils.getEnumItemValueByEnumId("签署失败",Long.parseLong(enumId)); + List conditions = new ArrayList<>(); + List updateFields = new ArrayList<>(); + updateFields.add(FormUpdateField.build().fieldName(status).value(enumItemId)); + conditions.add(FormWhereCondition.build().value(tempFormId).fieldName("ID")); + JSONObject params = new JSONObject(); + params.put("message", "签署失败: " + failMsg); + params.put("returnCode", 2); + protUtil.sendPostNotification(params.toString(), configProvider.getBizConfigByKey(EsignConfigConstants.nodeTokenUrl), oaFlowId); + FormTableExecutor.update(tableName,updateFields,conditions); + } + + private String getModuleId(String formId) { + JDBCAgent jdbcAgent = new JDBCAgent(); + try { + String sql = "SELECT MODULE_ID FROM ctp_content_all where CONTENT_DATA_ID = ?"; + List params = new ArrayList<>(); + params.add(formId); + jdbcAgent.execute(sql,params); + List list = jdbcAgent.resultSetToList(); + if(list == null || !(list.size() > 0)) { + throw new RuntimeException("MODULE_ID获取失败"); + } + Map map = (Map) list.get(0); + return map.get("module_id") +""; + }catch (Exception e) { + log.error(e.getMessage(),e); + } + return null; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/service/EsignFileTemplateService.java b/src/main/java/com/seeyon/apps/esign/service/EsignFileTemplateService.java new file mode 100644 index 0000000..d78a47c --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/EsignFileTemplateService.java @@ -0,0 +1,133 @@ +package com.seeyon.apps.esign.service; + +import com.seeyon.aicloud.common.JsonUtils; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignApiUrl; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.esignapi.EsignApiHeader; +import com.seeyon.apps.esign.po.esignapi.EsignBaseResp; +import com.seeyon.apps.esign.po.file.SignFile; +import com.seeyon.apps.esign.po.file.TemplateComponent; +import com.seeyon.apps.esign.utils.HttpClient; +import com.seeyon.ctp.common.AppContext; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class EsignFileTemplateService { + + private static final String ORI_TEMPLATE = "原始合同模板文件"; + private static final Log log = LogFactory.getLog(EsignFileTemplateService.class); + private EsignConfigProvider configProvider = EsignConfigProvider.getInstance(); + private TokenCacheManager tokenCacheManager = (TokenCacheManager) AppContext.getBean("tokenCacheManager"); + private EsignUploadFileService uploadFileService = (EsignUploadFileService) AppContext.getBean("esignUploadFileService"); + private String orgId; + + private void fillOrgId() { + EsignApiHeader esignApiHeader = EsignApiHeader.build(); + try { + esignApiHeader.token(tokenCacheManager.getToken()).appId(configProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)); + } catch (Exception e) { + e.printStackTrace(); + } + //获取orgId + String queryStr = EsignApiUrl.QUERY_ORGINFO + "?orgName=" + configProvider.getBizConfigByKey(EsignConfigConstants.UNITNAME); + String reqUrl = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + queryStr; + String orgInfoRespStr = HttpClient.httpGet(reqUrl, esignApiHeader.convert2Headers(), "UTF-8"); + EsignBaseResp orgInfoResp = JsonUtils.parseObject(orgInfoRespStr, EsignBaseResp.class); + Map orgData = (Map) orgInfoResp.getData(); + orgId = (String) orgData.get("orgId"); + } + + //填充模板 + public SignFile fillTemplate(String templateId, String fileName, Map params) { + //请求示例 + Map reqParams = new HashMap<>(); + reqParams.put("docTemplateId",templateId); + reqParams.put("fileName",fileName); + List components = new ArrayList<>(); + Set keySets = params.keySet(); + for (String key : keySets) { + TemplateComponent component = new TemplateComponent(); + component.setComponentKey(key); + component.setComponentValue((String) params.get(key)); + components.add(component); + } + reqParams.put("components",components); + String url = ""; + String resp = HttpClient.httpPostRaw(url, JsonUtils.toJSONString(reqParams), null, null); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(resp, EsignBaseResp.class); + if(esignBaseResp.getCode() != 0) {} + Map dataMap = (Map) esignBaseResp.getData(); + SignFile signFile = new SignFile(); + signFile.setFileId((String) dataMap.get("fileId")); + signFile.setFileName(fileName); + return signFile; + } + + //获取模板详细信息 + public Map getTemplateDetail(String signTemplateId,String orgId,Boolean queryComponents) { + String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + "?" + "signTemplateId=" + signTemplateId + "&orgId=" + orgId + (queryComponents != null ? "&queryComponents=" + queryComponents : ""); + String resp = HttpClient.httpGet(url, null, "UTF-8"); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(resp, EsignBaseResp.class); + Map data = (Map) esignBaseResp.getData(); + return data; + } + + //获取模板详细信息 + public Map getTemplateDetail(String signTemplateId,String orgId) { + String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.QUERY_TEMPLATE_DETAIL_URL + "?" +"orgId=" + orgId + "&signTemplateId=" + signTemplateId; + EsignApiHeader apiHeader = EsignApiHeader.build().token(tokenCacheManager.getToken()).appId(configProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)); + String resp = HttpClient.httpGet(url, apiHeader.convert2Headers(), "UTF-8"); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(resp, EsignBaseResp.class); + Map data = (Map) esignBaseResp.getData(); + return data; + } + + public Map queryTemplates(Integer pageNo) throws Exception { + EsignApiHeader esignApiHeader = EsignApiHeader.build().appId(configProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)); + String queryStr = EsignApiUrl.QUERY_TEMPLATES_URL + "?" + "orgId=" + obtainOrgId() + "&pageNum=" + pageNo + "&pageSize=" + 20 + "&status=" + 1; + esignApiHeader.token(tokenCacheManager.getToken()); + String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + queryStr; + String templateListRespStr = HttpClient.httpGet(url, esignApiHeader.convert2Headers(), "UTF-8"); + EsignBaseResp templateListResp = JsonUtils.parseObject(templateListRespStr, EsignBaseResp.class); + Map listData = (Map)templateListResp.getData(); + Integer total = (Integer) listData.get("total"); + Object[] templateArray = (Object[]) listData.get("signTemplates"); + Map pageMap = new HashMap<>(); + pageMap.put("total",total); + pageMap.put("templateList",templateArray); + return pageMap; + } + + public String getCompareDetailUrl(String templateRefId,String contractRefId) throws Exception { + String templateFileId = uploadFileService.uploadFileToEsign(templateRefId).get(0); + String contractFileId = uploadFileService.uploadFileToEsign(contractRefId).get(0); + String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.CONTRACT_COMPARE_GETURL; + Map params = new HashMap<>(); + params.put("standardFileId",templateFileId); + params.put("comparativeFileId",contractFileId); + String jsonString = JsonUtils.toJSONString(params); + EsignApiHeader esignApiHeader = EsignApiHeader.build().appId(configProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)); + esignApiHeader.token(tokenCacheManager.getToken()); + String respStr = HttpClient.httpPostRaw(url, jsonString, esignApiHeader.convert2Headers(), null); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); + log.info("调用获取合同比对链接接口返回值: " + JsonUtils.toJSONString(esignBaseResp)); + Map data = (Map) esignBaseResp.getData(); + String contractCompareUrl = data.get("contractCompareUrl"); + return contractCompareUrl; + } + + private String obtainOrgId() { + if(orgId == null) { + fillOrgId(); + } + return orgId; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/service/EsignFlowQueryService.java b/src/main/java/com/seeyon/apps/esign/service/EsignFlowQueryService.java new file mode 100644 index 0000000..0645893 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/EsignFlowQueryService.java @@ -0,0 +1,32 @@ +package com.seeyon.apps.esign.service; + +import com.seeyon.aicloud.common.JsonUtils; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignApiUrl; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.esignapi.EsignApiHeader; +import com.seeyon.apps.esign.po.esignapi.EsignBaseResp; +import com.seeyon.ctp.common.AppContext; +import com.seeyon.utils.http.HttpClient; + +import java.util.Map; + +public class EsignFlowQueryService { + + private TokenCacheManager tokenCacheManager = (TokenCacheManager) AppContext.getBean("tokenCacheManager"); + private EsignConfigProvider esignConfigProvider = EsignConfigProvider.getInstance(); + + public Integer queryFlow(String eFlowId){ + EsignApiHeader esignApiHeader = EsignApiHeader.build(); + esignApiHeader.token(tokenCacheManager.getToken()).appId(esignConfigProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)); + String url = esignConfigProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.SIGN_FLOW_QUERY.replace("{signFlowId}",eFlowId); + String respStr = HttpClient.httpGet(url, esignApiHeader.convert2Headers(), "UTF-8"); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); + Map dataMap = (Map) esignBaseResp.getData(); + if(!"0".equals(esignBaseResp.getCode() + "")) { + throw new RuntimeException("合同信息查询失败," + esignBaseResp.getMessage()); + } + Integer flowStatus = (Integer) dataMap.get("signFlowStatus"); + return flowStatus; //0 - 草稿 1 - 签署中 2 - 完成 3 - 撤销 5 - 过期(签署截至日期到期后触发)7 - 拒签 + } +} diff --git a/src/main/java/com/seeyon/apps/esign/service/EsignUploadFileService.java b/src/main/java/com/seeyon/apps/esign/service/EsignUploadFileService.java new file mode 100644 index 0000000..cc14353 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/EsignUploadFileService.java @@ -0,0 +1,187 @@ +package com.seeyon.apps.esign.service; + +import com.seeyon.aicloud.common.JsonUtils; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignApiUrl; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.esignapi.EsignApiHeader; +import com.seeyon.apps.esign.po.esignapi.EsignBaseResp; +import com.seeyon.apps.esign.po.upload.EsignFileUploadParams; +import com.seeyon.apps.esign.po.esignapi.EsignHttpResponse; +import com.seeyon.apps.esign.po.upload.GetUploadUrlResp; +import com.seeyon.apps.esign.utils.EsignHttpHelper; +import com.seeyon.apps.esign.utils.EsignRequestType; +import com.seeyon.apps.esign.utils.FileUtil; +import com.seeyon.apps.esign.utils.HttpClient; +import com.seeyon.ctp.common.AppContext; +import org.apache.commons.codec.binary.Base64; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class EsignUploadFileService { + + private EsignConfigProvider configProvider = EsignConfigProvider.getInstance(); + protected TokenCacheManager tokenCacheManager = (TokenCacheManager) AppContext.getBean("tokenCacheManager"); + + + public Object[] getDownloadFileInfo(String flowId) throws Exception { + String reqPath = EsignApiUrl.CONTRACT_DOWNLOAD_URL.replace("{signFlowId}", flowId); + String getDownloadUrl = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + reqPath; + EsignApiHeader esignApiHeader = EsignApiHeader.build().appId(configProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)).token(tokenCacheManager.getToken()); + String respStr = HttpClient.httpGet(getDownloadUrl, esignApiHeader.convert2Headers(), null); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); + if(esignBaseResp.getCode() != 0 || esignBaseResp.getData() == null){ + throw new RuntimeException("下载合同文件失败"); + } + Map data = (Map) esignBaseResp.getData(); + Object[] files = (Object[]) data.get("files"); + if(files.length <= 0) { + throw new RuntimeException("下载合同文件失败"); + } + return files; + } + + public GetUploadUrlResp getUploadFileUrl(String fileName,String fileMd5,Long fileSize,boolean convert2Pdf) throws Exception { + String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.GET_UPLOAD_FILE_URL; + GetUploadUrlResp resp = new GetUploadUrlResp(); + EsignApiHeader esignApiHeader = EsignApiHeader.build().appId(configProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)); + esignApiHeader.token(tokenCacheManager.getToken()); + EsignFileUploadParams params = new EsignFileUploadParams(); + params.setFileName(fileName); + params.setContentMd5(fileMd5); + params.setFileSize(fileSize); + params.setConvertToPDF(convert2Pdf); + params.setContentType("application/octet-stream"); + String respStr = HttpClient.httpPostRaw(url, JsonUtils.toJSONString(params),esignApiHeader.convert2Headers(), null); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); + Object data = esignBaseResp.getData(); + if (data != null) { + Map map = (Map) data; + resp.setFileUploadUrl((String) map.get("fileUploadUrl")); + resp.setFileId((String) map.get("fileId")); + return resp; + }else { + throw new RuntimeException("获取合同上传地址失败: " + esignBaseResp.getMessage()); + } + } + + + public List getSignPosition(String fileId,List keywords) { + try { + String reqPath = EsignApiUrl.SIGN_POSITION_URL.replace("{fileId}", fileId); + String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + reqPath; + EsignApiHeader esignApiHeader = EsignApiHeader.build().appId(configProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)); + Map params = new HashMap<>(); + params.put("keywords",keywords); + esignApiHeader.token(tokenCacheManager.getToken()); + String respStr = HttpClient.httpPostRaw(url,JsonUtils.toJSONString(params),esignApiHeader.convert2Headers(),null); + Map map = JsonUtils.parseObject(respStr, Map.class); + Map data = (Map) map.get("data"); + Object[] keywordPositions = (Object[]) data.get("keywordPositions"); + List resList = new ArrayList<>(); + for (Object position : keywordPositions) { + resList.add(position); + } + return resList; + }catch (Exception e){ + + } + return new ArrayList<>(); + } + + public List uploadFileToEsign(String refId) throws Exception { + String tempDir = System.getProperty("java.io.tmpdir"); + List fileIds = new ArrayList<>(); + File file = null; + FileUtil fileUtil = new FileUtil(); + try { + List paths = fileUtil.fieldFileDownload(Long.parseLong(refId), tempDir + File.separator + "oafile" + File.separator); + for (String path : paths) { + file = new File(path); + String contentMD5 = EsignUploadFileService.getFileContentMD5(path); + GetUploadUrlResp uploadResp = getUploadFileUrl(file.getName(), contentMD5, file.length(),false); + uploadFile(uploadResp.getFileUploadUrl(), contentMD5, file.getName(), path); + fileIds.add(uploadResp.getFileId()); + } + return fileIds; + }finally { + if(file != null) { + file.delete(); + } + } + } + + public static String getFileContentMD5(String filePath) throws Exception { + // 获取文件MD5的二进制数组(128位) + byte[] bytes = getFileMD5Bytes128(filePath); + // 对文件MD5的二进制数组进行base64编码 + return new String(Base64.encodeBase64String(bytes)); + } + + /*** + * 获取文件MD5-二进制数组(128位) + * + * @return + * @throws IOException + */ + public static byte[] getFileMD5Bytes128(String filePath) throws Exception { + byte[] md5Bytes = null; + File file = new File(filePath); + try (FileInputStream fis = new FileInputStream(file)) { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] buffer = new byte[1024]; + int length = -1; + while ((length = fis.read(buffer, 0, 1024)) != -1) { + md5.update(buffer, 0, length); + } + md5Bytes = md5.digest(); + } + return md5Bytes; + } + + + public String uploadFile(String uploadUrl,String contentMD5,String fileName,String filePath) throws Exception { +// EsignApiHeader esignApiHeader = EsignApiHeader.build(); +// esignApiHeader.setContentType("application/octet-stream"); +// esignApiHeader.setContentMd5(contentMD5); + EsignHttpResponse esignHttpResponse = EsignHttpHelper.doUploadHttp(uploadUrl, EsignRequestType.PUT, getBytes(filePath), contentMD5, "application/octet-stream", false); +// return HttpClient.uploadBinaryFile(uploadUrl,esignApiHeader.convert2Headers(),new FileInputStream(file),fileName); + return esignHttpResponse.getBody(); + } + + public static byte[] getBytes(String filePath) throws Exception { + File file = new File(filePath); + FileInputStream fis = null; + byte[] buffer = null; + try { + fis = new FileInputStream(file); + buffer = new byte[(int) file.length()]; + fis.read(buffer); + } catch (Exception e) { + Exception ex = new Exception("获取文件字节流失败", e); + ex.initCause(e); + throw ex; + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + Exception ex = new Exception("关闭文件字节流失败", e); + ex.initCause(e); + throw ex; + } + } + } + return buffer; + } + +} + diff --git a/src/main/java/com/seeyon/apps/esign/service/FlowFormSignParamBuildFactory.java b/src/main/java/com/seeyon/apps/esign/service/FlowFormSignParamBuildFactory.java new file mode 100644 index 0000000..dbf2893 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/FlowFormSignParamBuildFactory.java @@ -0,0 +1,102 @@ +package com.seeyon.apps.esign.service; + +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.param.FlowParamSource; +import com.seeyon.apps.esign.po.signer.DefaultSignerBuilder; +import com.seeyon.apps.esign.po.signer.Signer; +import com.seeyon.apps.ext.workflow.vo.FieldDataVo; +import com.seeyon.apps.ext.workflow.vo.FormDataVo; +import com.seeyon.ctp.common.AppContext; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FlowFormSignParamBuildFactory implements SignParamBuildFactory { + private EsignConfigProvider configProvider = EsignConfigProvider.getInstance(); + private EsignUploadFileService uploadFileService = (EsignUploadFileService) AppContext.getBean("esignUploadFileService"); + + @Override + public Map buildParam(SignParamSource source) throws Exception { + Map signParams = new HashMap<>(); + List fileIds = null; + if (source.isByFile()) { + String attachmentId = source.getSignFileRefId(); + if (attachmentId == null) throw new RuntimeException("合同文件不能为空"); + List> docs = buildDocs(attachmentId); + fileIds = new ArrayList<>(); + for (Map doc : docs) { + fileIds.add((String) doc.get("fileId")); + } + signParams.put("docs", docs); + } + Map signFlowConfig = buildSignFlowConfig(source.getContractTitle(),source.getCallbackUrl()); + DefaultSignerBuilder signerBuilder = new DefaultSignerBuilder(); + List keywords = new ArrayList<>(); + keywords.add(configProvider.getBizConfigByKey(EsignConfigConstants.aSignPositionKeyword)); + keywords.add(configProvider.getBizConfigByKey(EsignConfigConstants.bSignPositionKeyword)); + keywords.add(configProvider.getBizConfigByKey(EsignConfigConstants.lpSignPositionKeyword)); + List signers = signerBuilder.buildAll(fileIds,getKeywordPos(fileIds,keywords),source.getSignerParties()); + signParams.put("signFlowConfig", signFlowConfig); + signParams.put("signers", signers); + return signParams; + } + + @Override + public boolean support(SignParamSource source) { + return source instanceof FlowParamSource; + } + + private List getKeywordPos(List fileIds,List keywords){ + List pos = new ArrayList<>(); + for (String fileId : fileIds){ + List signPosition = uploadFileService.getSignPosition(fileId, keywords); + pos.addAll(signPosition); + } + return pos; + } + + /** + * 构建文档列表 + * @param attachmentId 如果 byFile=true,则上传并返回 fileId + */ + public List> buildDocs(String attachmentId) throws Exception { + List fileIds = uploadFileService.uploadFileToEsign(attachmentId); + List> docs = new ArrayList<>(); + for (String fileId : fileIds) { + Map docMap = new HashMap<>(); + docMap.put("fileId", fileId); + docs.add(docMap); + } + Thread.sleep(1000); // e签宝要求,可考虑异步 + return docs; + } + + public Map buildSignFlowConfig(String contractName,String callbackUrl) throws Exception { + if (contractName == null) throw new RuntimeException("合同名称不能为空"); + String signCallBackUrl = configProvider.getBizConfigByKey(EsignConfigConstants.OA_HOST) + callbackUrl; + Map config = new HashMap<>(); + config.put("signFlowTitle", contractName); + config.put("autoFinish", true); + config.put("notifyUrl", signCallBackUrl); + Map noticeConfig = new HashMap<>(); + noticeConfig.put("noticeTypes", "1"); + config.put("noticeConfig", noticeConfig); + Map signConfig = new HashMap<>(); + signConfig.put("showBatchDropSealButton", false); + config.put("signConfig", signConfig); + return config; + } + + private String getStringField(FormDataVo vo, String field) { + try { + FieldDataVo fieldData = vo.getFieldData(field); + return fieldData.getStringValue(); + }catch (Exception e) { + + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/service/NormalFormSignParamBuildFactory.java b/src/main/java/com/seeyon/apps/esign/service/NormalFormSignParamBuildFactory.java new file mode 100644 index 0000000..8c9a06c --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/NormalFormSignParamBuildFactory.java @@ -0,0 +1,88 @@ +package com.seeyon.apps.esign.service; + + +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.param.JsonParamSource; +import com.seeyon.apps.esign.po.signer.DefaultSignerBuilder; +import com.seeyon.apps.esign.po.signer.Signer; +import com.seeyon.apps.ext.workflow.vo.FieldDataVo; +import com.seeyon.apps.ext.workflow.vo.FormDataVo; +import com.seeyon.ctp.common.AppContext; + +import java.util.*; + +public class NormalFormSignParamBuildFactory implements SignParamBuildFactory { + + private EsignConfigProvider configProvider = EsignConfigProvider.getInstance(); + private EsignUploadFileService uploadFileService = (EsignUploadFileService) AppContext.getBean("esignUploadFileService"); + + @Override + public Map buildParam(SignParamSource source) throws Exception { + Map signParams = new HashMap<>(); + List fileIds = null; + if (source.isByFile()) { + String attachmentId = source.getSignFileRefId(); + if (attachmentId == null) throw new RuntimeException("合同文件不能为空"); + List> docs = buildDocs(attachmentId); + fileIds = new ArrayList<>(); + for (Map doc : docs) { + fileIds.add((String) doc.get("fileId")); + } + signParams.put("docs", docs); + } + Map signFlowConfig = buildSignFlowConfig(source.getContractTitle(),source.getCallbackUrl()); + DefaultSignerBuilder signerBuilder = new DefaultSignerBuilder(); + List signers = signerBuilder.buildAll(fileIds,null,source.getSignerParties()); + signParams.put("signFlowConfig", signFlowConfig); + signParams.put("signers", signers); + return signParams; + } + + @Override + public boolean support(SignParamSource source) { + return source instanceof JsonParamSource; + } + + /** + * 构建文档列表 + * @param attachmentId 如果 byFile=true,则上传并返回 fileId + */ + public List> buildDocs(String attachmentId) throws Exception { + List fileIds = uploadFileService.uploadFileToEsign(attachmentId); + List> docs = new ArrayList<>(); + for (String fileId : fileIds) { + Map docMap = new HashMap<>(); + docMap.put("fileId", fileId); + docs.add(docMap); + } + Thread.sleep(1000); // e签宝要求,可考虑异步 + return docs; + } + + public Map buildSignFlowConfig(String contractName,String callbackUrl) throws Exception { + if (contractName == null) throw new RuntimeException("合同名称不能为空"); + String signCallBackUrl = configProvider.getBizConfigByKey(EsignConfigConstants.OA_HOST) + callbackUrl; + Map config = new HashMap<>(); + config.put("signFlowTitle", contractName); + config.put("autoFinish", true); + config.put("notifyUrl", signCallBackUrl); + Map noticeConfig = new HashMap<>(); + noticeConfig.put("noticeTypes", "1"); + config.put("noticeConfig", noticeConfig); + Map signConfig = new HashMap<>(); + signConfig.put("showBatchDropSealButton", false); + config.put("signConfig", signConfig); + return config; + } + + private String getStringField(FormDataVo vo, String field) { + try { + FieldDataVo fieldData = vo.getFieldData(field); + return fieldData.getStringValue(); + }catch (Exception e) { + + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/service/SealService.java b/src/main/java/com/seeyon/apps/esign/service/SealService.java new file mode 100644 index 0000000..63e88f3 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/SealService.java @@ -0,0 +1,122 @@ +package com.seeyon.apps.esign.service; + +import cn.hutool.json.JSONObject; +import com.cedarsoftware.util.io.JsonObject; +import com.seeyon.aicloud.common.JsonUtils; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignApiUrl; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.esignapi.EsignApiHeader; +import com.seeyon.apps.esign.po.esignapi.EsignBaseResp; +import com.seeyon.apps.esign.po.seal.SealInfoVo; +import com.seeyon.apps.esign.utils.HttpClient; +import com.seeyon.ctp.common.AppContext; +import com.seeyon.ctp.services.ServiceException; +import com.seeyon.utils.form.*; +import com.seeyon.v3x.services.form.FormFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.*; + +public class SealService { + + private static final Log log = LogFactory.getLog(SealService.class); + private EsignConfigProvider configProvider = EsignConfigProvider.getInstance(); + private FormFactory formFactory; + + public FormFactory getFormFactory() { + if (formFactory == null) { + formFactory = (FormFactory) AppContext.getBean("formFactory"); + } + return formFactory; + } + + public void grantSeal(Map params) throws Exception { + JsonObject reqParams = new JsonObject(); + String sealId = getSealId((String) params.get("orgId"), (String) params.get("sealName")); + reqParams.put("sealId", sealId); + List authorizedPsnIds = new ArrayList<>(); + authorizedPsnIds.add((String) params.get("accountId")); + reqParams.put("authorizedPsnIds", authorizedPsnIds); + reqParams.put("sealRole", "SEAL_USER"); + reqParams.put("transactorPsnId", params.get("managerId")); + JSONObject sealAuthScope = new JSONObject(); + List templateIds = new ArrayList<>(); + templateIds.add("ALL"); + sealAuthScope.put("templateIds", templateIds); + reqParams.put("sealAuthScope", sealAuthScope); + EsignApiHeader esignApiHeader = EsignApiHeader.build(); + esignApiHeader.appId(configProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)) + .httpMethod("POST") + .heads("") + .pathAndParameters(EsignApiUrl.SEAL_GRANT_URL) + .contentMD5(reqParams.toString()) + .signature(configProvider.getBizConfigByKey(EsignConfigConstants.APP_SECRET)); + String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.SEAL_GRANT_URL; + HttpClient.httpPostRaw(url, reqParams.toString(), esignApiHeader.convert2Headers(), null); + } + + private String getSealId(String orgId, String sealName) throws Exception { + String host = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST); + int pageNo = 1; + int pageSize = 20; + EsignApiHeader esignApiHeader = EsignApiHeader.build(); + esignApiHeader.appId(configProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)) + .httpMethod("GET") + .heads("") + .pathAndParameters(EsignApiUrl.SEAL_QUERY_URL + "?" + HttpClient.sortQueryString("orgId=" + orgId + "&pageNo=" + pageNo + "&pageSize=" + pageSize)) + .contentMD5("") + .signature(configProvider.getBizConfigByKey(EsignConfigConstants.APP_SECRET)); + String url = host + EsignApiUrl.SEAL_QUERY_URL + "?orgId=" + orgId + "&pageNo=" + pageNo + "&pageSize=" + pageSize; + String respStr = HttpClient.httpGet(url, esignApiHeader.convert2Headers(), "UTF-8"); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); + Map data = (Map) esignBaseResp.getData(); + LinkedList seals = (LinkedList) data.get("seals"); + for (Object seal : seals) { + Map sealMap = (Map) seal; + if (sealMap.get("sealName").equals(sealName)) { + return (String) sealMap.get("sealId"); + } + } + return null; + } + + public void upsertOaSealDoc(SealInfoVo vo) { + String formNo = configProvider.getBizConfigByKey(EsignConfigConstants.sealInfoFormCode); + List conditions = new ArrayList<>(); + conditions.add(FormWhereCondition.build().display("印章ID").value(vo.getSealId())); + FormColumn formColumn = null; + TableContext master = null; + try { + master = FormTableExecutor.master(formNo); + formColumn = FormTableExecutor.queryOne(master,conditions,true); + } catch (Exception e) { + log.error("查询印章信息失败: " + e.getMessage(), e); + } + if (formColumn == null) { + if ("已启用".equals(vo.getStatusDescription())) { + Map params = new HashMap<>(); + params.put("印章ID", vo.getSealId()); + params.put("印章名称", vo.getSealName()); + params.put("印章类型", vo.getSealBizTypeDescription()); + params.put("印章状态", vo.getStatusDescription()); +// params.put("印章图片地址",vo.getSealImageDownloadUrl()); + + try { + FormSaveUtil.formSave(configProvider.getBizConfigByKey(EsignConfigConstants.formLoginName),configProvider.getBizConfigByKey(EsignConfigConstants.sealInfoFormCode),formFactory,params,null); + } catch (ServiceException e) { + log.error(vo.getSealName() + "信息写入OA档案失败," + e.getMessage(), e); + } + } + } else { + if (!"已启用".equals(vo.getStatusDescription()) && master != null) { + try { + FormTableExecutor.delete(master,conditions); + } catch (Exception e) { + log.error("删除停用印章失败" + e.getMessage(), e); + } + } + } + } +} diff --git a/src/main/java/com/seeyon/apps/esign/service/SignLinkService.java b/src/main/java/com/seeyon/apps/esign/service/SignLinkService.java new file mode 100644 index 0000000..464a394 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/SignLinkService.java @@ -0,0 +1,150 @@ +package com.seeyon.apps.esign.service; + +import com.seeyon.aicloud.common.JsonUtils; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignApiUrl; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.esignapi.EsignApiHeader; +import com.seeyon.apps.esign.po.esignapi.EsignBaseResp; +import com.seeyon.apps.ext.workflow.vo.FormDataVo; +import com.seeyon.cap4.form.bean.FormDataMasterBean; +import com.seeyon.ctp.common.AppContext; +import com.seeyon.ctp.util.JDBCAgent; +import com.seeyon.utils.http.HttpClient; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import www.seeyon.com.utils.UUIDUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class SignLinkService { + + private static final Log log = LogFactory.getLog(SignLinkService.class); + private EsignConfigProvider esignConfigProvider = (EsignConfigProvider) AppContext.getBean("esignConfigProvider"); + private TokenCacheManager tokenCacheManager = (TokenCacheManager) AppContext.getBean("tokenCacheManager"); + + public void saveGetLinkParam(FormDataVo formDataVo, FormDataMasterBean formDataMasterBean,String eFlowId) { + try { + String bSignerType = formDataVo.getFieldData("乙方签署类型").getStringValue(); + String psnAccount = null; + if(!"组织".equals(bSignerType) ) { + psnAccount = formDataVo.getFieldData("乙方电话").getStringValue(); + }else { + psnAccount = formDataVo.getFieldData("乙方经办人手机号").getStringValue(); + } + String contractNo = formDataVo.getFieldData("合同编号").getStringValue(); + Map param = new HashMap<>(); + Map operator = new HashMap<>(); + operator.put("psnAccount",psnAccount); + param.put("signFlowId",eFlowId); + param.put("operator",operator); + String paramStr = JsonUtils.toJSONString(param); + saveDb(paramStr,eFlowId,contractNo); + }catch (Exception e) { + log.error(e.getMessage(),e); + } + + } + + public void del(String eFlowId) { + String sqlDel = "delete FROM SIGN_LINK_PARAM where eFlowId = ?"; + JDBCAgent jdbcAgent = new JDBCAgent(); + try { + List param = new ArrayList<>(); + param.add(eFlowId); + jdbcAgent.execute(sqlDel,param); + }catch (Exception e) { + log.error(e.getMessage(),e); + }finally { + jdbcAgent.close(); + } + } + + private void saveDb(String jsonStr,String eFlowId,String contractNo) { + String sqlInsert = "insert into SIGN_LINK_PARAM (id,param,eFlowId,contractNo) values (?,?,?,?)"; + String sqlQuery = "select * FROM SIGN_LINK_PARAM where contractNo = ?"; + String sqlUpdate = "update SIGN_LINK_PARAM set param = ?,eFlowId = ? where id = ?"; + JDBCAgent jdbcAgent = new JDBCAgent(); + try { + List param = new ArrayList<>(); + param.add(contractNo); + jdbcAgent.execute(sqlQuery,param); + List list = jdbcAgent.resultSetToList(); + long id = UUIDUtil.getUUIDLong(); + if(list.size() == 0) { + param.clear(); + param.add(id); + param.add(jsonStr); + param.add(eFlowId); + param.add(contractNo); + jdbcAgent.execute(sqlInsert,param); + }else { + param.clear(); + Map tempMap = (Map) list.get(0); + param.add(jsonStr); + param.add(eFlowId); + param.add(tempMap.get("id")); + jdbcAgent.execute(sqlUpdate,param); + } + }catch (Exception e) { + log.error(e.getMessage(),e); + }finally { + jdbcAgent.close(); + } + } + + public String getSignLink(String eFlowId) { + String sqlQuery = "select * from SIGN_LINK_PARAM where eFlowId = ?"; + JDBCAgent jdbcAgent = new JDBCAgent(); + try { + List param = new ArrayList<>(); + param.add(eFlowId); + jdbcAgent.execute(sqlQuery,param); + List list = jdbcAgent.resultSetToList(); + if(list.size() > 0) { + Map tempMap = (Map) list.get(0); + String paramStr = (String)tempMap.get("param"); + if(!checkContractVaild(eFlowId)) { + throw new RuntimeException("当前合同无效"); + } + String url = esignConfigProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.SIGN_LINK_GET_URL.replace("{signFlowId}",eFlowId); + EsignApiHeader esignApiHeader = EsignApiHeader.build(); + esignApiHeader.token(tokenCacheManager.getToken()).appId(esignConfigProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)); + String respStr = HttpClient.httpPostRaw(url, paramStr,esignApiHeader.convert2Headers(), "UTF-8"); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); + Map dataMap = (Map) esignBaseResp.getData(); + if(!"0".equals(esignBaseResp.getCode() + "")) { + log.error("合同链接获取失败," + esignBaseResp.getMessage()); + }else { + return (String) dataMap.get("url"); + } + } + + }catch (Exception e) { + log.error("合同链接获取失败:"+e.getMessage(),e); + }finally { + jdbcAgent.close(); + } + return null; + } + + private boolean checkContractVaild(String eFlowId) { + EsignApiHeader esignApiHeader = EsignApiHeader.build(); + esignApiHeader.token(tokenCacheManager.getToken()).appId(esignConfigProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)); + String url = esignConfigProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.SIGN_FLOW_QUERY.replace("{signFlowId}",eFlowId); + String respStr = HttpClient.httpGet(url, esignApiHeader.convert2Headers(), "UTF-8"); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); + Map dataMap = (Map) esignBaseResp.getData(); + if(!"0".equals(esignBaseResp.getCode() + "")) { + log.error("合同信息查询失败," + esignBaseResp.getMessage()); + return false; + } + Integer flowStatus = (Integer) dataMap.get("signFlowStatus"); + return flowStatus == 1; + } + +} diff --git a/src/main/java/com/seeyon/apps/esign/service/SignParamBuildFactory.java b/src/main/java/com/seeyon/apps/esign/service/SignParamBuildFactory.java new file mode 100644 index 0000000..d4e9cc0 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/SignParamBuildFactory.java @@ -0,0 +1,15 @@ +package com.seeyon.apps.esign.service; + +import java.util.Map; + +public interface SignParamBuildFactory { + + /** + * 构建签署 + * @param source + * @return + */ + Map buildParam(SignParamSource source) throws Exception; + + boolean support(SignParamSource source); +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/service/SignParamSource.java b/src/main/java/com/seeyon/apps/esign/service/SignParamSource.java new file mode 100644 index 0000000..d4e2b62 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/SignParamSource.java @@ -0,0 +1,51 @@ +package com.seeyon.apps.esign.service; + +import com.seeyon.apps.esign.po.signer.SignParty; + +import java.util.List; + +public interface SignParamSource { + + /* ================= 文档相关 ================= */ + + /** 合同标题 */ + String getContractTitle(); + + /** 是否通过附件生成文件 */ + boolean isByFile(); + + /* ================= 签署流 / 回调 ================= */ + + /** 业务唯一标识(回调使用) */ + String getBizFormId(); + + /** 业务表名 */ + String getTableName(); + + String getSignFileRefId(); + + /** 盖章后附件字段 */ + String getSignedFileField(); + + /** 签署状态字段 */ + String getSignStatusField(); + + /** 签署状态枚举值 */ + Long getSignStatusEnumId(); + + boolean autoSign(); + + /* ================= 签署方编排(核心) ================= */ + + String getCallbackUrl(); + /** + * 所有签署方(顺序、类型、自动签等都在这里) + */ + List getSignerParties(); + + /** 通知类型 */ + default String noticeTypes() { + return "1"; + } + +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/service/TokenCacheManager.java b/src/main/java/com/seeyon/apps/esign/service/TokenCacheManager.java new file mode 100644 index 0000000..9c0e88e --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/service/TokenCacheManager.java @@ -0,0 +1,98 @@ +package com.seeyon.apps.esign.service; + +import com.seeyon.aicloud.common.JsonUtils; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignApiUrl; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import com.seeyon.apps.esign.po.esignapi.EsignApiHeader; +import com.seeyon.apps.esign.po.esignapi.EsignBaseResp; +import com.seeyon.apps.esign.po.esignapi.EsignToken; +import com.seeyon.apps.esign.utils.HttpClient; + +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + + +public class TokenCacheManager { + + private EsignConfigProvider esignConfigProvider = EsignConfigProvider.getInstance(); + private final AtomicReference tokenRef = new AtomicReference<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private static final long CHECK_INTERVAL = 2 * 60 * 60 * 1000; // 120分钟检查一次 + + public TokenCacheManager() { + // 启动定期检查任务 + scheduler.scheduleAtFixedRate(this::checkTokenExpiry, + CHECK_INTERVAL, CHECK_INTERVAL, TimeUnit.MILLISECONDS); + } + + private boolean checkExpired(String token) { + EsignApiHeader esignApiHeader = EsignApiHeader.build(); + try { + esignApiHeader.token(token).appId(esignConfigProvider.getBizConfigByKey(EsignConfigConstants.APP_ID)); + } catch (Exception e) { + e.printStackTrace(); + } + //获取orgId + String queryStr = EsignApiUrl.QUERY_ORGINFO + "?orgName=" + esignConfigProvider.getBizConfigByKey(EsignConfigConstants.UNITNAME); + String reqUrl = esignConfigProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + queryStr; + String orgInfoRespStr = HttpClient.httpGet(reqUrl, esignApiHeader.convert2Headers(), "UTF-8"); + EsignBaseResp orgInfoResp = JsonUtils.parseObject(orgInfoRespStr, EsignBaseResp.class); + if(orgInfoResp.getCode() == 401) { + return true; + } + return false; + } + + public String getToken() { + EsignToken current = tokenRef.get(); + if (current != null && !checkExpired(current.getTokenStr())) { + return current.getTokenStr(); + } + return refreshToken(); + } + + private String refreshToken() { + EsignToken newToken = fetchNewToken(); + tokenRef.set(newToken); + return newToken.getTokenStr(); + } + + private void checkTokenExpiry() { + EsignToken current = tokenRef.get(); + if (current != null && isTokenExpired(current)) { + refreshToken(); + } + } + + private boolean isTokenExpired(EsignToken token) { + return System.currentTimeMillis() >= token.getTtl(); + } + + private EsignToken fetchNewToken() { + // 实际获取token的逻辑 + String queryStr = "?appId=" + esignConfigProvider.getBizConfigByKey(EsignConfigConstants.APP_ID) + "&secret=" + esignConfigProvider.getBizConfigByKey(EsignConfigConstants.APP_SECRET) + "&grantType=client_credentials"; + String url = esignConfigProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.TOKEN_GET_URL + queryStr; + String respStr = HttpClient.httpGet(url, null, null); + EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); + if(esignBaseResp.getCode() != 0) { + throw new RuntimeException("获取e签宝token失败"); + } + Map dataMap = (Map) esignBaseResp.getData(); + String token = (String) dataMap.get("token"); + Long ttl = Long.parseLong((String) dataMap.get("expiresIn")) ; + String refreshToken = (String) dataMap.get("refreshToken"); + EsignToken esignToken = new EsignToken(); + esignToken.setTokenStr(token); + esignToken.setTtl(ttl); + esignToken.setRefreshToken(refreshToken); + return esignToken; + } + + public void shutdown() { + scheduler.shutdown(); + } +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/utils/EnHancedMap.java b/src/main/java/com/seeyon/apps/esign/utils/EnHancedMap.java new file mode 100644 index 0000000..d0cd7ef --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/utils/EnHancedMap.java @@ -0,0 +1,17 @@ +package com.seeyon.apps.esign.utils; + +import java.util.Map; + +public class EnHancedMap { + + private Map map; + + public EnHancedMap of(String key, Object value) { + map.put(key, value); + return this; + } + + public Map getMap() { + return map; + } +} diff --git a/src/main/java/com/seeyon/apps/esign/utils/EsignHttpCfgHelper.java b/src/main/java/com/seeyon/apps/esign/utils/EsignHttpCfgHelper.java new file mode 100644 index 0000000..232b29b --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/utils/EsignHttpCfgHelper.java @@ -0,0 +1,471 @@ +package com.seeyon.apps.esign.utils; +import com.seeyon.apps.esign.po.esignapi.EsignException; +import com.seeyon.apps.esign.po.esignapi.EsignHttpResponse; +import org.apache.http.*; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.LayeredConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.*; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.UnknownHostException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * @description Http请求 辅助类 + * @author 澄泓 + * @since JDK1.7 + */ +public class EsignHttpCfgHelper { + + private static Logger LOGGER = LoggerFactory.getLogger(EsignHttpCfgHelper.class); + /** + * 超时时间,默认15000毫秒 + */ + private static int MAX_TIMEOUT = 15000; + /** + * 请求池最大连接数,默认100个 + */ + private static int MAX_TOTAL=100; + /** + * 单域名最大的连接数,默认50个 + */ + private static int ROUTE_MAX_TOTAL=50; + /** + * 请求失败重试次数,默认3次 + */ + private static int MAX_RETRY = 3; + /** + * 是否需要域名校验,默认不需要校验 + */ + private static boolean SSL_VERIFY=false; + + /** + * 正向代理IP + */ + private static String PROXY_IP; + /** + * 正向代理端口,默认8888 + */ + private static int PROXY_PORT=8888; + /** + * 代理协议,默认http + */ + private static String PROXY_AGREEMENT="http"; + + /** + * 是否开启代理,默认false + */ + private static boolean OPEN_PROXY=false; + + /** + * 代理服务器用户名 + */ + private static String PROXY_USERNAME=""; + + /** + * 代理服务器密码 + */ + private static String PROXY_PASSWORD=""; + + + private static PoolingHttpClientConnectionManager connMgr; //连接池 + private static HttpRequestRetryHandler retryHandler; //重试机制 + + private static CloseableHttpClient httpClient=null; + + public static int getMaxTimeout() { + return MAX_TIMEOUT; + } + + public static void setMaxTimeout(int maxTimeout) { + MAX_TIMEOUT = maxTimeout; + } + + public static int getMaxTotal() { + return MAX_TOTAL; + } + + public static void setMaxTotal(int maxTotal) { + MAX_TOTAL = maxTotal; + } + + public static int getRouteMaxTotal() { + return ROUTE_MAX_TOTAL; + } + + public static void setRouteMaxTotal(int routeMaxTotal) { + ROUTE_MAX_TOTAL = routeMaxTotal; + } + + public static int getMaxRetry() { + return MAX_RETRY; + } + + public static void setMaxRetry(int maxRetry) { + MAX_RETRY = maxRetry; + } + + public static boolean isSslVerify() { + return SSL_VERIFY; + } + + public static void setSslVerify(boolean sslVerify) { + SSL_VERIFY = sslVerify; + } + + public static String getProxyIp() { + return PROXY_IP; + } + + public static void setProxyIp(String proxyIp) { + PROXY_IP = proxyIp; + } + + public static int getProxyPort() { + return PROXY_PORT; + } + + public static void setProxyPort(int proxyPort) { + PROXY_PORT = proxyPort; + } + + public static String getProxyAgreement() { + return PROXY_AGREEMENT; + } + + public static void setProxyAgreement(String proxyAgreement) { + PROXY_AGREEMENT = proxyAgreement; + } + + public static boolean getOpenProxy() { + return OPEN_PROXY; + } + + public static void setOpenProxy(boolean openProxy) { + OPEN_PROXY = openProxy; + } + + public static String getProxyUsername() { + return PROXY_USERNAME; + } + + public static void setProxyUserame(String proxyUsername) { + PROXY_USERNAME = proxyUsername; + } + + public static String getProxyPassword() { + return PROXY_PASSWORD; + } + + public static void setProxyPassword(String proxyPassword) { + PROXY_PASSWORD = proxyPassword; + } + + + + + /** + * 不允许外部创建实例 + */ + private EsignHttpCfgHelper() { + } + + //------------------------------公有方法start-------------------------------------------- + + + /** + * @description 发起HTTP / HTTPS 请求 + * + * @param reqType + * {@link EsignRequestType} 请求类型 GET、 POST 、 DELETE 、 PUT + * @param httpUrl + * {@link String} 请求目标地址 + * @param headers + * {@link Map} 请求头 + * @param param + * {@link Object} 参数 + * @return + * @throws EsignException + * @author 澄泓 + */ + public static EsignHttpResponse sendHttp(EsignRequestType reqType, String httpUrl, Map headers, Object param, boolean debug) + throws EsignException { + HttpRequestBase reqBase=null; + if(httpUrl.startsWith("http")){ + reqBase=reqType.getHttpType(httpUrl); + }else{ + throw new EsignException("请求url地址格式错误"); + } + if(debug){ + LOGGER.info("请求头:{}",headers+"\n"); + LOGGER.info("请求参数\n{}", param+"\n"); + LOGGER.info("请求地址\n:{}\n请求方式\n:{}",reqBase.getURI(),reqType+"\n"); + } + //请求方法不是GET或者DELETE时传入body体,否则不传入。 + String[] methods = {"DELETE", "GET"}; + if(param instanceof String&&Arrays.binarySearch(methods, reqType.name())<0){//POST或者PUT请求 + ((HttpEntityEnclosingRequest) reqBase).setEntity( + new StringEntity(String.valueOf(param), ContentType.create("application/json", "UTF-8"))); + } + //参数时字节流数组 + else if(param instanceof byte[]) { + reqBase=reqType.getHttpType(httpUrl); + byte[] paramBytes = (byte[])param; + ((HttpEntityEnclosingRequest) reqBase).setEntity(new ByteArrayEntity(paramBytes)); + } + //参数是form表单时 + else if(param instanceof List){ + ((HttpEntityEnclosingRequest) reqBase).setEntity(new UrlEncodedFormEntity((Iterable) param)); + } + httpClient = getHttpClient(); + config(reqBase); + + //设置请求头 + if(headers != null &&headers.size()>0) { + for(Map.Entry entry :headers.entrySet()) { + reqBase.setHeader(entry.getKey(), entry.getValue()); + } + } + //响应对象 + CloseableHttpResponse res = null; + //响应内容 + String resCtx = null; + int status; + EsignHttpResponse esignHttpResponse = new EsignHttpResponse(); + try { + //执行请求 + res = httpClient.execute(reqBase); + status=res.getStatusLine().getStatusCode(); + + //获取请求响应对象和响应entity + HttpEntity httpEntity = res.getEntity(); + if(httpEntity != null) { + resCtx = EntityUtils.toString(httpEntity,"utf-8"); + } + if(debug) { + LOGGER.info("响应\n{}", resCtx + "\n"); + LOGGER.info("----------------------------end------------------------"); + } + } catch (NoHttpResponseException e) { + throw new EsignException("服务器丢失了",e); + } catch (SSLHandshakeException e){ + String msg = MessageFormat.format("SSL握手异常", e); + EsignException ex = new EsignException(msg, e); + throw ex; + } catch (UnknownHostException e){ + EsignException ex = new EsignException("服务器找不到", e); + ex.initCause(e); + throw ex; + } catch(ConnectTimeoutException e){ + EsignException ex = new EsignException("连接超时", e); + ex.initCause(e); + throw ex; + } catch(SSLException e){ + EsignException ex = new EsignException("SSL异常",e); + ex.initCause(e); + throw ex; + } catch (ClientProtocolException e) { + EsignException ex = new EsignException("请求头异常",e); + ex.initCause(e); + throw ex; + } catch (IOException e) { + EsignException ex = new EsignException("网络请求失败",e); + ex.initCause(e); + throw ex; + } finally { + if(res != null) { + try { + res.close(); + } catch (IOException e) { + EsignException ex = new EsignException("--->>关闭请求响应失败",e); + ex.initCause(e); + throw ex; + } + } + } + esignHttpResponse.setStatus(status); + esignHttpResponse.setBody(resCtx); + return esignHttpResponse; + } + //------------------------------公有方法end---------------------------------------------- + + //------------------------------私有方法start-------------------------------------------- + + /** + * @description 请求头和超时时间配置 + * + * @param httpReqBase + * @author 澄泓 + */ + private static void config(HttpRequestBase httpReqBase) { + // 配置请求的超时设置 + RequestConfig.Builder builder = RequestConfig.custom() + .setConnectionRequestTimeout(MAX_TIMEOUT) + .setConnectTimeout(MAX_TIMEOUT) + .setSocketTimeout(MAX_TIMEOUT); + if(OPEN_PROXY){ + HttpHost proxy=new HttpHost(PROXY_IP,PROXY_PORT,PROXY_AGREEMENT); + builder.setProxy(proxy); + } + RequestConfig requestConfig = builder.build(); + httpReqBase.setConfig(requestConfig); + } + + /** + * @description 连接池配置 + * + * @return + * @author 澄泓 + */ + private static void cfgPoolMgr() throws EsignException { + ConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory(); + LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory.getSocketFactory(); + if(!SSL_VERIFY){ + sslsf=sslConnectionSocketFactory(); + } + + Registry registry = RegistryBuilder.create() + .register("http", plainsf) + .register("https", sslsf) + .build(); + + //连接池管理器 + connMgr = new PoolingHttpClientConnectionManager(registry); + //请求池最大连接数 + connMgr.setMaxTotal(MAX_TOTAL); + //但域名最大的连接数 + connMgr.setDefaultMaxPerRoute(ROUTE_MAX_TOTAL); + } + + + + + /** + * @description 设置重试机制 + * + * @author 澄泓 + */ + private static void cfgRetryHandler() { + retryHandler = new HttpRequestRetryHandler() { + + @Override + public boolean retryRequest(IOException e, int excCount, HttpContext ctx) { + //超过最大重试次数,就放弃 + if(excCount > MAX_RETRY) { + return false; + } + //服务器丢掉了链接,就重试 + if(e instanceof NoHttpResponseException) { + return true; + } + //不重试SSL握手异常 + if(e instanceof SSLHandshakeException) { + return false; + } + //中断 + if(e instanceof InterruptedIOException) { + return false; + } + //目标服务器不可达 + if(e instanceof UnknownHostException) { + return false; + } + //连接超时 + //SSL异常 + if(e instanceof SSLException) { + return false; + } + + HttpClientContext clientCtx = HttpClientContext.adapt(ctx); + HttpRequest req = clientCtx.getRequest(); + //如果是幂等请求,就再次尝试 + if(!(req instanceof HttpEntityEnclosingRequest)) { + return true; + } + return false; + } + }; + } + + /** + * 忽略域名校验 + */ + private static SSLConnectionSocketFactory sslConnectionSocketFactory() throws EsignException { + try { + SSLContext ctx = SSLContext.getInstance("TLS"); // 创建一个上下文(此处指定的协议类型似乎不是重点) + X509TrustManager tm = new X509TrustManager() { // 创建一个跳过SSL证书的策略 + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { + } + }; + ctx.init(null, new TrustManager[] { tm }, null); // 使用上面的策略初始化上下文 + return new SSLConnectionSocketFactory(ctx, new String[] { "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2" }, null, NoopHostnameVerifier.INSTANCE); + }catch (Exception e){ + EsignException ex = new EsignException("忽略域名校验失败",e); + ex.initCause(e); + throw ex; + } + + } + + /** + * @description 获取单例HttpClient + * + * @return + * @author 澄泓 + */ + private static synchronized CloseableHttpClient getHttpClient() throws EsignException { + if(httpClient==null) { + CredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials(new AuthScope(PROXY_IP,PROXY_PORT),new UsernamePasswordCredentials(PROXY_USERNAME, PROXY_PASSWORD)); + cfgPoolMgr(); + cfgRetryHandler(); + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); + httpClient = httpClientBuilder.setDefaultCredentialsProvider(credsProvider).setConnectionManager(connMgr).setRetryHandler(retryHandler).build(); + } + return httpClient; + + } + //------------------------------私有方法end---------------------------------------------- + + +} diff --git a/src/main/java/com/seeyon/apps/esign/utils/EsignHttpHelper.java b/src/main/java/com/seeyon/apps/esign/utils/EsignHttpHelper.java new file mode 100644 index 0000000..37573ed --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/utils/EsignHttpHelper.java @@ -0,0 +1,83 @@ +package com.seeyon.apps.esign.utils; + +import com.seeyon.apps.esign.po.esignapi.EsignException; +import com.seeyon.apps.esign.po.esignapi.EsignHttpResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +/** + * @description Http 请求 辅助类 + * @author 澄泓 + * @since JDK1.7 + */ +public class EsignHttpHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(EsignHttpHelper.class); + + /** + * 不允许外部创建实例 + */ + private EsignHttpHelper() { + + } + + /** + * @description 发送常规HTTP 请求 + * + * @param reqType 请求方式 + * @param url 请求路径 + * @param paramStr 请求参数 + * @return + * @throws EsignException + * @author 澄泓 + */ + public static EsignHttpResponse doCommHttp(String host, String url, EsignRequestType reqType, Object paramStr , Map httpHeader, boolean debug) throws EsignException { + return EsignHttpCfgHelper.sendHttp(reqType, host+url,httpHeader, paramStr, debug); + } + + + /** + * @description 发送文件流上传 HTTP 请求 + * + * @param reqType 请求方式 + * @param uploadUrl 请求路径 + * @param param 请求参数 + * @param fileContentMd5 文件fileContentMd5 + * @param contentType 文件MIME类型 + * @return + * @author 澄泓 + */ + public static EsignHttpResponse doUploadHttp( String uploadUrl,EsignRequestType reqType,byte[] param, String fileContentMd5, + String contentType, boolean debug) throws EsignException { + Map uploadHeader = buildUploadHeader(fileContentMd5, contentType); + if(debug){ + LOGGER.info("----------------------------start------------------------"); + LOGGER.info("fileContentMd5:{}",fileContentMd5); + LOGGER.info("contentType:{}",contentType); + } + return EsignHttpCfgHelper.sendHttp(reqType,uploadUrl, uploadHeader, param,debug); + } + + + + + /** + * @description 创建文件流上传 请求头 + * + * @param fileContentMd5 + * @param contentType + * @return + * @author 澄泓 + */ + public static Map buildUploadHeader(String fileContentMd5, String contentType) { + Map header = new HashMap<>(); + header.put("Content-MD5", fileContentMd5); + header.put("Content-Type", contentType); + + return header; + } + + // ------------------------------私有方法end---------------------------------------------- +} diff --git a/src/main/java/com/seeyon/apps/esign/utils/EsignRequestType.java b/src/main/java/com/seeyon/apps/esign/utils/EsignRequestType.java new file mode 100644 index 0000000..81de093 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/utils/EsignRequestType.java @@ -0,0 +1,38 @@ +package com.seeyon.apps.esign.utils; + +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; + +public enum EsignRequestType { + + POST{ + @Override + public HttpRequestBase getHttpType(String url) { + return new HttpPost(url); + } + }, + GET{ + @Override + public HttpRequestBase getHttpType(String url) { + return new HttpGet(url); + } + }, + DELETE{ + @Override + public HttpRequestBase getHttpType(String url) { + return new HttpDelete(url); + } + }, + PUT{ + @Override + public HttpRequestBase getHttpType(String url) { + return new HttpPut(url); + } + }, + ; + + public abstract HttpRequestBase getHttpType(String url); +} diff --git a/src/main/java/com/seeyon/apps/esign/utils/FileUtil.java b/src/main/java/com/seeyon/apps/esign/utils/FileUtil.java new file mode 100644 index 0000000..ff1b528 --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/utils/FileUtil.java @@ -0,0 +1,516 @@ +package com.seeyon.apps.esign.utils; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.seeyon.ctp.common.AppContext; +import com.seeyon.ctp.common.SystemEnvironment; +import com.seeyon.ctp.common.exceptions.BusinessException; +import com.seeyon.ctp.common.filemanager.manager.AttachmentManager; +import com.seeyon.ctp.common.filemanager.manager.FileManager; +import com.seeyon.ctp.common.po.filemanager.Attachment; +import com.seeyon.ctp.organization.bo.V3xOrgAccount; +import com.seeyon.ctp.organization.bo.V3xOrgMember; +import com.seeyon.ctp.organization.manager.OrgManager; +import com.seeyon.ctp.services.FileUploadExporter; +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import www.seeyon.com.utils.StringUtil; +import www.seeyon.com.utils.UUIDUtil; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class FileUtil { + + private static final long MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB + private static final String TEMP_DIR = System.getProperty("java.io.tmpdir") + File.separator + "seeyontempfile"; + private static final String DEFAULT_FILENAME_PREFIX = "没有文件名_"; + private static final String CATEGORY_CODE = "66"; + private FileManager fileManager; + + public void setFileManager(FileManager fileManager) { + this.fileManager = fileManager; + } + + public FileManager getFileManager() { + if (fileManager == null) { + fileManager = (FileManager) AppContext.getBean("fileManager"); + } + return fileManager; + } + + private AttachmentManager attachmentManager; + + public void setAttachmentManager(AttachmentManager attachmentManager) { + this.attachmentManager = attachmentManager; + } + + public AttachmentManager getAttachmentManager() { + if (attachmentManager == null) { + attachmentManager = (AttachmentManager) AppContext.getBean("attachmentManager"); + } + return attachmentManager; + } + + public void isFilePath(String filePath) { + File directory = new File(filePath); + if (!directory.exists()) { + directory.mkdirs(); + } + + } + + + /** + * 上传附件 + * + * @param fileNames + * @throws Exception + * @throws NumberFormatException + */ + public List fileUpload(List fileNames, String summaryId, Long memberId, Long accountId) throws NumberFormatException, Exception { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + FileUploadExporter fileUpload = new FileUploadExporter(); + List attachments = new ArrayList(); + File f = null; + long l = UUIDUtil.getUUIDLong(); + for (int i = 0; i < fileNames.size(); i++) { + String fileName = fileNames.get(i); + if (StringUtils.isNotEmpty(fileName)) { + f = new File(fileName); + if (!f.exists()) { + return null; + } + if (f.length() > 102400000) { + return null; + } + } + String[] strs = new String[]{f.getAbsolutePath()}; + String s = fileUpload.processUpload(strs); + Map map = new HashMap(); + String attachName = f.getName(); + String[] suffixNames = attachName.split("\\."); + map.put("type", "0"); + map.put("fileUrl", s); + map.put("mimeType", "application/" + suffixNames[suffixNames.length - 1].toLowerCase()); + map.put("size", f.length() + ""); + map.put("subReference", l + ""); + map.put("category", "66"); + map.put("createdate", sdf.format(new Date())); + map.put("filename", f.getName()); + map.put("reference", summaryId); + Attachment attachment = new Attachment(map); + attachments.add(attachment); + } + String str = getAttachmentManager().create(attachments, memberId, accountId); + return attachments; + } + + + public List fieldFileDownload(Long refId, String path) throws BusinessException { +// 判断路径是否存在 + File file = new File(path); + if (!file.exists()) { + file.mkdirs(); + } + List fileUrls = getAttachmentManager().getBySubReference(refId); + System.out.println(fileUrls.size()); + List filepaths = new ArrayList<>(); + for (Long fileUrl : fileUrls) { + Attachment attachment = getAttachmentManager().getAttachmentByFileURL(fileUrl); + InputStream inputStream = getFileManager().getFileInputStream(attachment.getFileUrl()); + String filepath = path + attachment.getFilename(); + try (OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(filepath))) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + outputStream.flush(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + filepaths.add(filepath); + } + return filepaths; + } + + + public JSONArray getFileContent(String filePath) throws IOException { + JSONArray jsonArray = new JSONArray(); + String houzhui = filePath.substring(filePath.lastIndexOf(".") + 1); + // 创建文件输入流 + FileInputStream file = new FileInputStream(new File(filePath)); + // 创建工作簿对象 + Workbook workbook; + System.out.println("当前文件为"+houzhui+"后缀"); + if ("xlsx".equals(houzhui) || "XLSX".equals(houzhui)) { + workbook = new XSSFWorkbook(file); + } else { + workbook = new HSSFWorkbook(file); + } + // 获取第一个工作表 + Sheet sheet = workbook.getSheetAt(0); + // 迭代行 + Iterator rowIterator = sheet.iterator(); +// 根据行跳过设置 + row : while (rowIterator.hasNext()) { + Row row = rowIterator.next(); + if (row.getRowNum() < 2) { + continue; + } + int num = 0; + int bj = 0; + JSONObject jsonObject = new JSONObject(); + JSONArray SPS = new JSONArray(); + JSONObject SP = new JSONObject(); + boolean isddh = false; + + // 迭代单元格 + Iterator cellIterator = row.cellIterator(); + cell : while (cellIterator.hasNext()) { +// 获取单元格对象 + Cell cell = cellIterator.next(); +// 单元格索引为1的数据(订单号) + if (cell.getColumnIndex() == 1) { + String ddh = ""; + switch (cell.getCellType()) { + case STRING: + ddh = cell.getStringCellValue(); + break; + case NUMERIC: + ddh = cell.getNumericCellValue()+""; + break; + default: + ddh = ""; + } +// 如果当前订单号为空,则跳过当前行数据 +// 查询当前订单号是否已经存在 + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject json = jsonArray.getJSONObject(i); + if (ddh.equals(json.getString("DDH"))) { +// 订单号已经存在 + bj = i; + isddh = true; + jsonObject = json; + SPS = jsonObject.getJSONArray("SPS"); + } else { + num++; + } + } + if (num == jsonArray.size()) { +// switch (cell.getCellType()) { +// case STRING: + jsonObject.put("DDH",ddh); +// break; +// case NUMERIC: +// jsonObject.put("DDH", cell.getNumericCellValue()); +// break; +// default: +// jsonObject.put("DDH", ""); +// } +//// jsonObject.put("DDH", cell.getStringCellValue()); + } + } + switch (cell.getColumnIndex()) { + case 3: + if (!isddh) { + switch (cell.getCellType()) { + case STRING: + jsonObject.put("GMFMC", cell.getStringCellValue()); + break; + case NUMERIC: + jsonObject.put("GMFMC", cell.getNumericCellValue()); + break; + default: + jsonObject.put("GMFMC", ""); + } + } + break; + case 4: + if (!isddh) { + switch (cell.getCellType()) { + case STRING: + jsonObject.put("GMFNSRSBH", cell.getStringCellValue()); + break; + case NUMERIC: + jsonObject.put("GMFNSRSBH", cell.getNumericCellValue()); + break; + default: + jsonObject.put("GMFNSRSBH", ""); + } + } + break; + case 5: + if (!isddh) { + switch (cell.getCellType()) { + case STRING: + jsonObject.put("GMFYX", cell.getStringCellValue()); + break; + case NUMERIC: + jsonObject.put("GMFYX", cell.getNumericCellValue()); + break; + default: + jsonObject.put("GMFYX", ""); + } + } + break; + case 6: + if (!isddh) { + switch (cell.getCellType()) { + case STRING: + jsonObject.put("GMFSJ", cell.getStringCellValue()); + break; + case NUMERIC: + jsonObject.put("GMFSJ", cell.getNumericCellValue()); + break; + default: + jsonObject.put("GMFSJ", ""); + } + } + break; + case 17: + if (!isddh) { + switch (cell.getCellType()) { + case STRING: + jsonObject.put("BZ", cell.getStringCellValue()); + break; + case NUMERIC: + jsonObject.put("BZ", cell.getNumericCellValue()); + break; + default: + jsonObject.put("BZ", ""); + } + } + break; + case 2: + switch (cell.getCellType()) { + case STRING: + SP.put("FXHXZ", cell.getStringCellValue()); + break; + case NUMERIC: + SP.put("FXHXZ", cell.getNumericCellValue()); + break; + default: + SP.put("FXHXZ", ""); + } + break; + case 7: + switch (cell.getCellType()) { + case STRING: + SP.put("XMMC", cell.getStringCellValue()); + break; + case NUMERIC: + SP.put("XMMC", cell.getNumericCellValue()); + break; + default: + SP.put("XMMC", ""); + } + break; + case 8: + switch (cell.getCellType()) { + case STRING: + SP.put("SPBM", cell.getStringCellValue()); + break; + case NUMERIC: + SP.put("SPBM", cell.getNumericCellValue()); + break; + default: + SP.put("SPBM", ""); + } + break; + case 9: + switch (cell.getCellType()) { + case STRING: + SP.put("GGXH", cell.getStringCellValue()); + break; + case NUMERIC: + SP.put("GGXH", cell.getNumericCellValue()); + break; + default: + SP.put("GGXH", ""); + } + break; + case 10: + switch (cell.getCellType()) { + case STRING: + SP.put("DW", cell.getStringCellValue()); + break; + case NUMERIC: + SP.put("DW", cell.getNumericCellValue()); + break; + default: + SP.put("DW", ""); + } + break; + case 11: + switch (cell.getCellType()) { + case STRING: + SP.put("NUM", cell.getStringCellValue()); + break; + case NUMERIC: + SP.put("NUM", cell.getNumericCellValue()); + break; + default: + SP.put("NUM", ""); + } + break; + case 12: + switch (cell.getCellType()) { + case STRING: + SP.put("SPDJ", cell.getStringCellValue()); + break; + case NUMERIC: + SP.put("SPDJ", cell.getNumericCellValue()); + break; + default: + SP.put("SPDJ", ""); + } + break; + case 13: + switch (cell.getCellType()) { + case STRING: + SP.put("JE", cell.getStringCellValue()); + break; + case NUMERIC: + SP.put("JE", cell.getNumericCellValue()); + break; + default: + SP.put("JE", ""); + } + break; + case 14: + switch (cell.getCellType()) { + case STRING: + SP.put("SL", cell.getStringCellValue()); + break; + case NUMERIC: + SP.put("SL", cell.getNumericCellValue()); + break; + default: + SP.put("SL", ""); + } + break; + case 15: + switch (cell.getCellType()) { + case STRING: + SP.put("ZKJE", cell.getStringCellValue()); + break; + case NUMERIC: + SP.put("ZKJE", cell.getNumericCellValue()); + break; + default: + SP.put("ZKJE", ""); + } + break; + case 16: + switch (cell.getCellType()) { + case STRING: + SP.put("YHZCBS", cell.getStringCellValue()); + break; + case NUMERIC: + SP.put("YHZCBS", cell.getNumericCellValue()); + break; + default: + SP.put("YHZCBS", ""); + } + break; + } + } + if(StringUtil.isEmpty(jsonObject.getString("DDH"))){ + continue row; + } + SPS.add(SP); + jsonObject.put("SPS", SPS); + if (num == jsonArray.size()) { + jsonArray.add(jsonObject); + } else { + jsonArray.set(bj, jsonObject); + } + } + // 关闭工作簿 + workbook.close(); + file.close(); + return jsonArray; + } + + public JSONArray getFileContent(long fileId) throws IOException { + System.out.println("获取参数附件ID"+fileId); +// 附件路径 + String path = SystemEnvironment.getApplicationFolder()+"/hxinvoiceFile/"+fileId+"/"; + System.out.println("文件下载路径"+path); + List fileUrls = getAttachmentManager().getBySubReference(fileId); + Attachment attachment = getAttachmentManager().getAttachmentByFileURL(fileUrls.get(0)); + String filePath = path+attachment.getFilename(); + JSONArray jsonArray = getFileContent(filePath); + return jsonArray; + } + + + public static String uploadContractToOA(Object[] fileInfos, String formId, String loginName, String updateAccountName) throws Exception { + + OrgManager orgManager = (OrgManager) AppContext.getBean("orgManager"); + V3xOrgMember member = orgManager.getMemberByLoginName(loginName); + V3xOrgAccount account = orgManager.getAccountByName(updateAccountName); + String refId = String.valueOf(Math.abs(UUID.randomUUID().getLeastSignificantBits())); + List attachments = new ArrayList<>(); + File tempDir = new File(TEMP_DIR); + if (!tempDir.exists()) { + tempDir.mkdirs(); + } + for (Object fileInfo : fileInfos) { + Map fileInfoMap = (Map) fileInfo; + String fileName = (String) fileInfoMap.get("fileName"); + String url = (String) fileInfoMap.get("downloadUrl"); + String savePath = TEMP_DIR + File.separator + fileName; + HttpClient.httpDownloadFile(url, null,savePath,"ITF-8"); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + FileUploadExporter fileUpload = new FileUploadExporter(); + File file = new File(savePath); + if (file.exists() && file.length() <= MAX_FILE_SIZE) { + String uploadedPath = fileUpload.processUpload(new String[]{file.getAbsolutePath()}); + Map attachMeta = new HashMap<>(); + attachMeta.put("type", "0"); + attachMeta.put("fileUrl", uploadedPath); + attachMeta.put("size", String.valueOf(file.length())); + attachMeta.put("subReference", refId); + attachMeta.put("category", CATEGORY_CODE); + attachMeta.put("createdate", sdf.format(new Date())); + attachMeta.put("filename", fileName); + attachMeta.put("reference", formId); + attachMeta.put("mimeType", "application/" + "pdf"); + attachments.add(new Attachment(attachMeta)); + file.delete(); + } + } + if (attachments.isEmpty()) return null; + AttachmentManager attachmentManager = (AttachmentManager) AppContext.getBean("attachmentManager"); + attachmentManager.create(attachments, member.getId(), account.getId()); + return refId; + } + + private static boolean isBlank(String str) { + return str == null || str.trim().isEmpty(); + } +} diff --git a/src/main/java/com/seeyon/apps/esign/utils/HmacSHA256Util.java b/src/main/java/com/seeyon/apps/esign/utils/HmacSHA256Util.java new file mode 100644 index 0000000..fb0b99e --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/utils/HmacSHA256Util.java @@ -0,0 +1,27 @@ +package com.seeyon.apps.esign.utils; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +public class HmacSHA256Util { + public static String encrypt(String data, String key) + throws NoSuchAlgorithmException, InvalidKeyException { + Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); + SecretKeySpec secret_key = new SecretKeySpec( + key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + sha256_HMAC.init(secret_key); + byte[] hash = sha256_HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(hash); + } + + public static void main(String[] args) throws Exception { + String data = "Hello World"; + String key = "secret"; + String result = encrypt(data, key); + System.out.println("HmacSHA256加密结果: " + result); + } +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/utils/HttpClient.java b/src/main/java/com/seeyon/apps/esign/utils/HttpClient.java new file mode 100644 index 0000000..a35fc5a --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/utils/HttpClient.java @@ -0,0 +1,499 @@ +package com.seeyon.apps.esign.utils; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * @ClassName: HttpClient + * @Description: HTTP请求工具类 + * @Author: GiikJc + * @Date: 2022/7/12 15:03 + */ + +/** + * 发送Get请求:HttpResponse httpGet(String url,Map headers,String encode) + *发送Post请求,同表单Post提交:HttpResponse httpPostForm(String url,Map params, Map headers,String encode) + *发送Post Raw请求:HttpResponse httpPostRaw(String url,String stringJson,Map headers, String encode) + *发送Put Raw请求:HttpResponse httpPutRaw(String url,String stringJson,Map headers, String encode) + *发送Delete请求:HttpResponse httpDelete(String url,Map headers,String encode) + */ +public class HttpClient { + + /** + * 发送 HTTP GET 请求下载文件 + * @param url 下载文件的 URL + * @param headers 请求头 + * @param savePath 文件保存的路径 + * @param encode 文件内容的编码 + * @return 下载成功返回 true,失败返回 false + */ + public static boolean httpDownloadFile(String url, Map headers, String savePath, String encode) { + if (encode == null) { + encode = "utf-8"; // 默认字符编码 + } + + CloseableHttpClient httpClient = null; + CloseableHttpResponse httpResponse = null; + InputStream inputStream = null; + OutputStream outputStream = null; + + try { + // 创建 HttpClient 实例 + httpClient = HttpClients.createDefault(); + HttpGet httpGet = new HttpGet(url); + + // 设置请求头 + if (headers != null && headers.size() > 0) { + for (Map.Entry entry : headers.entrySet()) { + httpGet.setHeader(entry.getKey(), entry.getValue()); + } + } + + // 执行请求 + httpResponse = httpClient.execute(httpGet); + HttpEntity entity = httpResponse.getEntity(); + + // 检查响应状态码 + if (httpResponse.getStatusLine().getStatusCode() == 200) { + inputStream = entity.getContent(); + + // 创建输出流,将文件保存到本地 + outputStream = new FileOutputStream(savePath); + + // 设置缓冲区 + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + + // 文件下载成功 + return true; + } else { + System.out.println("Download failed, HTTP error code: " + httpResponse.getStatusLine().getStatusCode()); + return false; + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + if (outputStream != null) { + outputStream.close(); + } + if (httpResponse != null) { + httpResponse.close(); + } + if (httpClient != null) { + httpClient.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * 对queryString参数按key进行字典序升序排序 + * @param queryString 原始参数字符串,如"b=2&a=1&c=3" + * @return 排序后的字符串,如"a=1&b=2&c=3" + */ + public static String sortQueryString(String queryString) { + if (queryString == null || queryString.isEmpty()) { + return queryString; + } + + String[] pairs = queryString.split("&"); + Map params = new TreeMap<>(); + + for (String pair : pairs) { + String[] kv = pair.split("="); + if (kv.length == 2) { + params.put(kv[0], kv[1]); + } else if (kv.length == 1) { + params.put(kv[0], ""); + } + } + + return params.entrySet().stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("&")); + } + /** + * 发送http get请求 + */ + public static String httpGet(String url,Map headers,String encode){ + + if(encode == null){ + encode = "utf-8"; + } + CloseableHttpResponse httpResponse = null; + CloseableHttpClient closeableHttpClient = null; + String content = null; + //since 4.3 不再使用 DefaultHttpClient + try { + closeableHttpClient = HttpClientBuilder.create().build(); + HttpGet httpGet = new HttpGet(url); + //设置header + if (headers != null && headers.size() > 0) { + for (Map.Entry entry : headers.entrySet()) { + httpGet.setHeader(entry.getKey(),entry.getValue()); + } + } + + httpResponse = closeableHttpClient.execute(httpGet); + HttpEntity entity = httpResponse.getEntity(); + content = EntityUtils.toString(entity, encode); + } catch (Exception e) { + e.printStackTrace(); + }finally{ + try { + httpResponse.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + try { //关闭连接、释放资源 + closeableHttpClient.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return content; + } + /** + * 发送 http post 请求,参数以form表单键值对的形式提交。 + */ + public static String httpPostForm(String url,Map params, Map headers,String encode){ + + if(encode == null){ + encode = "utf-8"; + } + + String content = null; + CloseableHttpResponse httpResponse = null; + CloseableHttpClient closeableHttpClient = null; + try { + + closeableHttpClient = HttpClients.createDefault(); + HttpPost httpost = new HttpPost(url); + + //设置header + if (headers != null && headers.size() > 0) { + for (Map.Entry entry : headers.entrySet()) { + httpost.setHeader(entry.getKey(),entry.getValue()); + } + } + //组织请求参数 + List paramList = new ArrayList (); + if(params != null && params.size() > 0){ + Set keySet = params.keySet(); + for(String key : keySet) { + paramList.add(new BasicNameValuePair(key, params.get(key))); + } + } + httpost.setEntity(new UrlEncodedFormEntity(paramList, encode)); + + + httpResponse = closeableHttpClient.execute(httpost); + HttpEntity entity = httpResponse.getEntity(); + content = EntityUtils.toString(entity, encode); + } catch (Exception e) { + e.printStackTrace(); + }finally{ + try { + httpResponse.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + try { //关闭连接、释放资源 + closeableHttpClient.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return content; + } + + /** + * 发送 http post 请求,参数以原生字符串进行提交 + * @param url + * @param encode + * @return + */ + public static String httpPostRaw(String url,String stringJson,Map headers, String encode){ + if(encode == null){ + encode = "utf-8"; + } + String content = null; + CloseableHttpResponse httpResponse = null; + CloseableHttpClient closeableHttpClient = null; + try { + + //HttpClients.createDefault()等价于 HttpClientBuilder.create().build(); + closeableHttpClient = HttpClients.createDefault(); + HttpPost httpost = new HttpPost(url); + + //设置header + httpost.setHeader("Content-type", "application/json"); + if (headers != null && headers.size() > 0) { + for (Map.Entry entry : headers.entrySet()) { + httpost.setHeader(entry.getKey(),entry.getValue()); + } + } + //组织请求参数 + StringEntity stringEntity = new StringEntity(stringJson, encode); + httpost.setEntity(stringEntity); + + + //响应信息 + httpResponse = closeableHttpClient.execute(httpost); + HttpEntity entity = httpResponse.getEntity(); + content = EntityUtils.toString(entity, encode); + } catch (Exception e) { + e.printStackTrace(); + }finally{ + try { + httpResponse.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + try { //关闭连接、释放资源 + closeableHttpClient.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return content; + } + + /** + * 发送 http put 请求,参数以原生字符串进行提交 + * @param url + * @param encode + * @return + */ + public static String httpPutRaw(String url,String stringJson,Map headers, String encode){ + if(encode == null){ + encode = "utf-8"; + } + CloseableHttpResponse httpResponse = null; + CloseableHttpClient closeableHttpClient = null; + String content = null; + //since 4.3 不再使用 DefaultHttpClient + try { + + //HttpClients.createDefault()等价于 HttpClientBuilder.create().build(); + closeableHttpClient = HttpClients.createDefault(); + HttpPut httpput = new HttpPut(url); + + //设置header + httpput.setHeader("Content-type", "application/json"); + if (headers != null && headers.size() > 0) { + for (Map.Entry entry : headers.entrySet()) { + httpput.setHeader(entry.getKey(),entry.getValue()); + } + } + //组织请求参数 + StringEntity stringEntity = new StringEntity(stringJson, encode); + httpput.setEntity(stringEntity); + //响应信息 + httpResponse = closeableHttpClient.execute(httpput); + HttpEntity entity = httpResponse.getEntity(); + content = EntityUtils.toString(entity, encode); + } catch (Exception e) { + e.printStackTrace(); + }finally{ + try { + httpResponse.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + try { + closeableHttpClient.close(); //关闭连接、释放资源 + } catch (IOException e) { + e.printStackTrace(); + } + return content; + } + /** + * 发送http delete请求 + */ + public static String httpDelete(String url,Map headers,String encode){ + if(encode == null){ + encode = "utf-8"; + } + String content = null; + CloseableHttpResponse httpResponse = null; + CloseableHttpClient closeableHttpClient = null; + try { + //since 4.3 不再使用 DefaultHttpClient + closeableHttpClient = HttpClientBuilder.create().build(); + HttpDelete httpdelete = new HttpDelete(url); + //设置header + if (headers != null && headers.size() > 0) { + for (Map.Entry entry : headers.entrySet()) { + httpdelete.setHeader(entry.getKey(),entry.getValue()); + } + } + + httpResponse = closeableHttpClient.execute(httpdelete); + HttpEntity entity = httpResponse.getEntity(); + content = EntityUtils.toString(entity, encode); + } catch (Exception e) { + e.printStackTrace(); + }finally{ + try { + httpResponse.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + try { //关闭连接、释放资源 + closeableHttpClient.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return content; + } + + /** + * 发送 http post 请求,支持文件上传 + */ + public static String httpPostFormMultipart(String url,Map params, List files,Map headers,String encode){ + if(encode == null){ + encode = "utf-8"; + } + CloseableHttpResponse httpResponse = null; + CloseableHttpClient closeableHttpClient = null; + String content = null; + //since 4.3 不再使用 DefaultHttpClient + try { + + closeableHttpClient = HttpClients.createDefault(); + HttpPost httpost = new HttpPost(url); + + //设置header + if (headers != null && headers.size() > 0) { + for (Map.Entry entry : headers.entrySet()) { + httpost.setHeader(entry.getKey(),entry.getValue()); + } + } + MultipartEntityBuilder mEntityBuilder = MultipartEntityBuilder.create(); + mEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + mEntityBuilder.setCharset(Charset.forName(encode)); + + // 普通参数 + ContentType contentType = ContentType.create("text/plain",Charset.forName(encode));//解决中文乱码 + if (params != null && params.size() > 0) { + Set keySet = params.keySet(); + for (String key : keySet) { + mEntityBuilder.addTextBody(key, params.get(key),contentType); + } + } + //二进制参数 + if (files != null && files.size() > 0) { + for (File file : files) { + mEntityBuilder.addBinaryBody("file", file); + } + } + httpost.setEntity(mEntityBuilder.build()); + httpResponse = closeableHttpClient.execute(httpost); + HttpEntity entity = httpResponse.getEntity(); + content = EntityUtils.toString(entity, encode); + } catch (Exception e) { + e.printStackTrace(); + }finally{ + try { + httpResponse.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + try { //关闭连接、释放资源 + closeableHttpClient.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return content; + } + + + public static String uploadBinaryFile(String url, Map headers, + InputStream io, String fileName) throws IOException { + + CloseableHttpClient client = HttpClients.createDefault(); + HttpPut put = new HttpPut(url); + String responseBody = null; + + try { + // 设置请求头 + for (String key : headers.keySet()) { + put.addHeader(key, headers.get(key)); + } + + // 构建多部分实体 + MultipartEntityBuilder builder = MultipartEntityBuilder.create() + .setMode(HttpMultipartMode.BROWSER_COMPATIBLE) + .setCharset(Charset.forName("UTF-8")); + builder.addBinaryBody("file", io, ContentType.MULTIPART_FORM_DATA, fileName); + + put.setEntity(builder.build()); + + // 执行请求 + CloseableHttpResponse response = null; + try { + response = client.execute(put); + HttpEntity resEntity = response.getEntity(); + responseBody = EntityUtils.toString(resEntity, "UTF-8"); + + // 无论状态码如何,都返回响应体 + return responseBody; + + } finally { + if(response != null) { + response.close(); + } + } + } finally { + client.close(); + io.close(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/utils/JsonCleaner.java b/src/main/java/com/seeyon/apps/esign/utils/JsonCleaner.java new file mode 100644 index 0000000..4c3d09c --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/utils/JsonCleaner.java @@ -0,0 +1,28 @@ +package com.seeyon.apps.esign.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Iterator; +import java.util.Map; + +public class JsonCleaner { + + public static String removeEmptyObjects(String jsonStr) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode node = (ObjectNode) mapper.readTree(jsonStr); + + Iterator> fields = node.fields(); + while (fields.hasNext()) { + Map.Entry entry = fields.next(); + if (entry.getValue().isObject() && entry.getValue().isEmpty()) { + fields.remove(); + } + } + + return mapper.writeValueAsString(node); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/seeyon/apps/esign/utils/ProtUtil.java b/src/main/java/com/seeyon/apps/esign/utils/ProtUtil.java new file mode 100644 index 0000000..820ffba --- /dev/null +++ b/src/main/java/com/seeyon/apps/esign/utils/ProtUtil.java @@ -0,0 +1,84 @@ +package com.seeyon.apps.esign.utils; + +import com.alibaba.fastjson.JSONObject; +import com.seeyon.aicloud.common.JsonUtils; +import com.seeyon.apps.esign.config.EsignConfigProvider; +import com.seeyon.apps.esign.constants.EsignConfigConstants; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicHeader; +import org.apache.http.util.EntityUtils; +import www.seeyon.com.utils.StringUtil; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Map; + + +public class ProtUtil { + + private static final Log log = LogFactory.getLog(ProtUtil.class); + private EsignConfigProvider configProvider = EsignConfigProvider.getInstance(); + + /** + * 获取一个token + * + * @return + * @throws IOException + * @throws FileNotFoundException + */ + private String getToken(String oatokenurl,String restName,String restPassword,String loginName) throws FileNotFoundException, IOException { + String address = oatokenurl + restName + "/" + restPassword; + if(StringUtil.isNotEmpty(loginName)){ + address = address +"?loginName="+loginName; + } + DefaultHttpClient client = new DefaultHttpClient(); + String result = ""; + HttpGet get = new HttpGet(address); + // 添加 Headers 信息 + get.addHeader(new BasicHeader("Accept", "application/json")); + try { + HttpResponse res = client.execute(get); + if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + result = EntityUtils.toString(res.getEntity()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + String token = ""; + if(result.contains("{")) { + JSONObject jsObj = JSONObject.parseObject(result); + token = jsObj.get("id").toString(); + }else { + token = result; + } + return token; + } + + + /** + * 调用post接口 + * @return + */ + public String sendPostNotification(String params, String urlParam, String token) throws IOException { + String host = configProvider.getBizConfigByKey(EsignConfigConstants.OA_HOST); + String tokenGetUrl = configProvider.getBizConfigByKey(EsignConfigConstants.getTokenUrl); + String restName = configProvider.getBizConfigByKey(EsignConfigConstants.restName); + String restPwd = configProvider.getBizConfigByKey(EsignConfigConstants.restPwd); + String oatoken = getToken(host + tokenGetUrl,restName,restPwd,null); + String url = host + urlParam+ token+"?token="+oatoken; + log.info("调用恢复超级节点接口请求地址为: " + url); + String respStr = HttpClient.httpPostRaw(url, params, null, null); + log.info("调用恢复超级节点接口响应结果: " + respStr); + if(!"1".equals(respStr)) { + Map map = JsonUtils.parseObject(respStr, Map.class); + throw new RuntimeException("恢复阻塞的超级节点失败: " + map.get("message")); + } + return respStr; + } + +} diff --git a/src/main/java/com/seeyon/ctp/rest/resources/esign/EsignFlowQueryResource.java b/src/main/java/com/seeyon/ctp/rest/resources/esign/EsignFlowQueryResource.java new file mode 100644 index 0000000..5d0e6b5 --- /dev/null +++ b/src/main/java/com/seeyon/ctp/rest/resources/esign/EsignFlowQueryResource.java @@ -0,0 +1,33 @@ +package com.seeyon.ctp.rest.resources.esign; + +import cn.hutool.log.Log; +import com.seeyon.apps.esign.service.EsignFlowQueryService; +import com.seeyon.ctp.common.AppContext; +import com.seeyon.ctp.rest.resources.BaseResource; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; + +@Path("/eFlow") +public class EsignFlowQueryResource extends BaseResource { + + private static final Log log = Log.get(EsignFlowQueryResource.class); + private EsignFlowQueryService esignFlowQueryService = (EsignFlowQueryService) AppContext.getBean("esignFlowQueryService"); + + @Context + HttpServletRequest req; + + @GET + @Produces({"application/json"}) + @Path("/queryFlowStatus") + public Response queryTemplates() { + try { + return success(esignFlowQueryService.queryFlow(req.getParameter("eFlowId"))); + }catch (Exception e) { + log.error(e.getMessage(),e); + } + return fail("失败"); + } +} diff --git a/src/main/java/com/seeyon/ctp/rest/resources/esign/EsignTemplateResource.java b/src/main/java/com/seeyon/ctp/rest/resources/esign/EsignTemplateResource.java new file mode 100644 index 0000000..59069e2 --- /dev/null +++ b/src/main/java/com/seeyon/ctp/rest/resources/esign/EsignTemplateResource.java @@ -0,0 +1,50 @@ +package com.seeyon.ctp.rest.resources.esign; + +import cn.hutool.log.Log; +import com.seeyon.apps.esign.service.EsignFileTemplateService; +import com.seeyon.ctp.common.AppContext; +import com.seeyon.ctp.rest.resources.BaseResource; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import java.util.Map; + +@Path("/etemplate") +public class EsignTemplateResource extends BaseResource { + + private static final Log log = Log.get(EsignTemplateResource.class); + private EsignFileTemplateService esignFileTemplateService = (EsignFileTemplateService) AppContext.getBean("esignFileTemplateService"); + + @GET + @Produces({"application/json"}) + @Consumes({"application/json"}) + @Path("/querypage") + public Response queryTemplates(@QueryParam("pageNum") Integer pageNum) { + try { + Map map = esignFileTemplateService.queryTemplates(pageNum); + return success(map); + }catch (Exception e) { + log.error(e.getMessage(),e); + } + return fail("失败"); + } + + @POST + @Produces({"application/json"}) + @Consumes({"application/json"}) + @Path("/compareurl") + public Response compare(@QueryParam("templateRefId") String templateRefId,@QueryParam("contractRefId") String contractRefId) { + try { + String compareDetailUrl = esignFileTemplateService.getCompareDetailUrl(templateRefId, contractRefId); + return success(compareDetailUrl); + }catch (Exception e) { + log.error(e.getMessage(),e); + } + return fail("失败"); + } +} diff --git a/src/test/java/TestOne.java b/src/test/java/TestOne.java new file mode 100644 index 0000000..5cafa32 --- /dev/null +++ b/src/test/java/TestOne.java @@ -0,0 +1,9 @@ +package java; + +import com.seeyon.apps.esign.EsignPluginApi; + +public class TestOne { + public static void main(String[] args) { + new EsignPluginApi(); + } +}