本文为您介绍Dataphin BPMS支持对接第三方审批流,包括开放了哪些接口能力,如何接入审批能力以及审批案例等内容。
前提条件
已在Dataphin系统配置审批设置,如何配置,详情请参见审批设置。运维租户参见审批设置中选择审批系统的其他配置项。
审批接口
{url} :为第三方审批流对接地址,http(s)://{url}为提交审批申请URL。
- 连接测试。- 请求方式:POST。 - 请求地址:http(s)://{url}/dataphin/bpms/check/connect。 - 请求时序图  - Query参数 - 参数名称 - 参数类型 - 描述 - accessToken - String - 调用服务端API的凭证。 - Dataphin页面自动生成,或用户生成填入Dataphin。如:6d1b2n1c。 - timestamp - String - 单位毫秒,信息发送时间戳。如:1654156836531。 - Body参数 - checkEvent(String):检查事件为连接测试。如:connectivity。 - 返回参数 - checkResult(String):检查结果:成功,仅当返回值为success时,认定为成功。其他值均被认为测试连接失败。如:success。 
- 提交审批。- 请求方式:POST。 - 请求地址:http(s)://{url}/dataphin/bpms/processinstance/create。 - 提交审批的时序图  - Query参数 - 参数名称 - 参数类型 - 描述 - accessToken - String - 调用服务端API的凭证。 - Dataphin页面自动生成,或用户生成填入Dataphin。如:6d1b2n1c。 - timestamp - String - 单位毫秒,信息发送时间戳。如:1654156836531。 - Body参数 - 参数名称 - 参数类型 - 描述 - applyId - String - Dataphin审批单Id。如:1223。 - title - String - Dataphin审批单标题。如:dataphin bpms。 - content - String - Dataphin审批单内容。如:bpms content。请参见审批消息内容元数据描述。 - type - String - 审批单类型:APPROVAL_ DOC_TYPE。 
- 代码审核:CODE_REVIEW。 
- 发布管控:PUBLISH。 
- 业务规划:BIZ_PLANNING。 
- 权限审批:AUTH。 
- 默认:DEFAULT。 
 - templateCode - String - 审批模板code,可为空,与页面配置有关。 - 审批消息内容元数据描述 - 参数名称 - 参数类型 - 描述 - resourceType - String - 审批任务类型,为枚举值,包含以下的类型: - PhysicalTable:物理表。 
- LogicTable:逻辑表。 
- MetaTable:实时元表。 
- MirrorTable:镜像表。 
- Fun:函数。 
- DataSource:数据源。 
- FeatureConfig:功能权限配置。 
- OSAPP:数据服务APP。 
- OSAPI:数据服务API 。 
- OSLogicUnit:数据服务单元。 
- OSDS:数据服务数据源。 
- SECRET_KEY:密钥。 
- GLOBAL_PARAM:全局变量。 
 - grantToUsers - List<GrantToUser> - 授权用户列表。请参见GrantToUser。 - bpmsEnvironment - BpmsEnvironment - 审批系统环境信息。请参见BpmsEnvironment。 - operates - List<String> - 申请的权限类型,包含以下类型: - SYNC_READ:数据源同步读。 
- SYNC_WRITE:数据源同步写。 
- SQL_QUERY: 物理表及逻辑表的SQL查询。 
- SQL_WRITE: 物理表的SQL写入。 
- SQL_ALTER: 物理表的SQL修改。 
- SQL_DROP: 物理表及逻辑表的SQL删除。 
- SELECT:数据服务API的查询。 
- WRITE:写入权限。 
- DEV:开发权限。 
- USE:使用权限。 
- UPDATE:改表数据。 
- PIPELINE_ENCRY:集成加密。 
- PIPELINE_DECRY:集成解密。 
 - levels - List<String> - 权限等级,包含三个等级:HIGH、MIDDLE、LOW。 - operations - List<String> - 权限类型,包含以下类型: - SELECT-查询。 
- DESCRIBE- 查表结构。 
- UPDATE- 改表数据。 
- ALTER- 改表结构。 
- DELETE-删除表。 
- COPY_TASK- 复制。 
 - resources - List<BpmsResource> - 资源内容,请参见BpmsResource。 - applyObject - ApplyObject - 申请对象信息,请参见ApplyObject。 - reason - String - 申请原因。 - GrantToUser - 参数名称 - 参数类型 - 描述 - account - Account - 授权账户列表,请参见Account。 - period - Period - 有效期,请参见Period。 - Account - 参数名称 - 参数类型 - 描述 - accountType - String - 授权类型,包括个人账号、生产账号和应用账号三种。 - PERSONAL:个人账号。 
