Compare commits
3 Commits
9fd80980e7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c5f62d201 | |||
| bf87c09803 | |||
| a716c481da |
@@ -1,3 +1,5 @@
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
log/
|
||||
server/src/main/resources/dist/
|
||||
doc/票通Demo/untitled/.idea/
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
.kotlin
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>ink.snowflake</groupId>
|
||||
<artifactId>untitled</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.8.9</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.14.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,17 @@
|
||||
package ink.snowflake;
|
||||
|
||||
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
|
||||
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
//TIP Press <shortcut actionId="ShowIntentionActions"/> with your caret at the highlighted text
|
||||
// to see how IntelliJ IDEA suggests fixing it.
|
||||
System.out.printf("Hello and welcome!");
|
||||
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
//TIP Press <shortcut actionId="Debug"/> to start debugging your code. We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
|
||||
// for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.
|
||||
System.out.println("i = " + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,824 @@
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 票通Demo 用于参考报文组装和加密示例
|
||||
*
|
||||
* HtppUtils :post请求用的java原生的IO.net,没有考虑超时/长连接等特殊情况.使用到真正项目时最好采用项目http框架
|
||||
*
|
||||
* demo在json转换的时候使用了Gson,单元测试的时候使用了Junit 以下是Maven依赖有需要添加即可:
|
||||
* <dependency>
|
||||
* <groupId>com.google.code.gson</groupId>
|
||||
* <artifactId>gson</artifactId>
|
||||
* <version>2.8.9</version>
|
||||
* </dependency>
|
||||
*
|
||||
*
|
||||
* <dependency>
|
||||
* <groupId>junit</groupId>
|
||||
* <artifactId>junit</artifactId>
|
||||
* <version>4.13.2</version>
|
||||
* <scope>test</scope>
|
||||
* </dependency>
|
||||
*
|
||||
*/
|
||||
public class Demo {
|
||||
|
||||
//私钥(与发给票通的公钥为一对)
|
||||
private String privateKey = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIVLAoolDaE7m5oMB1ZrILHkMXMF6qmC8I/FCejz4hwBcj59H3rbtcycBEmExOJTGwexFkNgRakhqM+3uP3VybWu1GBYNmqVzggWKKzThul9VPE3+OTMlxeG4H63RsCO1//J0MoUavXMMkL3txkZBO5EtTqek182eePOV8fC3ZxpAgMBAAECgYBp4Gg3BTGrZaa2mWFmspd41lK1E/kPBrRA7vltMfPj3P47RrYvp7/js/Xv0+d0AyFQXcjaYelTbCokPMJT1nJumb2A/Cqy3yGKX3Z6QibvByBlCKK29lZkw8WVRGFIzCIXhGKdqukXf8RyqfhInqHpZ9AoY2W60bbSP6EXj/rhNQJBAL76SmpQOrnCI8Xu75di0eXBN/bE9tKsf7AgMkpFRhaU8VLbvd27U9vRWqtu67RY3sOeRMh38JZBwAIS8tp5hgcCQQCyrOS6vfXIUxKoWyvGyMyhqoLsiAdnxBKHh8tMINo0ioCbU+jc2dgPDipL0ym5nhvg5fCXZC2rvkKUltLEqq4PAkAqBf9b932EpKCkjFgyUq9nRCYhaeP6JbUPN3Z5e1bZ3zpfBjV4ViE0zJOMB6NcEvYpy2jNR/8rwRoUGsFPq8//AkAklw18RJyJuqFugsUzPznQvad0IuNJV7jnsmJqo6ur6NUvef6NA7ugUalNv9+imINjChO8HRLRQfRGk6B0D/P3AkBt54UBMtFefOLXgUdilwLdCUSw4KpbuBPw+cyWlMjcXCkj4rHoeksekyBH1GrBJkLqDMRqtVQUubuFwSzBAtlc";
|
||||
|
||||
//票通公钥(票通提供)
|
||||
private String ptPublicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCJkx3HelhEm/U7jOCor29oHsIjCMSTyKbX5rpoAY8KDIs9mmr5Y9r+jvNJH8pK3u5gNnvleT6rQgJQW1mk0zHuPO00vy62tSA53fkSjtM+n0oC1Fkm4DRFd5qJgoP7uFQHR5OEffMjy2qIuxChY4Au0kq+6RruEgIttb7wUxy8TwIDAQAB";
|
||||
|
||||
//3DES秘钥(票通提供)
|
||||
private final static String password = "lsBnINDxtct8HZB7KCMyhWSJ";
|
||||
|
||||
//请更换请求平台简称(票通提供)
|
||||
private final static String platform_alias = "DEMK";
|
||||
|
||||
//请更换请求平台编码(票通提供)
|
||||
private final static String platform_code = "11111111";
|
||||
//销售方税号(测试环境票通提供,正式环境使用正式税号)
|
||||
private final static String taxpayerNum = "500102201007206608";
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
* @title: testRSAGenerate
|
||||
* @description: RAS公钥私钥的生成 1024bit pkcs8格式 公钥提供给票通 私钥保留
|
||||
*/
|
||||
@Test
|
||||
public void testRSAGenerate() throws Exception {
|
||||
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGen.initialize(1024);
|
||||
KeyPair keyPair = keyPairGen.generateKeyPair();
|
||||
PublicKey publicKey = keyPair.getPublic();
|
||||
PrivateKey privateKey = keyPair.getPrivate();
|
||||
String publicKeyStr = RSAUtil.getKeyString(publicKey);
|
||||
System.out.println("publicKeyString:" + publicKeyStr);
|
||||
String privateKeyStr = RSAUtil.getKeyString(privateKey);
|
||||
System.out.println("privateKeyString:" + privateKeyStr);
|
||||
}
|
||||
/**
|
||||
* @title: testRealEstateRentalInvoiceBlue
|
||||
* @description: 蓝票接口调用
|
||||
*/
|
||||
@Test
|
||||
public void testInvoiceBlue() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/invoiceBlue.pt";
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("taxpayerNum", taxpayerNum); //销方税号
|
||||
map.put("invoiceReqSerialNo", date(platform_alias));//发票请求流水号
|
||||
map.put("buyerName", "购买方名称");//购买方名称
|
||||
map.put("invoiceIssueKindCode", "82");//购买方名称
|
||||
map.put("buyerTaxpayerNum", "XX0000000000000000");//购买方税号(非必填,个人发票传null)
|
||||
map.put("remark", "扫码");//购买方税号(非必填,个人发票传null)
|
||||
List<Map<String, String>> list = new ArrayList<Map<String, String>>();
|
||||
Map<String, String> listMapOne = new HashMap<String, String>();
|
||||
listMapOne.put("taxClassificationCode", "3040101000000000000");//税收分类编码(可以按照Excel文档填写)
|
||||
listMapOne.put("quantity", "1.00");//数量
|
||||
listMapOne.put("goodsName", "研发服务");//货物名称
|
||||
listMapOne.put("unitPrice", "10");//单价
|
||||
listMapOne.put("invoiceIssueKindCode", "82");//发票种类 (注意:数电发票在81与82之间选择 )81:数电票(增值税专用发票)82:数电票(普通发票)10:增值税电子普通发票
|
||||
listMapOne.put("invoiceAmount", "10");//金额
|
||||
listMapOne.put("taxRateValue", "0.13");//税率(注:金税三期之后 不存在16% 与10%税率 16%自动会降为13% 10%自动降为9%) 如果使用一般人使用3% 或 5%税率请与财务确认是否享受了优惠政策
|
||||
listMapOne.put("includeTaxFlag", "0");//含税标识
|
||||
listMapOne.put("account", null);//数电账号,传入字段会指定开票员进行开票,不传则随机取一名开票员进行开票 (注: 开票员需要实现在票通登记)
|
||||
/**
|
||||
* 以下为零税率开票相关参数
|
||||
* 零税商品为特殊税率,正式环境使用零税要事先与相关财务人员沟通该企业是否有零税商品,且适用那种零税状态
|
||||
* (免税,不征税,普通零税率)其中一种
|
||||
*/
|
||||
zeroGoodsformat(null, listMapOne);//零税商品类型 1:免税,2:不征税,3:普通零税率,4:简易征收,5:按5%简易征收.其它:非零税率,
|
||||
list.add(listMapOne);
|
||||
map.put("itemList", list);
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
System.out.println(content);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
System.out.println(buildRequest);
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: testRealEstateRentalInvoiceBlue
|
||||
* @description: 蓝票接口调用(开具不动产租赁发票) 非特定业务无视即可. 停车费属于特定业务
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testRealEstateRentalInvoiceBlue() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/invoiceBlue.pt";
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("taxpayerNum", taxpayerNum); //销方税号
|
||||
map.put("invoiceReqSerialNo", date(platform_alias));//发票请求流水号
|
||||
map.put("buyerName", "购买方名称");//购买方名称
|
||||
map.put("invoiceIssueKindCode", "82");//购买方名称
|
||||
map.put("buyerTaxpayerNum", "XX0000000000000000");//购买方税号(非必填,个人发票传null)
|
||||
map.put("remark", "扫码");//购买方税号(非必填,个人发票传null)
|
||||
List<Map<String, String>> list = new ArrayList<Map<String, String>>();
|
||||
Map<String, String> listMapOne = new HashMap<String, String>();
|
||||
listMapOne.put("taxClassificationCode", "3040502020200000000");//税收分类编码(可以按照Excel文档填写)
|
||||
listMapOne.put("quantity", "1.00");//数量
|
||||
listMapOne.put("goodsName", "*货物*货物名称");//货物名称
|
||||
listMapOne.put("unitPrice", "10");//单价
|
||||
listMapOne.put("invoiceIssueKindCode", "82");//发票种类 (注意:数电发票在81与82之间选择 )81:数电票(增值税专用发票)82:数电票(普通发票)10:增值税电子普通发票
|
||||
listMapOne.put("invoiceAmount", "10");//金额
|
||||
listMapOne.put("taxRateValue", "0.13");//税率(注:金税三期之后 不存在16% 与10%税率 16%自动会降为13% 10%自动降为9%)
|
||||
listMapOne.put("includeTaxFlag", "0");//含税标识
|
||||
listMapOne.put("account", null);//数电账号,传入字段会指定开票员进行开票,不传则随机取一名开票员进行开票 (注: 开票员需要实现在票通登记)
|
||||
/**
|
||||
* 以下为零税率开票相关参数
|
||||
* 零税商品为特殊税率,正式环境使用零税要事先与相关财务人员沟通该企业是否有零税商品,且适用那种零税状态
|
||||
* (出口零税率,免税,不征税,普通零税率)其中一种
|
||||
*/
|
||||
zeroGoodsformat(null, listMapOne);//零税商品类型 0:出口零税率,1:免税,2:不征税,3:普通零税率,4:简易征收,5:按5%简易征收.其它:非零税率,
|
||||
Map<String, String> realEstateRentalService = new HashMap<String, String>();
|
||||
realEstateRentalService.put("region", "四川省绵阳市涪城区");
|
||||
realEstateRentalService.put("detailedAddress", "东江北路 68 号");
|
||||
realEstateRentalService.put("areaUnit", "平方米");
|
||||
realEstateRentalService.put("crossCitySign", "0");
|
||||
realEstateRentalService.put("leaseTerm", "2022-12-01 12:10 2022-12-12 15:00");
|
||||
realEstateRentalService.put("titleNo", "无");
|
||||
realEstateRentalService.put("carPlateNum", "京A4564651");
|
||||
map.put("realEstateRentalService", realEstateRentalService);//购买方税号(非必填,个人发票传null)
|
||||
list.add(listMapOne);
|
||||
map.put("itemList", list);
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
System.out.println(content);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
System.out.println(buildRequest);
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: testRegister
|
||||
* @description: 注册接口调用
|
||||
*/
|
||||
@Test
|
||||
public void testRegister() throws Exception {
|
||||
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/register.pt";
|
||||
Map<String, String> map = new HashMap<String, String>();
|
||||
map.put("taxpayerNum", date(platform_alias));//销方纳税人识别号 测试注册时也请使用贵公司的企业税号进行注册
|
||||
map.put("enterpriseName", "票通信息");//销方企业名称
|
||||
map.put("legalPersonName", "AA");//法人名称
|
||||
map.put("contactsName", "AA");//联系人名称
|
||||
map.put("contactsEmail", "1121@qq.com");//联系人邮箱
|
||||
map.put("contactsPhone", "15111111133");//联系人手机号
|
||||
map.put("regionCode", "11");//地区编码
|
||||
map.put("cityName", "海淀区");//市(区)名
|
||||
map.put("enterpriseAddress", "地址");//详细地址
|
||||
// TODO 请修改为正确的图片Base64传
|
||||
map.put("taxRegistrationCertificate", "sdddddddddddddddddddd");//证件图片base64
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
System.out.println(content);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
System.out.println(buildRequest);
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
String responseI = this.disposeResponse(response, ptPublicKey, password);
|
||||
System.out.println(response);
|
||||
System.out.println(responseI);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: testInvoiceRed
|
||||
* @description: 红票接口调用
|
||||
*/
|
||||
@Test
|
||||
public void testInvoiceRed() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/invoiceRed.pt";
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号(请于要冲红的蓝票税号一致)
|
||||
// TODO 请更换请求流水号前缀
|
||||
map.put("invoiceReqSerialNo", platform_alias + "5678902234568904");//发票流水号 (唯一, 与蓝票发票流水号不一致)
|
||||
//(invoiceCode和invoiceNo) 与 (blueAllEleInvNo) 只能传一个
|
||||
map.put("invoiceCode", "");//冲红发票的发票代码 原蓝票为税控发票时传递
|
||||
map.put("invoiceNo", "");//冲红发票的发票号码 原蓝票为税控发票时传递
|
||||
map.put("blueAllEleInvNo", "25019200000097980167");//冲红发票的发票号码 原蓝票为数电发票时传递
|
||||
|
||||
map.put("amount", "-11.30");//冲红金额 (要与原发票的总金额一致)
|
||||
// map.put("blueInvoiceDate", "20250101");//蓝票开票日期.冲红的原蓝票非票通开具的发票时候使用
|
||||
map.put("redReason", "01");//冲红原因冲红原因,不传默认 01 01:开票有误 02:销货退回 03:服务中止 04:销售折让
|
||||
map.put("invoiceKind", null);//发票种类 用于声明开具的红票是什么发票 不传跟随原蓝票的发票种类一致 (注意:数电发票在81与82之间选择 )81:数电票(增值税专用发票)82:数电票(普通发票)10:增值税电子普通发票
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
System.out.println(content);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
System.out.println(buildRequest);
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: testAuthWeChatCards
|
||||
* @description:查询发票
|
||||
*/
|
||||
@Test
|
||||
public void testqueryInvoice() throws Exception {
|
||||
// String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/queryInvoice.pt";
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/queryInvoiceInfo.pt"; //查询发票票面全面信息地址
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号(要跟被查询的发票税号一致)
|
||||
// TODO 请更换请求流水号前缀
|
||||
// map.put("invoiceReqSerialNo", platform_alias + "2026050711455858");//发票流水号 (查询的红票或者蓝票开具时所填写的invoiceReqSerialNo一致)
|
||||
map.put("invoiceReqSerialNo", "DEMK2026051814163083");//发票流水号 (查询的红票或者蓝票开具时所填写的invoiceReqSerialNo一致)
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
System.out.println(buildRequest);
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testgetUnLoginUrl() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/getUnLoginUrl.pt";
|
||||
// String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/queryInvoiceInfo.pt"; //查询发票票面全面信息地址
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号(要跟被查询的发票税号一致)
|
||||
// TODO 请更换请求流水号前缀
|
||||
map.put("redirectUrl", "jtgl.piaotong.vip");//发票流水号 (查询的红票或者蓝票开具时所填写的invoiceReqSerialNo一致)
|
||||
map.put("customData", "jtgl.piaotong.vip");
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
System.out.println(buildRequest);
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testgetUnAuthUrl() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/getUnAuthUrl.pt";
|
||||
// String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/queryInvoiceInfo.pt"; //查询发票票面全面信息地址
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号(要跟被查询的发票税号一致)
|
||||
// TODO 请更换请求流水号前缀
|
||||
map.put("token", "9297dbfcd1ff4b179d56406453a3c9f9");//发票流水号 (查询的红票或者蓝票开具时所填写的invoiceReqSerialNo一致)
|
||||
// map.put("customData", "jtgl.piaotong.vip");
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
System.out.println(buildRequest);
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: testGetInvoiceQrAndExtractCode
|
||||
* @description: 获取多项目开票二维码和提取码接口
|
||||
*/
|
||||
@Test
|
||||
public void testGetInvoiceIssueQrcodeItems() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/getInvoiceIssueQrcode.pt";
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("taxpayerNum", taxpayerNum); //销方纳税人识别号
|
||||
map.put("enterpriseName", taxpayerNum); //销方企业名称 如实填写 测试环境环境提供的测试税号名称与税号恰好一致. 正式环境不要模仿
|
||||
map.put("qrcodeNo", date(platform_alias));//二维码编号(唯一)
|
||||
map.put("tradeNo", "DEMO12345678900");//订单号
|
||||
map.put("tradeTime", "2017-06-26 09:15:54"); //交易时间
|
||||
map.put("invoiceAmount", "3"); //发票金额(含税)
|
||||
map.put("casherName", "收款人A"); //收款人姓名(校验规则: 中文/字母大小写/及其两者组合)
|
||||
map.put("reviewerName", "审核人A"); //审核人姓名(校验规则: 中文/字母大小写/及其两者组合)
|
||||
map.put("drawerName", "开票人A"); //开票人姓名(校验规则: 中文/字母大小写/及其两者组合)
|
||||
map.put("allowInvoiceCount", "1"); //允许开票张数(非必填 默认值:1)
|
||||
map.put("definedData", "所填软件");
|
||||
// map.put("smsFlag", "false"); //是否发送短信 (非必填 默认值:false 测试环境不发送短信)
|
||||
// map.put("expireTime", ""); //有效时间 (非必填 默认值:永久有效 填写格式 yyyy-MM-dd HH:mm:ss)
|
||||
// map.put("email","XXXXX@XX.com"); //二维码发送邮箱地址(非必填)
|
||||
//其他参数见接口文档
|
||||
//可选发票种类及分机列表信息
|
||||
List<Map<String, String>> invoiceIssueOptionsList = new ArrayList<Map<String, String>>();
|
||||
Map<String, String> invoiceIssueOptionsMap = new HashMap<String, String>();
|
||||
invoiceIssueOptionsMap.put("invoiceType", "82");//可选发票种类 10税控电普 08税控电专 82数电普票 81数电纸票
|
||||
// invoiceIssueOptionsMap.put("extensionNum","0");//分机号
|
||||
// invoiceIssueOptionsMap.put("machineCode",null);//机器编码
|
||||
invoiceIssueOptionsList.add(invoiceIssueOptionsMap);
|
||||
map.put("invoiceIssueOptions", invoiceIssueOptionsList);
|
||||
//商品行
|
||||
List<Map<String, String>> list = new ArrayList<Map<String, String>>();
|
||||
Map<String, String> listMapOne = new HashMap<String, String>();
|
||||
listMapOne.put("itemName", "小麦"); //开票项目名
|
||||
listMapOne.put("taxRateValue", "0.13"); //税率(注:金税三期之后 不存在16% 与10%税率 16%自动会降为13% 10%自动降为9%)
|
||||
listMapOne.put("taxClassificationCode", "1010101020000000000");//税收分类编码
|
||||
listMapOne.put("quantity", "1"); //数量
|
||||
listMapOne.put("unitPrice", "2"); //单价
|
||||
listMapOne.put("invoiceItemAmount", "2"); //金额
|
||||
Map<String, String> listMapTwo = new HashMap<String, String>();
|
||||
listMapTwo.put("itemName", "大米");
|
||||
listMapTwo.put("taxRateValue", "0.13");
|
||||
listMapTwo.put("taxClassificationCode", "1010101020000000000");
|
||||
listMapTwo.put("quantity", "1");
|
||||
listMapTwo.put("unitPrice", "1");
|
||||
listMapTwo.put("invoiceItemAmount", "1");
|
||||
list.add(listMapOne);
|
||||
list.add(listMapTwo);
|
||||
map.put("itemList", list);
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: queryInvoiceQrCode
|
||||
* @description:查询二维码信息
|
||||
*/
|
||||
@Test
|
||||
public void queryInvoiceQrCode() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/queryInvoiceQrCode.pt";
|
||||
HashMap<String, String> map = new HashMap<String, String>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号
|
||||
map.put("tradeNo", "DEMO2019090916264942");//订单号
|
||||
map.put("invoiceAmount", "3");//金额
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
System.out.println(content);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: testdeleteInvoiceQrCode
|
||||
* @description: 作废二维码接口
|
||||
*/
|
||||
@Test
|
||||
public void testdeleteInvoiceQrCode() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/deleteInvoiceQrCode.pt";
|
||||
List<Map<String, String>> list = new ArrayList<Map<String, String>>();
|
||||
Map<String, String> map = new HashMap<String, String>();
|
||||
map.put("taxpayerNum", taxpayerNum); //销方税号
|
||||
map.put("enterpriseName", taxpayerNum);//企业名称 如实填写 测试环境环境提供的测试税号名称与税号恰好一致. 正式环境不要模仿
|
||||
map.put("tradeNo", "CTXP2018091910241715");//与开票二维码的订单号一致
|
||||
map.put("tradeTime", "2017-06-26 09:15:54"); //与开票二维码的时间一致
|
||||
map.put("invoiceAmount", "1.00");//金额一致
|
||||
list.add(map);
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
System.out.println(buildRequest);
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: testAuthWeChatCards
|
||||
* @description:查询发票抬头信息
|
||||
*/
|
||||
@Test
|
||||
public void testTitleInfo() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/getInvoiceTitleInfo.pt";
|
||||
String content = "{\"enterpriseName\":\"北京票通\"}";
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
System.out.println(buildRequest);
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: testAuthWeChatCards
|
||||
* @description:微信卡包授
|
||||
*/
|
||||
@Test
|
||||
public void testAuthWeChatCards() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/authWeChatCards.pt";
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号(要跟被查询的发票税号一致)
|
||||
// TODO 请更换请求流水号前缀
|
||||
map.put("invoiceReqSerialNo", platform_alias + "2025021811251811");//发票流水号 (查询的红票或者蓝票开具时所填写的invoiceReqSerialNo一致)
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: queryInvoiceQrCode
|
||||
* @description:重发短信接口
|
||||
*/
|
||||
@Test
|
||||
public void resendEmailOrSMS() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/resendEmailOrSMS.pt";
|
||||
HashMap<String, String> map = new HashMap<String, String>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号
|
||||
map.put("invoiceReqSerialNo", "DEMO2019090916264942");//流水号 注:发票请求流水号和发票代码号码不能同时为空,如果都填写以发票请求流水号为准
|
||||
map.put("invoiceCode", "3");//发票代码
|
||||
map.put("invoiceNo", "3");//发票号码
|
||||
map.put("takerEmail", "3");// 收票人邮箱,不传的话默认使用开具时传的收票人邮箱
|
||||
map.put("takerTel", "3");// 收票人手机号,无默认,需要校验企业是否开通发送短信。
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
System.out.println(content);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
}
|
||||
/**
|
||||
*
|
||||
*
|
||||
* 以下为数电专用接口
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* @title: testregisterUser
|
||||
* @description:用户登记接口
|
||||
*/
|
||||
@Test
|
||||
public void testregisterUser() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/registerUser.pt";
|
||||
HashMap<String, String> map = new HashMap<String, String>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号
|
||||
map.put("loginMethod", "1");//登录方式。1:用户名(居民身份证号码/手机号码/用户名)+密码 只有1
|
||||
map.put("account", "DEMOadmin");//电子税局登录账号(手机号或身份证号),若平台已经存在做更新操作
|
||||
map.put("password", SecurityUtil.encrypt3DES(password, "ispassword"));//1电子税局登录密码 3DES 加密
|
||||
map.put("identityType", "01");// 登录身份类型. 要与电子税局身份一致 01:法定代表人02:财务负责人03:办税员 04:涉税服务人员05:管理员07:领票人09:开票员99:其他人员
|
||||
map.put("identityPwd", null);// 登录身份密码 (只有部分地区需要.目前多数区域不需要,预留不传值)
|
||||
map.put("phoneNum", "13000000000");// 手机号码。当前登记用户的手机号。如果是手机号+密码登录,该值必须和 account 一致
|
||||
map.put("name", "测试");//姓名 最好要跟登记的数电账户对应的开票人一致
|
||||
map.put("operationType", null);// 操作类型。默认 1 登记。 1:登记;2:删除 多数情况下不传
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
System.out.println(content);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: sendLoginSmsCode
|
||||
* @description:获取登录短信验证码
|
||||
*/
|
||||
@Test
|
||||
public void sendLoginSmsCode() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/sendLoginSmsCode.pt";
|
||||
HashMap<String, String> map = new HashMap<String, String>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号
|
||||
map.put("account", "DEMOadmin");//电子税局登录账号(手机号或身份证号),若平台已经存在做更新操作
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
System.out.println(content);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: smsLogin
|
||||
* @description:短信登录
|
||||
*/
|
||||
@Test
|
||||
public void smsLogin() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/smsLogin.pt";
|
||||
HashMap<String, String> map = new HashMap<String, String>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号
|
||||
map.put("account", "DEMOadmin");//电子税局登录账号(手机号或身份证号),若平台已经存在做更新操作
|
||||
map.put("smsCode", "123456");//短信验证码
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
System.out.println(content);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: getAuthenticationQrcode
|
||||
* @description:获取实名认证二维码
|
||||
*/
|
||||
@Test
|
||||
public void getAuthenticationQrcode() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/getAuthenticationQrcode.pt";
|
||||
HashMap<String, String> map = new HashMap<String, String>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号
|
||||
map.put("account", "DEMOadmin");//电子税局登录账号(手机号或身份证号),若平台已经存在做更新操作
|
||||
map.put("qrcodeType", "1");//二维码类型 1:税务 APP; 2:个人所得税 APP .不传值默认 1 税务 APP。
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
System.out.println(content);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: queryAuthQrcodeScanStatus
|
||||
* @description:查询实名认证二维码扫码状态
|
||||
*/
|
||||
@Test
|
||||
public void queryAuthQrcodeScanStatus() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/queryAuthQrcodeScanStatus.pt";
|
||||
HashMap<String, String> map = new HashMap<String, String>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号
|
||||
map.put("account", "DEMOadmin");//电子税局登录账号(手机号或身份证号),若平台已经存在做更新操作
|
||||
map.put("authId", "1");//认证 id,推送开票结果/查询开票结果/获取实名认证二维码时会随二维码一起返回
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
System.out.println(content);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: getTaxBureauAccountAuthStatus
|
||||
* @description:查询数电账号认证状态
|
||||
*/
|
||||
@Test
|
||||
public void getTaxBureauAccountAuthStatus() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/getTaxBureauAccountAuthStatus.pt";
|
||||
HashMap<String, String> map = new HashMap<String, String>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号
|
||||
map.put("account", "DEMOadmin");//电子税局登录账号(手机号或身份证号),若平台已经存在做更新操作
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
System.out.println(content);
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
}
|
||||
/**
|
||||
*
|
||||
* 以下为税控专用接口
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* @title: testGetPTBoxStatus
|
||||
* @description: 获取票通宝状态接口
|
||||
*/
|
||||
@Test
|
||||
public void testGetPTBoxStatus() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/getPTBoxStatus.pt";
|
||||
String content = "{\"taxpayerNum\":\"110105201606160003\",\"enterpriseName\":\"110105201606160003\"}";
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: testGetInvoiceRepertoryInfo
|
||||
* @description: 获取库存接口
|
||||
*/
|
||||
@Test
|
||||
public void testGetInvoiceRepertoryInfo() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/getInvoiceRepertoryInfo.pt";
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("taxpayerNum", taxpayerNum);//销方税号(要跟被查询的发票税号一致)
|
||||
// TODO 请更换请求流水号前缀
|
||||
map.put("enterpriseName", taxpayerNum);//企业名称 如实填写 测试环境环境提供的测试税号名称与税号恰好一致. 正式环境不要模仿
|
||||
Gson gson = new Gson();
|
||||
String content = gson.toJson(map);
|
||||
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: revokeInvoiceIssue
|
||||
* @description:撤销开票
|
||||
*/
|
||||
@Test
|
||||
public void revokeInvoiceIssue() throws Exception {
|
||||
String url = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/revokeInvoiceIssue.pt";
|
||||
String content = "{\"taxpayerNum\":\"110105201606160003\",\"invoiceReqSerialNo\":\"DEMO2019090916515155\"}";
|
||||
//OpenApi参数内容(3des秘钥(票通提供),平台编码(票通提供),平台前缀(票通提供),私钥)
|
||||
String buildRequest = this.buildRequestData(platform_code, platform_alias, content, password, privateKey);
|
||||
;
|
||||
String response = HttpUtils.postJson(url, buildRequest);
|
||||
System.out.println(response);
|
||||
System.out
|
||||
.println(this.disposeResponse(response, ptPublicKey, password));
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* 以下为便捷工具 ,非调用接口
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @title: test3DES
|
||||
* @description: 3DES加密
|
||||
*/
|
||||
@Test
|
||||
public void test3DES() throws Exception {
|
||||
String content = "{\"taxpayerNum\": \"9120931023801231\",\"enterpriseName\": \"西单大悦城有限公司\",\"paymentTransID\": \"12109238102831023102983\",\"paymentType\": \"2\",\"paymentTransTime\": \"2017-01-19 18:20:09\",\"paymentTransMoney\": \"20\",\"orderID\": \"12109238102831023102981\",\"orderMoney\": \"30\"}";
|
||||
System.out.println(SecurityUtil.encrypt3DES(password, content));
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: test3DESDecry
|
||||
* @description: 3DES解密
|
||||
*/
|
||||
@Test
|
||||
public void test3DESDecry() throws Exception {
|
||||
String str = "WkoTqkd08kNxEFqY6bTa/LVHoAj8nYPWJrX8KmI4rrFmJWXMlk5ik7QzwNTvN1Yiq5sGyS17ShQk6UdhwH5XftxVY9W3ytRZr35bic05cZBlq6VejY2AH9Ql5zZu/4xipBD1jTT/6CBeFU5ViYDbGChpDYf8hEVO4JQQl/H5a1SkwtEaPKT8BCQAvy2Sn0ffmCc0NPjaFWASk2bkqM1NzCqFt6BXUjao34IWG2IzUl9O/VXYFAItC/c67lLXu0ziTTK2n0FGLABED5V9uHvhNCvALC81PH1Fd+3KT11i1szg/F79DbzQOK8WrdSnsUSPbyPFC5kA4MbS1xuqEOHsgSBhw0/xPjpq4ODQwPRwjRI=\n";
|
||||
System.out.println(SecurityUtil.decrypt3DES(password, str));
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: testBase64
|
||||
* @description: Base64编码
|
||||
*/
|
||||
@Test
|
||||
public void testBase64() throws Exception {
|
||||
String str = "JJON0d93C9nQN013N+cCwwIYbRVYlWChGQkSgAWG8g4mD1xFU6oGPauqih5gW7ZTcpejSPS8TqRbdBFdBATSXdwZqPM0q8sVYf3xwlp8OEw6INcUCvRW7myiFkzSJLV4Ost42d5Xp+sicgMj0bn99BsRSqe06BMvYTA46L/vGGPqN4tuuy2B/enpkGLcOQdPdtC+wG8ub6+zykisJT5I7EMls73cjaSlj1iRw/PT9huULu97iPHIiqnKhK05AXkvgWMcfg42+bLeG/kPgbaAtwAkXN/yDkKACcDML2WE8TZ+BFsaQPbH+BfY/XQ4VXSYF5NGeulhDJr1DLIHgH+KNQ==";
|
||||
System.out.println(encode2String(str));
|
||||
}
|
||||
|
||||
/**
|
||||
* @title: testBase64
|
||||
* @description: Base64解码
|
||||
*/
|
||||
@Test
|
||||
public void testBase64toURL() throws Exception {
|
||||
String str = "SkpPTjBkOTNDOW5RTjAxM04rY0N3d0lZYlJWWWxXQ2hHUWtTZ0FXRzhnNG1EMXhGVTZvR1BhdXFpaDVnVzdaVGNwZWpTUFM4VHFSYmRCRmRCQVRTWGR3WnFQTTBxOHNWWWYzeHdscDhPRXc2SU5jVUN2Ulc3bXlpRmt6U0pMVjRPc3Q0MmQ1WHArc2ljZ01qMGJuOTlCc1JTcWUwNkJNdllUQTQ2TC92R0dQcU40dHV1eTJCL2VucGtHTGNPUWRQZHRDK3dHOHViNit6eWtpc0pUNUk3RU1sczczY2phU2xqMWlSdy9QVDlodVVMdTk3aVBISWlxbktoSzA1QVhrdmdXTWNmZzQyK2JMZUcva1BnYmFBdHdBa1hOL3lEa0tBQ2NETUwyV0U4VForQkZzYVFQYkgrQmZZL1hRNFZYU1lGNU5HZXVsaERKcjFETElIZ0grS05RPT0=";
|
||||
|
||||
System.out.println(decode2String(str));
|
||||
}
|
||||
|
||||
public String decode2String(String targetString) throws UnsupportedEncodingException {
|
||||
byte[] decodedBytes = Base64.getDecoder().decode(targetString);
|
||||
String decodedString = new String(decodedBytes);
|
||||
return decodedString;
|
||||
}
|
||||
|
||||
public String encode2String(String targetString) throws UnsupportedEncodingException {
|
||||
return Base64.getEncoder().encodeToString(targetString.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param s 零税商品类型 0:出口零税率,1:免税,2:不征税,3:普通零税率,4:简易征收,5:按5%简易征收.其它:非零税率,
|
||||
* @param listMapOne
|
||||
*/
|
||||
private void zeroGoodsformat(String s, Map<String, String> listMapOne) {
|
||||
if ("1".equals(s)) {
|
||||
listMapOne.put("taxRateValue", "0");
|
||||
listMapOne.put("zeroTaxFlag", "1");//零税率标识(空:非零税率,0:出口零税率,1:免税,2:不征税,3:普通零税率)
|
||||
listMapOne.put("preferentialPolicyFlag", "1");//优惠政策标识(空:不使用,1:使用) 注:零税率标识传非空 此字段必须填写为"1"
|
||||
listMapOne.put("vatSpecialManage", "免税");//增值税特殊管理(preferentialPolicyFlag为1 此参数必填)
|
||||
} else if ("0".equals(s)) {
|
||||
listMapOne.put("taxRateValue", "0");
|
||||
listMapOne.put("zeroTaxFlag", "0");//零税率标识(空:非零税率,0:出口零税率,1:免税,2:不征税,3:普通零税率)
|
||||
listMapOne.put("preferentialPolicyFlag", "1");//优惠政策标识(空:不使用,1:使用) 注:零税率标识传非空 此字段必须填写为"1"
|
||||
listMapOne.put("vatSpecialManage", "出口零税");//增值税特殊管理(preferentialPolicyFlag为1 此参数必填)
|
||||
} else if ("2".equals(s)) {
|
||||
listMapOne.put("taxRateValue", "0");
|
||||
listMapOne.put("zeroTaxFlag", "2");//零税率标识(空:非零税率,0:出口零税率,1:免税,2:不征税,3:普通零税率)
|
||||
listMapOne.put("preferentialPolicyFlag", "1");//优惠政策标识(空:不使用,1:使用) 注:零税率标识传非空 此字段必须填写为"1"
|
||||
listMapOne.put("vatSpecialManage", "不征税");//增值税特殊管理(preferentialPolicyFlag为1 此参数必填)
|
||||
} else if ("3".equals(s)) {
|
||||
listMapOne.put("taxRateValue", "0");
|
||||
listMapOne.put("zeroTaxFlag", "3");//零税率标识(空:非零税率,0:出口零税率,1:免税,2:不征税,3:普通零税率)
|
||||
listMapOne.put("preferentialPolicyFlag", null);//优惠政策标识(空:不使用,1:使用) 注:零税率标识传非空 此字段必须填写为"1"
|
||||
listMapOne.put("vatSpecialManage", null);//增值税特殊管理(preferentialPolicyFlag为1 此参数必填)
|
||||
} else if ("4".equals(s)) {
|
||||
listMapOne.put("preferentialPolicyFlag", "1");//优惠政策标识(空:不使用,1:使用) 注:零税率标识传非空 此字段必须填写为"1"
|
||||
listMapOne.put("vatSpecialManage", "简易征收");//增值税特殊管理(preferentialPolicyFlag为1 此参数必填)
|
||||
} else if ("5".equals(s)) {
|
||||
listMapOne.put("taxRateValue", "0.05");
|
||||
listMapOne.put("preferentialPolicyFlag", "1");//优惠政策标识(空:不使用,1:使用) 注:零税率标识传非空 此字段必须填写为"1"
|
||||
listMapOne.put("vatSpecialManage", "按5%简易征收");//增值税特殊管理(preferentialPolicyFlag为1 此参数必填)
|
||||
} else {
|
||||
listMapOne.put("zeroTaxFlag", null);//零税率标识(空:非零税率,0:出口零税率,1:免税,2:不征税,3:普通零税率)
|
||||
listMapOne.put("preferentialPolicyFlag", null);//优惠政策标识(空:不使用,1:使用) 注:零税率标识传非空 此字段必须填写为"1"
|
||||
listMapOne.put("vatSpecialManage", null);//增值税特殊管理(preferentialPolicyFlag为1 此参数必填)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public String buildRequestData(String platformCode, String prefix, String content, String password, String privateKey) throws Exception {
|
||||
Map<String, String> map = new HashMap();
|
||||
String reqContent = SecurityUtil.encrypt3DES(password, content);
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
map.put("platformCode", platformCode);
|
||||
map.put("signType", "RSA");
|
||||
map.put("format", "JSON");
|
||||
map.put("version", "1.0");
|
||||
map.put("content", reqContent);
|
||||
map.put("timestamp", sdf.format(new Date()));
|
||||
map.put("serialNo", date(prefix));
|
||||
map.put("sign", RSAUtil.sign(RSAUtil.getSignatureContent(map), privateKey));
|
||||
Gson gson = new Gson();
|
||||
return gson.toJson(map);
|
||||
}
|
||||
|
||||
public String disposeResponse(String jsonStr, String publicKey, String password) {
|
||||
JsonObject jsonObject = (new JsonParser()).parse(jsonStr).getAsJsonObject();
|
||||
Gson gson = new Gson();
|
||||
HashMap map = gson.fromJson(jsonStr, HashMap.class);
|
||||
String sign = (String) map.remove("sign");
|
||||
if (RSAUtil.verify(RSAUtil.getSignatureContent(map), sign, publicKey)) {
|
||||
String plainContent = SecurityUtil.decrypt3DES(password, (String) map.get("content"));
|
||||
jsonObject.add("content", (new JsonParser()).parse(plainContent));
|
||||
return jsonObject.toString();
|
||||
} else {
|
||||
throw new IllegalStateException("验签失败");
|
||||
}
|
||||
}
|
||||
|
||||
public String date(String prefix) {
|
||||
Date date = new Date();
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("YYYYMMddHHmmss");
|
||||
String str = prefix + sdf.format(date) + (int) (Math.random() * 90 + 10);
|
||||
System.out.println(str);
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* packageName com.example.demo
|
||||
*
|
||||
* @author congtiexin
|
||||
* @date 2025/6/3
|
||||
* @description TODO
|
||||
*/
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
public class HttpUtils {
|
||||
|
||||
private static final Gson gson = new Gson();
|
||||
|
||||
/**
|
||||
* 发送 POST 请求并返回 JSON 响应
|
||||
*
|
||||
* @param url 请求地址
|
||||
* @param request 请求对象(Java Bean 或 Map)
|
||||
* @return 响应内容(Map 或 自定义对象)
|
||||
*/
|
||||
public static <T> T postJson(String url, Object request, Class<T> responseType) throws Exception {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
// 初始化连接
|
||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
connection.setDoOutput(true);
|
||||
|
||||
// 序列化请求体
|
||||
String jsonInput = gson.toJson(request);
|
||||
try (OutputStream os = connection.getOutputStream()) {
|
||||
os.write(jsonInput.getBytes("UTF-8"));
|
||||
os.flush();
|
||||
}
|
||||
|
||||
// 处理响应
|
||||
int responseCode = connection.getResponseCode();
|
||||
if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw new RuntimeException("请求失败,状态码: " + responseCode);
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
try (BufferedReader br = new BufferedReader(
|
||||
new InputStreamReader(connection.getInputStream(), "UTF-8"))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
// 反序列化响应
|
||||
return gson.fromJson(response.toString(), responseType);
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速发送 JSON POST 请求并返回原始字符串响应
|
||||
*/
|
||||
public static String postJson(String url, String jsonBody) throws Exception {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
connection.setDoOutput(true);
|
||||
|
||||
try (OutputStream os = connection.getOutputStream()) {
|
||||
byte[] input = jsonBody.getBytes("UTF-8");
|
||||
os.write(input, 0, input.length);
|
||||
os.flush();
|
||||
}
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw new RuntimeException("请求失败,状态码: " + responseCode);
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
try (BufferedReader br = new BufferedReader(
|
||||
new InputStreamReader(connection.getInputStream(), "UTF-8"))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import javax.crypto.Cipher;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.*;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.*;
|
||||
|
||||
public class RSAUtil {
|
||||
public static final String SIGN_ALGORITHMS = "SHA1WithRSA";
|
||||
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
public RSAUtil() {
|
||||
}
|
||||
|
||||
public static String sign(String content, String privateKey) {
|
||||
try {
|
||||
PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey));
|
||||
KeyFactory keyf = KeyFactory.getInstance("RSA");
|
||||
PrivateKey priKey = keyf.generatePrivate(priPKCS8);
|
||||
Signature signature = Signature.getInstance("SHA1WithRSA");
|
||||
signature.initSign(priKey);
|
||||
signature.update(content.getBytes(DEFAULT_CHARSET));
|
||||
byte[] signed = signature.sign();
|
||||
return Base64.getEncoder().encodeToString(signed);
|
||||
} catch (Exception var7) {
|
||||
var7.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean verify(String content, String sign, String publicKey) {
|
||||
try {
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
byte[] encodedKey = Base64.getDecoder().decode(publicKey);
|
||||
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
|
||||
Signature signature = Signature.getInstance("SHA1WithRSA");
|
||||
signature.initVerify(pubKey);
|
||||
signature.update(content.getBytes(DEFAULT_CHARSET));
|
||||
boolean bverify = signature.verify(Base64.getDecoder().decode(sign));
|
||||
return bverify;
|
||||
} catch (Exception var8) {
|
||||
var8.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static String encrpyt(String content, String publicKeyStr) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance("RSA");
|
||||
cipher.init(1, getPublicKey(publicKeyStr));
|
||||
byte[] enBytes = cipher.doFinal(content.getBytes(DEFAULT_CHARSET));
|
||||
return Base64.getEncoder().encodeToString(enBytes);
|
||||
}
|
||||
|
||||
public static String decrypt(String content, String privateKeyStr) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance("RSA");
|
||||
cipher.init(2, getPrivateKey(privateKeyStr));
|
||||
byte[] deBytes = cipher.doFinal(Base64.getDecoder().decode(content));
|
||||
return new String(deBytes, DEFAULT_CHARSET);
|
||||
}
|
||||
|
||||
public static PublicKey getPublicKey(String key) throws Exception {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(key);
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
PublicKey publicKey = keyFactory.generatePublic(keySpec);
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public static PrivateKey getPrivateKey(String key) throws Exception {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(key);
|
||||
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
public static String getKeyString(Key key) throws Exception {
|
||||
byte[] keyBytes = key.getEncoded();
|
||||
return Base64.getEncoder().encodeToString(keyBytes);
|
||||
}
|
||||
|
||||
public static <T> String getSignatureContent(Map<String, T> params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
} else {
|
||||
StringBuffer content = new StringBuffer();
|
||||
List<String> keys = new ArrayList(params.keySet());
|
||||
Collections.sort(keys);
|
||||
|
||||
for(int i = 0; i < keys.size(); ++i) {
|
||||
String key = (String)keys.get(i);
|
||||
if (params.get(key) != null) {
|
||||
String value = String.valueOf(params.get(key));
|
||||
content.append((i == 0 ? "" : "&") + key + "=" + value);
|
||||
}
|
||||
}
|
||||
|
||||
return content.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static String getListSignatureContent(List<Map> mapList) {
|
||||
if (mapList == null) {
|
||||
return null;
|
||||
} else {
|
||||
List<String> listStr = new ArrayList();
|
||||
Iterator i$ = mapList.iterator();
|
||||
|
||||
while(i$.hasNext()) {
|
||||
Map<String, Object> map = (Map)i$.next();
|
||||
listStr.add(getSignatureContent(map));
|
||||
}
|
||||
|
||||
Collections.sort(listStr);
|
||||
return listStr.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by FernFlower decompiler)
|
||||
//
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Base64;
|
||||
|
||||
public class SecurityUtil {
|
||||
private static final String ALGORITHM_3DES = "DESede";
|
||||
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
public SecurityUtil() {
|
||||
}
|
||||
|
||||
public static byte[] encrypt3DES(String encryptPassword, byte[] encryptByte) {
|
||||
try {
|
||||
Cipher cipher = init3DES(encryptPassword, 1);
|
||||
byte[] doFinal = cipher.doFinal(encryptByte);
|
||||
return doFinal;
|
||||
} catch (Exception var4) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String encrypt3DES(String encryptPassword, String encryptStr) {
|
||||
try {
|
||||
Cipher cipher = init3DES(encryptPassword, 1);
|
||||
byte[] enBytes = cipher.doFinal(encryptStr.getBytes(DEFAULT_CHARSET));
|
||||
return Base64.getEncoder().encodeToString(enBytes);
|
||||
} catch (Exception var4) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] decrypt3DES(String decryptPassword, byte[] decryptByte) {
|
||||
try {
|
||||
Cipher cipher = init3DES(decryptPassword, 2);
|
||||
byte[] doFinal = cipher.doFinal(decryptByte);
|
||||
return doFinal;
|
||||
} catch (Exception var4) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String decrypt3DES(String decryptPassword, String decryptString) {
|
||||
try {
|
||||
Cipher cipher = init3DES(decryptPassword, 2);
|
||||
byte[] deBytes = cipher.doFinal(Base64.getDecoder().decode(decryptString));
|
||||
return new String(deBytes, DEFAULT_CHARSET);
|
||||
} catch (Exception var4) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Cipher init3DES(String decryptPassword, int cipherMode) throws Exception {
|
||||
SecretKey deskey = new SecretKeySpec(decryptPassword.getBytes(), "DESede");
|
||||
Cipher cipher = Cipher.getInstance("DESede");
|
||||
cipher.init(cipherMode, deskey);
|
||||
return cipher;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18.3
|
||||
container_name: ticket-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ticket
|
||||
POSTGRES_USER: ticket
|
||||
POSTGRES_PASSWORD: ticket_password
|
||||
TZ: Asia/Shanghai
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- platform_a_postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ticket -d ticket"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:8
|
||||
container_name: ticket-redis
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- redis-server
|
||||
- --requirepass
|
||||
- ticket_password
|
||||
- --appendonly
|
||||
- yes
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- platform_a_redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "ticket_password", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
server:
|
||||
image: docker.bbitcn.net/invoice/server:latest
|
||||
container_name: ticket-server
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "8070:8070"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_NAME: ticket
|
||||
DB_USER: ticket
|
||||
DB_PASSWORD: ticket_password
|
||||
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
REDIS_PASSWORD: ticket_password
|
||||
|
||||
volumes:
|
||||
platform_a_postgres_data:
|
||||
platform_a_redis_data:
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM amazoncorretto:21-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY build/install/ticket/ /app/
|
||||
|
||||
EXPOSE 8070
|
||||
|
||||
CMD ["./bin/ticket"]
|
||||
@@ -0,0 +1,50 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$VERSION = "1.0"
|
||||
|
||||
$IMAGE = "docker.bbitcn.net/bbit_invoice/server"
|
||||
|
||||
Write-Host "Gradle installDist"
|
||||
Write-Host "==============================="
|
||||
|
||||
.\gradlew.bat clean installDist
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "installDist failed"
|
||||
}
|
||||
|
||||
Write-Host "Docker Build"
|
||||
Write-Host "==============================="
|
||||
|
||||
docker build ` -t "${IMAGE}:${VERSION}"`
|
||||
-t "${IMAGE}:latest" `
|
||||
.
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "docker build failed"
|
||||
}
|
||||
|
||||
Write-Host "Push Version"
|
||||
Write-Host "==============================="
|
||||
|
||||
docker push "${IMAGE}:${VERSION}"
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "push version failed"
|
||||
}
|
||||
|
||||
Write-Host "Push Latest"
|
||||
Write-Host "==============================="
|
||||
|
||||
docker push "${IMAGE}:latest"
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "push latest failed"
|
||||
}
|
||||
|
||||
Write-Host "SUCCESS"
|
||||
Write-Host "==============================="
|
||||
Write-Host ""
|
||||
Write-Host "Published:"
|
||||
Write-Host " ${IMAGE}:${VERSION}"
|
||||
Write-Host " ${IMAGE}:latest"
|
||||
@@ -27,6 +27,8 @@ import com.bbit.ticket.service.openapi.OpenInvoiceTaskWorker
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.http.content.singlePageApplication
|
||||
import io.ktor.server.http.content.vue
|
||||
import io.ktor.server.netty.EngineMain
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.get
|
||||
@@ -47,7 +49,7 @@ fun Application.module() {
|
||||
configureCors()
|
||||
configureSecurity()
|
||||
configureDatabase()
|
||||
configureRedis()
|
||||
// configureRedis()
|
||||
runBlocking {
|
||||
DatabaseInitializer.initialize()
|
||||
SeedData.seed()
|
||||
@@ -58,6 +60,10 @@ fun Application.module() {
|
||||
get("/health") {
|
||||
call.respond(ok(mapOf("status" to "UP", "service" to AppConfig.app.name)))
|
||||
}
|
||||
singlePageApplication {
|
||||
useResources = true
|
||||
vue("dist")
|
||||
}
|
||||
route("/api") {
|
||||
registerAuthRoutes()
|
||||
registerUserRoutes()
|
||||
|
||||
@@ -105,10 +105,33 @@ object EnterpriseManageDao {
|
||||
}
|
||||
}
|
||||
|
||||
fun digitalAccountsForEnterprise(enterpriseId: Uuid): List<DigitalAccountManageItem> =
|
||||
PtDigitalAccountTable.selectAll()
|
||||
.where { (PtDigitalAccountTable.enterpriseId eq enterpriseId) and PtDigitalAccountTable.deletedAt.isNull() }
|
||||
fun digitalAccountsForEnterprise(
|
||||
enterpriseId: Uuid,
|
||||
account: String?,
|
||||
status: String?,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
): PageResult<DigitalAccountManageItem> {
|
||||
val where = digitalAccountWhere(enterpriseId, account, status)
|
||||
val total = PtDigitalAccountTable.selectAll().where { where }.count()
|
||||
val rows = PtDigitalAccountTable.selectAll()
|
||||
.where { where }
|
||||
.orderBy(PtDigitalAccountTable.createdAt, SortOrder.DESC)
|
||||
.limit(pageSize)
|
||||
.offset(pageOffset(page, pageSize))
|
||||
.map { it.toDigitalAccountItem() }
|
||||
return PageResult(rows, page, pageSize, total)
|
||||
}
|
||||
|
||||
fun digitalAccountOptionsForEnterprise(
|
||||
enterpriseId: Uuid,
|
||||
account: String?,
|
||||
limit: Int,
|
||||
): List<DigitalAccountManageItem> =
|
||||
PtDigitalAccountTable.selectAll()
|
||||
.where { digitalAccountWhere(enterpriseId, account, "ENABLED") }
|
||||
.orderBy(PtDigitalAccountTable.createdAt, SortOrder.DESC)
|
||||
.limit(limit)
|
||||
.map { it.toDigitalAccountItem() }
|
||||
|
||||
fun digitalAccount(id: Uuid): ResultRow? =
|
||||
@@ -134,6 +157,18 @@ object EnterpriseManageDao {
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
private fun digitalAccountWhere(enterpriseId: Uuid, account: String?, status: String?): Op<Boolean> {
|
||||
var where: Op<Boolean> =
|
||||
(PtDigitalAccountTable.enterpriseId eq enterpriseId) and PtDigitalAccountTable.deletedAt.isNull()
|
||||
account?.trim()?.takeIf { it.isNotEmpty() }?.let {
|
||||
where = where and (PtDigitalAccountTable.account like "%$it%")
|
||||
}
|
||||
status?.trim()?.takeIf { it.isNotEmpty() }?.let {
|
||||
where = where and (PtDigitalAccountTable.status eq it.uppercase())
|
||||
}
|
||||
return where
|
||||
}
|
||||
|
||||
fun upsertDigitalAccount(enterpriseId: Uuid, item: DigitalAccountInfo): Uuid {
|
||||
val existing = PtDigitalAccountTable.selectAll()
|
||||
.where {
|
||||
|
||||
+8
-7
@@ -1,4 +1,4 @@
|
||||
package com.bbit.ticket.database.piaotong
|
||||
package com.bbit.ticket.database.piaotong
|
||||
|
||||
import org.jetbrains.exposed.v1.core.Table
|
||||
import org.jetbrains.exposed.v1.javatime.timestampWithTimeZone
|
||||
@@ -306,13 +306,13 @@ object HistoryInvoiceBasicTable : Table("history_invoice_basic") {
|
||||
.nullable()
|
||||
|
||||
/**
|
||||
* 冲红状态
|
||||
* 红冲状态
|
||||
*
|
||||
* NOT_RED:未冲红
|
||||
* ALREADY_RED:已冲红
|
||||
* REDING:冲红中
|
||||
* RED_FAIL:冲红失败
|
||||
* PART_RED:部分冲红
|
||||
* NOT_RED:未红冲
|
||||
* ALREADY_RED:已红冲
|
||||
* REDING:红冲中
|
||||
* RED_FAIL:红冲失败
|
||||
* PART_RED:部分红冲
|
||||
*/
|
||||
val redFlag = varchar("red_flag", 32)
|
||||
.nullable()
|
||||
@@ -453,3 +453,4 @@ object HistoryInvoiceBasicTable : Table("history_invoice_basic") {
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package com.bbit.ticket.database.piaotong
|
||||
|
||||
@@ -26,13 +26,13 @@ object HistoryInvoiceRedTable : Table("history_invoice_red") {
|
||||
|
||||
/**
|
||||
* 发票代码
|
||||
* 需冲红的原发票代码,冲红增值税发票管理系统开具的发票或数电纸质发票时必填
|
||||
* 需红冲的原发票代码,红冲增值税发票管理系统开具的发票或数电纸质发票时必填
|
||||
*/
|
||||
val invoiceCode = varchar("invoice_code", 12).nullable()
|
||||
|
||||
/**
|
||||
* 发票请求流水号
|
||||
* 需冲红的原发票号码,冲红增值税发票管理系统开具的发票或数电纸质发票时必填
|
||||
* 需红冲的原发票号码,红冲增值税发票管理系统开具的发票或数电纸质发票时必填
|
||||
*/
|
||||
val invoiceNo = varchar("invoice_no", 8).nullable()
|
||||
|
||||
@@ -42,7 +42,7 @@ object HistoryInvoiceRedTable : Table("history_invoice_red") {
|
||||
val invoiceReqSerialNo = varchar("invoice_req_serial_no", 20)
|
||||
|
||||
/**
|
||||
* 冲红原因
|
||||
* 红冲原因
|
||||
* 01:开票有误
|
||||
* 02:销货退回
|
||||
* 03:服务中止
|
||||
@@ -61,9 +61,9 @@ object HistoryInvoiceRedTable : Table("history_invoice_red") {
|
||||
* 81:数电发票(增值税专用发票)
|
||||
* 82:数电发票(普通发票)。
|
||||
* 默认蓝票的发票种类代码。
|
||||
* 此字段目的解决数电发票冲红增值税发票,只有企业不再使用增值税系统时才可以跨票种冲红。
|
||||
* 数电发票(普通发票)可以冲红数电发票(普通发票)、增值税电子普通发票、增值税纸质普通发票。
|
||||
* 数电发票(增值税专用发票)可以冲红数电发票(增值税专用发票)、增值税电子专用发票、增值税纸质专用发票。
|
||||
* 此字段目的解决数电发票红冲增值税发票,只有企业不再使用增值税系统时才可以跨票种红冲。
|
||||
* 数电发票(普通发票)可以红冲数电发票(普通发票)、增值税电子普通发票、增值税纸质普通发票。
|
||||
* 数电发票(增值税专用发票)可以红冲数电发票(增值税专用发票)、增值税电子专用发票、增值税纸质专用发票。
|
||||
*/
|
||||
val invoiceKind = varchar("invoice_kind", 2).nullable()
|
||||
|
||||
@@ -104,3 +104,4 @@ object HistoryInvoiceRedTable : Table("history_invoice_red") {
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ object OpenInvoiceTaskTable : Table("open_invoice_task") {
|
||||
val taxAccount = varchar("tax_account", 64).nullable()
|
||||
val taskType = varchar("task_type", 32)
|
||||
val sourceType = varchar("source_type", 32)
|
||||
val runMode = varchar("run_mode", 16)
|
||||
val invoiceReqSerialNo = varchar("invoice_req_serial_no", 20)
|
||||
val batchNo = varchar("batch_no", 64).nullable()
|
||||
val status = varchar("status", 32)
|
||||
|
||||
@@ -9,7 +9,6 @@ data class OpenInvoiceTaskSubmitResponse(
|
||||
val invoiceReqSerialNo: String,
|
||||
val status: String,
|
||||
val taskType: String,
|
||||
val runMode: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -37,7 +36,6 @@ data class OpenInvoiceTaskItem(
|
||||
val account: String? = null,
|
||||
val taskType: String,
|
||||
val sourceType: String,
|
||||
val runMode: String,
|
||||
val invoiceReqSerialNo: String,
|
||||
val batchNo: String? = null,
|
||||
val status: String,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.bbit.ticket.entity.request
|
||||
package com.bbit.ticket.entity.request
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -28,7 +28,7 @@ data class CreateRedInvoiceRequest(
|
||||
/**
|
||||
* 蓝票代码
|
||||
*
|
||||
* 冲红税控发票或数电纸质发票时必填
|
||||
* 红冲税控发票或数电纸质发票时必填
|
||||
*/
|
||||
@SerialName("blueInvoiceCode")
|
||||
val blueInvoiceCode: String? = null,
|
||||
@@ -36,7 +36,7 @@ data class CreateRedInvoiceRequest(
|
||||
/**
|
||||
* 蓝票号码
|
||||
*
|
||||
* 冲红税控发票或数电纸质发票时必填
|
||||
* 红冲税控发票或数电纸质发票时必填
|
||||
*/
|
||||
@SerialName("blueInvoiceNo")
|
||||
val blueInvoiceNo: String? = null,
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
package com.bbit.ticket.entity.request
|
||||
package com.bbit.ticket.entity.request
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -20,25 +20,25 @@ data class InitRedInvoiceConfirmRequest(
|
||||
|
||||
/**
|
||||
* 税控蓝字发票代码(10或12位)
|
||||
* 冲红增值税发票管理系统开具的发票或数电纸票时必填
|
||||
* 红冲增值税发票管理系统开具的发票或数电纸票时必填
|
||||
*/
|
||||
val invoiceCode: String? = null,
|
||||
|
||||
/**
|
||||
* 税控蓝字发票号码(8位)
|
||||
* 冲红增值税发票管理系统开具的发票或数电纸票时必填
|
||||
* 红冲增值税发票管理系统开具的发票或数电纸票时必填
|
||||
*/
|
||||
val invoiceNo: String? = null,
|
||||
|
||||
/**
|
||||
* 蓝字数电票号码(20位)
|
||||
* 冲红数电发票时必填
|
||||
* 红冲数电发票时必填
|
||||
*/
|
||||
val blueAllEleInvNo: String? = null,
|
||||
|
||||
/**
|
||||
* 蓝字发票开票日期(yyyyMMdd)
|
||||
* 冲红非票通平台开具的发票时必填
|
||||
* 红冲非票通平台开具的发票时必填
|
||||
*/
|
||||
val blueInvoiceDate: String? = null,
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.bbit.ticket.entity.request
|
||||
package com.bbit.ticket.entity.request
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* 快捷冲红数电发票请求
|
||||
* 快捷红冲数电发票请求
|
||||
*/
|
||||
@Serializable
|
||||
data class QuickRedInvoiceRequest(
|
||||
@@ -29,7 +29,7 @@ data class QuickRedInvoiceRequest(
|
||||
/**
|
||||
* 原发票代码
|
||||
*
|
||||
* 冲红增值税系统发票或数电纸质发票时必填
|
||||
* 红冲增值税系统发票或数电纸质发票时必填
|
||||
*/
|
||||
@SerialName("invoiceCode")
|
||||
val invoiceCode: String? = null,
|
||||
@@ -37,7 +37,7 @@ data class QuickRedInvoiceRequest(
|
||||
/**
|
||||
* 原发票号码
|
||||
*
|
||||
* 冲红增值税系统发票或数电纸质发票时必填
|
||||
* 红冲增值税系统发票或数电纸质发票时必填
|
||||
*/
|
||||
@SerialName("invoiceNo")
|
||||
val invoiceNo: String? = null,
|
||||
@@ -45,7 +45,7 @@ data class QuickRedInvoiceRequest(
|
||||
/**
|
||||
* 原数电发票号码
|
||||
*
|
||||
* 冲红数电发票时必填
|
||||
* 红冲数电发票时必填
|
||||
*/
|
||||
@SerialName("blueAllEleInvNo")
|
||||
val blueAllEleInvNo: String? = null,
|
||||
@@ -56,14 +56,14 @@ data class QuickRedInvoiceRequest(
|
||||
* 格式:yyyyMMdd
|
||||
*
|
||||
* 场景:
|
||||
* 冲红非票通平台开具的发票时必填,
|
||||
* 红冲非票通平台开具的发票时必填,
|
||||
* 税局会结合该日期与数电发票号码拉取原票
|
||||
*/
|
||||
@SerialName("blueInvoiceDate")
|
||||
val blueInvoiceDate: String? = null,
|
||||
|
||||
/**
|
||||
* 冲红原因
|
||||
* 红冲原因
|
||||
*
|
||||
* 默认值:01
|
||||
*
|
||||
@@ -108,7 +108,7 @@ data class QuickRedInvoiceRequest(
|
||||
*
|
||||
* 默认取蓝票票种
|
||||
*
|
||||
* 用于跨票种冲红场景
|
||||
* 用于跨票种红冲场景
|
||||
*/
|
||||
@SerialName("invoiceKind")
|
||||
val invoiceKind: String? = null,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.bbit.ticket.entity.request
|
||||
package com.bbit.ticket.entity.request
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -7,7 +7,11 @@ data class RedCreateRequest(
|
||||
|
||||
val historyId: String,
|
||||
/**
|
||||
* 冲红原因
|
||||
* 平台数电账号 ID。企业管理员发起红冲时用于指定开票员。
|
||||
*/
|
||||
val digitalAccountId: String? = null,
|
||||
/**
|
||||
* 红冲原因
|
||||
*
|
||||
* 默认值:01
|
||||
*
|
||||
@@ -22,3 +26,4 @@ data class RedCreateRequest(
|
||||
val takerTel: String? = null,
|
||||
val takerEmail: String? = null,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package com.bbit.ticket.entity.response
|
||||
|
||||
@@ -100,7 +100,7 @@ data class InvoiceHistoryItem(
|
||||
// ===== 发票状态标记 =====
|
||||
/** 作废状态 */
|
||||
val invalidFlag: String? = null,
|
||||
/** 冲红状态 */
|
||||
/** 红冲状态 */
|
||||
val redFlag: String? = null,
|
||||
|
||||
// ===== 人员信息 =====
|
||||
@@ -149,3 +149,4 @@ data class InvoiceHistoryItem(
|
||||
/** 删除标记 */
|
||||
val invDeletedFlag: String? = null,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.bbit.ticket.entity.response
|
||||
package com.bbit.ticket.entity.response
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -168,13 +168,13 @@ data class InvoiceInfo(
|
||||
val invalidFlag: String,
|
||||
|
||||
/**
|
||||
* 冲红标志
|
||||
* 红冲标志
|
||||
*
|
||||
* NOT_RED:未冲红
|
||||
* ALREADY_RED:已冲红
|
||||
* REDING:冲红中
|
||||
* RED_FAIL:冲红失败
|
||||
* PART_RED:部分冲红
|
||||
* NOT_RED:未红冲
|
||||
* ALREADY_RED:已红冲
|
||||
* REDING:红冲中
|
||||
* RED_FAIL:红冲失败
|
||||
* PART_RED:部分红冲
|
||||
*/
|
||||
@SerialName("redFlag")
|
||||
val redFlag: String,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.bbit.ticket.entity.response
|
||||
package com.bbit.ticket.entity.response
|
||||
|
||||
import com.bbit.ticket.entity.request.InvoiceItem
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -187,12 +187,12 @@ data class GetInvoiceInfoResponse(
|
||||
val invalidFlag: String? = null,
|
||||
|
||||
/**
|
||||
* 冲红状态
|
||||
* NOT_RED:未冲红
|
||||
* ALREADY_RED:已冲红
|
||||
* REDING:冲红中
|
||||
* RED_FAIL:冲红失败
|
||||
* PART_RED:部分冲红
|
||||
* 红冲状态
|
||||
* NOT_RED:未红冲
|
||||
* ALREADY_RED:已红冲
|
||||
* REDING:红冲中
|
||||
* RED_FAIL:红冲失败
|
||||
* PART_RED:部分红冲
|
||||
*/
|
||||
val redFlag: String? = null,
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.bbit.ticket.entity.response
|
||||
package com.bbit.ticket.entity.response
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* 快捷冲红数电发票响应
|
||||
* 快捷红冲数电发票响应
|
||||
*/
|
||||
@Serializable
|
||||
data class QuickRedInvoiceResponse(
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package com.bbit.ticket.entity.response
|
||||
package com.bbit.ticket.entity.response
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* 红票申请信息
|
||||
*
|
||||
* 对应 [HistoryInvoiceRedTable] 的冲红申请字段,
|
||||
* 对应 [HistoryInvoiceRedTable] 的红冲申请字段,
|
||||
* 用于红票详情弹窗展示。
|
||||
*/
|
||||
@Serializable
|
||||
data class RedInvoiceInfoResponse(
|
||||
/** 冲红原因:01=开票有误 02=销货退回 03=服务中止 04=销售折让 */
|
||||
/** 红冲原因:01=开票有误 02=销货退回 03=服务中止 04=销售折让 */
|
||||
val redReason: String,
|
||||
/** 收票人名称 */
|
||||
val takerName: String? = null,
|
||||
@@ -19,3 +19,4 @@ data class RedInvoiceInfoResponse(
|
||||
/** 收票人邮箱 */
|
||||
val takerEmail: String? = null,
|
||||
)
|
||||
|
||||
|
||||
+2
-16
@@ -8,27 +8,13 @@ import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.post
|
||||
|
||||
fun Route.registerOpenInvoiceTaskRoutes() {
|
||||
post("/test") {
|
||||
post {
|
||||
val principal = call.requireOpenApiPrincipal()
|
||||
val request = call.receive<OpenBlueInvoiceCreateRequest>()
|
||||
call.respondOpenApi(principal, "blue-invoice-task.test", null) {
|
||||
call.respondOpenApi(principal, "blue-invoice-task.create", null) {
|
||||
OpenInvoiceTaskService.createIssueTask(
|
||||
principal = principal,
|
||||
request = request,
|
||||
runMode = OpenInvoiceTaskService.MODE_SIMULATED,
|
||||
sourceType = OpenInvoiceTaskService.SOURCE_TEST,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
post("/production") {
|
||||
val principal = call.requireOpenApiPrincipal()
|
||||
val request = call.receive<OpenBlueInvoiceCreateRequest>()
|
||||
call.respondOpenApi(principal, "blue-invoice-task.production", null) {
|
||||
OpenInvoiceTaskService.createIssueTask(
|
||||
principal = principal,
|
||||
request = request,
|
||||
runMode = OpenInvoiceTaskService.MODE_REAL,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -21,7 +21,6 @@ fun Route.registerOpenInvoiceTaskManageRoutes() {
|
||||
digitalAccountId = call.request.queryParameters["digitalAccountId"],
|
||||
status = call.request.queryParameters["status"],
|
||||
sourceType = call.request.queryParameters["sourceType"],
|
||||
runMode = call.request.queryParameters["runMode"],
|
||||
page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1,
|
||||
pageSize = call.request.queryParameters["pageSize"]?.toIntOrNull() ?: 20,
|
||||
)
|
||||
|
||||
@@ -12,6 +12,8 @@ import com.bbit.ticket.entity.request.UpdateDigitalAccountStatusRequest
|
||||
import com.bbit.ticket.entity.request.UpdateInvoiceSettingRequest
|
||||
import com.bbit.ticket.service.piaotong.PTAuthService
|
||||
import com.bbit.ticket.service.piaotong.PTConfigService
|
||||
import com.bbit.ticket.utils.queryInt
|
||||
import com.bbit.ticket.utils.queryString
|
||||
import com.bbit.ticket.utils.requireCurrentUser
|
||||
import io.ktor.server.request.receive
|
||||
import io.ktor.server.routing.Route
|
||||
@@ -59,7 +61,23 @@ fun Route.registerPTAuthRoutes() {
|
||||
|
||||
get("/digital-accounts") {
|
||||
call.respondPt("查询数电账号失败") {
|
||||
PTConfigService.listDigitalAccounts(call.requireCurrentUser())
|
||||
PTConfigService.listDigitalAccounts(
|
||||
user = call.requireCurrentUser(),
|
||||
account = call.queryString("account"),
|
||||
status = call.queryString("status"),
|
||||
page = call.queryInt("page", 1),
|
||||
pageSize = call.queryInt("pageSize", 20),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
get("/digital-accounts/options") {
|
||||
call.respondPt("查询数电账号选项失败") {
|
||||
PTConfigService.listDigitalAccountOptions(
|
||||
user = call.requireCurrentUser(),
|
||||
account = call.queryString("account"),
|
||||
limit = call.queryInt("limit", 200),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,9 +56,6 @@ object OpenInvoiceTaskService {
|
||||
const val TASK_ISSUE_BLUE = "ISSUE_BLUE"
|
||||
const val TASK_QUERY_BLUE = "QUERY_BLUE"
|
||||
const val SOURCE_SINGLE = "SINGLE"
|
||||
const val SOURCE_TEST = "TEST"
|
||||
const val MODE_REAL = "REAL"
|
||||
const val MODE_SIMULATED = "SIMULATED"
|
||||
|
||||
private const val STATUS_PENDING = "PENDING"
|
||||
private const val STATUS_PROCESSING = "PROCESSING"
|
||||
@@ -75,7 +72,6 @@ object OpenInvoiceTaskService {
|
||||
suspend fun createIssueTask(
|
||||
principal: OpenApiPrincipal,
|
||||
request: OpenBlueInvoiceCreateRequest,
|
||||
runMode: String,
|
||||
sourceType: String = SOURCE_SINGLE,
|
||||
batchNo: String? = null,
|
||||
): OpenInvoiceTaskSubmitResponse {
|
||||
@@ -97,12 +93,6 @@ object OpenInvoiceTaskService {
|
||||
.singleOrNull()
|
||||
}
|
||||
if (existing != null) {
|
||||
if (existing[OpenInvoiceTaskTable.runMode] != runMode) {
|
||||
throw BizException(
|
||||
ErrorCode.BAD_REQUEST.code,
|
||||
"invoiceReqSerialNo 已存在 ${existing[OpenInvoiceTaskTable.runMode]} 任务,不能重复创建 $runMode 任务",
|
||||
)
|
||||
}
|
||||
return existing.toSubmitResponse()
|
||||
}
|
||||
|
||||
@@ -129,7 +119,6 @@ object OpenInvoiceTaskService {
|
||||
it[taxAccount] = principal.taxAccount
|
||||
it[taskType] = TASK_ISSUE_BLUE
|
||||
it[OpenInvoiceTaskTable.sourceType] = sourceType
|
||||
it[OpenInvoiceTaskTable.runMode] = runMode
|
||||
it[OpenInvoiceTaskTable.invoiceReqSerialNo] = invoiceReqSerialNo
|
||||
it[OpenInvoiceTaskTable.batchNo] = batchNo
|
||||
it[status] = STATUS_PENDING
|
||||
@@ -146,19 +135,12 @@ object OpenInvoiceTaskService {
|
||||
}
|
||||
.single()
|
||||
}
|
||||
if (task[OpenInvoiceTaskTable.runMode] != runMode) {
|
||||
throw BizException(
|
||||
ErrorCode.BAD_REQUEST.code,
|
||||
"invoiceReqSerialNo 已存在 ${task[OpenInvoiceTaskTable.runMode]} 任务,不能重复创建 $runMode 任务",
|
||||
)
|
||||
}
|
||||
|
||||
return OpenInvoiceTaskSubmitResponse(
|
||||
taskId = task[OpenInvoiceTaskTable.id].toString(),
|
||||
invoiceReqSerialNo = invoiceReqSerialNo,
|
||||
status = task[OpenInvoiceTaskTable.status],
|
||||
taskType = TASK_ISSUE_BLUE,
|
||||
runMode = runMode,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -201,7 +183,6 @@ object OpenInvoiceTaskService {
|
||||
digitalAccountId: String?,
|
||||
status: String?,
|
||||
sourceType: String?,
|
||||
runMode: String?,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
): PageResult<OpenInvoiceTaskItem> = dbQuery {
|
||||
@@ -215,19 +196,32 @@ object OpenInvoiceTaskService {
|
||||
sourceType?.takeIf { it.isNotBlank() }?.let {
|
||||
where = where and (OpenInvoiceTaskTable.sourceType eq it)
|
||||
}
|
||||
runMode?.takeIf { it.isNotBlank() }?.let {
|
||||
where = where and (OpenInvoiceTaskTable.runMode eq it)
|
||||
}
|
||||
val accountRows = PtDigitalAccountTable.selectAll()
|
||||
.where { accountScope(user) }
|
||||
.associateBy { it[PtDigitalAccountTable.id] }
|
||||
val total = OpenInvoiceTaskTable.selectAll().where { where }.count()
|
||||
val items = OpenInvoiceTaskTable.selectAll()
|
||||
val taskRows = OpenInvoiceTaskTable.selectAll()
|
||||
.where { where }
|
||||
.orderBy(OpenInvoiceTaskTable.createdAt, SortOrder.DESC)
|
||||
.limit(pageSize)
|
||||
.offset(((page - 1).coerceAtLeast(0) * pageSize).toLong())
|
||||
.map { it.toTaskItem(accountRows[it[OpenInvoiceTaskTable.digitalAccountId]]?.get(PtDigitalAccountTable.account)) }
|
||||
.toList()
|
||||
val historyMessages = taskRows
|
||||
.map { it[OpenInvoiceTaskTable.invoiceReqSerialNo] }
|
||||
.distinct()
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { serialNos ->
|
||||
HistoryInvoiceBasicTable.selectAll()
|
||||
.where { HistoryInvoiceBasicTable.invoiceReqSerialNo inList serialNos }
|
||||
.associate { it[HistoryInvoiceBasicTable.invoiceReqSerialNo] to it[HistoryInvoiceBasicTable.msg] }
|
||||
}
|
||||
?: emptyMap()
|
||||
val items = taskRows.map {
|
||||
it.toTaskItem(
|
||||
accountRows[it[OpenInvoiceTaskTable.digitalAccountId]]?.get(PtDigitalAccountTable.account),
|
||||
historyMessages[it[OpenInvoiceTaskTable.invoiceReqSerialNo]],
|
||||
)
|
||||
}
|
||||
PageResult(items, page, pageSize, total)
|
||||
}
|
||||
|
||||
@@ -274,18 +268,13 @@ object OpenInvoiceTaskService {
|
||||
|
||||
private suspend fun processIssueTask(task: ResultRow) {
|
||||
val taskId = task[OpenInvoiceTaskTable.id]
|
||||
val runMode = task[OpenInvoiceTaskTable.runMode]
|
||||
val request = task[OpenInvoiceTaskTable.requestBody]
|
||||
?.let { myJson.decodeFromString<OpenBlueInvoiceCreateRequest>(it) }
|
||||
?: return failTask(taskId, "REQUEST_BODY_NULL", "任务请求体为空")
|
||||
val askRequest = request.toAskInvoiceRequest(task)
|
||||
createHistoryPlaceholder(task, askRequest)
|
||||
try {
|
||||
if (runMode == MODE_SIMULATED) {
|
||||
delay(2000)
|
||||
} else {
|
||||
PTApi.invoiceBlue(askRequest)
|
||||
}
|
||||
PTApi.invoiceBlue(askRequest)
|
||||
completeIssueTask(task)
|
||||
} catch (e: PTException) {
|
||||
failTask(taskId, e.code, e.message)
|
||||
@@ -298,31 +287,23 @@ object OpenInvoiceTaskService {
|
||||
}
|
||||
|
||||
private suspend fun processQueryTask(task: ResultRow) {
|
||||
val taskId = task[OpenInvoiceTaskTable.id]
|
||||
val runMode = task[OpenInvoiceTaskTable.runMode]
|
||||
val invoiceReqSerialNo = task[OpenInvoiceTaskTable.invoiceReqSerialNo]
|
||||
try {
|
||||
val code = if (runMode == MODE_SIMULATED) {
|
||||
delay(2000)
|
||||
simulatedQueryCode(invoiceReqSerialNo, task[OpenInvoiceTaskTable.pollCount])
|
||||
} else {
|
||||
val res = PTApi.queryInvoiceInfo(
|
||||
QueryInvoiceRequest(
|
||||
taxpayerNum = task[OpenInvoiceTaskTable.taxpayerNum],
|
||||
invoiceReqSerialNo = invoiceReqSerialNo,
|
||||
)
|
||||
val res = PTApi.queryInvoiceInfo(
|
||||
QueryInvoiceRequest(
|
||||
taxpayerNum = task[OpenInvoiceTaskTable.taxpayerNum],
|
||||
invoiceReqSerialNo = invoiceReqSerialNo,
|
||||
)
|
||||
)
|
||||
dbQuery {
|
||||
BlueInvoiceDao.upsertInvoiceInfo(
|
||||
task[OpenInvoiceTaskTable.userId],
|
||||
res,
|
||||
task[OpenInvoiceTaskTable.enterpriseId],
|
||||
task[OpenInvoiceTaskTable.digitalAccountId],
|
||||
)
|
||||
dbQuery {
|
||||
BlueInvoiceDao.upsertInvoiceInfo(
|
||||
task[OpenInvoiceTaskTable.userId],
|
||||
res,
|
||||
task[OpenInvoiceTaskTable.enterpriseId],
|
||||
task[OpenInvoiceTaskTable.digitalAccountId],
|
||||
)
|
||||
}
|
||||
res.code
|
||||
}
|
||||
handleQueryCode(task, code)
|
||||
handleQueryCode(task, res.code, res.msg)
|
||||
} catch (e: PTException) {
|
||||
retryOrFail(task, e.code, e.message)
|
||||
} catch (e: Exception) {
|
||||
@@ -352,7 +333,6 @@ object OpenInvoiceTaskService {
|
||||
it[taxAccount] = task[OpenInvoiceTaskTable.taxAccount]
|
||||
it[taskType] = TASK_QUERY_BLUE
|
||||
it[sourceType] = task[OpenInvoiceTaskTable.sourceType]
|
||||
it[runMode] = task[OpenInvoiceTaskTable.runMode]
|
||||
it[OpenInvoiceTaskTable.invoiceReqSerialNo] = invoiceReqSerialNo
|
||||
it[batchNo] = task[OpenInvoiceTaskTable.batchNo]
|
||||
it[status] = STATUS_PENDING
|
||||
@@ -365,23 +345,23 @@ object OpenInvoiceTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleQueryCode(task: ResultRow, code: String) {
|
||||
private suspend fun handleQueryCode(task: ResultRow, code: String, message: String?) {
|
||||
when (code) {
|
||||
"0000" -> finishQueryTask(task, STATUS_SUCCESS, code, null)
|
||||
"9999" -> finishQueryTask(task, STATUS_FAILED, code, "开票失败")
|
||||
"9999" -> finishQueryTask(task, STATUS_FAILED, code, message?.takeIf { it.isNotBlank() } ?: "开票失败")
|
||||
AUTH_REQUIRED_CODE -> {
|
||||
val message = "需要登录/风险认证"
|
||||
val authMessage = message?.takeIf { it.isNotBlank() } ?: "需要登录/风险认证"
|
||||
dbQuery {
|
||||
OpenInvoiceTaskTable.update({ OpenInvoiceTaskTable.id eq task[OpenInvoiceTaskTable.id] }) {
|
||||
it[status] = STATUS_WAITING_AUTH
|
||||
it[ptCode] = code
|
||||
it[errorMessage] = message
|
||||
it[errorMessage] = authMessage
|
||||
it[updatedAt] = OffsetDateTime.now()
|
||||
it[lockedBy] = null
|
||||
it[lockedAt] = null
|
||||
}
|
||||
}
|
||||
pauseApiKey(task[OpenInvoiceTaskTable.apiKey], code, message)
|
||||
pauseApiKey(task[OpenInvoiceTaskTable.apiKey], code, authMessage)
|
||||
}
|
||||
"7777", "6666" -> requeueQueryTask(task, code)
|
||||
else -> requeueQueryTask(task, code)
|
||||
@@ -644,15 +624,6 @@ object OpenInvoiceTaskService {
|
||||
orderList = orderList,
|
||||
)
|
||||
|
||||
private fun simulatedQueryCode(invoiceReqSerialNo: String, pollCount: Int): String =
|
||||
when {
|
||||
invoiceReqSerialNo.contains("3999", ignoreCase = true) -> AUTH_REQUIRED_CODE
|
||||
invoiceReqSerialNo.contains("FAIL", ignoreCase = true) -> "9999"
|
||||
pollCount <= 0 -> "7777"
|
||||
pollCount == 1 -> "6666"
|
||||
else -> "0000"
|
||||
}
|
||||
|
||||
private fun historyCode(code: String): String =
|
||||
if (code.length <= 8) code else HISTORY_FAILED_CODE
|
||||
|
||||
@@ -690,10 +661,9 @@ object OpenInvoiceTaskService {
|
||||
invoiceReqSerialNo = this[OpenInvoiceTaskTable.invoiceReqSerialNo],
|
||||
status = this[OpenInvoiceTaskTable.status],
|
||||
taskType = this[OpenInvoiceTaskTable.taskType],
|
||||
runMode = this[OpenInvoiceTaskTable.runMode],
|
||||
)
|
||||
|
||||
private fun ResultRow.toTaskItem(account: String?): OpenInvoiceTaskItem =
|
||||
private fun ResultRow.toTaskItem(account: String?, historyMessage: String?): OpenInvoiceTaskItem =
|
||||
OpenInvoiceTaskItem(
|
||||
id = this[OpenInvoiceTaskTable.id].toString(),
|
||||
digitalAccountId = this[OpenInvoiceTaskTable.digitalAccountId].toString(),
|
||||
@@ -701,12 +671,12 @@ object OpenInvoiceTaskService {
|
||||
account = account,
|
||||
taskType = this[OpenInvoiceTaskTable.taskType],
|
||||
sourceType = this[OpenInvoiceTaskTable.sourceType],
|
||||
runMode = this[OpenInvoiceTaskTable.runMode],
|
||||
invoiceReqSerialNo = this[OpenInvoiceTaskTable.invoiceReqSerialNo],
|
||||
batchNo = this[OpenInvoiceTaskTable.batchNo],
|
||||
status = this[OpenInvoiceTaskTable.status],
|
||||
ptCode = this[OpenInvoiceTaskTable.ptCode],
|
||||
errorMessage = this[OpenInvoiceTaskTable.errorMessage],
|
||||
errorMessage = this[OpenInvoiceTaskTable.errorMessage]
|
||||
?: historyMessage?.takeIf { this[OpenInvoiceTaskTable.status] == STATUS_FAILED },
|
||||
attemptCount = this[OpenInvoiceTaskTable.attemptCount],
|
||||
maxAttemptCount = this[OpenInvoiceTaskTable.maxAttemptCount],
|
||||
pollCount = this[OpenInvoiceTaskTable.pollCount],
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.bbit.ticket.dao.piaotong.EnterpriseManageDao
|
||||
import com.bbit.ticket.dao.system.UserDao
|
||||
import com.bbit.ticket.database.piaotong.PtDigitalAccountTable
|
||||
import com.bbit.ticket.database.system.SysUserTable
|
||||
import com.bbit.ticket.entity.common.PageResult
|
||||
import com.bbit.ticket.entity.request.CreateDigitalAccountRequest
|
||||
import com.bbit.ticket.entity.request.QueryDigitalAccountListRequest
|
||||
import com.bbit.ticket.entity.request.QueryEnterpriseBankAccountRequest
|
||||
@@ -62,15 +63,42 @@ object PTConfigService {
|
||||
return PTAuthService.queryEnterpriseBankInfo(QueryEnterpriseBankAccountRequest(taxpayerNum)).bankList
|
||||
}
|
||||
|
||||
suspend fun listDigitalAccounts(user: CurrentUser): List<DigitalAccountManageItem> = dbQuery {
|
||||
suspend fun listDigitalAccounts(
|
||||
user: CurrentUser,
|
||||
account: String?,
|
||||
status: String?,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
): PageResult<DigitalAccountManageItem> = dbQuery {
|
||||
when {
|
||||
user.isSuperAdmin || user.isEnterpriseAdmin -> {
|
||||
EnterpriseManageDao.digitalAccountsForEnterprise(requireEnterpriseId(user))
|
||||
EnterpriseManageDao.digitalAccountsForEnterprise(requireEnterpriseId(user), account, status, page, pageSize)
|
||||
}
|
||||
user.isDigitalOperator -> {
|
||||
val id = user.digitalAccountId
|
||||
?: throw BizException(ErrorCode.BAD_REQUEST.code, "当前账号未绑定数电账号")
|
||||
EnterpriseManageDao.digitalAccount(id)?.let { listOf(EnterpriseManageDao.run { it.toDigitalAccountItem() }) }
|
||||
val item = EnterpriseManageDao.digitalAccount(id)
|
||||
?.let { EnterpriseManageDao.run { it.toDigitalAccountItem() } }
|
||||
?.takeIf { account.isNullOrBlank() || it.account.contains(account.trim(), ignoreCase = true) }
|
||||
?.takeIf { status.isNullOrBlank() || it.status == status.trim().uppercase() }
|
||||
PageResult(listOfNotNull(item), page, pageSize, if (item == null) 0 else 1)
|
||||
}
|
||||
else -> PageResult(emptyList(), page, pageSize, 0)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun listDigitalAccountOptions(user: CurrentUser, account: String?, limit: Int): List<DigitalAccountManageItem> = dbQuery {
|
||||
when {
|
||||
user.isSuperAdmin || user.isEnterpriseAdmin -> {
|
||||
EnterpriseManageDao.digitalAccountOptionsForEnterprise(requireEnterpriseId(user), account, limit.coerceIn(1, 200))
|
||||
}
|
||||
user.isDigitalOperator -> {
|
||||
val id = user.digitalAccountId
|
||||
?: throw BizException(ErrorCode.BAD_REQUEST.code, "当前账号未绑定数电账号")
|
||||
EnterpriseManageDao.digitalAccount(id)
|
||||
?.let { EnterpriseManageDao.run { it.toDigitalAccountItem() } }
|
||||
?.takeIf { account.isNullOrBlank() || it.account.contains(account.trim(), ignoreCase = true) }
|
||||
?.let { listOf(it) }
|
||||
?: emptyList()
|
||||
}
|
||||
else -> emptyList()
|
||||
@@ -97,7 +125,7 @@ object PTConfigService {
|
||||
)
|
||||
}
|
||||
}
|
||||
return listDigitalAccounts(user)
|
||||
return listDigitalAccountOptions(user, null, 200)
|
||||
}
|
||||
|
||||
suspend fun createDigitalAccount(user: CurrentUser, req: CreateDigitalAccountRequest): DigitalAccountManageItem {
|
||||
|
||||
@@ -28,7 +28,7 @@ object PTRedService {
|
||||
* 红票接口调用 2.10
|
||||
*/
|
||||
suspend fun invoiceRed(user: CurrentUser, req: RedCreateRequest): String {
|
||||
val account = PTConfigService.requireDigitalAccountForAction(user, null)
|
||||
val account = PTConfigService.requireDigitalAccountForAction(user, req.digitalAccountId)
|
||||
val invoiceReqSerialNo = PTClient.ptDate()
|
||||
val historyId = Uuid.parse(req.historyId)
|
||||
val his = dbQuery {
|
||||
|
||||
@@ -59,6 +59,7 @@ object DatabaseInitializer {
|
||||
exec(it)
|
||||
}
|
||||
}
|
||||
exec("ALTER TABLE open_invoice_task DROP COLUMN IF EXISTS run_mode")
|
||||
}
|
||||
logger.info("Database schema initialized")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.bbit.ticket.utils.bootstrap
|
||||
|
||||
object Global {
|
||||
|
||||
val isDev = true
|
||||
val isDev = false
|
||||
|
||||
// 请求基础地址
|
||||
var baseUrl: String
|
||||
|
||||
Generated
+3310
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import http from '@/api/http'
|
||||
import http from '@/api/http'
|
||||
|
||||
/**
|
||||
* 账号状态
|
||||
@@ -158,8 +158,20 @@ export interface CreateDigitalAccountRequest {
|
||||
platformPassword: string
|
||||
}
|
||||
|
||||
export function listDigitalAccountsApi(): Promise<DigitalAccountItem[]> {
|
||||
return http.get('/pt/digital-accounts')
|
||||
export function listDigitalAccountsApi(params: {
|
||||
page: number
|
||||
pageSize: number
|
||||
account?: string | null
|
||||
status?: string | null
|
||||
}): Promise<PageResult<DigitalAccountItem>> {
|
||||
return http.get('/pt/digital-accounts', { params })
|
||||
}
|
||||
|
||||
export function digitalAccountOptionsApi(params?: {
|
||||
account?: string | null
|
||||
limit?: number
|
||||
}): Promise<DigitalAccountItem[]> {
|
||||
return http.get('/pt/digital-accounts/options', { params })
|
||||
}
|
||||
|
||||
export function refreshDigitalAccountsApi(): Promise<DigitalAccountItem[]> {
|
||||
@@ -244,7 +256,6 @@ export interface OpenInvoiceTaskItem {
|
||||
account?: string | null
|
||||
taskType: string
|
||||
sourceType: string
|
||||
runMode: string
|
||||
invoiceReqSerialNo: string
|
||||
batchNo?: string | null
|
||||
status: string
|
||||
@@ -271,7 +282,6 @@ export function openInvoiceTaskPageApi(params: {
|
||||
digitalAccountId?: string
|
||||
status?: string | null
|
||||
sourceType?: string | null
|
||||
runMode?: string | null
|
||||
}): Promise<PageResult<OpenInvoiceTaskItem>> {
|
||||
return http.get('/pt/openapi/tasks', { params })
|
||||
}
|
||||
@@ -458,7 +468,9 @@ export function invoiceIssueApi(payload: InvoiceRequest): Promise<string> {
|
||||
export interface RedCreateRequest {
|
||||
/** 蓝票历史记录 ID */
|
||||
historyId: string
|
||||
/** 冲红原因:01开票有误 02销货退回 03服务中止 04销售折让 */
|
||||
/** 平台数电账号 ID。企业管理员红冲时用于指定开票员 */
|
||||
digitalAccountId?: string | null
|
||||
/** 红冲原因:01开票有误 02销货退回 03服务中止 04销售折让 */
|
||||
redReason: string
|
||||
/** 收票人名称 */
|
||||
takerName?: string
|
||||
@@ -575,7 +587,7 @@ export interface InvoiceHistoryItem {
|
||||
// ===== 发票状态标记 =====
|
||||
/** 作废状态 */
|
||||
invalidFlag?: string
|
||||
/** 冲红状态 */
|
||||
/** 红冲状态 */
|
||||
redFlag?: string
|
||||
|
||||
// ===== 人员信息 =====
|
||||
@@ -847,7 +859,7 @@ export function queryInvoiceApi(
|
||||
|
||||
/** 红票申请信息 */
|
||||
export interface RedInvoiceInfo {
|
||||
/** 冲红原因:01开票有误 02销货退回 03服务中止 04销售折让 */
|
||||
/** 红冲原因:01开票有误 02销货退回 03服务中止 04销售折让 */
|
||||
redReason: string
|
||||
/** 收票人名称 */
|
||||
takerName?: string
|
||||
@@ -974,3 +986,4 @@ export function queryAuthStatusApi(payload: {
|
||||
}): Promise<AuthQrcodeStatusResponse> {
|
||||
return http.post('/pt/query-auth-status', payload)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
<div class="page-toolbar">
|
||||
<h2 class="page-toolbar-title">数电账号管理</h2>
|
||||
<div class="page-toolbar-actions">
|
||||
<n-input
|
||||
v-model:value="accountKeyword"
|
||||
clearable
|
||||
placeholder="税局账号"
|
||||
class="toolbar-filter-input"
|
||||
/>
|
||||
<n-button :loading="loading" @click="refreshAccounts">
|
||||
<template #icon><n-icon :component="RefreshCw" /></template>
|
||||
刷新
|
||||
@@ -22,9 +28,12 @@
|
||||
:columns="columns"
|
||||
:data="accounts"
|
||||
:loading="loading"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
:pagination="pagination"
|
||||
:row-key="(row: DigitalAccountItem) => row.id"
|
||||
:scroll-x="1460"
|
||||
remote
|
||||
@update:page="onPageChange"
|
||||
@update:page-size="onPageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -133,8 +142,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import type { DataTableColumns, FormInst, FormRules } from 'naive-ui'
|
||||
import { computed, h, onMounted, reactive, ref, watch } from 'vue'
|
||||
import type { DataTableColumns, FormInst, FormRules, PaginationProps } from 'naive-ui'
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
@@ -175,6 +184,8 @@ const authStore = useAuthStore()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const accounts = ref<DigitalAccountItem[]>([])
|
||||
const accountKeyword = ref('')
|
||||
let accountSearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const showCreate = ref(false)
|
||||
const showSms = ref(false)
|
||||
const showQrcode = ref(false)
|
||||
@@ -195,6 +206,17 @@ const canUpdateStatus = computed(() => authStore.hasPermission('digital-account:
|
||||
const qrcodeTypeLabel = computed(() =>
|
||||
qrcodeType.value === '2' ? '国家网络身份认证 APP' : '电子税务局 APP'
|
||||
)
|
||||
const pager = reactive({
|
||||
page: 1,
|
||||
pageSize: 10
|
||||
})
|
||||
const pagination = reactive<PaginationProps>({
|
||||
page: pager.page,
|
||||
pageSize: pager.pageSize,
|
||||
itemCount: 0,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50, 100]
|
||||
})
|
||||
|
||||
const createForm = reactive({
|
||||
account: '',
|
||||
@@ -352,7 +374,17 @@ function renderEllipsisText(value?: string | null) {
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
accounts.value = await listDigitalAccountsApi()
|
||||
const result = await listDigitalAccountsApi({
|
||||
page: pager.page,
|
||||
pageSize: pager.pageSize,
|
||||
account: accountKeyword.value.trim() || null
|
||||
})
|
||||
accounts.value = result.items
|
||||
pager.page = result.page
|
||||
pager.pageSize = result.pageSize
|
||||
pagination.page = result.page
|
||||
pagination.pageSize = result.pageSize
|
||||
pagination.itemCount = result.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -361,13 +393,26 @@ async function load() {
|
||||
async function refreshAccounts() {
|
||||
loading.value = true
|
||||
try {
|
||||
accounts.value = await refreshDigitalAccountsApi()
|
||||
await refreshDigitalAccountsApi()
|
||||
message.success('数电账号已刷新')
|
||||
pager.page = 1
|
||||
await load()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onPageChange(page: number) {
|
||||
pager.page = page
|
||||
load()
|
||||
}
|
||||
|
||||
function onPageSizeChange(pageSize: number) {
|
||||
pager.page = 1
|
||||
pager.pageSize = pageSize
|
||||
load()
|
||||
}
|
||||
|
||||
async function createAccount() {
|
||||
await createFormRef.value?.validate()
|
||||
saving.value = true
|
||||
@@ -451,6 +496,14 @@ async function loadQrcode() {
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
|
||||
watch(accountKeyword, () => {
|
||||
if (accountSearchTimer) clearTimeout(accountSearchTimer)
|
||||
accountSearchTimer = setTimeout(() => {
|
||||
pager.page = 1
|
||||
load()
|
||||
}, 300)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -461,6 +514,10 @@ onMounted(load)
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-filter-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="page-shell invoice-history-page">
|
||||
<div class="stats-row">
|
||||
<div v-for="s in stats" :key="s.label" class="stat-card">
|
||||
@@ -173,9 +173,9 @@
|
||||
}}</strong>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span>冲红状态</span>
|
||||
<span>红冲状态</span>
|
||||
<strong>{{
|
||||
redFlagMap[detailItem.redFlag || ''] || detailItem.redFlag || '未冲红'
|
||||
redFlagMap[detailItem.redFlag || ''] || detailItem.redFlag || '未红冲'
|
||||
}}</strong>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
@@ -227,10 +227,10 @@
|
||||
|
||||
<template v-if="detailItem.invoiceType === 'RED' && redInfo">
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">冲红申请</div>
|
||||
<div class="detail-section-title">红冲申请</div>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span>冲红原因</span>
|
||||
<span>红冲原因</span>
|
||||
<strong>{{ redReasonMap[redInfo.redReason] || redInfo.redReason }}</strong>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
@@ -419,7 +419,7 @@
|
||||
<n-modal
|
||||
v-model:show="showRedForm"
|
||||
preset="card"
|
||||
title="发起冲红"
|
||||
title="发起红冲"
|
||||
:style="{ width: '480px', maxWidth: '92vw' }"
|
||||
:mask-closable="false"
|
||||
content-style="padding: 20px"
|
||||
@@ -431,11 +431,21 @@
|
||||
label-placement="top"
|
||||
require-mark-placement="right-hanging"
|
||||
>
|
||||
<n-form-item label="冲红原因" path="redReason">
|
||||
<n-form-item label="开票员" path="digitalAccountId">
|
||||
<n-select
|
||||
v-model:value="redForm.digitalAccountId"
|
||||
:options="digitalAccountOptions"
|
||||
:loading="digitalAccountLoading"
|
||||
placeholder="请选择开票员"
|
||||
filterable
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="红冲原因" path="redReason">
|
||||
<n-select
|
||||
v-model:value="redForm.redReason"
|
||||
:options="redReasonOptions"
|
||||
placeholder="请选择冲红原因"
|
||||
placeholder="请选择红冲原因"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="收票人名称" path="takerName">
|
||||
@@ -451,7 +461,7 @@
|
||||
<div class="modal-actions">
|
||||
<n-button quaternary @click="showRedForm = false">取消</n-button>
|
||||
<n-button type="error" :loading="redSubmitting" @click="handleRedSubmit"
|
||||
>确认冲红</n-button
|
||||
>确认红冲</n-button
|
||||
>
|
||||
</div>
|
||||
</n-form>
|
||||
@@ -496,6 +506,7 @@ import {
|
||||
invoiceHistoryApi,
|
||||
invoiceKindMap,
|
||||
invoiceStatusMap,
|
||||
digitalAccountOptionsApi,
|
||||
queryInvoiceApi,
|
||||
redInvoiceCreateApi,
|
||||
redInvoiceDownloadUrlApi,
|
||||
@@ -507,6 +518,7 @@ import type {
|
||||
InvoiceDetailGoods,
|
||||
InvoiceDetailResponse,
|
||||
InvoiceDetailVoucher,
|
||||
DigitalAccountItem,
|
||||
InvoiceHistoryItem,
|
||||
RedCreateRequest,
|
||||
RedInvoiceInfo
|
||||
@@ -527,11 +539,11 @@ const invalidFlagMap: Record<string, string> = {
|
||||
}
|
||||
|
||||
const redFlagMap: Record<string, string> = {
|
||||
NOT_RED: '未冲红',
|
||||
ALREADY_RED: '已冲红',
|
||||
REDING: '冲红中',
|
||||
RED_FAIL: '冲红失败',
|
||||
PART_RED: '部分冲红'
|
||||
NOT_RED: '未红冲',
|
||||
ALREADY_RED: '已红冲',
|
||||
REDING: '红冲中',
|
||||
RED_FAIL: '红冲失败',
|
||||
PART_RED: '部分红冲'
|
||||
}
|
||||
|
||||
const redInvoiceKindCodes = new Set(['81', '82', '83', '87'])
|
||||
@@ -728,7 +740,7 @@ function getRowActions(row: InvoiceHistoryItem) {
|
||||
}
|
||||
|
||||
if (canCreateRedInvoice(row)) {
|
||||
actions.push({ label: '冲红', icon: RotateCcw, onClick: () => startRedTask(row) })
|
||||
actions.push({ label: '红冲', icon: RotateCcw, onClick: () => startRedTask(row) })
|
||||
}
|
||||
|
||||
return actions
|
||||
@@ -739,7 +751,7 @@ function getActionsColumnWidth() {
|
||||
详情: 58,
|
||||
刷新: 58,
|
||||
查看票样: 86,
|
||||
冲红: 58
|
||||
红冲: 58
|
||||
}
|
||||
const rowWidths = dataSource.value.map((row) => {
|
||||
const actions = getRowActions(row)
|
||||
@@ -835,7 +847,7 @@ const columns = computed<DataTableColumns<InvoiceHistoryItem>>(() => {
|
||||
: h('span', { style: 'color:#9ca3af' }, '-')
|
||||
},
|
||||
{
|
||||
title: '冲红状态',
|
||||
title: '红冲状态',
|
||||
key: 'redFlag',
|
||||
width: 110,
|
||||
render: (row: InvoiceHistoryItem) => {
|
||||
@@ -850,7 +862,7 @@ const columns = computed<DataTableColumns<InvoiceHistoryItem>>(() => {
|
||||
}[row.status]
|
||||
: undefined)
|
||||
if (!redFlag || redFlag === 'NOT_RED') {
|
||||
return h(NTag, { size: 'small', round: true }, () => '未冲红')
|
||||
return h(NTag, { size: 'small', round: true }, () => '未红冲')
|
||||
}
|
||||
const typeMap: Record<string, 'error' | 'warning' | 'default'> = {
|
||||
ALREADY_RED: 'error',
|
||||
@@ -973,9 +985,12 @@ async function refreshStatus(item: InvoiceHistoryItem) {
|
||||
const showRedForm = ref(false)
|
||||
const redSubmitting = ref(false)
|
||||
const redFormRef = ref()
|
||||
const digitalAccountLoading = ref(false)
|
||||
const digitalAccounts = ref<DigitalAccountItem[]>([])
|
||||
|
||||
const redForm = reactive({
|
||||
historyId: '',
|
||||
digitalAccountId: null as string | null,
|
||||
redReason: '01',
|
||||
takerName: '',
|
||||
takerTel: '',
|
||||
@@ -989,17 +1004,40 @@ const redReasonOptions = [
|
||||
{ label: '销售折让', value: '04' }
|
||||
]
|
||||
|
||||
const digitalAccountOptions = computed(() =>
|
||||
digitalAccounts.value.map((item) => ({
|
||||
label: `${item.account}${item.name ? `(${item.name})` : ''}`,
|
||||
value: item.id
|
||||
}))
|
||||
)
|
||||
|
||||
const redFormRules = {
|
||||
redReason: [{ required: true, message: '请选择冲红原因', trigger: 'change' }]
|
||||
digitalAccountId: [{ required: true, message: '请选择开票员', trigger: 'change' }],
|
||||
redReason: [{ required: true, message: '请选择红冲原因', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function startRedTask(item: InvoiceHistoryItem) {
|
||||
async function ensureDigitalAccountsLoaded() {
|
||||
if (digitalAccounts.value.length > 0) return
|
||||
digitalAccountLoading.value = true
|
||||
try {
|
||||
digitalAccounts.value = await digitalAccountOptionsApi({ limit: 200 })
|
||||
} finally {
|
||||
digitalAccountLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function startRedTask(item: InvoiceHistoryItem) {
|
||||
redForm.historyId = item.id
|
||||
redForm.digitalAccountId = null
|
||||
redForm.redReason = '01'
|
||||
redForm.takerName = ''
|
||||
redForm.takerTel = ''
|
||||
redForm.takerEmail = ''
|
||||
showRedForm.value = true
|
||||
await ensureDigitalAccountsLoaded()
|
||||
if (!redForm.digitalAccountId && digitalAccounts.value.length === 1) {
|
||||
redForm.digitalAccountId = digitalAccounts.value[0].id
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRedSubmit() {
|
||||
@@ -1012,17 +1050,18 @@ async function handleRedSubmit() {
|
||||
try {
|
||||
const payload: RedCreateRequest = {
|
||||
historyId: redForm.historyId,
|
||||
digitalAccountId: redForm.digitalAccountId,
|
||||
redReason: redForm.redReason,
|
||||
takerName: redForm.takerName || undefined,
|
||||
takerTel: redForm.takerTel || undefined,
|
||||
takerEmail: redForm.takerEmail || undefined
|
||||
}
|
||||
await redInvoiceCreateApi(payload)
|
||||
message.success('冲红任务创建成功')
|
||||
message.success('红冲任务创建成功')
|
||||
showRedForm.value = false
|
||||
fetchData()
|
||||
} catch {
|
||||
message.error('创建冲红任务失败')
|
||||
message.error('创建红冲任务失败')
|
||||
} finally {
|
||||
redSubmitting.value = false
|
||||
}
|
||||
@@ -1471,3 +1510,4 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -931,7 +931,7 @@ import {
|
||||
invoiceIssueApi,
|
||||
getEnterpriseInfoApi,
|
||||
getPresetDataApi,
|
||||
listDigitalAccountsApi
|
||||
digitalAccountOptionsApi
|
||||
} from '@/api/piaotong'
|
||||
import type { DigitalAccountItem, InvoiceRequest, InvoiceItem, VariableLevyProof } from '@/api/piaotong'
|
||||
import type { FormInst, FormRules } from 'naive-ui'
|
||||
@@ -1574,7 +1574,7 @@ onMounted(async () => {
|
||||
const [enterpriseInfo, presetData, accounts] = await Promise.all([
|
||||
getEnterpriseInfoApi(),
|
||||
getPresetDataApi(),
|
||||
listDigitalAccountsApi()
|
||||
digitalAccountOptionsApi({ limit: 200 })
|
||||
])
|
||||
digitalAccounts.value = accounts
|
||||
const defaultAccount =
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<n-drawer v-model:show="drawerVisible" :width="760">
|
||||
<n-drawer v-model:show="drawerVisible" :width="980">
|
||||
<n-drawer-content :title="drawerTitle" closable>
|
||||
<div class="drawer-toolbar">
|
||||
<n-select
|
||||
@@ -66,15 +66,6 @@
|
||||
class="task-filter"
|
||||
@update:value="loadTasks(1)"
|
||||
/>
|
||||
<n-select
|
||||
v-model:value="taskRunMode"
|
||||
clearable
|
||||
size="small"
|
||||
placeholder="全部模式"
|
||||
:options="runModeOptions"
|
||||
class="task-filter"
|
||||
@update:value="loadTasks(1)"
|
||||
/>
|
||||
<n-button size="small" :loading="taskLoading" @click="loadTasks(taskPage)">
|
||||
<template #icon><n-icon :component="RefreshCw" /></template>
|
||||
刷新
|
||||
@@ -87,7 +78,7 @@
|
||||
:data="tasks"
|
||||
:loading="taskLoading"
|
||||
:pagination="taskPagination"
|
||||
:scroll-x="980"
|
||||
:scroll-x="1280"
|
||||
remote
|
||||
@update:page="loadTasks"
|
||||
@update:page-size="changeTaskPageSize"
|
||||
@@ -122,7 +113,6 @@ const taskPageSize = ref(20)
|
||||
const taskTotal = ref(0)
|
||||
const taskStatus = ref<string | null>(null)
|
||||
const taskSourceType = ref<string | null>(null)
|
||||
const taskRunMode = ref<string | null>(null)
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '待处理', value: 'PENDING' },
|
||||
@@ -134,14 +124,35 @@ const statusOptions = [
|
||||
|
||||
const sourceTypeOptions = [
|
||||
{ label: '单笔', value: 'SINGLE' },
|
||||
{ label: '批量', value: 'BATCH' },
|
||||
{ label: '测试', value: 'TEST' }
|
||||
{ label: '批量', value: 'BATCH' }
|
||||
]
|
||||
|
||||
const runModeOptions = [
|
||||
{ label: '生产', value: 'REAL' },
|
||||
{ label: '模拟', value: 'SIMULATED' }
|
||||
]
|
||||
const queueStatusLabelMap: Record<string, string> = {
|
||||
RUNNING: '运行中',
|
||||
PAUSED: '已暂停'
|
||||
}
|
||||
|
||||
const taskStatusLabelMap: Record<string, string> = {
|
||||
PENDING: '待处理',
|
||||
PROCESSING: '处理中',
|
||||
SUCCESS: '成功',
|
||||
FAILED: '失败',
|
||||
WAITING_AUTH: '需认证'
|
||||
}
|
||||
|
||||
const taskTypeLabelMap: Record<string, string> = {
|
||||
ISSUE_BLUE: '蓝票开具',
|
||||
QUERY_BLUE: '蓝票查询'
|
||||
}
|
||||
|
||||
const sourceTypeLabelMap: Record<string, string> = {
|
||||
SINGLE: '单笔',
|
||||
BATCH: '批量'
|
||||
}
|
||||
|
||||
function labelOf(map: Record<string, string>, value?: string | null) {
|
||||
return value ? map[value] || value : '-'
|
||||
}
|
||||
|
||||
const totals = computed(() =>
|
||||
rows.value.reduce(
|
||||
@@ -177,7 +188,11 @@ const columns: DataTableColumns<OpenInvoiceTaskOverviewItem> = [
|
||||
key: 'status',
|
||||
width: 110,
|
||||
render: (row) =>
|
||||
h(NTag, { type: statusTagType(row.status), size: 'small' }, { default: () => row.status })
|
||||
h(
|
||||
NTag,
|
||||
{ type: statusTagType(row.status), size: 'small' },
|
||||
{ default: () => labelOf(queueStatusLabelMap, row.status) }
|
||||
)
|
||||
},
|
||||
{ title: '待处理', key: 'pending', width: 90 },
|
||||
{ title: '处理中', key: 'processing', width: 90 },
|
||||
@@ -265,21 +280,24 @@ function rowProps(row: OpenInvoiceTaskOverviewItem) {
|
||||
|
||||
const taskColumns: DataTableColumns<OpenInvoiceTaskItem> = [
|
||||
{ title: '票号', key: 'invoiceReqSerialNo', minWidth: 170 },
|
||||
{ title: '任务', key: 'taskType', width: 120 },
|
||||
{ title: '来源', key: 'sourceType', width: 90 },
|
||||
{ title: '模式', key: 'runMode', width: 90 },
|
||||
{ title: '任务', key: 'taskType', width: 120, render: (row) => labelOf(taskTypeLabelMap, row.taskType) },
|
||||
{ title: '来源', key: 'sourceType', width: 90, render: (row) => labelOf(sourceTypeLabelMap, row.sourceType) },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 110,
|
||||
render: (row) =>
|
||||
h(NTag, { type: statusTagType(row.status), size: 'small' }, { default: () => row.status })
|
||||
h(
|
||||
NTag,
|
||||
{ type: statusTagType(row.status), size: 'small' },
|
||||
{ default: () => labelOf(taskStatusLabelMap, row.status) }
|
||||
)
|
||||
},
|
||||
{ title: 'PT码', key: 'ptCode', width: 90 },
|
||||
{ title: '错误', key: 'errorMessage', minWidth: 260, ellipsis: { tooltip: true } },
|
||||
{ title: '查询次数', key: 'pollCount', width: 90, render: (row) => `${row.pollCount}/${row.maxPollCount}` },
|
||||
{ title: '重试', key: 'attemptCount', width: 90, render: (row) => `${row.attemptCount}/${row.maxAttemptCount}` },
|
||||
{ title: '下次执行', key: 'nextRunAt', minWidth: 150 },
|
||||
{ title: '错误', key: 'errorMessage', minWidth: 180 }
|
||||
{ title: '下次执行', key: 'nextRunAt', minWidth: 150 }
|
||||
]
|
||||
|
||||
const taskPagination = reactive<PaginationProps>({
|
||||
@@ -307,7 +325,6 @@ async function loadTasks(page = 1) {
|
||||
digitalAccountId: selected.value.digitalAccountId,
|
||||
status: taskStatus.value,
|
||||
sourceType: taskSourceType.value,
|
||||
runMode: taskRunMode.value,
|
||||
page,
|
||||
pageSize: taskPageSize.value
|
||||
})
|
||||
@@ -326,7 +343,6 @@ function openDrawer(row: OpenInvoiceTaskOverviewItem) {
|
||||
drawerVisible.value = true
|
||||
taskStatus.value = null
|
||||
taskSourceType.value = null
|
||||
taskRunMode.value = null
|
||||
loadTasks(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ function renderLabel(payload: { option: unknown }) {
|
||||
? h(NTag, { size: 'small', type: 'primary' }, { default: () => '基础内置' })
|
||||
: null
|
||||
]),
|
||||
h(NSpace, { size: 6, class: 'menu-tree-actions' }, () => [
|
||||
h(NSpace, { size: 6, align: 'center', class: 'menu-tree-actions table-action-buttons' }, () => [
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'small', tertiary: true, class: 'action-btn', onClick: () => openCreate(node.id) },
|
||||
|
||||
@@ -256,7 +256,7 @@ const columns = computed<DataTableColumns<RoleItem>>(() => [
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render: (r) =>
|
||||
h(NSpace, { size: 6 }, () => [
|
||||
h(NSpace, { size: 6, align: 'center', class: 'table-action-buttons' }, () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
|
||||
@@ -660,7 +660,7 @@ const columns = computed<DataTableColumns<UserListItem>>(() => [
|
||||
render: (row) =>
|
||||
h(
|
||||
NSpace,
|
||||
{ size: 6 },
|
||||
{ size: 6, align: 'center', class: 'table-action-buttons' },
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
|
||||
@@ -213,6 +213,40 @@ select {
|
||||
min-width: 68px;
|
||||
}
|
||||
|
||||
.table-action-buttons,
|
||||
.row-actions,
|
||||
.table-actions,
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-action-buttons {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.table-action-buttons .n-button,
|
||||
.row-actions .n-button,
|
||||
.table-actions .n-button,
|
||||
.action-buttons .n-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table-action-buttons .n-button__content,
|
||||
.row-actions .n-button__content,
|
||||
.table-actions .n-button__content,
|
||||
.action-buttons .n-button__content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.soft-stat {
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: 12px;
|
||||
|
||||
Reference in New Issue
Block a user