1
0
mirror of synced 2026-03-25 14:18:45 +08:00

🆕 #3902 【小程序】新增人脸核身服务的接口实现

This commit is contained in:
Copilot
2026-03-06 12:16:00 +08:00
committed by GitHub
parent 960b54d605
commit 26224372d2
10 changed files with 495 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
package cn.binarywang.wx.miniapp.api;
import cn.binarywang.wx.miniapp.bean.face.WxMaFaceGetVerifyIdRequest;
import cn.binarywang.wx.miniapp.bean.face.WxMaFaceGetVerifyIdResponse;
import cn.binarywang.wx.miniapp.bean.face.WxMaFaceQueryVerifyInfoRequest;
import cn.binarywang.wx.miniapp.bean.face.WxMaFaceQueryVerifyInfoResponse;
import me.chanjar.weixin.common.error.WxErrorException;
/**
* 微信小程序人脸核身相关接口
* <p>
* 文档地址:<a href="https://developers.weixin.qq.com/miniprogram/dev/server/API/face/">微信人脸核身接口列表</a>
* </p>
*
* @author <a href="https://github.com/github-copilot">GitHub Copilot</a>
*/
public interface WxMaFaceService {
/**
* 获取用户人脸核身会话唯一标识
* <p>
* 业务方后台根据「用户实名信息(姓名+身份证)」调用 getVerifyId 接口获取人脸核身会话唯一标识 verifyId 字段,
* 然后给到小程序前端调用 wx.requestFacialVerify 接口使用。
* </p>
* <p>
* 文档地址:<a href="https://developers.weixin.qq.com/miniprogram/dev/server/API/face/api_getverifyid">获取用户人脸核身会话唯一标识</a>
* </p>
*
* @param request 请求参数
* @return 包含 verifyId 的响应实体
* @throws WxErrorException 调用微信接口失败时抛出
*/
WxMaFaceGetVerifyIdResponse getVerifyId(WxMaFaceGetVerifyIdRequest request) throws WxErrorException;
/**
* 查询用户人脸核身真实验证结果
* <p>
* 业务方后台根据人脸核身会话唯一标识 verifyId 字段调用 queryVerifyInfo 接口查询用户人脸核身真实验证结果。
* 核身通过的判断条件errcode=0 且 verify_ret=10000。
* </p>
* <p>
* 文档地址:<a href="https://developers.weixin.qq.com/miniprogram/dev/server/API/face/api_queryverifyinfo">查询用户人脸核身真实验证结果</a>
* </p>
*
* @param request 请求参数
* @return 包含 verifyRet 的响应实体
* @throws WxErrorException 调用微信接口失败时抛出
*/
WxMaFaceQueryVerifyInfoResponse queryVerifyInfo(WxMaFaceQueryVerifyInfoRequest request) throws WxErrorException;
}

View File

@@ -631,4 +631,13 @@ public interface WxMaService extends WxService {
* @return 用工关系服务对象WxMaEmployeeRelationService
*/
WxMaEmployeeRelationService getEmployeeRelationService();
/**
* 获取人脸核身服务对象。
* <br>
* 文档https://developers.weixin.qq.com/miniprogram/dev/server/API/face/
*
* @return 人脸核身服务对象WxMaFaceService
*/
WxMaFaceService getFaceService();
}

View File

@@ -169,6 +169,7 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
private final WxMaComplaintService complaintService = new WxMaComplaintServiceImpl(this);
private final WxMaEmployeeRelationService employeeRelationService =
new WxMaEmployeeRelationServiceImpl(this);
private final WxMaFaceService faceService = new WxMaFaceServiceImpl(this);
private Map<String, WxMaConfig> configMap = new HashMap<>();
private int retrySleepMillis = 1000;
@@ -1055,4 +1056,9 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
public WxMaEmployeeRelationService getEmployeeRelationService() {
return this.employeeRelationService;
}
@Override
public WxMaFaceService getFaceService() {
return this.faceService;
}
}

