1、申请密钥
2、验证本地服务器与乐企服务器的连通性
乐企服务器生产和测试域名均为:https://lqpt.chinatax.gov.cn:8443。开发者可以在“能力中心”查看基础公用能力详情,按照能力接入和开发指引完成接口对接,验证服务器连通性和证书配置正确性。
能力编码:能力是乐企平台提供某一具体业务的形式,某一具体能力由一个或多个相互关联的服务(接口)和调用规则组成,能力编码是乐企平台对某一具体能力的唯一识别编号,可在能力订阅请求页面和测试中心详情页查询:
服务编码:服务是构成乐企平台某一具体能力的接口,服务(接口)分为实时和异步,实时接口可实时返回请求所需数据,异步接口返回流水号或批次流水号,可在处理完成后调用对应查询接口获得所需数据,服务编码是乐企平台某一具体能力中的服务(接口)的唯一识别码,可在能力详情页面和测试中心详情页查询。
用例编码:用例是乐企能力订阅沙箱测试的场景及要求描述,用例编码是乐企平台某一具体能力用例的唯一识别码,可在测试中心详情页查询。用例编码在沙箱能力请求的报文头中,和沙箱环境测试标识配套,两者互斥使用,用例编码仅在乐企沙箱订阅测试使用。
接口开发背景:
是帮助别的公司搭建乐企平台,客户在申请乐企资质的时候 填写的ip是他们自己服务器的ip(内网ip)还需要使用VPN访问,这就给本地开发测试造成很大的麻烦,我们这边的解决思路是:在客户的内网环境的nginx配置代理:
location /leqi-api/ {
proxy_pass https://lqpt.chinatax.gov.cn:8843/
}
之后本地通过url进行请求,url对应的ip是客户的内网ip(但凡是公网ip都不用这么麻烦!)
3、后端代码
基础入参:
@Data
public class BaseLeQiReq {
private String url;
/**
* 能力编码
*/
private String nlbm;
/**
* 服务编码
*/
private String fwbm;
/**
* 用例编码
*/
private String ylbm;
/**
* 接入单位id
*/
private String jrdybm;
/**
* 使用单位id
*/
private String sydwbm;
/**
* 请求参数(单个对象)
*/
private JSONObject param;
/**
* 请求参数(数组)
*/
private List<JSONObject> paramList;
}
基础出参
@Data
public class BaseLeQiResp {
private ResponseData Response;
@Data
public static class ResponseData {
private String RequestId;
/**
* 成功数据
* returncode: "00"
*/
private String Data;
/**
* 失败数据(存在Error节点标识失败)
*/
private JSONObject Error;
}
}
加密工具类(直接按照乐企平台给出的示例代码即可)
import org.apache.commons.lang3.ObjectUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import org.bouncycastle.util.encoders.Hex;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Base64;
/**
* User: yanjun.hou
* Date: 2024/8/27 09:51
* Description:
*/
public class SM4Util {
static {
Security.addProvider(new BouncyCastleProvider());
}
private static final Charset ENCODING = StandardCharsets.UTF_8;
public static final String ALGORITHM_NAME = "SM4";
// 加密算法/分组加密模式/分组填充方式
// PKCS5Padding-以8个字节为一组进行分组加密
// 定义分组加密模式使用:PKCS5Padding
public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";
/**
* 生成ECB暗号
*
* @param algorithmName 算法名称
* @param mode 模式
* @param key 密码
* @explain ECB模式(电子密码本模式:Electronic codebook)
*/
private static Cipher generateEcbCipher(String algorithmName, int mode, byte[] key) throws Exception {
Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME);
cipher.init(mode, sm4Key);
return cipher;
}
/**
* sm4加密
*
* @param hexKey 16进制密钥(忽略大小写)
* @param paramStr 待加密字符串
* @return 返回Base64后加密字符串
* @explain 加密模式:ECB
*/
public static String encryptEcb(String hexKey, String paramStr) throws Exception {
// 16进制字符串-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// String-->byte[]
byte[] srcData = paramStr.getBytes(ENCODING);
// 加密后的数组
byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
// byte[]-->hexString
String cipherText = Base64.getEncoder().encodeToString(cipherArray);
return cipherText;
}
/**
* 加密模式为ECB
*
* @param key 2进制密钥
* @param data 2进制原文
* @return 二进制密文
*/
public static byte[] encrypt_Ecb_Padding(byte[] key, byte[] data) throws Exception {
Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(data);
}
/**
* sm4解密
*
* @param hexKey 16进制密钥
* @param cipherText 16进制的加密字符串(忽略大小写)
* @return 解密后的字符串
* @explain 解密模式:采用ECB
*/
@SuppressWarnings("UnnecessaryLocalVariable")
public static String decryptEcb(String hexKey, String cipherText) throws Exception {
// hexString-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// hexString-->byte[]
byte[] cipherData = Base64.getDecoder().decode(cipherText);
// 解密
byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData);
// 接收解密后的字符串 byte[]-->String
String decryptStr = new String(srcData, ENCODING);
return decryptStr;
}
/**
* sm4解密
*
* @param key 2进制密钥
* @param cipherText 2进制密文
* @return 解密后的2进制原文
*/
public static byte[] decrypt_Ecb_Padding(byte[] key, byte[] cipherText) throws Exception {
Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key);
return cipher.doFinal(cipherText);
}
}
调用接口工具类
import cn.hutool.core.date.DateUtil;
import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.leqi.domain.req.BaseLeQiReq;
import com.ruoyi.leqi.domain.resp.BaseLeQiResp;
import netscape.javascript.JSObject;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
/**
* User: yanjun.hou
* Date: 2024/8/27 09:59
* Description:
*/
public class InvokeLeQiUtils {
static Logger logger = LoggerFactory.getLogger(InvokeLeQiUtils.class);
static String secret = "xxx";//乐企秘钥
static String jrdwptbh = "xxx";//接入单位id
static String sydwptbh = "xxx";//使用单位id
static String ylbm = "911001";//用例编码
static String nsrsbh = "xxx";//纳税人识别号
static String reqUrl = "https://lqpt.chinatax.gov.cn:8443/access/sandbox/v2/invoke";//沙箱环境
public static JSONObject invokeApi(BaseLeQiReq baseLeQiReq) {
String nlbm = baseLeQiReq.getNlbm();
String fubm = baseLeQiReq.getFwbm();
if (ObjectUtils.isNotEmpty(baseLeQiReq.getUrl())) {
reqUrl = baseLeQiReq.getUrl();
}
String url = String.format("%s/%s/%s", reqUrl, nlbm, fubm);
if (reqUrl.contains("kzfw")) {//如果是控制服务,则不拼接能力编码
url = String.format("%s/%s", reqUrl, fubm);
}
if (ObjectUtils.isNotEmpty(baseLeQiReq.getYlbm())) {
ylbm = baseLeQiReq.getYlbm();
}
return invoke(url, baseLeQiReq, secret, jrdwptbh, sydwptbh, ylbm);
}
/**
* 调用乐企接口
*
* @param url 接口地址
* @param baseLeQiReq
* @param key 密钥
* @param jrdwptbh 接入单位ID
* @param sydwptbh 使用单位ID
* @param ylbm 用例编码
* @return
*/
private static JSONObject invoke(String url, BaseLeQiReq baseLeQiReq, String key, String jrdwptbh, String sydwptbh, String ylbm) {
try {
if (ObjectUtils.isNotEmpty(baseLeQiReq.getFwbm())) {
switch (baseLeQiReq.getFwbm()) {
case "QDFPPLFM":
handlerParams(baseLeQiReq, jrdwptbh, sydwptbh);
case "XZTHSXED":
handlerParams(baseLeQiReq, jrdwptbh, sydwptbh);
default:
}
}
String param = JSON.toJSONString(baseLeQiReq.getParam());
//如果接口接收的参数是list 比如【数字化电子发票上传】接口
if (ObjectUtils.isNotEmpty(baseLeQiReq.getParamList())) {
param = JSON.toJSONString(baseLeQiReq.getParamList());
}
String encParam = SM4Util.encryptEcb(key, param);
logger.info(String.format("InvokeLeQiUtils | invoke 请求乐企服务地址:%s ;请求乐企原始报文:%s ;请求乐企加密报文:%s ;", url, param, encParam));
HttpRequest request = HttpRequest.post(url)
.header("Content-Type", "application/json;charset=UTF-8")
.header("fwbm", baseLeQiReq.getFwbm())//添加服务编码
.header("jrdwptbh", jrdwptbh)//添加接入平台编号
.header("sydwptbh", sydwptbh)//添加使用平台编号
.header("nlbm", baseLeQiReq.getNlbm())//添加能力编码
.header("ylbm", ylbm)//添加用例编码
.header("access_signature", "")// 添加访问签名
.header("sxcsbz", "") // 添加沙箱测试标志
.charset(StandardCharsets.UTF_8)
.timeout(90000);//超时,毫秒;
if (ObjectUtils.isNotEmpty(encParam)) {
request.body(encParam);
}
String encRes = request.execute().body();
logger.info(String.format("InvokeLeQiUtils | invoke 请求乐企响应加密报文:%s ", encRes));
JSONObject jsonObject = JSON.parseObject(encRes);
if (ObjectUtils.isEmpty(jsonObject.getString("body"))) {
return jsonObject;
}
if (!"200".equals(jsonObject.getString("httpStatusCode"))) {
return jsonObject;
}
BaseLeQiResp resp = JSON.parseObject(jsonObject.getString("body"), BaseLeQiResp.class);
if (ObjectUtils.isEmpty(resp.getResponse()) || ObjectUtils.isEmpty(resp.getResponse().getData())) {
return JSONObject.parseObject(JSON.toJSONString(resp));
}
String decRes = SM4Util.decryptEcb(key, resp.getResponse().getData());
logger.info(String.format("InvokeLeQiUtils | invoke 请求乐企响应解密报文:%s ;", decRes));
return JSON.parseObject(decRes);
} catch (Exception e) {
logger.error(String.format("InvokeLeQiUtils | invoke 请求乐企接口异常,异常信息:%s ", e));
throw new RuntimeException(e);
}
}
/**
* 处理参数缺失,按照规则提供默认值
* @param baseLeQiReq
* @param jrdwptbh
* @param sydwptbh
*/
private static void handlerParams(BaseLeQiReq baseLeQiReq, String jrdwptbh, String sydwptbh) {
if (ObjectUtils.isNotEmpty(baseLeQiReq.getParam())) {
JSONObject param = baseLeQiReq.getParam();
if (ObjectUtils.isEmpty(param.getString("ywlsh"))) {
param.put("ywlsh", sydwptbh + jrdwptbh + UUID.randomUUID().toString().replaceAll("-", ""));
}
}
}
}
4、接口调用
示例图
说明:其中参数中的【url】使用的是对应在乐企申请资质的时候报备的ip 由于我们这边的特殊性质(内网+不是自己服务器ip)所以加了这个url进行转发,实际情况看自己使用(我代码里有判断,如果参数存在url则按照url请求,否则按照税局的沙箱环境地址请求);
响应参数:我这边只取了乐企接口返回的body解密的数据
首先调用【开票控制服务】的几个接口
1、获取数字化电子发票批量预赋码信息
{
"nlbm": "202007",
"fwbm": "QDFPPLFM",
"param": {
"nsrsbh": "x x x x",
"lysl": "5000",
"ywlsh": ""
},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
2、查询授信额度
{
"nlbm": "202007",
"fwbm": "CXSXED",
"param": {
"nsrsbh": "x x x x"
},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
3、下载或退回授信额度
{
"nlbm": "202007",
"fwbm": "XZTHSXED",
"param": {
"nsrsbh": "x x x x",
"ptbh": "x x x x",
"sqlx": "0",
"sqed": "1100.00",
"ywlsh": ""
},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
4、调整授信额度有效期
{
"nlbm": "202007",
"fwbm": "TZSXEDYXQ",
"param": {
"xsfnsrsbh": "x x x x",
"sxedsq": "2024-08"
},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
5、查询纳税人风险信息
{
"nlbm": "202007",
"fwbm": "CXNSRFXXX",
"param": {
"nsrsbh": "x x x x"
},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
6、查询纳税人基本信息
{
"nlbm": "202007",
"fwbm": "CXNSRJBXX",
"param": {
"nsrsbh": "x x x x"
},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
7、查询可用税率信息
{
"nlbm": "202007",
"fwbm": "CXKYSL",
"param": {},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
8、查询税收分类编码信息
{
"nlbm": "202007",
"fwbm": "CXSSFLBM",
"param": {
"nsrsbh": "x x x x",
"sjc": "",
"sjswjgdm": "14200000000"
},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
9、查询数字化电子红字确认单列表信息
{
"nlbm": "202007",
"fwbm": "CXQDHZQRDLB",
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke",
"param": {
"yhjslx": "0",
"xsfnsrsbh": "x x x x",
"xsfmc": "****集团有限责任公司",
"gmfnsrsbh": "",
"gmfmc": "",
"lrfsf": "0",
"lrrqq": "2024-01-01",
"lrrqz": "2024-09-01",
"lzfpdm": "",
"lzfphm": "",
"hzfpxxqrdbh": "",
"hzqrxxztDm": "",
"fppzDm": "",
"pageNumber": "1",
"pageSize": "10"
}
}
10、查询数字化电子红字确认单明细信息
{
"nlbm": "202007",
"fwbm": "CXQDHZQRDMX",
"param": {
"uuid": "321312312312312 ",
"xsfnsrsbh": "x x x x"
}
}
11、数字化电子发票上传
{
"nlbm": "202007",
"fwbm": "QDFPSC",
"paramList": [
{
"fphm": "24997000006472271411",//对应【获取数字化电子发票批量预赋码信息】起始号段 【开始>=该值<=结束】都可以
"lzfpbz": "0",
"ptbh": "x x x x",
"fppz": "02",
"gmfzrrbz": "N",
"tdys": "",
"qyDm": "420600000000",
"cezslxDm": "",
"sgfplxDm": "",
"ckywsyzcDm": "",
"zzsjzjtDm": "",
"xsfnsrsbh": "x x x x",
"xsfmc": "***有限责任公司",
"xsfdz": "",
"xsfdh": "",
"xsfkhh": "",
"xsfzh": "",
"gmfnsrsbh": "9******372095",//对应[税控蓝票初始化的购方税号]
"gmfmc": "北京***科技有限公司",
"gmfdz": "",
"gmfdh": "",
"gmfkhh": "",
"gmfzh": "",
"gmfjbr": "",
"jbrsfzjhm": "",
"gmfjbrlxdh": "",
"hjje": 100,
"hjse": 13,
"jshj": 113,
"skyhmc": "",
"skyhzh": "",
"jsfs": "",
"ysxwfsd": "",
"kpr": "张三",
"kprzjhm": "412725****57***",
"kprzjlx": "201",
"dylzfphm": "",
"hzqrxxdbh": "",
"hzqrduuid": "",
"bz": "备注",
"ip": "报备乐企对应ip",
"macdz": "20-16-1E-00-02-12",
"cpuid": "",
"zbxlh": "",
"kprq": "2024-08-28 11:11:11",
"sfzsxsfyhzhbq": "",
"sfzsgmfyhzhbq": "",
"skrxm": "",
"fhrxm": "",
"fpmxList": [
{
"mxxh": "1",
"dylzfpmxxh": "",
"hwhyslwfwmc": "*信息技术服务*技术服务费",
"spfwjc": "信息技术服务",
"xmmc": "技术服务费",
"ggxh": "",
"dw": "",
"sl": "1",
"dj": "100",
"je": 100,
"slv": 0.13,
"se": 13,
"hsje": 113,
"kce": "",
"sphfwssflhbbm": "3040201010000000000",
"fphxz": "00",
"yhzcbs": ""
}
],
"fjysList": [
{
"fjysmc": "",
"fjyslx": "",
"fjysz": ""
}
]
}
],
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
12、查询数字化电子发票上传结果
{
"nlbm": "202007",
"fwbm": "CXQDFPSCJG",
"param": {
"sllsh": "902aedfe8dc141928b170bf3fd8bd0fd"
},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
13、数字化电子红字确认单申请
{
"nlbm": "202007",
"fwbm": "QDHZQRDSQ",
"param": {
"lrfsf": "0",
"xsfnsrsbh": "x x x x",
"xsfmc": "****集团有限责任公司",
"gmfnsrsbh": "91990****372095",
"gmfmc": "北京**科技有限公司",
"lzfpdm": "",
"lzfphm": "24997000006472271411",
"sfzzfpbz": "N",
"lzkprq": "2024-08-28 11:11:11",
"lzhjje": "113.00",
"lzhjse": "13.00",
"lzfppzDm": "01",
"lzfpTdyslxDm": "",
"hzcxje": "-100.00",
"hzcxse": "-13.00",
"chyyDm": "01",
"hzqrdmxList": [
{
"lzmxxh": "1",
"xh": "1",
"sphfwssflhbbm": "3040201010000000000",
"hwhyslwfwmc": "*信息技术服务*技术开发费",
"spfwjc": "信息技术服务",
"xmmc": "技术开发费",
"ggxh": "",
"dw": "",
"fpspdj": "",
"fpspsl": "",
"je": "100.00",
"sl1": "0.13",
"se": "13"
}
]
},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
14、数字化电子红字确认单确认
{
"nlbm": "202007",
"fwbm": "QDHZQRDQR",
"param": {
"xsfnsrsbh": "x x x x",
"uuid": "9d1c747182d043b892bcf9e4816b7cfe",
"hzqrdbh": "99000024088000309024",
"qrlx": "C"
},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
15、上传发票汇总确认信息
{
"nlbm": "202007",
"fwbm": "SCFPHZQRXX",
"param": {
"xsfnsrsbh": "x x x x",
"xsfsjswjgdm": "14200000000",
"ywlx": "0",
"ptbh": "x x x x",
"yf": "2024-08",
"lzfpsl": "0",
"lzfpje": "0.00",
"lzfpse": "0.00",
"hzfpsl": "0",
"hzfpje": "0.00",
"hzfpse": "0.00"
},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
16、查询发票汇总确认信息
{
"nlbm": "202007",
"fwbm": "CXFPHZQRXX",
"param": {
"xsfnsrsbh": "x x x x",
"xsfsjswjgdm": "14200000000",
"ptbh": "x x x x",
"yf": "2024-08"
},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
17、查询差额征税编码
{
"nlbm": "202007",
"fwbm": "CXCEZSBM",
"param": {},
"url": "http://报备乐企对应ip端口/leqi-api/access/sandbox/v2/invoke"
}
5、最后测试效果图