- PRODUCE:生产账号。 
- APPLICATION:应用账号。 
 - userName - String - 结束时间格式为yyyy-mm-dd。如:2022-09-11。 - Period - 参数名称 - 参数类型 - 描述 - periodType - String - 有效期类型支持长期(LONG_TERM)。 - periodEnd - String - 权限到期时间格式为yyyy-mm-dd。如:2022-09-11。 - BpmsEnvironment - 参数名称 - 参数类型 - 描述 - projectName - String - 项目名称。 - bizUnitName - String - 业务板块名称。 - resourceEnv - String - 环境分为生产环境和开发环境两种环境。 - PROD:生产。 
- DEV:开发。 
 - BpmsResource - 参数名称 - 参数类型 - 描述 - resourceType - String - 请参见审批消息内容元数据描述。 - resourceName - String - 资源名称。 - children - List<Children> - 字段列表,请参见Children。 - operations - List<String> - 请参见审批消息内容元数据描述的operates。 - authTypes - String - 申请权限类型。 - Children - 参数名称 - 参数类型 - 描述 - resourceName - String - 字段名称。 - resourceProperties - String - 字段属性。 - ResourceProperties - 参数名称 - 参数类型 - 描述 - columnType - String - 字段类型。 - columnIsPartition - String - 是否为分区字段。 - columnIsPk - String - 是否为主键。 - ApplyObject - 参数名称 - 参数类型 - 描述 - objectName - String - 对象名称。 - codeContent - String - 代码内容。 - name - String - 业务活动英文名称。 - bizObjectType - String - 对象类型。 - bizObjectChangeType - String - 变更类型。 - bizObjectPkField - String - 组件字段。 - bizObjectParent - String - 所属父对象。 - bizObjectChildren - String - 下游子对象。 - bizProcessCn - String - 活动名称。 - bizProcessType - String - 活动类型。 - bizProcessChangeType - String - 变更类型。 - bizProcessNodes - String - 流程节点。 - atomicIndexCn - String - 指标名称。 - dataType - String - 数据类型。 - unit - String - 计量单位。 - bizProcess - String - 活动名称。 - des - String - 活动口径。 - derivedLogic - String - 衍生公司。 - protoLogics - String - 计算逻辑。 - 返回参数 - processInstanceId(String):第三方审批实例ID。如:6d1b2n1c。 - 撤销审批。说明- 提交审批后,若申请内容不符合预期,可撤销审批。 - 请求方式:POST。 - 请求地址:http(s)://{url}/dataphin/bpms/processinstance/revoke。 - Query参数 - 参数名称 - 参数类型 - 描述 - accessToken - String - 调用服务端API的凭证。 - Dataphin页面自动生成,或用户生成填入Dataphin。如:6d1b2n1c。 - timestamp - String - 单位毫秒,信息发送时间戳。如:1654156836531。 - Body参数 - 参数名称 - 参数类型 - 描述 - applyId - String - Dataphin审批单Id。如:1223。 - processInstanceId - String - 第三方审批实例Id。如:6d1b2a3c。 - operatingUserId - String - 操作人工号。如:6d1bx1d2。 - 返回参数 - result(String):撤回结果信息。仅当返回值为success时被认定为成功。 
- 查询审批实例Url。- 请求方式:GET。 - 请求地址:http(s)://{url}/dataphin/bpms/processinstance/apply。 - Query参数 - 参数名称 - 参数类型 - 描述 - accessToken - String - 调用服务端API的凭证。 - Dataphin页面自动生成,或用户生成填入Dataphin。如:6d1b2n1c。 - timestamp - String - 单位毫秒,信息发送时间戳。如:1654156836531。 
回调接口
- https://{callbackUrl}即为审批设置中其他配置的Callback URL。 
- accessToken:调用服务端API的凭证。Dataphin页面自动生成,或用户生成填入Dataphin。 
- callBackAesKey:回调认证凭证。Dataphin页面自动生成,或用户生成填入Dataphin。 
- 回调方法。- 请求方式:POST。 - 请求地址:http://{callbackUrl}。 - Query参数 - 参数名称 - 参数类型 - 描述 - signature - String - 消息认证信息,请参见加解密方法。 - timestamp - String - 单位毫秒,信息发送时间戳。如:1654156836531。 - nonce - String - 随机数,请参见加解密方法。 - Body参数 - encrypt(String):加密信息。如:ajls384k。 - 返回参数 - encrypt(String):回调状态加密信息。如:ajls384k。 
- 回调加密内容。- 回调连通性校验。- 传入加密参数: - applyStatus(String):检查连通性参数。如:CHECK。 
- 返回加密参数: - success:成功回调返回值。 
 - 回调返回bpms实例审批结果。- 传入加密参数: - 参数名称 - 参数类型 - 描述 - applyId - String - Dataphin审批单Id。如:1223。 - processInstanceId - String - 第三方审批实例Id。如:6d1b2a3c。 - comment - String - 审批实例评论信息。 - applyStatus - String - accept:接受。 