View File

@@ -0,0 +1,37 @@
package cn.binarywang.wx.miniapp.api.impl;
import cn.binarywang.wx.miniapp.api.WxMaFaceService;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.face.WxMaFaceGetVerifyIdRequest;
import cn.binarywang.wx.miniapp.bean.face.WxMaFaceGetVerifyIdResponse;
import cn.binarywang.wx.miniapp.bean.face.WxMaFaceQueryVerifyInfoRequest;
import cn.binarywang.wx.miniapp.bean.face.WxMaFaceQueryVerifyInfoResponse;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.common.error.WxErrorException;
import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.Face.GET_VERIFY_ID_URL;
import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.Face.QUERY_VERIFY_INFO_URL;
/**
* 微信小程序人脸核身相关接口实现
*
* @author <a href="https://github.com/github-copilot">GitHub Copilot</a>
*/
@RequiredArgsConstructor
public class WxMaFaceServiceImpl implements WxMaFaceService {
private final WxMaService service;
@Override
public WxMaFaceGetVerifyIdResponse getVerifyId(WxMaFaceGetVerifyIdRequest request)
throws WxErrorException {
String responseContent = this.service.post(GET_VERIFY_ID_URL, request.toJson());
return WxMaFaceGetVerifyIdResponse.fromJson(responseContent);
}
@Override
public WxMaFaceQueryVerifyInfoResponse queryVerifyInfo(WxMaFaceQueryVerifyInfoRequest request)
throws WxErrorException {
String responseContent = this.service.post(QUERY_VERIFY_INFO_URL, request.toJson());
return WxMaFaceQueryVerifyInfoResponse.fromJson(responseContent);
}
}

View File

@@ -0,0 +1,101 @@
package cn.binarywang.wx.miniapp.bean.face;
import cn.binarywang.wx.miniapp.json.WxMaGsonBuilder;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 获取用户人脸核身会话唯一标识 请求实体
* <p>
* 文档地址:<a href="https://developers.weixin.qq.com/miniprogram/dev/server/API/face/api_getverifyid">获取用户人脸核身会话唯一标识</a>
* </p>
*
* @author <a href="https://github.com/github-copilot">GitHub Copilot</a>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WxMaFaceGetVerifyIdRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:业务方系统内部流水号
* 是否必填:是
* 描述要求5-32个字符内只能包含数字、大小写字母和_-字符且在同一个appid下唯一
* </pre>
*/
@SerializedName("out_seq_no")
private String outSeqNo;
/**
* <pre>
* 字段名:用户身份信息
* 是否必填:是
* 描述:证件信息对象
* </pre>
*/
@SerializedName("cert_info")
private CertInfo certInfo;
/**
* <pre>
* 字段名:用户身份标识
* 是否必填:是
* 描述用户的openid
* </pre>
*/
@SerializedName("openid")
private String openid;
/**
* 用户身份信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class CertInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:证件类型
* 是否必填:是
* 描述:证件类型,身份证填 IDENTITY_CARD
* </pre>
*/
@SerializedName("cert_type")
private String certType;
/**
* <pre>
* 字段名:证件姓名
* 是否必填:是
* 描述证件上的姓名UTF-8编码
* </pre>
*/
@SerializedName("cert_name")
private String certName;
/**
* <pre>
* 字段名:证件号码
* 是否必填:是
* 描述:证件号码
* </pre>
*/
@SerializedName("cert_no")
private String certNo;
}
public String toJson() {
return WxMaGsonBuilder.create().toJson(this);
}
}

View File

