目前做几个系统的整合,将之前做的移动端,小程序,PC端整合到一个系统中,今天整合电子发票的开具功能。发现去年写的代码真是low到家了,重新梳理了一下,现在做一下总结。
今天查看诺诺发票官网,发现已经更新到2.0了,maven包也更新到1.0.4。
如果是新开发的发票系统,请先登录诺诺开放平台进行注册,这里要用公司的税号去注册的,然后选到自己需要的文档进行开发。我这里只用到了开具发票和根据发票流水号查询发票以及根据订单号查询发票三个接口。
代码实现如下:
首先,定义公共参数,我这里新建了一个类:
public class InvoiceConstants {
/**
* 授权企业税号
*/
public static final String TAX_NUM = "**********";
/**
* 生产环境
* 平台分配给应用的appKey【消息体】
*/
public static final String APP_KEY = "**********";
/**
* 生产环境
* 授权码【消息头】
*/
public static final String APP_SECRET = "**********";
/**
* 沙箱环境
* 平台分配给应用的appKey【消息体】
*/
public static final String TEST_APP_KEY = "**********";
/**
* 授权码【消息头】
*/
public static final String TEST_APP_SECRET = "**********";
/**
* 申请开具发票的API方法名
*/
public static final String APPLY_METHOD = "nuonuo.electronInvoice.requestBilling";
/**
* 生产环境
* 请求地址
*/
public static final String URL = "https://sdk.nuonuo.com/open/v1/services";
/**
* 沙箱请求地址
*/
public static final String TEST_URL = "https://sandbox.nuonuocs.cn/open/v1/services";
/**
* 开票流水号查询发票API方法名
*/
public static final String CHECK_METHOD = "nuonuo.electronInvoice.CheckEInvoice";
/**
* 根据订单号,开发流水号查询
*/
public static final String QUERY_METHOD = "nuonuo.electronInvoice.querySerialNum";
/**
* 售方信息
*/
public static final String SALER_ACCOUNT = "";
/**
* 沙箱环境
* 销方银行账号和开户行地址
*/
public static final String TEST_SALER_ACCOUNT = "杭州银行彭埠支行120200590990432278";
/**
* 售方地址
*/
public static final String SALER_ADDRESS = "";
/**
* 沙箱环境
* 销方地址
*/
public static final String TEST_SALER_ADDRESS = "杭州市西湖区万塘路30号高新东方科技园";
/**
* 销方电话
*/
public static final String SALER_TEL = "";
/**
* 沙箱环境
* 销方电话
*/
public static final String TEST_SALER_TEL = "0571-81029365";
/**
* 沙箱环境
* 销方税号
*/
public static final String TEST_SALER_TAX_NUM ="339901999999142";
/**
* 税率
*/
public static final String TAX_RATE ="0.13";
/**
* 发票行性质:0,正常行;1,折扣行;2,被折扣行
*/
public static final String INVOICE_LINE_PROPERTY ="0";
/**
* 产品规格型号
* specType
*/
public static final String SPEC_TYPE ="";
/**
* 不含税金额。红票为负。 N
* 不含税金额、税额、含税金额任何一个不传时,会根据传入的单价,数量进行计算,可能和实际数值存在误差,建议都传入
* taxExcludedAmount
*/
public static final String TAX_EXCLUDED_AMOUNT ="";
/**
* 优惠政策标识:0,不使用;1,使用
*/
public static final String FAVOURED_POLICY_FLAG ="0";
/**
* 增值税特殊管理(优惠政策名称),当favouredPolicyFlag为1时,此项必填 N
*/
public static final String FAVOURED_POLICY_NAME ="";
/**
* 单价含税标志:0:不含税,1:含税
*/
public static final String WITH_TAX_FLAG ="1";
/**
*税额,[不含税金额] * [税率] = [税额];税额允许误差为 0.06。红票为负。
*不含税金额、税额、含税金额任何一个不传时,会根据传入的单价,数量进行计算,可能和实际数值存在误差,建议都传入
*/
public static final String TAX ="";
/**
* 产品单位
*/
public static final String UNIT ="***";
/**
* 扣除额。差额征收时填写,目前只支持填写一项
*/
public static final String DEDUCTION ="0";
/**
* 零税率标识:空,非零税率;1,免税;2,不征税;3,普通零税率
*/
public static final String ZERO_RATE_FLAG ="";
/**
* 开票类型:1,正票;2,红票
*/
public static final String INVOICE_TYPE ="1";
/**
* 发票种类:p,普通发票(电票)(默认);c,普通发票(纸票);s,专用发票;e,收购发票(电票);f,收购发票(纸质) N
*/
public static final String INVOICE_LINE ="p";
/**
* 清单标志:0,根据项目名称数,自动产生清单;1,将项目信息打印至清单 N
*/
public static final String LIST_FLAG ="0";
/**
* 推送方式:-1,不推送;0,邮箱;1,手机(默认);2,邮箱、手机 N
*/
public static final String PUSH_MODE ="2";
/**
* 成品油标志:0,非成品油(默认);1,成品油 N
*/
public static final String PRODUCT_OIL_FLAG ="0";
/**
* 代开标志:0非代开;1代开。 N
* 代开蓝票时备注要求填写文案:代开企业税号:***,代开企业名称:***;
* 代开红票时备注要求填写文案:对应正数发票代码:***号码:***代开企业税号:***代开企业名称:***
*/
public static final String PROXY_INVOICE_FLAG ="0";
/**
* 发票申请成功标示
*/
public static final String APPLY_SUCCESS_CODE ="E0000";
/**
* 发票申请开具成功标识
*/
public static final String SUCCESS_CODE ="2";
第二获取开具发票需要的token,这个token是有限制的,每个令牌是存在有效期(24小时)的,且令牌30天内的调用上限为50次,所以一定要保存好:
我这里是保存到了数据库中,之前做企业微信开发是保存到xml文件中的。
CREATE TABLE `invoice_token` (
`id` int NOT NULL AUTO_INCREMENT,
`access_token` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT 'token值',
`expires_in` bigint DEFAULT NULL COMMENT 'token的有效时间',
`creat_time` datetime DEFAULT NULL COMMENT 'token的获取时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC;
获取token的方法:
private String getInvoiceToken() {
String token;
InvoiceToken appToken = new InvoiceToken();
InvoiceToken appToken2 = invoiceTokenService.selectByPrimaryKey(1);
if (null == appToken2) {
String json = NNOpenSDK.getIntance().getMerchantToken(InvoiceConstants.TEST_APP_KEY, InvoiceConstants.TEST_APP_SECRET);
JSONObject newresult = JSONObject.parseObject(json);
token = (String) newresult.get("access_token");
Integer expiresIn = (Integer) newresult.get("expires_in");
appToken.setAccessToken(token);
appToken.setExpiresIn(expiresIn);
int insert = invoiceTokenService.insert(appToken);
System.out.println(insert);
} else {
long expiresIn2 = ((Number) appToken2.getExpiresIn()).longValue();
Date creatTime = appToken2.getCreatTime();
Calendar dateOne = Calendar.getInstance();
Calendar dateTwo = Calendar.getInstance();
dateOne.setTime(new Date());
dateTwo.setTime(creatTime);
long timeOne = dateOne.getTimeInMillis();
long timeTwo = dateTwo.getTimeInMillis();
long minute = (timeOne - timeTwo) / 1000;
if (expiresIn2 < minute) {
String json = NNOpenSDK.getIntance().getMerchantToken(InvoiceConstants.TEST_APP_KEY, InvoiceConstants.TEST_APP_SECRET);
JSONObject newresult = JSONObject.parseObject(json);
token = (String) newresult.get("access_token");
Integer expiresIn = (Integer) newresult.get("expires_in");
appToken.setAccessToken(token);
appToken.setExpiresIn(expiresIn);
appToken.setId(1);
int insert = invoiceTokenService.updateByPrimaryKey(appToken);
System.out.println(insert);
} else {
token = appToken2.getAccessToken();
}
}
return token;
}
然后就可以进入主题,开具发票和查询发票,由于诺诺平台这么一个问题,发票申请提交后,可能短时间内无法返回发票是否开具和开具是否成功的问题,所以我使用了两种不同的方法进行查询发票。
public ResultBean<String> makeInvoice(HttpServletRequest request) {
ResultBean<String> resultBean = new ResultBean<>();
resultBean.setCode(-1);
String flowNo = request.getParameter("flowNo");
String loginName = request.getParameter("loginName");
if (StringUtils.isBlank(flowNo)) {
resultBean.setMsg("流水号不能为空");
return resultBean;
}
Map<String, Object> map = invoiceService.getAllInvoiceInfoByFlowNo(flowNo);
NNOpenSDK sdk = NNOpenSDK.getIntance();
String token = getInvoiceToken();
String invoiceHead = (String) map.get("invoiceHead");
String dutyParagraph = map.get("dutyParagraph").toString();
String goodsName= map.get("goodsName").toString();
String phone = map.get("phone").toString();
String email = map.get("email").toString();
String buyerAccount = (String) map.get("buyerAccount");
String buyerAddress = (String) map.get("buyerAddress");
//因为如果字符为null,会直接显示在发票上,显得很不正规,所以这里统一做判断,赋值为空字符串
if (dutyParagraph == null) {
dutyParagraph = "";
}
if (buyerAddress == null) {
buyerAddress = "";
}
if (buyerAccount == null) {
buyerAccount = "";
}
String num = map.get("num").toString();
double price = Double.parseDouble(map.get("price").toString()) * 4;
String nickname = map.get("nickname").toString();
String flowTime = map.get("flowTime").toString();
String receivableAmount = map.get("receivableAmount").toString();
//一定要带转义字符标识,不然会报格式不正确
String content = "{\"order\":"
+ "{\"invoiceDetail\":[" + "{"
+ "\"taxRate\":\"" + InvoiceConstants.TAX_RATE + "\","
+ "\"invoiceLineProperty\":\"" + InvoiceConstants.INVOICE_LINE_PROPERTY + "\","
+ "\"goodsName\":\"" + goodsName+ "\","
+ "\"specType\":\"" + InvoiceConstants.SPEC_TYPE + "\","
+ "\"taxExcludedAmount\":\"" + InvoiceConstants.TAX_EXCLUDED_AMOUNT + "\","
+ "\"favouredPolicyFlag\":\"" + InvoiceConstants.FAVOURED_POLICY_FLAG + "\","
+ "\"favouredPolicyName\":\"" + InvoiceConstants.FAVOURED_POLICY_NAME + "\","
+ "\"withTaxFlag\":\"" + InvoiceConstants.WITH_TAX_FLAG + "\","
+ "\"num\":\"" + num + "\","
+ "\"tax\":\"" + InvoiceConstants.TAX + "\","
+ "\"unit\":\"" + InvoiceConstants.UNIT + "\","
+ "\"deduction\":\"" + InvoiceConstants.DEDUCTION + "\","
+ "\"price\":\"" + price + "\","
+ "\"zeroRateFlag\":\"" + InvoiceConstants.ZERO_RATE_FLAG + "\","
// 商品编码(商品税收分类编码开发者自行填写) N
+ "\"goodsCode\":\"" + "" + "\","
// 自行编码(可不填) N
+ "\"selfCode\":\"" + "" + "\","
// 含税金额,[不含税金额] + [税额] = [含税金额],红票为负。 N
// 不含税金额、税额、含税金额任何一个不传时,会根据传入的单价,数量进行计算,可能和实际数值存在误差,建议都传入
+ "\"taxIncludedAmount\":\"" + receivableAmount + "\"}],"
// 购方电话 N
+ "\"buyerTel\":\"" + "" + "\","
// 购方名称 Y
+ "\"buyerName\":\"" + invoiceHead + "\","
// 购方地址 N
+ "\"buyerAddress\":\"" + buyerAddress + "\","
// 购方税号(企业要填,个人可为空) N
+ "\"buyerTaxNum\":\"" + dutyParagraph + "\","
+ "\"invoiceType\":\"" + InvoiceConstants.INVOICE_TYPE + "\","
+ "\"invoiceLine\":\"" + InvoiceConstants.INVOICE_LINE + "\","
// 购方银行账号及开户行地址 N
+ "\"buyerAccount\":\"" + buyerAccount + "\","
+ "\"listFlag\":\"" + InvoiceConstants.LIST_FLAG + "\","
+ "\"pushMode\":\"" + InvoiceConstants.PUSH_MODE + "\","
// 购方手机(开票成功会短信提醒购方,不受推送方式影响) Y
+ "\"buyerPhone\":\"" + phone + "\","
+ "\"email\":\"" + email + "\","
// 部门门店id(诺诺系统中的id) N
+ "\"departmentId\":\"" + "" + "\","
// 开票员id(诺诺系统中的id) N
+ "\"clerkId\":\"" + loginName + "\","
// 复核人 N
+ "\"checker\":\"" + "" + "\","
// 冲红时,在备注中注明“对应正数发票代码:XXXXXXXXX号码:YYYYYYYY”文案,
// 其中“X”为发票代码,“Y”为发票号码,可以不填,接口会自动添加该文案 N
+ "\"remark\":\"备注信息\","
+ "\"payer\":\"" + nickname + "\","
+ "\"salerAccountr\":\"" + InvoiceConstants.TEST_SALER_ACCOUNT + "\","
+ "\"salerAddressr\":\"" + InvoiceConstants.TEST_SALER_ADDRESS + "\","
// 订单号(每个企业唯一) Y
+ "\"orderNo\":\"" + flowNo + "\","
+ "\"salerTelr\":\"" + InvoiceConstants.TEST_SALER_TEL + "\","
// 订单时间 Y
+ "\"invoiceDate\":\"" + flowTime + "\","
// 冲红时填写的对应蓝票发票代码(红票必填,不满12位请左补0) N
+ "\"invoiceCode\":\"\","
// 冲红时填写的对应蓝票发票号码(红票必填,不满8位请左补0) N
+ "\"invoiceNum\":\"\","
// 开票员 Y
+ "\"clerk\":\"" + "" + "\","
+ "\"productOilFlag\":\"" + InvoiceConstants.PRODUCT_OIL_FLAG + "\","
+ "\"salerTaxNum\":\"" + InvoiceConstants.TEST_SALER_TAX_NUM + "\","
// 清单项目名称:打印清单时对应发票票面项目名称
// (listFlag为1是,此项为必填,默认为“详见销货清单”) N
+ "\"listName\":\"" + "" + "\","
+ "\"proxyInvoiceFlag\":\"" + InvoiceConstants.PROXY_INVOICE_FLAG + "\"}}";
String senid = UUID.randomUUID().toString().replace("-", "");
String result = sdk.sendPostSyncRequest(InvoiceConstants.TEST_URL, senid, InvoiceConstants.TEST_APP_KEY, InvoiceConstants.TEST_APP_SECRET, token, InvoiceConstants.TAX_NUM, InvoiceConstants.APPLY_METHOD, content);
JSONObject newresult = JSONObject.parseObject(result);
String code = newresult.getString("code");
String describe = newresult.getString("describe");
String result1 = newresult.getString("result");
if (InvoiceConstants.APPLY_SUCCESS_CODE.equals(code)) {
JSONObject newresult1 = JSONObject.parseObject(result1);
String invoiceSerialNum = newresult1.getString("invoiceSerialNum");
//通过发票流水号查询发票信息
Map<String, String> map2 = getInvoiceUrlByInvoiceSerialNum(invoiceSerialNum);
String code1 = map2.get("code");
String statusMsg = map2.get("statusMsg");
if (InvoiceConstants.APPLY_SUCCESS_CODE.equals(code1)) {
String status = map2.get("status");
Invoice invoice = new Invoice();
invoice.setStatus(status);
invoice.setStatusMsg(statusMsg);
invoice.setFlowNo(flowNo);
if (InvoiceConstants.SUCCESS_CODE.equals(status)) {
String invoiceFileUrl = map2.get("invoiceFileUrl");
String invoiceImageUrl = map2.get("invoiceImageUrl");
invoice.setInvoiceSerialNum(invoiceSerialNum);
invoice.setInvoiceFileUrl(invoiceFileUrl);
invoice.setInvoiceImageUrl(invoiceImageUrl);
}
invoiceService.updateInvoice(invoice);
resultBean.setCode(0);
resultBean.setMsg("发票申请提交成功");
} else {
resultBean.setMsg(statusMsg);
}
} else {
resultBean.setMsg(describe);
}
return resultBean;
}
通过发票流水号查询发票信息:
public Map<String, String> getInvoiceUrlByInvoiceSerialNum(String invoiceSerialNum) {
Map<String, String> map = new HashMap<>();
NNOpenSDK sdk = NNOpenSDK.getIntance();
String token = getInvoiceToken();
String content = "{" + "\"invoiceSerialNum\":[" + invoiceSerialNum + "]" + "}";
String senid = UUID.randomUUID().toString().replace("-", "");
String result = sdk.sendPostSyncRequest(InvoiceConstants.TEST_URL, senid, InvoiceConstants.TEST_APP_KEY, InvoiceConstants.TEST_APP_SECRET, token, InvoiceConstants.TAX_NUM, InvoiceConstants.CHECK_METHOD, content);
System.out.println(result);
JSONObject newresult = JSONObject.parseObject(result);
String code = newresult.getString("code");
String describe = newresult.getString("describe");
map.put("code", code);
if (InvoiceConstants.APPLY_SUCCESS_CODE.equals(code)) {
JSONArray eventInfoData = (JSONArray) newresult.get("result");
JSONObject jsonArray = eventInfoData.getJSONObject(0);
String invoiceImageUrl = jsonArray.getString("invoiceImageUrl");
String invoiceFileUrl = jsonArray.getString("invoiceFileUrl");
String status = jsonArray.getString("status");
String statusMsg = jsonArray.getString("statusMsg");
map.put("status", status);
map.put("statusMsg", statusMsg);
if (InvoiceConstants.SUCCESS_CODE.equals(status)) {
map.put("invoiceFileUrl", invoiceFileUrl);
map.put("invoiceImageUrl", invoiceImageUrl);
}
return map;
} else {
map.put("statusMsg", describe);
}
return map;
}
通过订单号查询发票信息:
public ResultBean<String> updateInvoiceStatus(HttpServletRequest request) {
ResultBean<String> resultBean = new ResultBean<>();
resultBean.setCode(-1);
String flowNo = request.getParameter("flowNo");
String status = request.getParameter("status");
if (StringUtils.isBlank(flowNo)) {
resultBean.setMsg("流水号不能为空!");
return resultBean;
}
if (StringUtils.isBlank(status)) {
resultBean.setMsg("当前开票状态不能为空!");
return resultBean;
}else if(InvoiceConstants.SUCCESS_CODE.equals(status)){
resultBean.setMsg("已经开过发票了,无需再次查询!");
return resultBean;
}
NNOpenSDK sdk = NNOpenSDK.getIntance();
String token = getInvoiceToken();
System.out.println(token);
String content = "{" + "\"orderNo\":[" + flowNo + "]" + "}";
String senid = UUID.randomUUID().toString().replace("-", "");
String result = sdk.sendPostSyncRequest(InvoiceConstants.TEST_URL, senid, InvoiceConstants.TEST_APP_KEY, InvoiceConstants.TEST_APP_SECRET, token, InvoiceConstants.TAX_NUM, InvoiceConstants.QUERY_METHOD, content);
System.out.println(result);
JSONObject newresult = JSONObject.parseObject(result);
String code = newresult.getString("code");
String describe = newresult.getString("describe");
if (InvoiceConstants.APPLY_SUCCESS_CODE.equals(code)) {
JSONArray eventInfoData = (JSONArray) newresult.get("result");
JSONObject jsonArray = eventInfoData.getJSONObject(0);
String invoiceSerialNum = jsonArray.getString("invoiceSerialNum");
String invoiceImageUrl = jsonArray.getString("invoiceImageUrl");
String invoiceFileUrl = jsonArray.getString("invoiceFileUrl");
// 开票状态:2为开票成功,其他状态分别为0:未开票;1:开票中;3:开票失败;
String status1 = jsonArray.getString("status");
String statusMsg = jsonArray.getString("statusMsg");
Invoice invoice = new Invoice();
invoice.setFlowNo(flowNo);
invoice.setStatus(status1);
invoice.setStatusMsg(statusMsg);
if (InvoiceConstants.SUCCESS_CODE.equals(status1)) {
invoice.setInvoiceSerialNum(invoiceSerialNum);
invoice.setInvoiceFileUrl(invoiceFileUrl);
invoice.setInvoiceImageUrl(invoiceImageUrl);
}
invoiceService.updateInvoice(invoice);
resultBean.setCode(0);
resultBean.setMsg("查询开票结果成功");
} else {
resultBean.setMsg(describe);
}
return resultBean;
}
诺诺平台,正常情况下,只支持每天5000张电子发票的发票开具 ,如果遇到开具张数超过5000就会拒绝开具,针对这一情况,我联系了客服,升级到了100000张每天,哈哈,好给力的客服。
针对每天可能开具发票较多,用户收到了发票信息,但我方平台没有收到发票信息的情况,我写了个定时任务,每日凌晨自动查询未开具发票或发票开具中的订单,统一查询发票开具情况,更新数据。