- reject:拒绝。 
- revoke:撤销。 
 
- 返回加密参数: - success:成功回调返回值。 
 
- 加解密方法。- 加密示例: - Map<String, String> callBackJson = Maps.newHashMap(); callBackJson.put("applyId", "1"); callBackJson.put("applyStatus", "accept"); callBackJson.put("processInstanceId", "2"); callBackJson.put("comment", "test"); String callBackResult = JSON.toJSONString(callBackJson); ThirdPartyCrypto callbackCrypto = new ThirdPartyCrypto(token, aesKey); String timestamp = String.valueOf(System.currentTimeMillis()); String nonce = ThirdPartyCrypto.Utils.getRandomStr(16); String encrypt = callbackCrypto.encrypt(nonce, callBackResult); String signature = callbackCrypto.getSignature(token, timestamp, nonce, encrypt);- 解密示例: - String encryptMsg = JSONObject.parseObject(encrypt); String decryptMsg = callbackCrypto.getDecryptMsg(signature, timestamp, nonce, encryptMsg); if ("success".equalsIgnoreCase(decryptMsg)) { log.error("call back success"); }- 加解密工具 - import com.alibaba.fastjson.JSON; import org.apache.commons.codec.binary.Base64; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayOutputStream; import java.lang.reflect.Field; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.Permission; import java.security.PermissionCollection; import java.security.Security; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Random; /** * Dataphin开放平台加解密方法 */ public class ThirdPartyCrypto { private static final Charset CHARSET = StandardCharsets.UTF_8; private static final Base64 BASE_64 = new Base64(); private final byte[] AES_KEY; private final String TOKEN; /** * ask getPaddingBytes key固定长度 **/ private static final Integer AES_ENCODE_KEY_LENGTH = 43; /** * 加密随机字符串字节长度 **/ private static final Integer RANDOM_LENGTH = 16; /** * 构造函数 * * @param token 开发者设置的token * @param encodingAesKey 开发者设置的EncodingAESKey * * @throws ThirdPartyEncryptException 执行失败,请查看该异常的错误码和具体的错误信息 */ public ThirdPartyCrypto(String token, String encodingAesKey) throws ThirdPartyEncryptException { if (null == encodingAesKey || encodingAesKey.length() != AES_ENCODE_KEY_LENGTH) { throw new ThirdPartyEncryptException(ThirdPartyEncryptException.AES_KEY_ILLEGAL); } this.TOKEN = token; AES_KEY = Base64.decodeBase64(encodingAesKey + "="); } public Map<String, String> getEncryptedMap(String plaintext) throws ThirdPartyEncryptException { return getEncryptedMap(plaintext, System.currentTimeMillis(), Utils.getRandomStr(16)); } /** * 将和Dataphin同步的消息体加密,返回加密Map * * @param plaintext 传递的消息体明文 * @param timeStamp 时间戳 * @param nonce 随机字符串 * @return * @throws ThirdPartyEncryptException */ public Map<String, String> getEncryptedMap(String plaintext, Long timeStamp, String nonce) throws ThirdPartyEncryptException { if (null == plaintext) { throw new ThirdPartyEncryptException(ThirdPartyEncryptException.ENCRYPTION_PLAINTEXT_ILLEGAL); } if (null == timeStamp) { throw new ThirdPartyEncryptException(ThirdPartyEncryptException.ENCRYPTION_TIMESTAMP_ILLEGAL); } if (null == nonce) { throw new ThirdPartyEncryptException(ThirdPartyEncryptException.ENCRYPTION_NONCE_ILLEGAL); } // 加密 String encrypt = encrypt(Utils.getRandomStr(RANDOM_LENGTH), plaintext); String signature = getSignature(TOKEN, String.valueOf(timeStamp), nonce, encrypt); Map<String, String> resultMap = new HashMap<String, String>(); resultMap.put("msg_signature", signature); resultMap.put("encrypt", encrypt); resultMap.put("timeStamp", String.valueOf(timeStamp)); resultMap.put("nonce", nonce); return resultMap; } /** * 密文解密 * * @param msgSignature 签名串 * @param timeStamp 时间戳 * @param nonce 随机串 * @param encryptMsg 密文 * @return 解密后的原文 * @throws ThirdPartyEncryptException */ public String getDecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptMsg) throws ThirdPartyEncryptException { //校验签名 String signature = getSignature(TOKEN, timeStamp, nonce, encryptMsg); if (!signature.equals(msgSignature)) { throw new ThirdPartyEncryptException(ThirdPartyEncryptException.COMPUTE_SIGNATURE_ERROR); } // 解密 return decrypt(encryptMsg); } /** * 对明文加密. * @param plaintext 需要加密的明文 * @return 加密后base64编码的字符串 */ public String encrypt(String random, String plaintext) throws ThirdPartyEncryptException { try { byte[] randomBytes = random.getBytes(CHARSET); byte[] plainTextBytes = plaintext.getBytes(CHARSET); byte[] lengthByte = Utils.int2Bytes(plainTextBytes.length); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); byteStream.write(randomBytes); byteStream.write(lengthByte); byteStream.write(plainTextBytes); byte[] padBytes = PKCS7Padding.getPaddingBytes(byteStream.size()); byteStream.write(padBytes); byte[] unencrypted = byteStream.toByteArray(); byteStream.close(); Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(AES_KEY, "AES"); IvParameterSpec iv = new IvParameterSpec(AES_KEY, 0, 16); cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); byte[] encrypted = cipher.doFinal(unencrypted); String result = BASE_64.encodeToString(encrypted); return result; } catch (Exception e) { throw new ThirdPartyEncryptException(ThirdPartyEncryptException.COMPUTE_ENCRYPT_TEXT_ERROR); } } /** * 对密文进行解密. * @param text 需要解密的密文 * @return 解密得到的明文 */ private String decrypt(String text) throws ThirdPartyEncryptException { byte[] originalArr; try { // 设置解密模式为AES的CBC模式 Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(AES_KEY, "AES"); IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(AES_KEY, 0, 16)); cipher.init(Cipher.DECRYPT_MODE, keySpec, iv); // 使用BASE64对密文进行解码 byte[] encrypted = Base64.decodeBase64(text); // 解密 originalArr = cipher.doFinal(encrypted); } catch (Exception e) { throw new ThirdPartyEncryptException(ThirdPartyEncryptException.COMPUTE_DECRYPT_TEXT_ERROR); } String plainText; try { // 去除补位字符 byte[] bytes = PKCS7Padding.removePaddingBytes(originalArr); // 分离16位随机字符串,网络字节序和corpId byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20); int plainTextLegth = Utils.bytes2int(networkOrder); plainText = new String(Arrays.copyOfRange(bytes, 20, 20 + plainTextLegth), CHARSET); } catch (Exception e) { throw new ThirdPartyEncryptException(ThirdPartyEncryptException.COMPUTE_DECRYPT_TEXT_LENGTH_ERROR); } return plainText; } /** * 数字签名 * * @param token isv token * @param timestamp 时间戳 * @param nonce 随机串 * @param encrypt 加密文本 * @return * @throws ThirdPartyEncryptException */ public String getSignature(String token, String timestamp, String nonce, String encrypt) throws ThirdPartyEncryptException { try { String[] array = new String[] {token, timestamp, nonce, encrypt}; Arrays.sort(array); System.out.println(JSON.toJSONString(array)); StringBuffer sb = new StringBuffer(); for (int i = 0; i < 4; i++) { sb.append(array[i]); } String str = sb.toString(); System.out.println(str); MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update(str.getBytes()); byte[] digest = md.digest(); StringBuffer hexstr = new StringBuffer(); String shaHex = ""; for (int i = 0; i < digest.length; i++) { shaHex = Integer.toHexString(digest[i] & 0xFF); if (shaHex.length() < 2) { hexstr.append(0); } hexstr.append(shaHex); } return hexstr.toString(); } catch (Exception e) { throw new ThirdPartyEncryptException(ThirdPartyEncryptException.COMPUTE_SIGNATURE_ERROR); } } public static class Utils { public Utils() { } public static String getRandomStr(int count) { String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < count; ++i) { int number = random.nextInt(base.length()); sb.append(base.charAt(number)); } return sb.toString(); } public static byte[] int2Bytes(int count) { byte[] byteArr = new byte[] {(byte)(count >> 24 & 255), (byte)(count >> 16 & 255), (byte)(count >> 8 & 255), (byte)(count & 255)}; return byteArr; } public static int bytes2int(byte[] byteArr) { int count = 0; for (int i = 0; i < 4; ++i) { count <<= 8; count |= byteArr[i] & 255; } return count; } } public static class PKCS7Padding { private static final Charset CHARSET = StandardCharsets.UTF_8; private static final int BLOCK_SIZE = 32; public PKCS7Padding() { } public static byte[] getPaddingBytes(int count) { int amountToPad = 32 - count % 32; if (amountToPad == 0) { amountToPad = 32; } char padChr = chr(amountToPad); String tmp = new String(); for (int index = 0; index < amountToPad; ++index) { tmp = tmp + padChr; } return tmp.getBytes(CHARSET); } public static byte[] removePaddingBytes(byte[] decrypted) { int pad = decrypted[decrypted.length - 1]; if (pad < 1 || pad > 32) { pad = 0; } return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad); } private static char chr(int a) { byte target = (byte)(a & 255); return (char)target; } } public static class ThirdPartyEncryptException extends Exception { public static final int SUCCESS = 0; public static final int ENCRYPTION_PLAINTEXT_ILLEGAL = 900001; public static final int ENCRYPTION_TIMESTAMP_ILLEGAL = 900002; public static final int ENCRYPTION_NONCE_ILLEGAL = 900003; public static final int AES_KEY_ILLEGAL = 900004; public static final int SIGNATURE_NOT_MATCH = 900005; public static final int COMPUTE_SIGNATURE_ERROR = 900006; public static final int COMPUTE_ENCRYPT_TEXT_ERROR = 900007; public static final int COMPUTE_DECRYPT_TEXT_ERROR = 900008; public static final int COMPUTE_DECRYPT_TEXT_LENGTH_ERROR = 900009; public static final int COMPUTE_DECRYPT_TEXT_CORPID_ERROR = 900010; private static Map<Integer, String> msgMap = new HashMap(); private Integer code; static { msgMap.put(0, "成功"); msgMap.put(900001, "加密明文文本非法"); msgMap.put(900002, "加密时间戳参数非法"); msgMap.put(900003, "加密随机字符串参数非法"); msgMap.put(900005, "签名不匹配"); msgMap.put(900006, "签名计算失败"); msgMap.put(900004, "不合法的aes key"); msgMap.put(900007, "计算加密文字错误"); msgMap.put(900008, "计算解密文字错误"); msgMap.put(900009, "计算解密文字长度不匹配"); msgMap.put(900010, "计算解密文字corpid不匹配"); } public Integer getCode() { return this.code; } public ThirdPartyEncryptException(Integer exceptionCode) { super((String)msgMap.get(exceptionCode)); this.code = exceptionCode; } } static { try { Security.setProperty("crypto.policy", "limited"); RemoveCryptographyRestrictions(); } catch (Exception var1) { } } private static void RemoveCryptographyRestrictions() throws Exception { Class<?> jceSecurity = getClazz("javax.crypto.JceSecurity"); Class<?> cryptoPermissions = getClazz("javax.crypto.CryptoPermissions"); Class<?> cryptoAllPermission = getClazz("javax.crypto.CryptoAllPermission"); if (jceSecurity != null) { setFinalStaticValue(jceSecurity, "isRestricted", false); PermissionCollection defaultPolicy = (PermissionCollection)getFieldValue(jceSecurity, "defaultPolicy", (Object)null, PermissionCollection.class); if (cryptoPermissions != null) { Map<?, ?> map = (Map)getFieldValue(cryptoPermissions, "perms", defaultPolicy, Map.class); map.clear(); } if (cryptoAllPermission != null) { Permission permission = (Permission)getFieldValue(cryptoAllPermission, "INSTANCE", (Object)null, Permission.class); defaultPolicy.add(permission); } } } private static Class<?> getClazz(String className) { Class clazz = null; try { clazz = Class.forName(className); } catch (Exception var3) { } return clazz; } private static void setFinalStaticValue(Class<?> srcClazz, String fieldName, Object newValue) throws Exception { Field field = srcClazz.getDeclaredField(fieldName); field.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & -17); field.set((Object)null, newValue); } private static <T> T getFieldValue(Class<?> srcClazz, String fieldName, Object owner, Class<T> dstClazz) throws Exception { Field field = srcClazz.getDeclaredField(fieldName); field.setAccessible(true); return dstClazz.cast(field.get(owner)); } }