diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaFaceService.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaFaceService.java new file mode 100644 index 000000000..1b8d3cd74 --- /dev/null +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaFaceService.java @@ -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; + +/** + * 微信小程序人脸核身相关接口 + *

+ * 文档地址:微信人脸核身接口列表 + *

+ * + * @author GitHub Copilot + */ +public interface WxMaFaceService { + + /** + * 获取用户人脸核身会话唯一标识 + *

+ * 业务方后台根据「用户实名信息(姓名+身份证)」调用 getVerifyId 接口获取人脸核身会话唯一标识 verifyId 字段, + * 然后给到小程序前端调用 wx.requestFacialVerify 接口使用。 + *

+ *

+ * 文档地址:获取用户人脸核身会话唯一标识 + *

+ * + * @param request 请求参数 + * @return 包含 verifyId 的响应实体 + * @throws WxErrorException 调用微信接口失败时抛出 + */ + WxMaFaceGetVerifyIdResponse getVerifyId(WxMaFaceGetVerifyIdRequest request) throws WxErrorException; + + /** + * 查询用户人脸核身真实验证结果 + *

+ * 业务方后台根据人脸核身会话唯一标识 verifyId 字段调用 queryVerifyInfo 接口查询用户人脸核身真实验证结果。 + * 核身通过的判断条件:errcode=0 且 verify_ret=10000。 + *

+ *

+ * 文档地址:查询用户人脸核身真实验证结果 + *

+ * + * @param request 请求参数 + * @return 包含 verifyRet 的响应实体 + * @throws WxErrorException 调用微信接口失败时抛出 + */ + WxMaFaceQueryVerifyInfoResponse queryVerifyInfo(WxMaFaceQueryVerifyInfoRequest request) throws WxErrorException; + +} diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java index 37a6ca8de..730a8c584 100644 --- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java @@ -631,4 +631,13 @@ WxMaApiResponse execute( * @return 用工关系服务对象WxMaEmployeeRelationService */ WxMaEmployeeRelationService getEmployeeRelationService(); + + /** + * 获取人脸核身服务对象。 + *
+ * 文档:https://developers.weixin.qq.com/miniprogram/dev/server/API/face/ + * + * @return 人脸核身服务对象WxMaFaceService + */ + WxMaFaceService getFaceService(); } diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/BaseWxMaServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/BaseWxMaServiceImpl.java index c0e1ff4a4..47ce08bef 100644 --- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/BaseWxMaServiceImpl.java +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/BaseWxMaServiceImpl.java @@ -169,6 +169,7 @@ public abstract class BaseWxMaServiceImpl 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 configMap = new HashMap<>(); private int retrySleepMillis = 1000; @@ -1055,4 +1056,9 @@ public WxMaComplaintService getComplaintService() { public WxMaEmployeeRelationService getEmployeeRelationService() { return this.employeeRelationService; } + + @Override + public WxMaFaceService getFaceService() { + return this.faceService; + } } diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaFaceServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaFaceServiceImpl.java new file mode 100644 index 000000000..7b3e2a1a5 --- /dev/null +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaFaceServiceImpl.java @@ -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 GitHub Copilot + */ +@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); + } +} diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceGetVerifyIdRequest.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceGetVerifyIdRequest.java new file mode 100644 index 000000000..4d83ba808 --- /dev/null +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceGetVerifyIdRequest.java @@ -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; + +/** + * 获取用户人脸核身会话唯一标识 请求实体 + *

+ * 文档地址:获取用户人脸核身会话唯一标识 + *

+ * + * @author GitHub Copilot + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WxMaFaceGetVerifyIdRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:业务方系统内部流水号
+   * 是否必填:是
+   * 描述:要求5-32个字符内,只能包含数字、大小写字母和_-字符,且在同一个appid下唯一
+   * 
+ */ + @SerializedName("out_seq_no") + private String outSeqNo; + + /** + *
+   * 字段名:用户身份信息
+   * 是否必填:是
+   * 描述:证件信息对象
+   * 
+ */ + @SerializedName("cert_info") + private CertInfo certInfo; + + /** + *
+   * 字段名:用户身份标识
+   * 是否必填:是
+   * 描述:用户的openid
+   * 
+ */ + @SerializedName("openid") + private String openid; + + /** + * 用户身份信息 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CertInfo implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+     * 字段名:证件类型
+     * 是否必填:是
+     * 描述:证件类型,身份证填 IDENTITY_CARD
+     * 
+ */ + @SerializedName("cert_type") + private String certType; + + /** + *
+     * 字段名:证件姓名
+     * 是否必填:是
+     * 描述:证件上的姓名,UTF-8编码
+     * 
+ */ + @SerializedName("cert_name") + private String certName; + + /** + *
+     * 字段名:证件号码
+     * 是否必填:是
+     * 描述:证件号码
+     * 
+ */ + @SerializedName("cert_no") + private String certNo; + } + + public String toJson() { + return WxMaGsonBuilder.create().toJson(this); + } +} diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceGetVerifyIdResponse.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceGetVerifyIdResponse.java new file mode 100644 index 000000000..8fe8b191a --- /dev/null +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceGetVerifyIdResponse.java @@ -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; + +/** + * 获取用户人脸核身会话唯一标识 响应实体 + *

+ * 文档地址:获取用户人脸核身会话唯一标识 + *

+ * + * @author GitHub Copilot + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class WxMaFaceGetVerifyIdResponse implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:错误码
+   * 是否必填:是
+   * 类型:number
+   * 描述:0表示成功,其他值表示失败
+   * 
+ */ + @SerializedName("errcode") + private Integer errcode; + + /** + *
+   * 字段名:错误信息
+   * 是否必填:是
+   * 类型:string
+   * 描述:错误信息描述
+   * 
+ */ + @SerializedName("errmsg") + private String errmsg; + + /** + *
+   * 字段名:人脸核身会话唯一标识
+   * 是否必填:否
+   * 类型:string
+   * 描述:微信侧生成的人脸核身会话唯一标识,用于后续接口调用,长度不超过256字符
+   * 
+ */ + @SerializedName("verify_id") + private String verifyId; + + /** + *
+   * 字段名:有效期
+   * 是否必填:否
+   * 类型:number
+   * 描述:verify_id有效期,过期后无法发起核身,默认值3600,单位:秒
+   * 
+ */ + @SerializedName("expires_in") + private Integer expiresIn; + + public static WxMaFaceGetVerifyIdResponse fromJson(String json) { + return WxMaGsonBuilder.create().fromJson(json, WxMaFaceGetVerifyIdResponse.class); + } +} diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceQueryVerifyInfoRequest.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceQueryVerifyInfoRequest.java new file mode 100644 index 000000000..7ab8f7fbf --- /dev/null +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceQueryVerifyInfoRequest.java @@ -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; + +/** + * 查询用户人脸核身真实验证结果 请求实体 + *

+ * 文档地址:查询用户人脸核身真实验证结果 + *

+ * + * @author GitHub Copilot + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WxMaFaceQueryVerifyInfoRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:人脸核身会话唯一标识
+   * 是否必填:是
+   * 描述:getVerifyId接口返回的人脸核身会话唯一标识
+   * 
+ */ + @SerializedName("verify_id") + private String verifyId; + + /** + *
+   * 字段名:业务方系统外部流水号
+   * 是否必填:是
+   * 描述:必须和getVerifyId接口传入的out_seq_no一致
+   * 
+ */ + @SerializedName("out_seq_no") + private String outSeqNo; + + /** + *
+   * 字段名:证件信息摘要
+   * 是否必填:是
+   * 描述:根据getVerifyId中传入的证件信息生成的信息摘要。
+   *       计算方式:对cert_info中的cert_type、cert_name、cert_no字段内容进行标准base64编码,
+   *       按顺序拼接:cert_type=xxx&cert_name=xxx&cert_no=xxx,再对拼接串进行SHA256输出十六进制小写结果
+   * 
+ */ + @SerializedName("cert_hash") + private String certHash; + + /** + *
+   * 字段名:用户身份标识
+   * 是否必填:是
+   * 描述:必须和getVerifyId接口传入的openid一致
+   * 
+ */ + @SerializedName("openid") + private String openid; + + public String toJson() { + return WxMaGsonBuilder.create().toJson(this); + } + + /** + * 计算证件信息摘要(cert_hash) + *

+ * 计算规则(参见官方文档): + * 1. 对 cert_type、cert_name、cert_no 字段内容进行标准 base64 编码(若含中文等 Unicode 字符,先进行 UTF-8 编码) + * 2. 按顺序拼接各个字段:cert_type=xxx&cert_name=xxx&cert_no=xxx + * 3. 对拼接串进行 SHA256 并输出十六进制小写结果 + *

+ * + * @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); + } + } +} diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceQueryVerifyInfoResponse.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceQueryVerifyInfoResponse.java new file mode 100644 index 000000000..85d984e1d --- /dev/null +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceQueryVerifyInfoResponse.java @@ -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; + +/** + * 查询用户人脸核身真实验证结果 响应实体 + *

+ * 文档地址:查询用户人脸核身真实验证结果 + *

+ * + * @author GitHub Copilot + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class WxMaFaceQueryVerifyInfoResponse implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:错误码
+   * 是否必填:是
+   * 类型:number
+   * 描述:0表示成功,其他值表示失败
+   * 
+ */ + @SerializedName("errcode") + private Integer errcode; + + /** + *
+   * 字段名:错误信息
+   * 是否必填:是
+   * 类型:string
+   * 描述:错误信息描述
+   * 
+ */ + @SerializedName("errmsg") + private String errmsg; + + /** + *
+   * 字段名:人脸核身验证结果
+   * 是否必填:否
+   * 类型:number
+   * 描述:核身通过的判断条件:errcode=0 且 verify_ret=10000
+   *       枚举值说明:
+   *       10000 - 识别成功
+   *       10001 - 参数错误
+   *       10002 - 人脸特征检测失败
+   *       10003 - 身份证号不匹配
+   *       10004 - 比对人脸信息不匹配
+   *       10005 - 正在检测中
+   *       10006 - appid没有权限
+   *       10300 - 未完成核身
+   *       90001 - 设备不支持人脸检测
+   *       90002 - 用户取消
+   *       其他枚举值请参见官方文档
+   * 
+ */ + @SerializedName("verify_ret") + private Integer verifyRet; + + public static WxMaFaceQueryVerifyInfoResponse fromJson(String json) { + return WxMaGsonBuilder.create().fromJson(json, WxMaFaceQueryVerifyInfoResponse.class); + } +} diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaApiUrlConstants.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaApiUrlConstants.java index 58b10039a..40633ea6d 100644 --- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaApiUrlConstants.java +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaApiUrlConstants.java @@ -1018,4 +1018,17 @@ public interface Employee { /** 推送用工消息 */ String SEND_EMPLOYEE_MSG_URL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/employeerelationmsg/send"; } + + /** + * 微信人脸核身接口 + *
+   * 文档地址: https://developers.weixin.qq.com/miniprogram/dev/server/API/face/
+   * 
+ */ + 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"; + } } diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaFaceServiceImplTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaFaceServiceImplTest.java new file mode 100644 index 000000000..77dea4610 --- /dev/null +++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaFaceServiceImplTest.java @@ -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 GitHub Copilot + */ +@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"); + } +}