破解招采围标排查痛点:从离线人工调档到多层股权图谱直连
在集团型央企与大型国企的集中招标采购体系中,供应商准入合规审计与同标段围标串标关联排查是保障阳光采购的核心防线。在实际招采业务中,部分投标主体表面上拥有独立的工商登记信息与法人代表,但在多层级的股权架构背后,可能由同一自然人股东、同一投资机构或存在交叉持股关系的母公司实际控制。传统的合规审查高度依赖评标专家或风控专员在开标前人工查阅纸质章程、手动检索工商公示信息,面对动辄三四层以上的嵌套控股结构与数十家同标段竞标企业,人工核对不仅耗时费力、响应滞后,更难以在短时间内精准识别跨层级的隐性关联关系。
为了在招采网关层实现自动化的关联关系穿透,微服务架构可集成天远股权穿透接口直接调取权威工商股权链路数据。在取得投标企业签署的合规授权书后,Java 招采风控后端只需传入目标供应商的 ent_code(企业编码)、flag(穿透层次,最高支持 4 层)、dir(穿透方向,up 向上查股东、down 向下查对外投资)以及 min_percent 与 max_percent(股权穿透比例上下限区间),即可通过加密通道获取结构化的股权节点列表。解密后的响应报文不仅包含各层级节点的 name(公司或人名)、lable(对象类型:Company 公司、Human 自然人、Other 其他)与 percent(持股占比),还提供了唯一标识企业节点的 creditCode(统一社会信用代码)、唯一标识自然人节点的 pid(自然人 pid)、反映主体存续健康的 regStatus(企业状态),以及指示当前节点在设定层级外是否仍存在下属或上级节点的 open(true-可延伸、false-不可延伸)状态位。
将天远股权穿透能力无缝嵌入基于 Spring Boot 的央企供应商准入与评标风控微服务中,能够把原本离散的单点企业资质核查升级为多维度的股权关系图谱比对。系统可在投标报名截止与资格预审阶段,自动对同一标段的所有报名供应商执行向上 4 层股权穿透,利用集合交集算法比对各供应商背后的 pid 与 creditCode,并在毫秒级时间内输出关联关系审计报告,实现前置准入校验与人工复核提醒的有机闭环。
1. Java 加密通信集成:构建高可用审核管道
1. 核心参数与加密配置
- 接口地址 :
https://api.tianyuanapi.com/api/v1/QYGLP0HT(需在 URL 附加?t=13位时间戳) - 请求方式 :
POST - 请求头 :
Access-Id: 账号的 Access-Id (必填)Content-Type:application/json
- 关键入参 :
ent_code: 企业编码,支持统一社会信用代码或工商注册编码(必填)flag: 股权穿透层次,数值型,最大支持4层(必填)dir: 穿透方向,可选值为up(向上穿透查询股东及实际控制脉络)、down(向下穿透查询控股及对外投资子公司)(必填)min_percent: 股权穿透比例下限,字符串格式(大于等于该比例,如"0.05"代表持股 5% 及以上)(必填)max_percent: 股权穿透比例上限,字符串格式(小于等于该比例,如"1.0"代表持股 100% 及以下)(必填)
- 鉴权与加密机制 : 使用账户的 16 进制 Access Key 作为密钥,采用 AES-128 算法的 CBC 模式。每次请求需动态生成 16 字节的 IV(初始化向量),并配合 PKCS7 填充,最终将 IV 与密文拼接后进行 Base64 编码放入请求体
data字段中。
2. 标准化调用代码 (Java)
以下完整代码展示了如何在集团央企招采合规微服务中,封装 AES-128-CBC 加解密管道,并对同一标段的两家投标供应商执行向上 4 层股权穿透,自动比对是否存在重叠的自然人股东(pid)或法人股东(creditCode):
java
package com.tianyuan.bidding.compliance;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
/**
* 集团型央企供应商准入合规审计与围标串标关联排查客户端
* 对接接口:天远股权穿透 (QYGLP0HT)
*/
public class SupplierEquityPenetrationService {
private static final String BASE_URL = "https://api.tianyuanapi.com/api/v1/QYGLP0HT";
private static final String AES_TRANSFORMATION = "AES/CBC/PKCS5Padding"; // JVM中PKCS5Padding与16字节块PKCS7等价
private static final int IV_LENGTH = 16;
private final String accessId;
private final byte[] accessKeyBytes;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
private final SecureRandom secureRandom;
public SupplierEquityPenetrationService(String accessId, String hexAccessKey) {
this.accessId = Objects.requireNonNull(accessId, "Access-Id 不能为空");
this.accessKeyBytes = hexToBytes(hexAccessKey);
if (this.accessKeyBytes.length != 16) {
throw new IllegalArgumentException("AES-128 密钥长度必须为 16 字节 (32位十六进制字符)");
}
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
this.objectMapper = new ObjectMapper();
this.secureRandom = new SecureRandom();
}
/**
* 股权穿透请求参数模型
*/
public record EquityQueryRequest(
String ent_code,
int flag,
String dir,
String min_percent,
String max_percent
) {}
/**
* 股权穿透响应节点模型(严格映射接口返回字段)
* 注意:接口文档中对象类型字段名为 lable
*/
public record EquityNode(
String name,
Long id,
String pid,
String lable,
String creditCode,
String regStatus,
String open,
Double percent
) {}
/**
* 执行单家投标供应商的股权穿透查询
*/
public List<EquityNode> querySupplierEquityTree(
String entCode, int maxLayer, String direction, String minPercent, String maxPercent) throws Exception {
if (maxLayer < 1 || maxLayer > 4) {
throw new IllegalArgumentException("穿透层次 flag 必须在 1 到 4 之间");
}
// 1. 组装业务参数并序列化为 JSON
EquityQueryRequest bizParam = new EquityQueryRequest(entCode, maxLayer, direction, minPercent, maxPercent);
String plainJson = objectMapper.writeValueAsString(bizParam);
// 2. AES-128-CBC 加密:随机生成16字节IV,将 IV + 密文 拼接后进行 Base64 编码
String encryptedData = encryptPayload(plainJson);
String requestBody = objectMapper.writeValueAsString(Collections.singletonMap("data", encryptedData));
// 3. 构造带 13 位毫秒时间戳的请求 URL
long timestamp = System.currentTimeMillis();
URI uri = URI.create(BASE_URL + "?t=" + timestamp);
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(uri)
.timeout(Duration.ofSeconds(10))
.header("Access-Id", this.accessId)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
.build();
// 4. 发送请求并解析外层公共响应参数
HttpResponse<String> response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (response.statusCode() != 200) {
throw new RuntimeException("HTTP 响应状态异常: " + response.statusCode());
}
JsonNode rootNode = objectMapper.readTree(response.body());
int code = rootNode.path("code").asInt(-1);
String message = rootNode.path("message").asText();
String transactionId = rootNode.path("transaction_id").asText();
if (code != 0 && code != 200) {
throw new RuntimeException(String.format("股权穿透调用未通过 [流水号=%s, code=%d, message=%s]",
transactionId, code, message));
}
String encryptedResult = rootNode.path("data").asText();
if (encryptedResult == null || encryptedResult.isBlank()) {
return Collections.emptyList();
}
// 5. 解密 data 密文并反序列化为股权节点列表
String decryptedJson = decryptPayload(encryptedResult);
JsonNode dataNode = objectMapper.readTree(decryptedJson);
if (dataNode.isArray()) {
return objectMapper.readValue(decryptedJson, new TypeReference<List<EquityNode>>() {});
} else if (dataNode.has("list") && dataNode.get("list").isArray()) {
return objectMapper.convertValue(dataNode.get("list"), new TypeReference<List<EquityNode>>() {});
}
return Collections.singletonList(objectMapper.treeToValue(dataNode, EquityNode.class));
}
/**
* 同标段两家投标企业向上股权穿透交叉排查(识别共同自然人股东或共同控股母公司)
*/
public void auditBiddingAssociation(String bidderACode, String bidderBCode) throws Exception {
// 向上穿透4层,排查持股比例 5% ~ 100% 的关键出资方
List<EquityNode> nodesA = querySupplierEquityTree(bidderACode, 4, "up", "0.05", "1.0");
List<EquityNode> nodesB = querySupplierEquityTree(bidderBCode, 4, "up", "0.05", "1.0");
// 提取投标人A背后的自然人股东 pid 集合与企业股东 creditCode 集合
Set<String> humanPidsA = nodesA.stream()
.filter(n -> "Human".equalsIgnoreCase(n.lable()) && n.pid() != null && !n.pid().isBlank())
.map(EquityNode::pid)
.collect(Collectors.toSet());
Set<String> companyCreditCodesA = nodesA.stream()
.filter(n -> "Company".equalsIgnoreCase(n.lable()) && n.creditCode() != null && !n.creditCode().isBlank())
.map(EquityNode::creditCode)
.collect(Collectors.toSet());
// 交叉比对投标人B的向上穿透节点
for (EquityNode nodeB : nodesB) {
if ("Human".equalsIgnoreCase(nodeB.lable()) && humanPidsA.contains(nodeB.pid())) {
System.out.printf("[合规复核提醒] 发现同标段供应商存在共同自然人出资方: 姓名=%s, 脱敏PID=%s, 持股占比=%.2f%%%n",
maskName(nodeB.name()), maskPid(nodeB.pid()), nodeB.percent() * 100);
}
if ("Company".equalsIgnoreCase(nodeB.lable()) && companyCreditCodesA.contains(nodeB.creditCode())) {
System.out.printf("[合规复核提醒] 发现同标段供应商存在共同法人股东: 企业名=%s, 统一社会信用代码=%s, 状态=%s, 是否可继续延伸=%s%n",
nodeB.name(), nodeB.creditCode(), nodeB.regStatus(), nodeB.open());
}
}
}
private String encryptPayload(String plainText) throws Exception {
byte[] iv = new byte[IV_LENGTH];
secureRandom.nextBytes(iv);
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
SecretKeySpec keySpec = new SecretKeySpec(this.accessKeyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] cipherBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
byte[] combined = new byte[IV_LENGTH + cipherBytes.length];
System.arraycopy(iv, 0, combined, 0, IV_LENGTH);
System.arraycopy(cipherBytes, 0, combined, IV_LENGTH, cipherBytes.length);
return Base64.getEncoder().encodeToString(combined);
}
private String decryptPayload(String base64CipherText) throws Exception {
byte[] combined = Base64.getDecoder().decode(base64CipherText);
if (combined.length <= IV_LENGTH) {
throw new IllegalArgumentException("加密响应数据长度不足16字节IV");
}
byte[] iv = Arrays.copyOfRange(combined, 0, IV_LENGTH);
byte[] cipherBytes = Arrays.copyOfRange(combined, IV_LENGTH, combined.length);
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
SecretKeySpec keySpec = new SecretKeySpec(this.accessKeyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] plainBytes = cipher.doFinal(cipherBytes);
return new String(plainBytes, StandardCharsets.UTF_8);
}
private static byte[] hexToBytes(String hex) {
if (hex == null || hex.length() % 2 != 0) {
throw new IllegalArgumentException("非法的十六进制密钥字符串");
}
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16));
}
return data;
}
private static String maskName(String name) {
if (name == null || name.isEmpty()) return "*";
return name.charAt(0) + "*".repeat(Math.max(1, name.length() - 1));
}
private static String maskPid(String pid) {
if (pid == null || pid.length() < 8) return "****";
return pid.substring(0, 4) + "****" + pid.substring(pid.length() - 4);
}
public static void main(String[] args) throws Exception {
String accessId = System.getenv().getOrDefault("TIANYUAN_ACCESS_ID", "TY_DEMO_ACCESS_ID");
String accessKeyHex = System.getenv().getOrDefault("TIANYUAN_ACCESS_KEY", "0123456789abcdef0123456789abcdef");
SupplierEquityPenetrationService service = new SupplierEquityPenetrationService(accessId, accessKeyHex);
// 示例:对同标段两家投标企业统一社会信用代码执行向上4层穿透排查
service.auditBiddingAssociation("91110108MA0012345X", "91110108MA0067890Y");
}
}
3. 终端快捷验证 (cURL)
在接入联调阶段,研发人员可先将序列化并完成 AES-128-CBC 加密后的 Base64 载荷通过 cURL 工具直接发送至网关进行连通性验证:
bash
# 明文请求示例: {"ent_code":"91110108MA0012345X","flag":4,"dir":"up","min_percent":"0.05","max_percent":"1.0"}
curl -X POST "https://api.tianyuanapi.com/api/v1/QYGLP0HT?t=1727520000000" \
-H "Access-Id: YOUR_ACCESS_ID" \
-H "Content-Type: application/json" \
-d '{
"data": "5f8a9b1c2d3e4f5061728394a5b6c7d8U2FsdGVkX1+vupppZksvRf5pq5g5XjFRlipRkwB0K1Y="
}'
接口返回的公共响应外层包含 transaction_id 与加密的 data 字符串,对其进行 Base64 解码并截取前 16 字节 IV 解密后,即可得到如下结构的明文 JSON 数据:
json
[
{
"name": "华曜实业控股集团有限公司",
"id": 908172635,
"pid": "",
"lable": "Company",
"creditCode": "91110000710928374K",
"regStatus": "存续(在营、开业、在册)",
"open": "true",
"percent": 0.65
},
{
"name": "周明远",
"id": 102938475,
"pid": "P_88f91a04c7e24b119d30",
"lable": "Human",
"creditCode": "",
"regStatus": "",
"open": "false",
"percent": 0.35
}
]
2. 核心股权穿透数据解析与业务映射
在构建央企供应商准入合规与围标排查图谱时,需要将解密后的股权节点字段准确映射到领域模型中。下表详细说明了各响应字段的技术定义及业务处理逻辑:
| 字段名 | 字段类型 | 长度/格式 | 官方字段说明 | 招采合规与围标排查映射逻辑 |
|---|---|---|---|---|
name |
String |
varchar(255) |
公司或人名 | 穿透节点的展示名称;若 lable 为 Human,在写入审计日志时需进行姓名掩码处理。 |
id |
Number |
整型/长整型 | 公司或人id | 底层企业库或人员库内部的唯一数字标识,可用于构建本地内存图数据库(如 Neo4j / JGraphT)的顶点主键。 |
pid |
String |
varchar(100) |
自然人pid | 自然人股东的跨企业唯一标识。核心防同名误判字段 :当不同投标企业的自然人股东姓名相同,需对比 pid 是否一致来判定是否为同一自然人。 |
lable |
String |
varchar(20) |
对象类型 | 枚举值:Company-公司,Human-人,Other-其他(如事业单位、社团法人或境外特殊主体)。注意字段拼写为 lable,定义 Jackson 映射时不可误写为 label。 |
creditCode |
String |
varchar(50) |
统一社会信用代码 | 当 lable 为 Company 时的 18 位标准信用代码,用于跨标段比对法人股东重合度,以及关联集团内部黑名单或回避主体库。 |
regStatus |
String |
varchar(50) |
企业状态 | 反映法人股东当前的工商登记状态(如"存续"、"在业"、"吊销"、"注销")。若控股母公司处于非存续状态,需触发供应商资信复核提醒。 |
open |
String |
varchar(6) |
是否可延伸 | 枚举值:true-可延伸,false-不可延伸。当请求 flag=4 到达第 4 层边界且某法人股东的 open 仍为 "true" 时,表明该节点上方仍有更深层的股东结构,可将该节点的 creditCode 作为新入参 ent_code 发起二次接力穿透。 |
percent |
double |
double |
占比 | 当前节点对下级(或上级)主体的持股比例,用于计算间接持股权重及识别超过重大影响阈值(如 5% 或 25%)的受益所有人。 |
技术提示 :在招采合规审计与日志落盘过程中,务必遵循最小必要原则对个人敏感信息(PII)进行脱敏处理。例如自然人股东姓名展示为
周*远、自然人唯一标识pid掩码为P_88****9d30,若业务上下文关联了经办人手机号,需统一脱敏为138****0000格式后再输出至监控终端或审计报表。同时需要注意接口返回的实体类型字段名称严格为lable(非label),在定义 Java Record 或 POJO 属性时需保持一致,或使用@JsonProperty("lable")显式绑定。
3. 场景化应用:让核验数据赋能合规闭环
1. 同标段投标主体隐性控制人交叉比对(围标串标前置排查)
在央企工程建设与大宗物资采购项目中,招标文件通常明确规定"单位负责人为同一人或者存在控股、管理关系的不同单位,不得参加同一标段投标"。在资格预审阶段,招采微服务针对同一标段报名的 NNN 家供应商,统一以 dir="up"、flag=4、min_percent="0.01"、max_percent="1.0" 调用天远股权穿透接口:
- 通过/匹配流转 :若所有投标供应商向上 4 层穿透所得的
Company节点creditCode集合与Human节点pid集合两两之间均无交集,系统自动标记"股权独立性核验通过",供应商顺畅进入评标环节。 - 引导补充验证/转人工复核 :一旦算法检测到投标人甲与投标人乙在第 2 至第 4 层出现了相同的自然人
pid(且累计或单层percent较高)或相同的控股母公司creditCode,系统立即生成可视化的股权交叉路径图,将该标段标记为"疑似关联投标",推送至招采合规委员会工作台进行人工复核提醒,并要求相关投标主体在线补充提交独立经营声明及非一致行动人证明文件。
2. 超深层复杂股权链条(open="true")接力穿透与母公司经营状态(regStatus)评估
部分大型工程承包商或跨境合资供应商的股权结构往往超过 4 层,或者通过多层有限合伙企业(投资平台)嵌套持股。单次调用 flag=4 可能尚未触达最终自然人受益人:
- 通过/匹配流转 :当向上穿透至顶层节点时,所有顶层节点的
open字段均为"false"(表明已穿透至自然人、国资委或最终端主体),且链路中所有lable="Company"的控股节点regStatus均为"存续/在营",系统直接完成最终受益人(UBO)归档。 - 引导补充验证/转人工复核 :若第 4 层某个核心控股股东(如
percent >= 0.25)的open字段返回"true",Java 审计服务会自动提取该第 4 层节点的creditCode作为新的ent_code执行第二轮向上穿透;若接力穿透后仍存在lable="Other"(如境外离岸主体或特殊资管计划)导致无法确认最终自然人,或者中间控股母公司的regStatus显示为"吊销/清算/迁出",系统将自动暂缓自动入库流程,引导供应商在线补充披露完整的最终受益人架构图,并交由法务合规岗介入审查。
3. 集团内部关联方回避与阳光采购双向穿透核查
大型央企集团往往拥有数百家二三级全资或控股子公司,集团招采制度严格要求防范未经审批的内部关联方违规参与外部公开招标,或供应商与本集团内部管理人员存在利益交织:
- 通过/匹配流转 :在供应商准入建档时,系统一方面通过
dir="up"向上核查其股东中是否包含集团内部员工的pid,另一方面通过dir="down"、flag=3向下核查该供应商对外控股的子公司(creditCode)是否与本集团下属单位存在合资重叠。若双向穿透均无命中集团回避名录,准入申请自动流转放行。 - 引导补充验证/转人工复核 :若穿透结果命中集团下属参股企业
creditCode或特定关联自然人pid,系统会自动触发阳光招采利益冲突申报流程,提醒采购组织方按规定启动关联交易专项审批或专家回避程序。
4. 生产环境接入的安全与合规边界
- 隐私授权与合规留痕 :
在发起股权穿透与自然人股东关联排查前,招采门户必须在《供应商入驻及投标合规承诺书》中取得投标企业的明确授权。每次调用接口时,应将请求入参摘要與响应报文中的transaction_id(流水号)绑定存储至合规审计表中,确保每一笔股权穿透核查均可追溯、可举证。 - 密文传输与敏感字段脱敏存储 :
天远股权穿透接口全链路采用 AES-128-CBC + 动态 16 字节随机 IV 机制加密传输。在企业内部微服务间流转及落库时,严禁将 Access Key 硬编码在业务源码或配置文件中,应通过 KMS(密钥管理系统)或环境变量注入;对于解析出的自然人pid与姓名,在非核心审计视图及通用应用日志中必须执行掩码脱敏。 - 限流控制、超时熔断与图谱缓存 :
由于多层股权穿透(尤其是flag=4)涉及大量底层工商关系图谱的实时聚合计算,建议在 Java 客户端将连接超时设为 5 秒、读取超时设为 10 秒,并配合 Resilience4j 配置合理的并发舱壁与重试退避策略。对于同一家供应商在同一个招标周期(如 24 小时内)参与多个标段投标的场景,可以ent_code + flag + dir + min_percent + max_percent为组合键在 Redis 中缓存加密后的穿透结果,避免重复调用造成配额浪费并显著降低开标高峰期的响应延迟。