@@ -0,0 +1,72 @@
package cn.binarywang.wx.miniapp.bean.face;
import cn.binarywang.wx.miniapp.json.WxMaGsonBuilder;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 获取用户人脸核身会话唯一标识 响应实体
* <p>
* 文档地址:<a href="https://developers.weixin.qq.com/miniprogram/dev/server/API/face/api_getverifyid">获取用户人脸核身会话唯一标识</a>
* </p>
*
* @author <a href="https://github.com/github-copilot">GitHub Copilot</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WxMaFaceGetVerifyIdResponse implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:错误码
* 是否必填:是
* 类型number
* 描述0表示成功其他值表示失败
* </pre>
*/
@SerializedName("errcode")
private Integer errcode;
/**
* <pre>
* 字段名:错误信息
* 是否必填:是
* 类型string
* 描述:错误信息描述
* </pre>
*/
@SerializedName("errmsg")
private String errmsg;
/**
* <pre>
* 字段名:人脸核身会话唯一标识
* 是否必填:否
* 类型string
* 描述微信侧生成的人脸核身会话唯一标识用于后续接口调用长度不超过256字符
* </pre>
*/
@SerializedName("verify_id")
private String verifyId;
/**
* <pre>
* 字段名:有效期
* 是否必填:否
* 类型number
* 描述verify_id有效期过期后无法发起核身默认值3600单位
* </pre>
*/
@SerializedName("expires_in")
private Integer expiresIn;
public static WxMaFaceGetVerifyIdResponse fromJson(String json) {
return WxMaGsonBuilder.create().fromJson(json, WxMaFaceGetVerifyIdResponse.class);
}
}

View File

@@ -0,0 +1,108 @@
package cn.binarywang.wx.miniapp.bean.face;
import cn.binarywang.wx.miniapp.json.WxMaGsonBuilder;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* 查询用户人脸核身真实验证结果 请求实体
* <p>
* 文档地址:<a href="https://developers.weixin.qq.com/miniprogram/dev/server/API/face/api_queryverifyinfo">查询用户人脸核身真实验证结果</a>
* </p>
*
* @author <a href="https://github.com/github-copilot">GitHub Copilot</a>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WxMaFaceQueryVerifyInfoRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:人脸核身会话唯一标识
* 是否必填:是
* 描述getVerifyId接口返回的人脸核身会话唯一标识
* </pre>
*/
@SerializedName("verify_id")
private String verifyId;
/**
* <pre>
* 字段名:业务方系统外部流水号
* 是否必填:是
* 描述必须和getVerifyId接口传入的out_seq_no一致
* </pre>
*/
@SerializedName("out_seq_no")
private String outSeqNo;
/**
* <pre>
* 字段名:证件信息摘要
* 是否必填:是
* 描述根据getVerifyId中传入的证件信息生成的信息摘要。
* 计算方式对cert_info中的cert_type、cert_name、cert_no字段内容进行标准base64编码
* 按顺序拼接cert_type=xxx&amp;cert_name=xxx&amp;cert_no=xxx再对拼接串进行SHA256输出十六进制小写结果
* </pre>
*/
@SerializedName("cert_hash")
private String certHash;
/**
* <pre>
* 字段名:用户身份标识
* 是否必填:是
* 描述必须和getVerifyId接口传入的openid一致
* </pre>
*/
@SerializedName("openid")
private String openid;
public String toJson() {
return WxMaGsonBuilder.create().toJson(this);
}
/**
* 计算证件信息摘要cert_hash
* <p>
* 计算规则(参见官方文档):
* 1. 对 cert_type、cert_name、cert_no 字段内容进行标准 base64 编码(若含中文等 Unicode 字符,先进行 UTF-8 编码)
* 2. 按顺序拼接各个字段cert_type=xxx&amp;cert_name=xxx&amp;cert_no=xxx
* 3. 对拼接串进行 SHA256 并输出十六进制小写结果
* </p>
*
* @param certType 证件类型
* @param certName 证件姓名
* @param certNo 证件号码
* @return cert_hash 十六进制小写字符串
*/
public static String calcCertHash(String certType, String certName, String certNo) {
String encodedType = Base64.getEncoder().encodeToString(certType.getBytes(StandardCharsets.UTF_8));
String encodedName = Base64.getEncoder().encodeToString(certName.getBytes(StandardCharsets.UTF_8));
String encodedNo = Base64.getEncoder().encodeToString(certNo.getBytes(StandardCharsets.UTF_8));
String raw = "cert_type=" + encodedType + "&cert_name=" + encodedName + "&cert_no=" + encodedNo;
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(raw.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hashBytes) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 algorithm not available", e);
}
}
}

View File

@@ -0,0 +1,73 @@
package cn.binarywang.wx.miniapp.bean.face;
import cn.binarywang.wx.miniapp.json.WxMaGsonBuilder;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 查询用户人脸核身真实验证结果 响应实体
* <p>
* 文档地址:<a href="https://developers.weixin.qq.com/miniprogram/dev/server/API/face/api_queryverifyinfo">查询用户人脸核身真实验证结果</a>
* </p>
*
* @author <a href="https://github.com/github-copilot">GitHub Copilot</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WxMaFaceQueryVerifyInfoResponse implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:错误码
* 是否必填:是
* 类型number
* 描述0表示成功其他值表示失败
* </pre>
*/
@SerializedName("errcode")
private Integer errcode;
/**
* <pre>
* 字段名:错误信息
* 是否必填:是
* 类型string
* 描述:错误信息描述
* </pre>
*/
@SerializedName("errmsg")
private String errmsg;
/**
* <pre>
* 字段名:人脸核身验证结果
* 是否必填:否
* 类型number
* 描述核身通过的判断条件errcode=0 且 verify_ret=10000
* 枚举值说明:
* 10000 - 识别成功
* 10001 - 参数错误
* 10002 - 人脸特征检测失败
* 10003 - 身份证号不匹配
* 10004 - 比对人脸信息不匹配
* 10005 - 正在检测中
* 10006 - appid没有权限
* 10300 - 未完成核身
* 90001 - 设备不支持人脸检测
* 90002 - 用户取消
* 其他枚举值请参见官方文档
* </pre>
*/
@SerializedName("verify_ret")
private Integer verifyRet;
public static WxMaFaceQueryVerifyInfoResponse fromJson(String json) {
return WxMaGsonBuilder.create().fromJson(json, WxMaFaceQueryVerifyInfoResponse.class);
}
}

View File

@@ -1018,4 +1018,17 @@ public class WxMaApiUrlConstants {
/** 推送用工消息 */
String SEND_EMPLOYEE_MSG_URL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/employeerelationmsg/send";
}
/**
* 微信人脸核身接口
* <pre>
* 文档地址: https://developers.weixin.qq.com/miniprogram/dev/server/API/face/
* </pre>
*/
public interface Face {
/** 获取用户人脸核身会话唯一标识 */
String GET_VERIFY_ID_URL = "https://api.weixin.qq.com/cityservice/face/identify/getverifyid";
/** 查询用户人脸核身真实验证结果 */
String QUERY_VERIFY_INFO_URL = "https://api.weixin.qq.com/cityservice/face/identify/queryverifyinfo";
}
}

View File

@@ -0,0 +1,25 @@
package cn.binarywang.wx.miniapp.api.impl;
import cn.binarywang.wx.miniapp.bean.face.WxMaFaceQueryVerifyInfoRequest;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
/**
* 微信小程序人脸核身服务本地计算测试类
*
* @author <a href="https://github.com/github-copilot">GitHub Copilot</a>
*/
@Test
public class WxMaFaceServiceImplTest {
@Test
public void testCalcCertHash() {
// 验证官方文档给出的测试用例:
// cert_info: {"cert_type":"IDENTITY_CARD","cert_name":"张三","cert_no":"310101199801011234"}
// 期望结果3c241f7ff324977aeb91f173bb2a7b06569e6fd784d5573db34a636d8671108b
String certHash = WxMaFaceQueryVerifyInfoRequest.calcCertHash(
"IDENTITY_CARD", "张三", "310101199801011234");
assertEquals(certHash, "3c241f7ff324977aeb91f173bb2a7b06569e6fd784d5573db34a636d8671108b");
}
}