From cf59c548d06bcb8dd824a97403658ca90164abfd Mon Sep 17 00:00:00 2001
From: Helius <wangdoubleone@gmail.com>
Date: Tue, 23 Jan 2024 10:06:08 +0800
Subject: [PATCH] Merge branch 'blnka' into blnka-vip

---
 src/main/java/cc/mrbird/febs/mall/controller/ViewMallOrderController.java  |   36 
 src/main/java/cc/mrbird/febs/mall/service/MallInvoiceService.java          |   31 
 src/main/java/cc/mrbird/febs/pay/service/WxFaPiaoService.java              |   22 
 src/main/java/cc/mrbird/febs/pay/model/FPEncryptCertificate.java           |   11 
 src/test/java/cc/mrbird/febs/ProfitTest.java                               |  276 +++++-
 src/main/java/cc/mrbird/febs/pay/util/RequestBodyUtils.java                |   76 +
 src/main/java/cc/mrbird/febs/pay/util/JCEUtil.java                         |   70 +
 src/main/resources/templates/febs/views/modules/order/invoiceList.html     |  233 +++++
 pom.xml                                                                    |   10 
 src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateCustomCellDto.java    |   21 
 src/main/java/cc/mrbird/febs/mall/entity/MallOrderItem.java                |    3 
 src/main/java/cc/mrbird/febs/mall/vo/AdminInvoiceDetailVo.java             |   47 +
 src/main/java/cc/mrbird/febs/mall/controller/AdminSystemController.java    |   44 
 src/main/java/cc/mrbird/febs/pay/controller/XcxPayController.java          |   26 
 src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateDto.java              |   17 
 src/main/java/cc/mrbird/febs/pay/model/FPUserTitleDto.java                 |   20 
 src/main/resources/templates/febs/views/modules/order/invoiceDetail.html   |  248 ++++++
 src/main/java/cc/mrbird/febs/mall/controller/AdminMallOrderController.java |   43 +
 src/main/java/cc/mrbird/febs/mall/entity/MallInvoice.java                  |   65 +
 src/main/resources/mapper/modules/MallInvoiceMapper.xml                    |   67 +
 src/main/resources/wxP12/apiclient_key.pem                                 |   28 
 src/main/java/cc/mrbird/febs/mall/dto/AdminMallInvoiceDto.java             |   16 
 src/main/java/cc/mrbird/febs/mall/mapper/MallOrderInfoMapper.java          |    4 
 src/main/java/cc/mrbird/febs/pay/model/FPCertificateVo.java                |   10 
 src/main/resources/mapper/modules/MallOrderInfoMapper.xml                  |    8 
 src/main/java/cc/mrbird/febs/mall/mapper/MallInvoiceMapper.java            |   26 
 src/main/java/cc/mrbird/febs/mall/quartz/WxxcxJob.java                     |   13 
 src/main/java/cc/mrbird/febs/mall/vo/ApiMallInvoiceVo.java                 |   30 
 src/main/java/cc/mrbird/febs/pay/model/FPCertificates.java                 |   13 
 src/main/java/cc/mrbird/febs/common/utils/AppContants.java                 |    3 
 src/main/java/cc/mrbird/febs/mall/dto/ApiMallInvoiceDto.java               |   23 
 src/main/java/cc/mrbird/febs/mall/service/impl/MallInvoiceServiceImpl.java |  160 ++++
 src/main/java/cc/mrbird/febs/mall/entity/MallOrderInfo.java                |    4 
 src/main/java/cc/mrbird/febs/mall/controller/ApiMallInvoiceController.java |   33 
 src/main/java/cc/mrbird/febs/pay/service/impl/WxFaPiaoServiceImpl.java     |  615 +++++++++++++-
 src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateInformationDto.java   |   22 
 36 files changed, 2,230 insertions(+), 144 deletions(-)

diff --git a/pom.xml b/pom.xml
index 3f7a5b4..af6a5ee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,6 +30,11 @@
     <dependencies>
         <dependency>
             <groupId>com.github.wechatpay-apiv3</groupId>
+            <artifactId>wechatpay-apache-httpclient</artifactId>
+            <version>0.4.7</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.wechatpay-apiv3</groupId>
             <artifactId>wechatpay-java</artifactId>
             <version>0.2.12</version>
         </dependency>
@@ -357,6 +362,11 @@
             <artifactId>poi-ooxml-schemas</artifactId>
             <version>3.8</version>
         </dependency>
+        <dependency>
+            <groupId>com.github.wechatpay-apiv3</groupId>
+            <artifactId>wechatpay-apache-httpclient</artifactId>
+            <version>0.4.7</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/java/cc/mrbird/febs/common/utils/AppContants.java b/src/main/java/cc/mrbird/febs/common/utils/AppContants.java
index 43d6356..a212e5c 100644
--- a/src/main/java/cc/mrbird/febs/common/utils/AppContants.java
+++ b/src/main/java/cc/mrbird/febs/common/utils/AppContants.java
@@ -23,6 +23,8 @@
 
     public static final String PC_LOGIN_PREFIX = "pc_";
 
+    public static final String FP_TOKEN_HEADER_TYPE = "WECHATPAY2-SHA256-RSA2048 ";
+
     /**
      * token头部
      */
@@ -72,5 +74,6 @@
     public static final String AGENT_LEVEL_REQUIRE = "AGENT_LEVEL_REQUIRE";
 
     public static final String SIGN_MD5 = "MD5";
+    public static final String WX_CARD_NUM = "221D49AEC4EA538A63941D1936709C8559EB05C5";//证书编号
 
 }
diff --git a/src/main/java/cc/mrbird/febs/mall/controller/AdminMallOrderController.java b/src/main/java/cc/mrbird/febs/mall/controller/AdminMallOrderController.java
index 4265f7a..cff652b 100644
--- a/src/main/java/cc/mrbird/febs/mall/controller/AdminMallOrderController.java
+++ b/src/main/java/cc/mrbird/febs/mall/controller/AdminMallOrderController.java
@@ -22,6 +22,7 @@
 import cc.mrbird.febs.mall.service.IAdminMallGoodsService;
 import cc.mrbird.febs.mall.service.IAdminMallOrderService;
 import cc.mrbird.febs.mall.service.IApiMallTeamLeaderService;
+import cc.mrbird.febs.mall.service.MallInvoiceService;
 import cc.mrbird.febs.mall.vo.AdminAddAddressTreeVo;
 import cc.mrbird.febs.mall.vo.AdminMallOrderRefundAddressVo;
 import cc.mrbird.febs.pay.model.OrderStateDto;
@@ -661,4 +662,46 @@
         return new FebsResponse().success().data(data);
     }
 
+
+    private final MallInvoiceService mallInvoiceService;
+    /**
+     * 发票列表
+     * @param mallInvoiceDto
+     * @param request
+     * @return
+     */
+    @GetMapping("invoiceList")
+    public FebsResponse invoiceList(AdminMallInvoiceDto mallInvoiceDto, QueryRequest request) {
+        Map<String, Object> data = getDataTable(mallInvoiceService.getInvoiceList(mallInvoiceDto, request));
+        return new FebsResponse().success().data(data);
+    }
+
+    /**
+     * 发票列表-订单子表
+     */
+    @GetMapping(value = "/invoiceItemList")
+    public FebsResponse invoiceItemList() {
+        return new FebsResponse().success().data(mallInvoiceService.invoiceItemList());
+    }
+
+    /**
+     * 发票列表-上传发票
+     */
+    @PostMapping("addInvoiceUrl")
+    @ControllerEndpoint(operation = " 发票列表-上传发票", exceptionMessage = "操作失败")
+    public FebsResponse addInvoiceUrl(@Valid MallInvoice mallInvoice) {
+        return mallInvoiceService.addInvoiceUrl(mallInvoice);
+    }
+
+    /**
+     * 发票列表-更新抬头
+     * @param id
+     * @return
+     */
+    @GetMapping("updateInvoiceDetail/{id}")
+    @ControllerEndpoint(operation = "发票列表-更新抬头", exceptionMessage = "操作失败")
+    public FebsResponse updateInvoiceDetail(@NotNull(message = "{required}") @PathVariable Long id) {
+        return mallInvoiceService.updateInvoiceDetail(id);
+    }
+
 }
diff --git a/src/main/java/cc/mrbird/febs/mall/controller/AdminSystemController.java b/src/main/java/cc/mrbird/febs/mall/controller/AdminSystemController.java
index bef7b04..3f9d310 100644
--- a/src/main/java/cc/mrbird/febs/mall/controller/AdminSystemController.java
+++ b/src/main/java/cc/mrbird/febs/mall/controller/AdminSystemController.java
@@ -3,6 +3,7 @@
 import cc.mrbird.febs.common.annotation.ControllerEndpoint;
 import cc.mrbird.febs.common.entity.FebsResponse;
 import cc.mrbird.febs.common.enumerates.DataDictionaryEnum;
+import cc.mrbird.febs.common.utils.AppContants;
 import cc.mrbird.febs.mall.dto.*;
 import cc.mrbird.febs.mall.entity.DataDictionaryCustom;
 import cc.mrbird.febs.mall.mapper.DataDictionaryCustomMapper;
@@ -29,6 +30,8 @@
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -136,39 +139,34 @@
                 DataDictionaryEnum.FP_CALLBACK_URL.getCode(),
                 faPiaoDto.getCallbackUrl()
         );
-        KeyPair privateKey = wxFaPiaoService.getPrivateKey();
         HeaderDto headerDto = new HeaderDto();
-        headerDto.setCallback_url(faPiaoDto.getCallbackUrl());
+        headerDto.setCallback_url("https://api.blnka.cn/api/xcxPay/fapiaoCallBack");
         headerDto.setShow_fapiao_cell(false);
         String parseObj = JSONUtil.parseObj(headerDto).toString();
         String baseUrl = "https://api.mch.weixin.qq.com";
         String canonicalUrl = "/v3/new-tax-control-fapiao/merchant/development-config";
-        String postStr = wxFaPiaoService.createAuthorization(
-                "POST",
-                canonicalUrl,
-                parseObj,
-                privateKey
-        );
-        // 创建httppost
+        String postStr = null;
         try {
-            HttpClient httpClient = new HttpClient();
-            PostMethod post = new PostMethod(baseUrl+canonicalUrl);
-            post.setRequestHeader("Accept", "application/json");
-            post.setRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36");
-            post.setRequestHeader("Content-Type", "application/json");
-            post.setRequestHeader("Connection", "keep-alive");
-            post.setRequestHeader("Authorization",  "WECHATPAY2-SHA256-RSA2048 "+postStr);
-            RequestEntity entity = new StringRequestEntity(parseObj, "text/html", "utf-8");
-            post.setRequestEntity(entity);
-            httpClient.executeMethod(post);
-            String responseBodyAsString = post.getResponseBodyAsString();
-            cn.hutool.json.JSONObject maps = JSONUtil.parseObj(responseBodyAsString);
-            System.out.println(maps);
+
+            PrivateKey privateKey = wxFaPiaoService.getPrivateKeyV3();
+            postStr = wxFaPiaoService.createAuthorization(
+                    "PATCH",
+                    baseUrl+canonicalUrl,
+                    parseObj,
+                    privateKey
+            );
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
         } catch (IOException e) {
             e.printStackTrace();
         }
 
-        return new FebsResponse().success().message("操作成功");
+        String token = AppContants.FP_TOKEN_HEADER_TYPE+postStr;
+        System.out.println("WECHATPAY2-SHA256-RSA2048 "+postStr);
+        String s = wxFaPiaoService.sendPatch(baseUrl + canonicalUrl, parseObj, token);
+        log.info("配置开发选项:"+s);
+
+        return new FebsResponse().success().message(s);
     }
 
     @PostMapping(value = "/agentDetail")
diff --git a/src/main/java/cc/mrbird/febs/mall/controller/ApiMallInvoiceController.java b/src/main/java/cc/mrbird/febs/mall/controller/ApiMallInvoiceController.java
new file mode 100644
index 0000000..5c92b77
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/mall/controller/ApiMallInvoiceController.java
@@ -0,0 +1,33 @@
+package cc.mrbird.febs.mall.controller;
+
+import cc.mrbird.febs.common.entity.FebsResponse;
+import cc.mrbird.febs.mall.dto.ApiMallInvoiceDto;
+import cc.mrbird.febs.mall.service.MallInvoiceService;
+import cc.mrbird.febs.mall.vo.ApiMallInvoiceVo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping(value = "/api/invoice")
+@Api(value = "ApiMallInvoiceController", tags = "发票管理")
+public class ApiMallInvoiceController {
+
+    private final MallInvoiceService mallInvoiceService;
+
+    @ApiOperation(value = "获取发票列表", notes = "获取发票列表")
+    @ApiResponses({
+            @ApiResponse(code = 200, message = "success", response = ApiMallInvoiceVo.class)
+    })
+    @PostMapping(value = "/getInvoices")
+    public FebsResponse getInvoices(@RequestBody ApiMallInvoiceDto apiMallInvoiceDto) {
+        return mallInvoiceService.getInvoices(apiMallInvoiceDto);
+    }
+
+}
diff --git a/src/main/java/cc/mrbird/febs/mall/controller/ViewMallOrderController.java b/src/main/java/cc/mrbird/febs/mall/controller/ViewMallOrderController.java
index 678afeb..a91c4db 100644
--- a/src/main/java/cc/mrbird/febs/mall/controller/ViewMallOrderController.java
+++ b/src/main/java/cc/mrbird/febs/mall/controller/ViewMallOrderController.java
@@ -8,8 +8,10 @@
 import cc.mrbird.febs.mall.entity.MallExpressInfo;
 import cc.mrbird.febs.mall.entity.MallOrderRefund;
 import cc.mrbird.febs.mall.mapper.MallExpressInfoMapper;
+import cc.mrbird.febs.mall.mapper.MallInvoiceMapper;
 import cc.mrbird.febs.mall.mapper.MallOrderRefundMapper;
 import cc.mrbird.febs.mall.service.IAdminMallOrderService;
+import cc.mrbird.febs.mall.service.MallInvoiceService;
 import cc.mrbird.febs.mall.vo.*;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.ObjectUtil;
@@ -29,6 +31,7 @@
 public class ViewMallOrderController extends BaseController {
 
     private final IAdminMallOrderService adminMallOrderService;
+    private final MallInvoiceMapper mallInvoiceMapper;
 
     private final MallOrderRefundMapper mallOrderRefundMapper;
 
@@ -255,4 +258,37 @@
         return FebsUtil.view("modules/order/goodsStatistics");
     }
 
+
+    /**
+     * 发票列表
+     * @return
+     */
+    @GetMapping("invoiceList")
+    @RequiresPermissions("invoiceList:view")
+    public String invoiceList() {
+        return FebsUtil.view("modules/order/invoiceList");
+    }
+
+    /**
+     * 发票列表-详情
+     * @param id
+     * @param model
+     * @return
+     */
+    @GetMapping("invoiceDetail/{id}")
+    @RequiresPermissions("invoiceDetail:view")
+    public String invoiceDetail(@PathVariable long id, Model model) {
+        String existToken = redisUtils.getString("ADMIN_INVOICE_ID");
+        if (StrUtil.isNotBlank(existToken)) {
+            Object o = redisUtils.get(existToken);
+            if (ObjectUtil.isNotEmpty(o)) {
+                redisUtils.del(existToken);
+            }
+        }
+        redisUtils.set("ADMIN_INVOICE_ID", id, -1);
+        AdminInvoiceDetailVo data = mallInvoiceMapper.getAdminInvoiceDetailVoById(id);
+        model.addAttribute("invoiceDetail", data);
+        return FebsUtil.view("modules/order/invoiceDetail");
+    }
+
 }
diff --git a/src/main/java/cc/mrbird/febs/mall/dto/AdminMallInvoiceDto.java b/src/main/java/cc/mrbird/febs/mall/dto/AdminMallInvoiceDto.java
new file mode 100644
index 0000000..9c04ce0
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/mall/dto/AdminMallInvoiceDto.java
@@ -0,0 +1,16 @@
+package cc.mrbird.febs.mall.dto;
+
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+@Data
+@ApiModel(value = "AdminMallInvoiceDto", description = "参数接收类")
+public class AdminMallInvoiceDto {
+
+    private Integer state;//开票状态:0:未开票 1:开票中 2:已开票
+
+    private String orderNo;
+
+    private String payOrderNo;
+
+}
diff --git a/src/main/java/cc/mrbird/febs/mall/dto/ApiMallInvoiceDto.java b/src/main/java/cc/mrbird/febs/mall/dto/ApiMallInvoiceDto.java
new file mode 100644
index 0000000..85c38f5
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/mall/dto/ApiMallInvoiceDto.java
@@ -0,0 +1,23 @@
+package cc.mrbird.febs.mall.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel(value = "ApiMallInvoiceDto", description = "参数接收类")
+public class ApiMallInvoiceDto {
+
+    @ApiModelProperty(value = "一页数量", example = "10")
+    private Integer pageSize;
+
+    @ApiModelProperty(value = "第几页", example = "1")
+    private Integer pageNum;
+
+    @ApiModelProperty(value = "开票状态:0:未开票 1:开票中 2:已开票")
+    private Integer state;
+
+    @ApiModelProperty(hidden = true)
+    private Long memberId;
+
+}
diff --git a/src/main/java/cc/mrbird/febs/mall/entity/MallInvoice.java b/src/main/java/cc/mrbird/febs/mall/entity/MallInvoice.java
new file mode 100644
index 0000000..d13954c
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/mall/entity/MallInvoice.java
@@ -0,0 +1,65 @@
+package cc.mrbird.febs.mall.entity;
+
+import cc.mrbird.febs.common.entity.BaseEntity;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@TableName("mall_invoice")
+public class MallInvoice extends BaseEntity {
+
+    private Long memberId;
+    /**
+     * fapiao_apply_id
+     * 发票申请单号,唯一标识一次开票行为。微信支付场景下,为微信支付订单号;非微信支付场景下,为调用【获取抬头填写链接】接口时指定的发票申请单号
+     * 对应订单信息的pay_order_no支付订单号
+     */
+    private String fapiaoApplyId;
+    /**
+     *【购买方类型】 购买方类型
+     * 可选取值:
+     * INDIVIDUAL: 个人
+     * ORGANIZATION: 单位
+     */
+    private String type;
+    /**
+     *【名称】 购买方名称
+     */
+    private String name;
+    /**
+     *【纳税人识别号】 购买方纳税人识别号,购买方类型为ORGANIZATION时必须存在
+     */
+    private String taxpayerId;
+    /**
+     *【地址】 购买方地址
+     */
+    private String address;
+    /**
+     *【电话】 购买方电话
+     */
+    private String telephone;
+    /**
+     *【开户银行】 购买方开户银行
+     */
+    private String bankName;
+    /**
+     *【银行账号】 购买方银行账号
+     */
+    private String bankAccount;
+
+
+    /**
+     * 匹配的订单ID
+     */
+    private Long orderId;
+    private String orderNo;//订单编号
+    private Integer status;//订单状态
+    private BigDecimal amount;//订单金额
+    private String goodsImg;//商品图片
+
+    private Integer state;//开票状态:0:未开票 1:开票中 2:已开票
+    private String invoiceUrl;//发票的URL路径
+}
diff --git a/src/main/java/cc/mrbird/febs/mall/entity/MallOrderInfo.java b/src/main/java/cc/mrbird/febs/mall/entity/MallOrderInfo.java
index cef66a2..56224de 100644
--- a/src/main/java/cc/mrbird/febs/mall/entity/MallOrderInfo.java
+++ b/src/main/java/cc/mrbird/febs/mall/entity/MallOrderInfo.java
@@ -132,6 +132,10 @@
      * 是否送货上门  1:是 2:否
      */
     private Integer isHome;
+    /**
+     * 是否开过发票 0:未开 1:已开
+     */
+    private Integer isInvoice;
 
     @TableField(exist = false)
     private String orderIds;
diff --git a/src/main/java/cc/mrbird/febs/mall/entity/MallOrderItem.java b/src/main/java/cc/mrbird/febs/mall/entity/MallOrderItem.java
index d2a07b9..3bdbce5 100644
--- a/src/main/java/cc/mrbird/febs/mall/entity/MallOrderItem.java
+++ b/src/main/java/cc/mrbird/febs/mall/entity/MallOrderItem.java
@@ -52,4 +52,7 @@
 
     @TableField(exist = false)
     private long[] orderIdsStr;
+
+    @TableField(exist = false)
+    private String unit;
 }
diff --git a/src/main/java/cc/mrbird/febs/mall/mapper/MallInvoiceMapper.java b/src/main/java/cc/mrbird/febs/mall/mapper/MallInvoiceMapper.java
new file mode 100644
index 0000000..74c7d8a
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/mall/mapper/MallInvoiceMapper.java
@@ -0,0 +1,26 @@
+package cc.mrbird.febs.mall.mapper;
+
+import cc.mrbird.febs.mall.dto.AdminMallInvoiceDto;
+import cc.mrbird.febs.mall.dto.ApiMallInvoiceDto;
+import cc.mrbird.febs.mall.entity.MallInvoice;
+import cc.mrbird.febs.mall.entity.MallOrderItem;
+import cc.mrbird.febs.mall.vo.AdminInvoiceDetailVo;
+import cc.mrbird.febs.mall.vo.ApiMallInvoiceVo;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface MallInvoiceMapper extends BaseMapper<MallInvoice> {
+
+    IPage<ApiMallInvoiceVo> selectApiMallInvoiceVoInPage(IPage<ApiMallInvoiceVo> page, @Param("record")ApiMallInvoiceDto apiMallInvoiceDto);
+
+    List<MallInvoice> selectByFapiaoApplyIdAndMemberId(@Param("fapiaoApplyId")String fapiaoApplyId, @Param("memberId")Long memberId);
+
+    IPage<MallInvoice> getInvoiceListInPage(Page<MallInvoice> page, @Param("record")AdminMallInvoiceDto mallInvoiceDto);
+
+    AdminInvoiceDetailVo getAdminInvoiceDetailVoById(@Param("id")long id);
+    List<MallOrderItem> getInvoiceItemList(@Param("record")MallInvoice mallInvoice);
+}
diff --git a/src/main/java/cc/mrbird/febs/mall/mapper/MallOrderInfoMapper.java b/src/main/java/cc/mrbird/febs/mall/mapper/MallOrderInfoMapper.java
index 30edcae..841527e 100644
--- a/src/main/java/cc/mrbird/febs/mall/mapper/MallOrderInfoMapper.java
+++ b/src/main/java/cc/mrbird/febs/mall/mapper/MallOrderInfoMapper.java
@@ -47,6 +47,8 @@
 
     List<MallOrderInfo> selectOrderInfoByStatus(@Param("status") Integer status);
 
+    List<MallOrderInfo> selectOrderInfoByStatusAndIsInvoice(@Param("status") Integer status,@Param("isInvoice") Integer isInvoice);
+
     List<MallOrderInfo> selectOrderInfoUpTime(@Param("status") Integer status);
 
     Integer selectCntDirectOrTeam(@Param("type") Integer type, @Param("inviteId") String inviteId);
@@ -83,4 +85,6 @@
     IPage<AdminGoodsStatisticsVo> getGoodsStatisticsInPage(Page<AdminGoodsStatisticsVo> page, @Param("record") MallOrderItem mallOrderItem);
 
     BigDecimal selectAmountOrTeamAmount(@Param("inviteId") String inviteId, @Param("type") Integer type);
+
+    MallOrderInfo selectBypayOrderNo(@Param("payOrderNo")String payOrderNo);
 }
diff --git a/src/main/java/cc/mrbird/febs/mall/quartz/WxxcxJob.java b/src/main/java/cc/mrbird/febs/mall/quartz/WxxcxJob.java
index 2c84ea3..fc4cb16 100644
--- a/src/main/java/cc/mrbird/febs/mall/quartz/WxxcxJob.java
+++ b/src/main/java/cc/mrbird/febs/mall/quartz/WxxcxJob.java
@@ -6,6 +6,7 @@
 import cc.mrbird.febs.common.utils.SpringContextHolder;
 import cc.mrbird.febs.mall.entity.MallMemberCoupon;
 import cc.mrbird.febs.mall.mapper.MallMemberCouponMapper;
+import cc.mrbird.febs.mall.service.MallInvoiceService;
 import cc.mrbird.febs.pay.util.WechatConfigure;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.DateUtil;
@@ -37,6 +38,8 @@
     private RedisUtils redisUtils;
     @Autowired
     private MallMemberCouponMapper mallMemberCouponMapper;
+    @Autowired
+    private MallInvoiceService mallInvoiceService;
     @Resource
     RestTemplate restTemplate;
 //    @Autowired
@@ -87,4 +90,14 @@
 
     }
 
+    /**
+     * 更新发票记录表数据
+     * 定时,每天凌晨一点
+     * 已完成的订单,更新到发票记录中
+     */
+    @Scheduled(cron = "0 0 1 * * ?")
+    public void mallInvoiceJob() {
+        mallInvoiceService.mallInvoiceJob();
+    }
+
 }
diff --git a/src/main/java/cc/mrbird/febs/mall/service/MallInvoiceService.java b/src/main/java/cc/mrbird/febs/mall/service/MallInvoiceService.java
new file mode 100644
index 0000000..4be679a
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/mall/service/MallInvoiceService.java
@@ -0,0 +1,31 @@
+package cc.mrbird.febs.mall.service;
+
+import cc.mrbird.febs.common.entity.FebsResponse;
+import cc.mrbird.febs.common.entity.QueryRequest;
+import cc.mrbird.febs.mall.dto.AdminMallInvoiceDto;
+import cc.mrbird.febs.mall.dto.ApiMallInvoiceDto;
+import cc.mrbird.febs.mall.entity.MallInvoice;
+import cc.mrbird.febs.mall.entity.MallOrderItem;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+
+import java.util.List;
+
+public interface MallInvoiceService {
+
+    FebsResponse getInvoices(ApiMallInvoiceDto apiMallInvoiceDto);
+
+     /**
+     * 更新发票记录表数据
+     * 定时,每天凌晨一点
+     * 已完成的订单,更新到发票记录中
+     */
+    void mallInvoiceJob();
+
+    IPage<MallInvoice> getInvoiceList(AdminMallInvoiceDto mallInvoiceDto, QueryRequest request);
+
+    List<MallOrderItem> invoiceItemList();
+
+    FebsResponse addInvoiceUrl(MallInvoice mallInvoice);
+
+    FebsResponse updateInvoiceDetail(Long id);
+}
diff --git a/src/main/java/cc/mrbird/febs/mall/service/impl/MallInvoiceServiceImpl.java b/src/main/java/cc/mrbird/febs/mall/service/impl/MallInvoiceServiceImpl.java
new file mode 100644
index 0000000..1739659
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/mall/service/impl/MallInvoiceServiceImpl.java
@@ -0,0 +1,160 @@
+package cc.mrbird.febs.mall.service.impl;
+
+import cc.mrbird.febs.common.entity.FebsResponse;
+import cc.mrbird.febs.common.entity.QueryRequest;
+import cc.mrbird.febs.common.enumerates.OrderStatusEnum;
+import cc.mrbird.febs.common.utils.LoginUserUtil;
+import cc.mrbird.febs.common.utils.RedisUtils;
+import cc.mrbird.febs.mall.dto.AdminMallInvoiceDto;
+import cc.mrbird.febs.mall.dto.ApiMallInvoiceDto;
+import cc.mrbird.febs.mall.entity.MallInvoice;
+import cc.mrbird.febs.mall.entity.MallMember;
+import cc.mrbird.febs.mall.entity.MallOrderInfo;
+import cc.mrbird.febs.mall.entity.MallOrderItem;
+import cc.mrbird.febs.mall.mapper.MallInvoiceMapper;
+import cc.mrbird.febs.mall.mapper.MallOrderInfoMapper;
+import cc.mrbird.febs.mall.mapper.MallOrderItemMapper;
+import cc.mrbird.febs.mall.service.MallInvoiceService;
+import cc.mrbird.febs.mall.vo.AdminMallOrderInfoVo;
+import cc.mrbird.febs.mall.vo.ApiMallInvoiceVo;
+import cc.mrbird.febs.pay.service.WxFaPiaoService;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.IOException;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MallInvoiceServiceImpl extends ServiceImpl<MallInvoiceMapper, MallInvoice> implements MallInvoiceService {
+
+    private final MallOrderInfoMapper mallOrderInfoMapper;
+    private final MallOrderItemMapper mallOrderItemMapper;
+    private final RedisUtils redisUtils;
+    private final WxFaPiaoService wxFaPiaoService;
+    @Override
+    public FebsResponse getInvoices(ApiMallInvoiceDto apiMallInvoiceDto) {
+        MallMember member = LoginUserUtil.getLoginUser();
+        apiMallInvoiceDto.setMemberId(member.getId());
+        IPage<ApiMallInvoiceVo> page = new Page<>(apiMallInvoiceDto.getPageNum(), apiMallInvoiceDto.getPageSize());
+        IPage<ApiMallInvoiceVo> apiMallInvoiceVoIPage = this.baseMapper.selectApiMallInvoiceVoInPage(page, apiMallInvoiceDto);
+        return new FebsResponse().success().data(apiMallInvoiceVoIPage);
+    }
+
+    @Override
+    public void mallInvoiceJob() {
+        /**
+         * 获取当前已经是已完成且未开过发票的订单
+         */
+        List<MallOrderInfo> mallOrderInfos = mallOrderInfoMapper.selectOrderInfoByStatusAndIsInvoice(OrderStatusEnum.FINISH.getValue(), 0);
+        if(CollUtil.isEmpty(mallOrderInfos)){
+            return;
+        }
+        for(MallOrderInfo mallOrderInfo : mallOrderInfos){
+            /**
+             * 生成开票记录
+             */
+            Long memberId = mallOrderInfo.getMemberId();
+            /**
+             * fapiao_apply_id
+             * 发票申请单号,唯一标识一次开票行为。微信支付场景下,为微信支付订单号;非微信支付场景下,为调用【获取抬头填写链接】接口时指定的发票申请单号
+             * 对应订单信息的pay_order_no支付订单号
+             */
+            String payOrderNo = mallOrderInfo.getPayOrderNo();
+            List<MallInvoice> mallInvoices = this.baseMapper.selectByFapiaoApplyIdAndMemberId(payOrderNo, memberId);
+            if(CollUtil.isNotEmpty(mallInvoices)){
+                MallInvoice mallInvoice = mallInvoices.get(0);
+                mallInvoice.setStatus(mallOrderInfo.getStatus());
+                this.baseMapper.updateById(mallInvoice);
+            }else{
+                MallInvoice mallInvoice = new MallInvoice();
+                mallInvoice.setMemberId(memberId);
+                mallInvoice.setFapiaoApplyId(payOrderNo);
+                mallInvoice.setOrderNo(mallOrderInfo.getOrderNo());
+                mallInvoice.setOrderId(mallOrderInfo.getId());
+                mallInvoice.setStatus(mallOrderInfo.getStatus());
+                mallInvoice.setAmount(mallOrderInfo.getAmount());
+                List<MallOrderItem> mallOrderItemList = mallOrderItemMapper.selectListByOrderId(mallOrderInfo.getId());
+                mallInvoice.setGoodsImg(mallOrderItemList.get(0).getSkuImage());
+                mallInvoice.setState(0);
+                this.baseMapper.insert(mallInvoice);
+            }
+
+            /**
+             * 更新订单为已开票
+             */
+            mallOrderInfo.setIsInvoice(1);
+            mallOrderInfoMapper.updateById(mallOrderInfo);
+        }
+
+    }
+
+    @Override
+    public IPage<MallInvoice> getInvoiceList(AdminMallInvoiceDto mallInvoiceDto, QueryRequest request) {
+        Page<MallInvoice> page = new Page<>(request.getPageNum(), request.getPageSize());
+        return  this.baseMapper.getInvoiceListInPage(page, mallInvoiceDto);
+    }
+
+    @Override
+    public List<MallOrderItem> invoiceItemList() {
+        Long existToken = Long.parseLong(redisUtils.getString("ADMIN_INVOICE_ID"));
+        MallInvoice mallInvoice = this.baseMapper.selectById(existToken);
+        if(ObjectUtil.isEmpty(mallInvoice)){
+            return null;
+        }
+        return this.baseMapper.getInvoiceItemList(mallInvoice);
+    }
+
+    @Override
+    @Transactional
+    public FebsResponse addInvoiceUrl(MallInvoice mallInvoice) {
+        Long id = mallInvoice.getId();
+        MallInvoice mallInvoiceOld = this.baseMapper.selectById(id);
+        if(ObjectUtil.isEmpty(mallInvoiceOld)){
+            return new FebsResponse().fail().message("开票信息异常。");
+        }
+        Integer status = mallInvoiceOld.getStatus();
+        if(OrderStatusEnum.FINISH.getValue() != status){
+            return new FebsResponse().fail().message("订单还未完成,请勿开发票。");
+        }
+        mallInvoiceOld.setInvoiceUrl(mallInvoice.getInvoiceUrl());
+        mallInvoiceOld.setState(2);
+        this.baseMapper.updateById(mallInvoiceOld);
+        return new FebsResponse().success();
+    }
+
+    @Override
+    @Transactional
+    public FebsResponse updateInvoiceDetail(Long id) {
+        MallInvoice mallInvoice = this.baseMapper.selectById(id);
+        if(ObjectUtil.isEmpty(mallInvoice)){
+            return new FebsResponse().fail().message("开票信息异常。");
+        }
+        String fapiaoApplyId = mallInvoice.getFapiaoApplyId();
+        try {
+            String userInvoiceInfo = wxFaPiaoService.getUserInvoiceInfo(fapiaoApplyId);
+            JSONObject userInvoiceInfoJson = JSONUtil.parseObj(userInvoiceInfo);
+            mallInvoice.setType(userInvoiceInfoJson.getStr("type"));
+            mallInvoice.setName(userInvoiceInfoJson.getStr("name"));
+            mallInvoice.setTaxpayerId(userInvoiceInfoJson.getStr("taxpayer_id"));
+            mallInvoice.setAddress(userInvoiceInfoJson.getStr("address"));
+            mallInvoice.setTelephone(userInvoiceInfoJson.getStr("telephone"));
+            mallInvoice.setBankName(userInvoiceInfoJson.getStr("bank_name"));
+            mallInvoice.setBankAccount(userInvoiceInfoJson.getStr("bank_account"));
+            this.baseMapper.updateById(mallInvoice);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return new FebsResponse().success();
+    }
+}
diff --git a/src/main/java/cc/mrbird/febs/mall/vo/AdminInvoiceDetailVo.java b/src/main/java/cc/mrbird/febs/mall/vo/AdminInvoiceDetailVo.java
new file mode 100644
index 0000000..043767f
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/mall/vo/AdminInvoiceDetailVo.java
@@ -0,0 +1,47 @@
+package cc.mrbird.febs.mall.vo;
+
+import lombok.Data;
+import java.math.BigDecimal;
+
+@Data
+public class AdminInvoiceDetailVo {
+    private Long id;
+    /**
+     *【购买方类型】 购买方类型
+     * 可选取值:
+     * INDIVIDUAL: 个人
+     * ORGANIZATION: 单位
+     */
+    private String type;
+    /**
+     *【名称】 购买方名称
+     */
+    private String name;
+    /**
+     *【纳税人识别号】 购买方纳税人识别号,购买方类型为ORGANIZATION时必须存在
+     */
+    private String taxpayerId;
+    /**
+     *【地址】 购买方地址
+     */
+    private String address;
+    /**
+     *【电话】 购买方电话
+     */
+    private String telephone;
+    /**
+     *【开户银行】 购买方开户银行
+     */
+    private String bankName;
+    /**
+     *【银行账号】 购买方银行账号
+     */
+    private String bankAccount;
+
+    private String orderNo;//订单编号
+
+    private BigDecimal amount;//订单金额
+
+    private String invoiceUrl;//订单编号
+
+}
diff --git a/src/main/java/cc/mrbird/febs/mall/vo/ApiMallInvoiceVo.java b/src/main/java/cc/mrbird/febs/mall/vo/ApiMallInvoiceVo.java
new file mode 100644
index 0000000..86d138e
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/mall/vo/ApiMallInvoiceVo.java
@@ -0,0 +1,30 @@
+package cc.mrbird.febs.mall.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel(value = "ApiMallInvoiceVo", description = "发票")
+public class ApiMallInvoiceVo {
+
+    @ApiModelProperty(value = "ID")
+    private Long id;
+
+    @ApiModelProperty(value = "订单编号")
+    private String orderNo;
+
+    @ApiModelProperty(value = "订单金额")
+    private BigDecimal amount;
+
+    @ApiModelProperty(value = "商品图片")
+    private String goodsImg;
+
+    @ApiModelProperty(value = "开票状态:0:未开票 1:开票中 2:已开票")
+    private Integer state;
+
+    @ApiModelProperty(value = "发票的URL路径")
+    private String invoiceUrl;
+}
diff --git a/src/main/java/cc/mrbird/febs/pay/controller/XcxPayController.java b/src/main/java/cc/mrbird/febs/pay/controller/XcxPayController.java
index 6830a9f..0d95241 100644
--- a/src/main/java/cc/mrbird/febs/pay/controller/XcxPayController.java
+++ b/src/main/java/cc/mrbird/febs/pay/controller/XcxPayController.java
@@ -13,6 +13,7 @@
 import cc.mrbird.febs.mall.service.IMallMoneyFlowService;
 import cc.mrbird.febs.pay.model.NotifyData;
 import cc.mrbird.febs.pay.service.IXcxPayService;
+import cc.mrbird.febs.pay.service.WxFaPiaoService;
 import cc.mrbird.febs.pay.util.Signature;
 import cc.mrbird.febs.pay.util.Util;
 import cc.mrbird.febs.pay.util.WechatConfigure;
@@ -21,19 +22,27 @@
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
+import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.io.BufferedReader;
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.text.ParseException;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
@@ -68,6 +77,9 @@
 
     @Autowired
     private IXcxPayService iXcxPayService;
+
+    @Autowired
+    private WxFaPiaoService wxFaPiaoService;
 
     private final XcxProperties xcxProperties = SpringContextHolder.getBean(XcxProperties.class);
     /**
@@ -238,17 +250,16 @@
 //    }
     /**
      * 微信电子发票回调接口
+     * POST方式回调
      * @return
      */
     @Transactional(rollbackFor = Exception.class)
     @RequestMapping(value = "/fapiaoCallBack")
-    public Map<Object, Object> fapiaoCallBack(HttpServletResponse response, HttpServletRequest request) throws IOException {
-        log.info("微信电子发票回调接口....");
-        Map<Object, Object> objectObjectHashMap = new HashMap<>();
-        objectObjectHashMap.put("code","SUCCESS");
-        objectObjectHashMap.put("message","");
-        return objectObjectHashMap;
+    public Map<String, Object> fapiaoCallBack(HttpServletRequest request, @RequestBody Map<String, Object> requestBody) {
+        return wxFaPiaoService.fapiaoCallBack(request,requestBody);
+
     }
+
     /**
      * 微信支付回调接口
      */
@@ -295,6 +306,8 @@
                         resXml = WechatConfigure.RESULT_XML_SUCCESS;
                         // 支付费用
                         Double total_fee = Double.parseDouble(data.getTotal_fee());
+                        //微信支付订单号
+                        String transaction_id = data.getTransaction_id();
                         // 商户订单号
                         String payNum = data.getOut_trade_no();
 
@@ -310,6 +323,7 @@
                             order.setPayResult("1");
                             order.setPayTime(new Date());
                             order.setDeliveryState(OrderDeliveryStateEnum.DELIVERY_WAIT.getValue());
+                            order.setPayOrderNo(transaction_id);
                             mallOrderInfoMapper.updateById(order);
 
                             agentProducer.sendOrderCoupon(order.getId());
diff --git a/src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateCustomCellDto.java b/src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateCustomCellDto.java
new file mode 100644
index 0000000..ece6db2
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateCustomCellDto.java
@@ -0,0 +1,21 @@
+package cc.mrbird.febs.pay.model;
+
+import lombok.Data;
+
+@Data
+public class FPCardTemplateCustomCellDto {
+    /**
+     * 【cell位文字】 展示在卡券详情页自定义cell位上的文字
+     */
+    private String words;
+    /**
+     * 【cell位描述】 展示在卡券详情页自定义cell位右侧的描述
+     */
+    private String description;
+
+    private String jump_url;
+
+    private String miniprogram_user_name;
+
+    private String miniprogram_path;
+}
diff --git a/src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateDto.java b/src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateDto.java
new file mode 100644
index 0000000..525fc96
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateDto.java
@@ -0,0 +1,17 @@
+package cc.mrbird.febs.pay.model;
+
+import lombok.Data;
+
+@Data
+public class FPCardTemplateDto {
+    /**
+     * 【插卡公众号AppID】 插卡公众号AppID。
+     * 若是服务商模式,则可以是服务商申请的AppId,也可以是子商户申请的AppId;
+     * 若是直连模式,则是直连商户申请的AppId
+     */
+    private String card_appid;
+    /**
+     * 【卡券模板信息】 卡券模板信息
+     */
+    private FPCardTemplateInformationDto card_template_information;
+}
diff --git a/src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateInformationDto.java b/src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateInformationDto.java
new file mode 100644
index 0000000..56f2851
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateInformationDto.java
@@ -0,0 +1,22 @@
+package cc.mrbird.febs.pay.model;
+
+import lombok.Data;
+
+/**
+ * 【卡券模板信息】 卡券模板信息
+ */
+@Data
+public class FPCardTemplateInformationDto {
+    /**
+     * 【收款方名称】 收款方名称,显示在电子发票卡券信息中,若不传则默认取商户名称
+     */
+    private String payee_name;
+    /**
+     * 【卡券logo地址】 卡券logo地址
+     */
+    private String logo_url;
+    /**
+     * 【卡券自定义cell位配置】
+     */
+    private FPCardTemplateCustomCellDto custom_cell;
+}
diff --git a/src/main/java/cc/mrbird/febs/pay/model/FPCertificateVo.java b/src/main/java/cc/mrbird/febs/pay/model/FPCertificateVo.java
new file mode 100644
index 0000000..2490f68
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/pay/model/FPCertificateVo.java
@@ -0,0 +1,10 @@
+package cc.mrbird.febs.pay.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class FPCertificateVo {
+    private List<FPCertificates> data;
+}
diff --git a/src/main/java/cc/mrbird/febs/pay/model/FPCertificates.java b/src/main/java/cc/mrbird/febs/pay/model/FPCertificates.java
new file mode 100644
index 0000000..07d8e95
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/pay/model/FPCertificates.java
@@ -0,0 +1,13 @@
+package cc.mrbird.febs.pay.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class FPCertificates {
+    private String serial_no;
+    private String effective_time;
+    private String expire_time;
+    private FPEncryptCertificate encrypt_certificate;
+}
diff --git a/src/main/java/cc/mrbird/febs/pay/model/FPEncryptCertificate.java b/src/main/java/cc/mrbird/febs/pay/model/FPEncryptCertificate.java
new file mode 100644
index 0000000..fe4005a
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/pay/model/FPEncryptCertificate.java
@@ -0,0 +1,11 @@
+package cc.mrbird.febs.pay.model;
+
+import lombok.Data;
+
+@Data
+public class FPEncryptCertificate {
+    private String algorithm;
+    private String nonce;
+    private String associated_data;
+    private String ciphertext;
+}
diff --git a/src/main/java/cc/mrbird/febs/pay/model/FPUserTitleDto.java b/src/main/java/cc/mrbird/febs/pay/model/FPUserTitleDto.java
new file mode 100644
index 0000000..48889ec
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/pay/model/FPUserTitleDto.java
@@ -0,0 +1,20 @@
+package cc.mrbird.febs.pay.model;
+
+import lombok.Data;
+
+@Data
+public class FPUserTitleDto {
+    /**
+     * 【发票申请单号】 发票申请单号,唯一标识一次开票行为。
+     * 当开票场景为WITHOUT_WECHATPAY时,为调用【获取抬头填写链接】接口时指定的发票申请单号;
+     * 当开票场景为WITH_WECHATPAY时,为与本次开票关联的微信支付订单号,且必须是属于相应商户的订单
+     */
+    private String fapiao_apply_id;
+    /**
+     * 【开票场景】 开票场景
+     * 可选取值:
+     * WITH_WECHATPAY: 微信支付场景
+     * WITHOUT_WECHATPAY: 非微信支付场景
+     */
+    private String scene;
+}
diff --git a/src/main/java/cc/mrbird/febs/pay/service/WxFaPiaoService.java b/src/main/java/cc/mrbird/febs/pay/service/WxFaPiaoService.java
index 960e8e2..7b73f49 100644
--- a/src/main/java/cc/mrbird/febs/pay/service/WxFaPiaoService.java
+++ b/src/main/java/cc/mrbird/febs/pay/service/WxFaPiaoService.java
@@ -1,11 +1,27 @@
 package cc.mrbird.febs.pay.service;
 
-import java.security.KeyPair;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.util.Map;
 
 public interface WxFaPiaoService {
 
-    String createAuthorization(String method, String canonicalUrl, String body, KeyPair keyPair);//生成Token
+    String createAuthorization(String method, String canonicalUrl, String body, PrivateKey keyPair) throws UnsupportedEncodingException, NoSuchAlgorithmException;//生成Token
 
-    KeyPair getPrivateKey();//通过证书获取私钥公钥
+    PrivateKey getPrivateKeyV3() throws IOException;//获取私钥
 
+    String sendPatch(String url, String params, String token);
+
+    String sendPost(String url, String params, String token);
+
+    String sendGet(String url, Map<String, Object> params, String token);
+
+    Map<String, Object> fapiaoCallBack(HttpServletRequest request, @RequestBody Map<String, Object> requestBody);
+
+    String getUserInvoiceInfo(String fapiaoApplyId) throws IOException;
 }
diff --git a/src/main/java/cc/mrbird/febs/pay/service/impl/WxFaPiaoServiceImpl.java b/src/main/java/cc/mrbird/febs/pay/service/impl/WxFaPiaoServiceImpl.java
index a35aa8d..935ab4a 100644
--- a/src/main/java/cc/mrbird/febs/pay/service/impl/WxFaPiaoServiceImpl.java
+++ b/src/main/java/cc/mrbird/febs/pay/service/impl/WxFaPiaoServiceImpl.java
@@ -1,108 +1,597 @@
 package cc.mrbird.febs.pay.service.impl;
 
 import cc.mrbird.febs.common.properties.XcxProperties;
+import cc.mrbird.febs.common.utils.AppContants;
 import cc.mrbird.febs.common.utils.SpringContextHolder;
+import cc.mrbird.febs.mall.entity.MallInvoice;
+import cc.mrbird.febs.mall.entity.MallOrderInfo;
+import cc.mrbird.febs.mall.entity.MallOrderItem;
+import cc.mrbird.febs.mall.mapper.MallInvoiceMapper;
+import cc.mrbird.febs.mall.mapper.MallOrderInfoMapper;
+import cc.mrbird.febs.mall.mapper.MallOrderItemMapper;
+import cc.mrbird.febs.pay.model.FPCertificateVo;
+import cc.mrbird.febs.pay.model.FPCertificates;
+import cc.mrbird.febs.pay.model.FPEncryptCertificate;
 import cc.mrbird.febs.pay.service.WxFaPiaoService;
+import cc.mrbird.febs.pay.util.JCEUtil;
 import cc.mrbird.febs.pay.util.RandomStringGenerator;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
 import lombok.RequiredArgsConstructor;
-import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import okhttp3.HttpUrl;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPatch;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.stereotype.Service;
 import org.springframework.util.Base64Utils;
+import org.springframework.web.bind.annotation.RequestBody;
 
+import javax.servlet.http.HttpServletRequest;
 import java.io.*;
 import java.nio.charset.StandardCharsets;
 import java.security.*;
+import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
-import java.util.Map;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class WxFaPiaoServiceImpl implements WxFaPiaoService {
 
+    private final MallOrderInfoMapper mallOrderInfoMapper;
+    private final MallOrderItemMapper mallOrderItemMapper;
+    private final MallInvoiceMapper mallInvoiceMapper;
+
     private final XcxProperties xcxProperties = SpringContextHolder.getBean(XcxProperties.class);
 
     @Override
-    public String createAuthorization(String method, String canonicalUrl, String body, KeyPair keyPair) {
+    public String createAuthorization(String method, String canonicalUrl, String body, PrivateKey keyPair) throws UnsupportedEncodingException, NoSuchAlgorithmException {
         String nonceStr = RandomStringGenerator.getRandomStringByLength(32);//随机字符串
         long timestamp = System.currentTimeMillis() / 1000;//时间戳
-        String signature = sign(method, canonicalUrl, timestamp, nonceStr, body, keyPair);//签名加密
+        HttpUrl httpurl = HttpUrl.parse(canonicalUrl);
+        String message = buildMessage(method, httpurl, timestamp, nonceStr, body);
+        log.info("签名串:\n"+message);
+        log.info("签名串长度:\n"+getWordCount(message));
+        String signature = sign2(message.getBytes("utf-8"), keyPair);
+
+        log.info("签名串sign:\n"+signature);
+        log.info("签名串长度sign:\n"+getWordCount(signature));
+//        String yourCertificateSerialNo = "221D49AEC4EA538A63941D1936709C8559EB05C5";
         return "mchid=\"" + xcxProperties.getWecharpayMchid() + "\","
                 + "nonce_str=\"" + nonceStr + "\","
                 + "timestamp=\"" + timestamp + "\","
-                + "serial_no=\"" + "50F37206347BCC9E6AC9860DAACE52AC035F7C24" + "\","//证书序列号
+                + "serial_no=\"" + AppContants.WX_CARD_NUM + "\","
                 + "signature=\"" + signature + "\"";
     }
 
-    @Override
-    public KeyPair getPrivateKey() {
-        return createPKCS12("Tenpay Certificate", "1658958205");
-    }
-    /**
-     * V3  SHA256withRSA 签名.
-     *
-     * @param method       请求方法  GET  POST PUT DELETE 等
-     * @param canonicalUrl 例如  https://api.mch.weixin.qq.com/v3/pay/transactions/app?version=1 ——> /v3/pay/transactions/app?version=1
-     * @param timestamp    当前时间戳   因为要配置到TOKEN 中所以 签名中的要跟TOKEN 保持一致
-     * @param nonceStr     随机字符串  要和TOKEN中的保持一致
-     * @param body         请求体 GET 为 "" POST 为JSON
-     * @param keyPair      商户API 证书解析的密钥对  实际使用的是其中的私钥
-     * @return the string
-     */
-    @SneakyThrows
-    public String sign(String method, String canonicalUrl, long timestamp, String nonceStr, String body, KeyPair keyPair)  {
-        String signatureStr = Stream.of(method, canonicalUrl, String.valueOf(timestamp), nonceStr, body)
-                .collect(Collectors.joining("\n", "", "\n"));
-        Signature sign = Signature.getInstance("SHA256withRSA");
-        sign.initSign(keyPair.getPrivate());
-        sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
-        return Base64Utils.encodeToString(sign.sign());
-    }
+    public   int  getWordCount(String s)
+    {
+        int  length  =   0 ;
+        for ( int  i  =   0 ; i  <  s.length(); i ++ )
+        {
+            int  ascii  =  Character.codePointAt(s, i);
+            if (ascii  >=   0   &&  ascii  <= 255 )
+                length ++ ;
+            else
+                length  +=   2 ;
 
-    /**
-     * 获取公私钥.通过证书
-     */
-    private KeyStore store;
-    private final Object lock = new Object();
-    public KeyPair createPKCS12(String keyAlias, String keyPass) {
-        ClassPathResource resource = new ClassPathResource(xcxProperties.getCertLocalPath());
-//        File file = new File("src/main/resources/wxP12/apiclient_cert.p12");
-        char[] pem = keyPass.toCharArray();
-        try {
-            synchronized (lock) {
-                if (store == null) {
-                    synchronized (lock) {
-                        store = KeyStore.getInstance("PKCS12");
-                        store.load(resource.getInputStream(), pem);
-//                        store.load(new FileInputStream(file), pem);
-                    }
-                }
-            }
-            X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias);
-            certificate.checkValidity();
-            // 证书的序列号 也有用 50F37206347BCC9E6AC9860DAACE52AC035F7C24
-            String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
-            // 证书的 公钥
-            PublicKey publicKey = certificate.getPublicKey();
-            // 证书的私钥
-            PrivateKey storeKey = (PrivateKey) store.getKey(keyAlias, pem);
-            return new KeyPair(publicKey, storeKey);
-        } catch (Exception e) {
-            throw new IllegalStateException("Cannot load keys from store: " , e);
         }
+        return  length;
+
     }
 
-    public static void main(String[] args) {
+    public String sign2(byte[] message,PrivateKey keyPair) throws NoSuchAlgorithmException {
+        Signature sign = Signature.getInstance("SHA256withRSA");
+        String s = null;
         try {
-            System.out.println(new ClassPathResource("wxP12/apiclient_cert.p12").getFile().exists());
-        } catch (IOException e) {
+            sign.initSign(keyPair);
+            sign.update(message);
+            s = Base64.getEncoder().encodeToString(sign.sign());
+        } catch (InvalidKeyException e) {
+            e.printStackTrace();
+        } catch (SignatureException e) {
             e.printStackTrace();
         }
+        return s;
+    }
+
+    public String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
+        String canonicalUrl = url.encodedPath();
+        if (url.encodedQuery() != null) {
+            canonicalUrl += "?" + url.encodedQuery();
+        }
+        return method + "\n"
+                + canonicalUrl + "\n"
+                + timestamp + "\n"
+                + nonceStr + "\n"
+                + body + "\n";
+    }
+
+    @Override
+    public PrivateKey getPrivateKeyV3() throws IOException {
+        InputStream inputStream = new ClassPathResource("wxP12/apiclient_key.pem")
+                .getInputStream();
+
+        String content = new BufferedReader(new InputStreamReader(inputStream))
+                .lines().collect(Collectors.joining(System.lineSeparator()));
+        try {
+            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
+                    .replace("-----END PRIVATE KEY-----", "")
+                    .replaceAll("\\s+", "");
+
+            KeyFactory kf = KeyFactory.getInstance("RSA");
+            return kf.generatePrivate(
+                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("当前Java环境不支持RSA", e);
+        } catch (InvalidKeySpecException e) {
+            throw new RuntimeException("无效的密钥格式");
+        }
+    }
+
+    @Override
+    public String sendPatch(String url, String params, String token) {
+        String result = "";
+        CloseableHttpClient httpClient = HttpClients.createDefault();
+        HttpPatch httpPatch = new HttpPatch(url);
+        CloseableHttpResponse response = null;
+        httpPatch.setHeader("Content-type", "application/json");
+        httpPatch.setHeader("Charset", "utf-8");
+        httpPatch.setHeader("Accept", "application/json");
+        httpPatch.setHeader("Accept-Charset", "utf-8");
+        httpPatch.setHeader("Authorization", token);
+        try {
+            StringEntity data = new StringEntity(params, "utf-8");
+            httpPatch.setEntity(data);
+            response = httpClient.execute(httpPatch);
+            HttpEntity entity = response.getEntity();
+            result = EntityUtils.toString(entity);
+        } catch (Exception e) {
+            result = "{\"status\":\"1\",\"error\":\"" + e.getMessage() + "\"}";
+        }finally {
+            try {
+                httpClient.close();
+                if (response != null) {
+                    response.close();
+                }
+            } catch (IOException var22) {
+                var22.printStackTrace();
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public String sendPost(String url, String params, String token) {
+        String result = "";
+        int err = 0;
+        while (true) {
+            CloseableHttpClient client = HttpClients.createDefault();
+            HttpPost httpPost = new HttpPost(url);
+            CloseableHttpResponse response = null;
+            try {
+                httpPost.addHeader("Content-type", "application/json");
+                httpPost.addHeader("Charset", "utf-8");
+                httpPost.addHeader("Accept", "application/json");
+                httpPost.addHeader("Accept-Charset", "utf-8");
+                httpPost.addHeader("Authorization", token);
+
+                StringEntity data = new StringEntity(params, "utf-8");
+                httpPost.setEntity(data);
+                response = client.execute(httpPost);
+                HttpEntity resEntity = response.getEntity();
+                result = EntityUtils.toString(resEntity);
+                return result;
+            } catch (IOException e) {
+                result = "{\"status\":\"1\",\"errors\":\"" + e.getMessage() + "\"}";
+                if (err++ > 2) {
+                    break;
+                }
+                try {
+                    Thread.sleep((err + 2) * 1000);
+                } catch (InterruptedException e1) {
+                    result = "{\"status\":\"1\",\"errors\":\"" + e1.getMessage() + "\"}";
+                }
+            }finally {
+                try {
+                    client.close();
+                    if (response != null) {
+                        response.close();
+                    }
+                } catch (IOException var22) {
+                    var22.printStackTrace();
+                }
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public String sendGet(String url, Map<String, Object> params, String token) {
+        String result = "";
+        int err = 0;
+        while (true) {
+            CloseableHttpClient httpClient = HttpClients.createDefault();
+            CloseableHttpResponse response = null;
+            try {
+                List<BasicNameValuePair> parameters = new ArrayList<>();
+                for (Map.Entry<String, Object> entry : params.entrySet()) {
+                    parameters.add(new BasicNameValuePair(entry.getKey(), entry.getValue().toString()));
+                }
+                StringBuilder dataparm = new StringBuilder();
+                params.forEach((k, v) -> dataparm.append("&" + k + "=" + v));
+                String urlparm = "";
+                String string = dataparm.toString();
+                if (!"".equals(string)) {
+                    urlparm = dataparm.toString().substring(1);
+                }
+                HttpGet httpget = new HttpGet(url + "?" + urlparm);
+
+                httpget.addHeader("Accept", "application/json");
+                httpget.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36");
+                httpget.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
+                httpget.addHeader("Authorization", token);
+                response = httpClient.execute(httpget, HttpClientContext.create());
+
+                HttpEntity resEntity = response.getEntity();
+                result = EntityUtils.toString(resEntity);
+                return result;
+            } catch (IOException e) {
+                result = "{\"status\":\"1\",\"errors\":\"" + e.getMessage() + "\"}";
+                if (err++ > 2) {
+                    break;
+                }
+                try {
+                    Thread.sleep((err + 2) * 1000);
+                } catch (InterruptedException e1) {
+                    result = "{\"status\":\"1\",\"errors\":\"" + e1.getMessage() + "\"}";
+                }
+            }finally {
+                try {
+                    httpClient.close();
+                    if (response != null) {
+                        response.close();
+                    }
+                } catch (IOException var22) {
+                    var22.printStackTrace();
+                }
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> fapiaoCallBack(HttpServletRequest request, @RequestBody Map<String, Object> requestBody){
+        Map<String,Object> map = new HashMap<>();
+        String signature = request.getHeader("Wechatpay-Signature");
+        String timestamp = request.getHeader("Wechatpay-Timestamp");
+        String nonce = request.getHeader("Wechatpay-Nonce");
+        //平台证书序列号不是API证书序列号
+        String serial = request.getHeader("Wechatpay-Serial");
+        String body = com.alibaba.fastjson.JSONObject.toJSONString(requestBody);
+        log.info("头信息---签名:" + signature);
+        log.info("头信息---时间戳:" + timestamp);
+        log.info("头信息---随机字符:" + nonce);
+        log.info("头信息---平台证书序列号:" + serial);
+        log.info("获取到的body信息:" + body);
+        //应对签名探测流量
+        if(signature.contains("WECHATPAY/SIGNTEST")){
+            map.put("code",500);
+            map.put("message", "失败");
+            return map;
+        }
+        //验签
+        boolean signCheck = false;
+        try {
+            signCheck = signCheck(timestamp, nonce, requestBody, signature);
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        } catch (SignatureException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (InvalidKeyException e) {
+            e.printStackTrace();
+        } catch (ParseException e) {
+            e.printStackTrace();
+        }
+        log.info("验签结果:" + signCheck);
+        if (signCheck) {
+            try {
+                //解析请求体
+                JSONObject jsonObject = JSONUtil.parseObj(body);
+                log.info("微信电子发票回调接口....解析请求体:"+jsonObject);
+
+                String id = jsonObject.getStr("id");//可能是支付业务的回调数据
+                String create_time = jsonObject.getStr("create_time");//可能是支付业务的回调数据
+                String resource_type = jsonObject.getStr("resource_type");//可能是支付业务的回调数据
+                String event_type = jsonObject.getStr("event_type");//可能是支付业务的回调数据
+                String resource = jsonObject.getStr("resource");//可能是支付业务的回调数据
+                log.info("微信电子发票回调接口....id:"+ id);
+                log.info("微信电子发票回调接口....create_time:"+ create_time);
+                log.info("微信电子发票回调接口....resource_type:"+ resource_type);
+                log.info("微信电子发票回调接口....event_type:"+ event_type);
+                log.info("微信电子发票回调接口....resource:"+ resource);
+                if ("FAPIAO.USER_APPLIED".equals(event_type)//用户发票抬头填写完成类型:FAPIAO.USER_APPLIED
+                        && "encrypt-resource".equals(resource_type)) {//通知的资源数据类型,确认成功通知为encryptresource。
+                    JCEUtil.removeCryptographyRestrictions();
+                    //解密
+                    AesUtil aesUtil = new AesUtil(xcxProperties.getWecharpaySecretV3().getBytes("utf-8"));
+                    JSONObject jsonObjectResource = JSONUtil.parseObj(resource);
+                    String original_type = jsonObjectResource.getStr("original_type");//可能是支付业务的回调数据
+                    String algorithm = jsonObjectResource.getStr("algorithm");//可能是支付业务的回调数据
+                    String ciphertext = jsonObjectResource.getStr("ciphertext");//可能是支付业务的回调数据
+                    String associated_data = jsonObjectResource.getStr("associated_data");//可能是支付业务的回调数据
+                    String nonceStr = jsonObjectResource.getStr("nonce");//可能是支付业务的回调数据
+                    String decryptToString = aesUtil.decryptToString(
+                            associated_data.getBytes("utf-8"),
+                            nonceStr.getBytes("utf-8"),
+                            ciphertext);
+                    log.info("微信电子发票回调接口....resource解密:"+decryptToString);
+
+                    JSONObject parseObj = JSONUtil.parseObj(decryptToString);
+                    log.info("微信电子发票回调接口....resource解密-JSONObject:"+parseObj);
+                    String fapiao_apply_id = parseObj.getStr("fapiao_apply_id");
+                    log.info("微信电子发票回调接口....resource解密-fapiao_apply_id:"+fapiao_apply_id);
+                    MallOrderInfo mallOrderInfo = mallOrderInfoMapper.selectBypayOrderNo(fapiao_apply_id);
+                    if(ObjectUtil.isEmpty(mallOrderInfo)){
+                        map.put("code",404);
+                        map.put("message", "订单不存在");
+                        return map;
+                    }
+
+                    if(ObjectUtil.isNotEmpty(mallOrderInfo)){
+                        //更新订单状态
+//                        mallOrderInfo.setIsInvoice(1);
+//                        mallOrderInfoMapper.updateById(mallOrderInfo);
+                        //获取用户的抬头信息
+                        String userInvoiceInfo = this.getUserInvoiceInfo(fapiao_apply_id);
+                        JSONObject userInvoiceInfoJson = JSONUtil.parseObj(userInvoiceInfo);
+                        //查看当前订单是否已经开具发票
+                        Long memberId = mallOrderInfo.getMemberId();
+                        String payOrderNo = mallOrderInfo.getPayOrderNo();
+                        List<MallInvoice> mallInvoices = mallInvoiceMapper.selectByFapiaoApplyIdAndMemberId(fapiao_apply_id, mallOrderInfo.getMemberId());
+                        if(CollUtil.isEmpty(mallInvoices)){
+                            MallInvoice mallInvoice = new MallInvoice();
+                            mallInvoice.setMemberId(memberId);
+                            mallInvoice.setFapiaoApplyId(payOrderNo);
+                            mallInvoice.setOrderNo(mallOrderInfo.getOrderNo());
+                            mallInvoice.setOrderId(mallOrderInfo.getId());
+                            mallInvoice.setStatus(mallOrderInfo.getStatus());
+                            mallInvoice.setAmount(mallOrderInfo.getAmount());
+                            List<MallOrderItem> mallOrderItemList = mallOrderItemMapper.selectListByOrderId(mallOrderInfo.getId());
+                            mallInvoice.setGoodsImg(mallOrderItemList.get(0).getSkuImage());
+                            mallInvoice.setState(1);
+                            mallInvoice.setType(userInvoiceInfoJson.getStr("type"));
+                            mallInvoice.setName(userInvoiceInfoJson.getStr("name"));
+                            mallInvoice.setTaxpayerId(userInvoiceInfoJson.getStr("taxpayer_id"));
+                            mallInvoice.setAddress(userInvoiceInfoJson.getStr("address"));
+                            mallInvoice.setTelephone(userInvoiceInfoJson.getStr("telephone"));
+                            mallInvoice.setBankName(userInvoiceInfoJson.getStr("bank_name"));
+                            mallInvoice.setBankAccount(userInvoiceInfoJson.getStr("bank_account"));
+                            mallInvoiceMapper.insert(mallInvoice);
+                        }else{
+                            MallInvoice mallInvoice = mallInvoices.get(0);
+                            if(2 != mallInvoice.getState()){
+                                mallInvoice.setState(1);
+                                mallInvoice.setType(userInvoiceInfoJson.getStr("type"));
+                                mallInvoice.setName(userInvoiceInfoJson.getStr("name"));
+                                mallInvoice.setTaxpayerId(userInvoiceInfoJson.getStr("taxpayer_id"));
+                                mallInvoice.setAddress(userInvoiceInfoJson.getStr("address"));
+                                mallInvoice.setTelephone(userInvoiceInfoJson.getStr("telephone"));
+                                mallInvoice.setBankName(userInvoiceInfoJson.getStr("bank_name"));
+                                mallInvoice.setBankAccount(userInvoiceInfoJson.getStr("bank_account"));
+                                mallInvoiceMapper.updateById(mallInvoice);
+                            }
+                        }
+                        //省略查询订单
+                        //此处处理业务
+                        map.put("code","SUCCESS");
+                        map.put("message","成功");
+                        //消息推送成功
+                        return map;
+                    }
+                }
+                map.put("code",404);
+                map.put("message", "订单不存在");
+                return map;
+            }catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        map.put("code",500);
+        map.put("message", "失败");
+        return map;
+    }
+    /**
+     * 获取用户的抬头信息
+     */
+    @Override
+    public String getUserInvoiceInfo(String fapiaoApplyId) throws IOException {
+
+        //初始化请求参数
+        Map<String, Object> params = new HashMap<>();
+        params.put("scene","WITH_WECHATPAY");
+//        params.put("fapiao_apply_id","4200002070202401175245187105");
+        params.put("fapiao_apply_id",fapiaoApplyId);
+
+        String baseUrl = "https://api.mch.weixin.qq.com";
+        String canonicalUrl = "/v3/new-tax-control-fapiao/user-title";
+
+        String urlparm = baseUrl+canonicalUrl;//有参数的get请求,加密是需要把参数加载进去URL
+        List<BasicNameValuePair> parameters = new ArrayList<>();
+        for (Map.Entry<String, Object> entry : params.entrySet()) {
+            parameters.add(new BasicNameValuePair(entry.getKey(), entry.getValue().toString()));
+        }
+        StringBuilder dataparm = new StringBuilder();
+        params.forEach((k, v) -> dataparm.append("&" + k + "=" + v));
+        String string = dataparm.toString();
+        if (!"".equals(string)) {
+            urlparm = baseUrl+canonicalUrl + "?" + dataparm.substring(1);
+        }
+        PrivateKey privateKey = this.getPrivateKeyV3();
+        String postStr = null;
+        try {
+            postStr = this.createAuthorization(
+                    "GET",
+                    urlparm,
+                    "",
+                    privateKey
+            );
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        }
+        return this.sendGet(baseUrl+canonicalUrl, params, "WECHATPAY2-SHA256-RSA2048 "+postStr);
+    }
+
+    /**
+     * 验证签名
+     *
+     * @param timestamp   微信平台传入的时间戳
+     * @param nonce       微信平台传入的随机字符串
+     * @param requestBody 微信平台传入的消息体
+     * @param signature   微信平台传入的签名
+     * @return
+     * @throws NoSuchAlgorithmException
+     * @throws SignatureException
+     * @throws IOException
+     * @throws InvalidKeyException
+     */
+    public boolean signCheck(String timestamp, String nonce, Map<String, Object> requestBody, String signature) throws
+            NoSuchAlgorithmException,
+            SignatureException,
+            IOException,
+            InvalidKeyException,
+            ParseException {
+        //构造验签名串
+        String signatureStr = timestamp + "\n" + nonce + "\n" + com.alibaba.fastjson.JSONObject.toJSONString(requestBody) + "\n";
+        // 加载SHA256withRSA签名器
+        Signature signer = Signature.getInstance("SHA256withRSA");
+        // 用微信平台公钥对签名器进行初始化(调上一节中的获取平台证书方法)
+        signer.initVerify(getCertificates());
+        // 把我们构造的验签名串更新到签名器中
+        signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
+        // 把请求头中微信服务器返回的签名用Base64解码 并使用签名器进行验证
+        boolean result = signer.verify(Base64Utils.decodeFromString(signature));
+        return result;
+    }
+
+
+    /**
+     * 获取平台证书
+     */
+    public X509Certificate getCertificates() throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, ParseException {
+        JCEUtil.removeCryptographyRestrictions();
+        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+
+        PrivateKey privateKey = this.getPrivateKeyV3();
+        String baseUrl = "https://api.mch.weixin.qq.com";
+        String canonicalUrl = "/v3/certificates";
+        String postStr = null;
+        try {
+            postStr = this.createAuthorization(
+                    "GET",
+                    baseUrl+canonicalUrl,
+                    "",
+                    privateKey
+
+            );
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        }
+        //完成签名并执行请求
+        X509Certificate x509Certificate = null;
+        try {
+            String responseBodyAsString = this.sendGet(baseUrl + canonicalUrl, new HashMap<>(), "WECHATPAY2-SHA256-RSA2048 "+postStr);
+            FPCertificateVo certificateVo = com.alibaba.fastjson.JSONObject.parseObject(responseBodyAsString, FPCertificateVo.class);
+            for (FPCertificates certificates : certificateVo.getData()) {
+                if (format.parse(certificates.getEffective_time()).before(new Date())
+                        && format.parse(certificates.getExpire_time()).after(new Date())) {
+                    FPEncryptCertificate encrypt_certificate = certificates.getEncrypt_certificate();
+                    //解密
+                    AesUtil aesUtil = new AesUtil(xcxProperties.getWecharpaySecretV3().getBytes("utf-8"));
+                    String pulicKey = aesUtil.decryptToString(
+                            encrypt_certificate.getAssociated_data().getBytes("utf-8"),
+                            encrypt_certificate.getNonce().getBytes("utf-8"),
+                            encrypt_certificate.getCiphertext());
+                    //获取平台证书
+                    final CertificateFactory cf = CertificateFactory.getInstance("X509");
+
+                    ByteArrayInputStream inputStream = new ByteArrayInputStream(pulicKey.getBytes(StandardCharsets.UTF_8));
+
+                    x509Certificate = (X509Certificate) cf.generateCertificate(inputStream);
+                }
+            }
+            return x509Certificate;
+        } catch (GeneralSecurityException | ParseException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static void main(String[] args) throws UnsupportedEncodingException, GeneralSecurityException {
+        String signature = "WECHATPAY/SIGNTEST/9WwfXW/noMdzDPcOrFD51Bf5YIThXLTtLCl5hoGlSfIBdY2UpE+5eHLs0XyF7y2cQc4OlzTzDDxSMTp/p/8ZenE2hMBaQjxVrdbTtrOLA1h13/WjCNaTr1URTqQ5+IF5bLCxAW8BmZ0jEqpmI/HYR3wdK8/7W91c1zfKODYgkvJzxFzd8OWi2GtELE5tLamkOLyb0GEcsGww2DUOypO5HjNyITgsy9R00w3OH92UInCD8Z6c5BAsQNySqFK8N52y38AlIWTtKROHmJtu+kHbt+nMOU0kbB5bZcPAptsnVYpp9KMmjMrmyjiKtHZb2TGIWiN/L+bgceyN3g==";
+        if(signature.contains("WECHATPAY/SIGNTEST")){
+            System.out.println(1);
+        }
+
+//        JCEUtil.removeCryptographyRestrictions();
+//        String body = "{\"id\":\"5fb7f522-466f-5c99-a325-0eb4001f1ec3\",\"create_time\":\"2024-01-17T15:38:33+08:00\",\"resource_type\":\"encrypt-resource\",\"event_type\":\"FAPIAO.USER_APPLIED\",\"summary\":\"用户已申请开票\",\"resource\":{\"original_type\":\"fapiao\",\"algorithm\":\"AEAD_AES_256_GCM\",\"ciphertext\":\"bBOs5WTUV1AhwiYSIUugaw9z4EHXxC28ZVsGHOppCNP4qpxYgnR+W+l983UyETmD/Wgu8ukJhe/xYkSsF947pgDP1ElTXC8MJA3lCc8NlsXQ1csPfWTS6A0XrxBoNMgwGEsYFo/7tPW/zDrEpzRZL/GgEwEtK6X1Y4Ci9IofzeI=\",\"associated_data\":\"fapiao\",\"nonce\":\"7a5dzuYqStef\"}}";
+//        JSONObject jsonObject = JSONUtil.parseObj(body);
+//        log.info("微信电子发票回调接口....解析请求体:"+jsonObject);
+//
+//        String id = jsonObject.getStr("id");//可能是支付业务的回调数据
+//        String create_time = jsonObject.getStr("create_time");//可能是支付业务的回调数据
+//        String resource_type = jsonObject.getStr("resource_type");//可能是支付业务的回调数据
+//        String event_type = jsonObject.getStr("event_type");//可能是支付业务的回调数据
+//        String resource = jsonObject.getStr("resource");//可能是支付业务的回调数据
+//        log.info("微信电子发票回调接口....id:"+ id);
+//        log.info("微信电子发票回调接口....create_time:"+ create_time);
+//        log.info("微信电子发票回调接口....resource_type:"+ resource_type);
+//        log.info("微信电子发票回调接口....event_type:"+ event_type);
+//        log.info("微信电子发票回调接口....resource:"+ resource);
+//
+//        if ("FAPIAO.USER_APPLIED".equals(event_type)//用户发票抬头填写完成类型:FAPIAO.USER_APPLIED
+//                && !"encryptresource".equals(resource_type)) {//通知的资源数据类型,确认成功通知为encryptresource。
+//            //解密
+//            AesUtil aesUtil = new AesUtil("daL341aN5orDt13puXadsAf2rpuX12v3".getBytes("utf-8"));
+//            JSONObject jsonObjectResource = JSONUtil.parseObj(resource);
+////            String original_type = jsonObjectResource.getStr("original_type");//可能是支付业务的回调数据
+////            String algorithm = jsonObjectResource.getStr("algorithm");//可能是支付业务的回调数据
+//            String ciphertext = jsonObjectResource.getStr("ciphertext");//可能是支付业务的回调数据
+//            String associated_data = jsonObjectResource.getStr("associated_data");//可能是支付业务的回调数据
+//            String nonce = jsonObjectResource.getStr("nonce");//可能是支付业务的回调数据
+//            String decryptToString = aesUtil.decryptToString(
+//                    associated_data.getBytes("utf-8"),
+//                    nonce.getBytes("utf-8"),
+//                    ciphertext);
+//            log.info("微信电子发票回调接口....resource解密:"+decryptToString);
+//
+//            JSONObject parseObj = JSONUtil.parseObj(decryptToString);
+//            log.info("微信电子发票回调接口....resource解密-JSONObject:"+parseObj);
+//            String fapiao_apply_id = parseObj.getStr("fapiao_apply_id");
+//            log.info("微信电子发票回调接口....resource解密-fapiao_apply_id:"+fapiao_apply_id);
+//
+//        }
     }
 
 }
diff --git a/src/main/java/cc/mrbird/febs/pay/util/JCEUtil.java b/src/main/java/cc/mrbird/febs/pay/util/JCEUtil.java
new file mode 100644
index 0000000..1bb2695
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/pay/util/JCEUtil.java
@@ -0,0 +1,70 @@
+package cc.mrbird.febs.pay.util;
+
+import javax.crypto.Cipher;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.security.NoSuchAlgorithmException;
+import java.security.Permission;
+import java.security.PermissionCollection;
+import java.util.Map;
+
+/**
+ * 解决因jdk版本问题不支持aes256加密问题(Illegal key size or default parameters)
+ */
+public class JCEUtil {
+    public static void removeCryptographyRestrictions() {
+        if (!isRestrictedCryptography()) {
+            System.out.println("Cryptography restrictions removal not needed");
+            return;
+        }
+        try {
+            /*
+             * Do the following, but with reflection to bypass access checks:
+             *
+             * JceSecurity.isRestricted = false;
+             * JceSecurity.defaultPolicy.perms.clear();
+             * JceSecurity.defaultPolicy.add(CryptoAllPermission.INSTANCE);
+             */
+            final Class<?> jceSecurity = Class.forName("javax.crypto.JceSecurity");
+            final Class<?> cryptoPermissions = Class.forName("javax.crypto.CryptoPermissions");
+            final Class<?> cryptoAllPermission = Class.forName("javax.crypto.CryptoAllPermission");
+
+            final Field isRestrictedField = jceSecurity.getDeclaredField("isRestricted");
+            isRestrictedField.setAccessible(true);
+            //去除isRestricted的final限制
+            final Field modifiersField = Field.class.getDeclaredField("modifiers");
+            modifiersField.setAccessible(true);
+            modifiersField.setInt(isRestrictedField, isRestrictedField.getModifiers() & ~Modifier.FINAL);
+            isRestrictedField.set(null, false);
+
+            final Field defaultPolicyField = jceSecurity.getDeclaredField("defaultPolicy");
+            defaultPolicyField.setAccessible(true);
+            final PermissionCollection defaultPolicy = (PermissionCollection) defaultPolicyField.get(null);
+
+            final Field perms = cryptoPermissions.getDeclaredField("perms");
+            perms.setAccessible(true);
+            ((Map<?, ?>) perms.get(defaultPolicy)).clear();
+
+            final Field instance = cryptoAllPermission.getDeclaredField("INSTANCE");
+            instance.setAccessible(true);
+            defaultPolicy.add((Permission) instance.get(null));
+
+        } catch (final Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static boolean isRestrictedCryptography() {
+        // This matches Oracle Java 7 and 8, but not Java 9 or OpenJDK.
+        final String name = System.getProperty("java.runtime.name");
+        final String ver = System.getProperty("java.version");
+        return name != null && name.equals("Java(TM) SE Runtime Environment")
+                && ver != null && (ver.startsWith("1.7") || ver.startsWith("1.8"));
+    }
+
+    public static void main(String[] args) throws NoSuchAlgorithmException {
+//        removeCryptographyRestrictions();
+        long length = Cipher.getMaxAllowedKeyLength("AES/CBC/PKCS5Padding");
+        System.out.println(length);
+    }
+}
diff --git a/src/main/java/cc/mrbird/febs/pay/util/RequestBodyUtils.java b/src/main/java/cc/mrbird/febs/pay/util/RequestBodyUtils.java
new file mode 100644
index 0000000..eff106d
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/pay/util/RequestBodyUtils.java
@@ -0,0 +1,76 @@
+package cc.mrbird.febs.pay.util;
+
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.*;
+
+public class RequestBodyUtils {
+
+    private static final int BUFFER_SIZE = 1024 * 8;
+
+    /**
+     * read string.
+     *
+     * @param reader Reader instance.
+     * @return String.
+     * @throws IOException
+     */
+    public static String read(Reader reader) throws IOException {
+        try (StringWriter writer = new StringWriter()) {
+            write(reader, writer);
+            return writer.getBuffer().toString();
+        }
+    }
+
+    /**
+     * write.
+     *
+     * @param reader Reader.
+     * @param writer Writer.
+     * @return count.
+     * @throws IOException
+     */
+    public static long write(Reader reader, Writer writer) throws IOException {
+
+        return write(reader, writer, BUFFER_SIZE);
+    }
+
+    /**
+     * write.
+     *
+     * @param reader     Reader.
+     * @param writer     Writer.
+     * @param bufferSize buffer size.
+     * @return count.
+     * @throws IOException
+     */
+    public static long write(Reader reader, Writer writer, int bufferSize) throws IOException {
+
+        int read;
+        long total = 0;
+        char[] buf = new char[bufferSize];
+        while ((read = reader.read(buf)) != -1) {
+
+            writer.write(buf, 0, read);
+            total += read;
+        }
+        return total;
+    }
+
+    /**
+     * 获取requestBody
+     */
+    public static String getRequestBody() throws IOException {
+        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
+        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) attributes;
+        assert servletRequestAttributes != null;
+        HttpServletRequest request = servletRequestAttributes.getRequest();
+        BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
+        //读取输入流的内容转换为String类型IOUtils必须引入org.apache.dubbo.common.utils.IOUtils;包
+        return RequestBodyUtils.read(reader);
+    }
+
+}
diff --git a/src/main/resources/mapper/modules/MallInvoiceMapper.xml b/src/main/resources/mapper/modules/MallInvoiceMapper.xml
new file mode 100644
index 0000000..e717aa3
--- /dev/null
+++ b/src/main/resources/mapper/modules/MallInvoiceMapper.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cc.mrbird.febs.mall.mapper.MallInvoiceMapper">
+
+    <select id="selectApiMallInvoiceVoInPage" resultType="cc.mrbird.febs.mall.vo.ApiMallInvoiceVo">
+        select
+            a.id,
+            a.order_no,
+            a.amount,
+            a.goods_img,
+            a.state,
+            a.invoice_url
+        from mall_invoice a
+        <where>
+            <if test="record.memberId != null">
+                and a.member_id=#{record.memberId}
+            </if>
+            <if test="record.state != null">
+                and a.state=#{record.state}
+            </if>
+        </where>
+        order by a.created_time desc
+    </select>
+
+    <select id="selectByFapiaoApplyIdAndMemberId" resultType="cc.mrbird.febs.mall.entity.MallInvoice">
+        select
+            a.*
+        from mall_invoice a
+        where fapiao_apply_id = #{fapiaoApplyId}
+            and member_id = #{memberId}
+    </select>
+
+    <select id="getInvoiceListInPage" resultType="cc.mrbird.febs.mall.entity.MallInvoice">
+        select
+        a.*
+        from mall_invoice a
+        <where>
+            <if test="record.state != null">
+                and a.state = #{record.state}
+            </if>
+            <if test="record.orderNo != null and record.orderNo != ''">
+                and a.order_no = #{record.orderNo}
+            </if>
+            <if test="record.payOrderNo != null and record.payOrderNo != ''">
+                and a.fapiao_apply_id = #{record.payOrderNo}
+            </if>
+        </where>
+        order by a.created_time desc
+    </select>
+
+    <select id="getAdminInvoiceDetailVoById" resultType="cc.mrbird.febs.mall.vo.AdminInvoiceDetailVo">
+        select
+            a.*
+        from mall_invoice a
+        where id = #{id}
+    </select>
+
+    <select id="getInvoiceItemList" resultType="cc.mrbird.febs.mall.entity.MallOrderItem">
+        select
+        a.*,
+               b.unit
+        from mall_order_item a
+        left join mall_goods b on a.goods_id = b.id
+        where order_id = #{record.orderId}
+    </select>
+
+</mapper>
\ No newline at end of file
diff --git a/src/main/resources/mapper/modules/MallOrderInfoMapper.xml b/src/main/resources/mapper/modules/MallOrderInfoMapper.xml
index d082bf8..e1f180e 100644
--- a/src/main/resources/mapper/modules/MallOrderInfoMapper.xml
+++ b/src/main/resources/mapper/modules/MallOrderInfoMapper.xml
@@ -233,6 +233,10 @@
         select * from mall_order_info where status=#{status}
     </select>
 
+    <select id="selectOrderInfoByStatusAndIsInvoice" resultType="cc.mrbird.febs.mall.entity.MallOrderInfo">
+        select * from mall_order_info where status = #{status} and is_invoice = #{isInvoice}
+    </select>
+
     <select id="selectOrderInfoUpTime" resultType="cc.mrbird.febs.mall.entity.MallOrderInfo">
         select * from mall_order_info where status=#{status} and now() > date_add(order_time,interval 1440 minute)
     </select>
@@ -458,4 +462,8 @@
             </if>
         </where>
     </select>
+
+    <select id="selectBypayOrderNo" resultType="cc.mrbird.febs.mall.entity.MallOrderInfo">
+        select * from mall_order_info where pay_order_no = #{payOrderNo}
+    </select>
 </mapper>
\ No newline at end of file
diff --git a/src/main/resources/templates/febs/views/modules/order/invoiceDetail.html b/src/main/resources/templates/febs/views/modules/order/invoiceDetail.html
new file mode 100644
index 0000000..215ac03
--- /dev/null
+++ b/src/main/resources/templates/febs/views/modules/order/invoiceDetail.html
@@ -0,0 +1,248 @@
+<style>
+    #invoice-info {
+        padding: 20px 25px 25px 0;
+    }
+
+    #invoice-info .layui-treeSelect .ztree li a, .ztree li span {
+        margin: 0 0 2px 3px !important;
+    }
+    #invoice-info #data-permission-tree-block {
+        border: 1px solid #eee;
+        border-radius: 2px;
+        padding: 3px 0;
+    }
+    #invoice-info .layui-treeSelect .ztree li span.button.switch {
+        top: 1px;
+        left: 3px;
+    }
+
+</style>
+<div class="layui-fluid" id="invoice-info">
+    <blockquote class="layui-elem-quote blue-border">抬头信息</blockquote>
+    <form class="layui-form" action="" lay-filter="invoice-info-form">
+        <div class="layui-card">
+            <div class="layui-form-item febs-hide">
+                <label class="layui-form-label febs-form-item-require">id:</label>
+                <div class="layui-input-block">
+                    <input type="text" name="id">
+                </div>
+            </div>
+            <div class="layui-row layui-col-space10 layui-form-item">
+                <div class="layui-col-lg6">
+                    <label class="layui-form-label febs-form-item-require">开票类型:</label>
+                    <div class="layui-input-block">
+                        <input type="radio" name="type" value="INDIVIDUAL" title="个人" checked="" readonly>
+                        <input type="radio" name="type" value="ORGANIZATION" title="单位" readonly>
+                    </div>
+                </div>
+            </div>
+            <div class="layui-row layui-col-space10 layui-form-item">
+                <div class="layui-col-lg6">
+                    <label class="layui-form-label febs-form-item-require">名称:</label>
+                    <div class="layui-input-block">
+                        <input type="text" name="name" autocomplete="off" class="layui-input" readonly>
+                    </div>
+                </div>
+                <div class="layui-col-lg6">
+                    <label class="layui-form-label febs-form-item-require">识别号:</label>
+                    <div class="layui-input-block">
+                        <input type="text" name="taxpayerId" autocomplete="off" class="layui-input" readonly>
+                    </div>
+                </div>
+            </div>
+            <div class="layui-row layui-col-space10 layui-form-item">
+                <div class="layui-col-lg6">
+                    <label class="layui-form-label febs-form-item-require">地址:</label>
+                    <div class="layui-input-block">
+                        <input type="text" name="address" autocomplete="off" class="layui-input" readonly>
+                    </div>
+                </div>
+                <div class="layui-col-lg6">
+                    <label class="layui-form-label febs-form-item-require">电话:</label>
+                    <div class="layui-input-block">
+                        <input type="text" name="telephone" autocomplete="off" class="layui-input" readonly>
+                    </div>
+                </div>
+            </div>
+            <div class="layui-row layui-col-space10 layui-form-item">
+                <div class="layui-col-lg6">
+                    <label class="layui-form-label febs-form-item-require">开户银行:</label>
+                    <div class="layui-input-block">
+                        <input type="text" name="bankName" autocomplete="off" class="layui-input" readonly>
+                    </div>
+                </div>
+                <div class="layui-col-lg6">
+                    <label class="layui-form-label febs-form-item-require">银行账号:</label>
+                    <div class="layui-input-block">
+                        <input type="text" name="bankAccount" autocomplete="off" class="layui-input" readonly>
+                    </div>
+                </div>
+            </div>
+            <div class="layui-row layui-col-space10 layui-form-item">
+                <div class="layui-col-lg6">
+                    <label class="layui-form-label febs-form-item-require">订单编号:</label>
+                    <div class="layui-input-block">
+                        <input type="text" name="orderNo" autocomplete="off" class="layui-input" readonly>
+                    </div>
+                </div>
+                <div class="layui-col-lg6">
+                    <label class="layui-form-label febs-form-item-require">订单金额:</label>
+                    <div class="layui-input-block">
+                        <input type="text" name="amount" autocomplete="off" class="layui-input" readonly>
+                    </div>
+                </div>
+            </div>
+            <div class="layui-form-item">
+                <label class="layui-form-label febs-form-item-require">发票:</label>
+                <div class="layui-input-block">
+                    <div class="layui-upload">
+                        <button type="button" class="layui-btn layui-btn-normal layui-btn" id="test2">上传发票</button>
+                        <blockquote class="layui-elem-quote layui-quote-nm" style="margin-top: 10px;">
+                            <div class="layui-upload-list" id="demo2"></div>
+                        </blockquote>
+                        <div class="layui-word-aux">双击图片删除</div>
+                    </div>
+                </div>
+            </div>
+            <div class="layui-form-item febs-hide">
+                <label class="layui-form-label">发票链接:</label>
+                <div class="layui-input-block">
+                    <input type="text" id="invoiceUrl" lay-verify="required" name="invoiceUrl" autocomplete="off" class="layui-input" readonly>
+                </div>
+            </div>
+            <div class="layui-row layui-col-space10 layui-form-item" style="text-align: center;">
+                    <button  class="layui-btn layui-btn-normal" lay-submit=""
+                            lay-filter="invoice-detail-form-submit" id="submit">保存</button>
+            </div>
+        </div>
+    </form>
+</div>
+    <blockquote class="layui-elem-quote blue-border">订单详情</blockquote>
+    <div class="layui-card-body febs-table-full">
+        <table id="invoiceOrderItemTable" lay-filter="invoiceOrderItemTable"></table>
+    </div>
+
+<script data-th-inline="javascript">
+    layui.use(['upload','febs', 'form', 'formSelects', 'validate', 'treeSelect', 'table','eleTree'], function () {
+        var $ = layui.$,
+            febs = layui.febs,
+            layer = layui.layer,
+            table = layui.table,
+            formSelects = layui.formSelects,
+            treeSelect = layui.treeSelect,
+            form = layui.form,
+            eleTree = layui.eleTree,
+            invoiceDetail = [[${invoiceDetail}]],
+            $view = $('#invoice-info'),
+            validate = layui.validate,
+            upload = layui.upload,
+            _deptTree;
+
+        form.render();
+
+        table.render({
+            elem: '#invoiceOrderItemTable'
+            ,cols: [[ //表头
+                {field: 'skuName', title: '货物、应税劳务、服务名称', minWidth: 300,align:'left'},
+                {field: 'skuName', title: '规格型号', minWidth: 150,align:'left'},
+                {field: 'unit', title: '单位', minWidth: 100,align:'left'},
+                {field: 'cnt', title: '数量', minWidth: 100,align:'left'},
+                {field: 'price', title: '单价', minWidth: 100,align:'left'},
+                {field: 'amount', title: '金额', minWidth: 150,align:'left'}
+            ]]
+            ,data: []
+        });
+        dicDataReq("invoiceOrderItemTable");
+        function dicDataReq(type) {
+            $.get(ctx + 'admin/order/invoiceItemList/', function (r) {
+                if (r.code === 200) {
+                    var data = r.data;
+                    table.reload('invoiceOrderItemTable', {
+                        data : data
+                    });
+                }
+            });
+        }
+        // 表格初始化
+        initInvoiceDetailValue();
+        function initInvoiceDetailValue() {
+            if(invoiceDetail.invoiceUrl != null || invoiceDetail.invoiceUrl != ''){
+                $('#demo2').append('<img src="'+ invoiceDetail.invoiceUrl +'" class="layui-upload-img" style="width: 100px">')
+                $("#invoiceUrl").val(invoiceDetail.invoiceUrl);
+            }
+            form.val("invoice-info-form", {
+                "id": invoiceDetail.id,
+                "type": invoiceDetail.type,
+                "name": invoiceDetail.name,
+                "taxpayerId": invoiceDetail.taxpayerId,
+                "address": invoiceDetail.address,
+                "telephone": invoiceDetail.telephone,
+                "bankName": invoiceDetail.bankName,
+                "bankAccount": invoiceDetail.bankAccount,
+                "orderNo": invoiceDetail.orderNo,
+                "invoiceUrl": invoiceDetail.invoiceUrl,
+                "amount": invoiceDetail.amount
+            });
+        }
+
+        //图片上传
+        upload.render({
+            elem: '#test2'
+            ,url: ctx + 'admin/goods/uploadFileBase64' //改成您自己的上传接口
+            // ,multiple: true
+            ,accept: 'file'
+            ,before: function(obj){
+                //预读本地文件示例,不支持ie8
+                obj.preview(function(index, file, result){
+                    if ($("#invoiceUrl").val()) {
+                        $('#demo2').html('<img src="'+ result +'" alt="'+ file.name +'" class="layui-upload-img single-image" style="width: 130px">')
+                    } else {
+                        $('#demo2').append('<img src="'+ result +'" alt="'+ file.name +'" class="layui-upload-img single-image" style="width: 130px">')
+                    }
+                });
+            }
+            ,done: function(res){
+                var item = this.item;
+                //如果上传失败
+                if(res.code !== 0){
+                    return layer.msg('上传失败');
+                }
+                $("#invoiceUrl").val(res.data.src);
+                layer.msg('上传完毕', {icon: 1});
+                imgUnBind(".single-image");
+                imgSingleBind();
+            }
+            ,error: function(err){
+                return layer.msg('上传失败');
+            }
+        });
+
+        function imgUnBind(className) {
+            $(className).each(function() {
+                $(this).unbind('dblclick');
+            })
+        }
+
+        function imgSingleBind() {
+            $(".single-image").each(function(index, element) {
+                $(this).on("dblclick", function() {
+                    var imgThumb = $(".single-image")[index];
+                    $(imgThumb).remove();
+                    $("#invoiceUrl").val("");
+
+                    imgUnBind(".single-image");
+                    imgSingleBind();
+                });
+            })
+        }
+
+        form.on('submit(invoice-detail-form-submit)', function (data) {
+            febs.post(ctx + 'admin/order/addInvoiceUrl', data.field, function () {
+                layer.closeAll();
+                febs.alert.success('操作成功');
+                $('#febs-invoice').find('#query').click();
+            });
+            return false;
+        });
+    });
+</script>
\ No newline at end of file
diff --git a/src/main/resources/templates/febs/views/modules/order/invoiceList.html b/src/main/resources/templates/febs/views/modules/order/invoiceList.html
new file mode 100644
index 0000000..5a19dee
--- /dev/null
+++ b/src/main/resources/templates/febs/views/modules/order/invoiceList.html
@@ -0,0 +1,233 @@
+<div class="layui-fluid layui-anim febs-anim" id="febs-invoice" lay-title="发票管理">
+    <div class="layui-row febs-container">
+        <div class="layui-col-md12">
+            <div class="layui-card">
+                <div class="layui-card-body febs-table-full">
+                    <form class="layui-form layui-table-form" lay-filter="user-table-form">
+                        <div class="layui-form-item">
+                            <div class="layui-col-md10">
+                                <div class="layui-inline">
+                                    <label class="layui-form-label">编号:</label>
+                                    <div class="layui-input-inline">
+                                        <input type="text" placeholder="发票申请单号/微信支付订单号" name="payOrderNo" autocomplete="off" class="layui-input">
+                                    </div>
+                                </div>
+                                <div class="layui-inline">
+                                    <label class="layui-form-label">订单编号:</label>
+                                    <div class="layui-input-inline">
+                                        <input type="text" placeholder="订单编号" name="orderNo" autocomplete="off" class="layui-input">
+                                    </div>
+                                </div>
+                                <div class="layui-inline">
+                                    <label class="layui-form-label layui-form-label-sm">状态:</label>
+                                    <div class="layui-input-inline">
+                                        <select name="state">
+                                            <option value="">请选择</option>
+                                            <option value="0">待申请</option>
+                                            <option value="1">待开票</option>
+                                            <option value="2">已开票</option>
+                                        </select>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div class="layui-col-md2 layui-col-sm12 layui-col-xs12 table-action-area">
+                                <div class="layui-btn layui-btn-sm layui-btn-primary febs-button-blue-plain table-action" id="query">
+                                    <i class="layui-icon">&#xe848;</i>
+                                </div>
+                                <div class="layui-btn layui-btn-sm layui-btn-primary febs-button-green-plain table-action" id="reset">
+                                    <i class="layui-icon">&#xe79b;</i>
+                                </div>
+                            </div>
+                            <div class="layui-col-md10">
+                                <label class="layui-form-label layui-form-label-sm">注意:</label>
+                                <div class="layui-inline">
+                                    <div class="layui-form-mid layui-word-aux">
+                                        <span style="color:red;">订单状态【已完成】,才能开具发票。</span>
+                                    </div>
+                                    <div class="layui-form-mid layui-word-aux">
+                                        <span style="color:red;">开具发票之前,请先点击【更新抬头】按钮,确保用户提供了抬头信息。</span>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </form>
+                    <table lay-filter="invoiceTable" lay-data="{id: 'invoiceTable'}"></table>
+                    <style type="text/css">
+                        .layui-table cell{
+                            text-align:center;
+                            height: auto;
+                            white-space: nowrap; /*文本不会换行,在同一行显示*/
+                            overflow: hidden; /*超出隐藏*/
+                            text-overflow: ellipsis; /*省略号显示*/
+                        }
+                        .layui-table img{
+                            max-width:100px
+                        }
+
+                        ::-webkit-scrollbar {
+                            height: 20px !important;
+                            background-color: #f4f4f4;
+                        }
+                    </style>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<!-- 表格操作栏 start -->
+<script type="text/html" id="user-option">
+    <span shiro:lacksPermission="user:view,user:update,user:delete">
+        <span class="layui-badge-dot febs-bg-orange"></span> 无权限
+    </span>
+    <a lay-event="edit" shiro:hasPermission="user:update"><i
+            class="layui-icon febs-edit-area febs-blue">&#xe7a5;</i></a>
+</script>
+<!-- 表格操作栏 end -->
+<script data-th-inline="none" type="text/javascript">
+    // 引入组件并初始化
+    layui.use([ 'jquery', 'form', 'table', 'febs', 'formSelects', 'upload','laydate'], function () {
+        var $ = layui.jquery,
+            febs = layui.febs,
+            form = layui.form,
+            table = layui.table,
+            upload = layui.upload,
+            $view = $('#febs-invoice'),
+            $query = $view.find('#query'),
+            $reset = $view.find('#reset'),
+            $searchForm = $view.find('form'),
+            $add = $view.find('#add'),
+            sortObject = {field: 'orderTime', type: 'desc'},
+            formSelects = layui.formSelects,
+            laydate = layui.laydate,
+            tableIns;
+
+        let currPageOrder = 1;//首先默认值为1,防止出错
+        //获取当前页
+        currPageOrder = $view.find(".layui-laypage-em").next().html();
+
+        form.render();
+
+        formSelects.render();
+
+        // 表格初始化
+        initInvoiceTable();
+
+        // 初始化表格操作栏各个按钮功能
+        table.on('tool(invoiceTable)', function (obj) {
+            var data = obj.data,
+                layEvent = obj.event;
+            if (layEvent === 'seeInvoiceDetail') {
+                febs.modal.open('开票信息', 'modules/order/invoiceDetail/'+ data.id, {
+                    area:['100%','100%'],
+                });
+            }
+            if (layEvent === 'updateInvoiceDetail') {
+                febs.modal.confirm('更新', '是否更新用户的抬头信息?', function () {
+                    updateInvoiceDetail(data.id);
+                });
+            }
+        });
+
+        function updateInvoiceDetail(id) {
+            febs.get(ctx + 'admin/order/updateInvoiceDetail/' + id, null, function () {
+                febs.alert.success('操作成功');
+                $query.click();
+            });
+        }
+
+        // 查询按钮
+        $query.on('click', function () {
+            var params = $.extend(getQueryParams(), {field: sortObject.field, order: sortObject.type});
+            tableIns.reload({where: params, page: {curr: currPageOrder}});
+        });
+
+        // 刷新按钮
+        $reset.on('click', function () {
+            $searchForm[0].reset();
+            sortObject.type = 'null';
+            tableIns.reload({where: getQueryParams(), page: {curr: currPageOrder}, initSort: sortObject});
+        });
+
+        function initInvoiceTable() {
+            tableIns = febs.table.init({
+                elem: $view.find('table'),
+                id: 'invoiceTable',
+                url: ctx + 'admin/order/invoiceList',
+                defaultToolbar: [],
+                //系统自带打印导出
+                totalRow : true,
+                cols: [[
+                        {field: 'orderNo', title: '订单编号', minWidth: 100,align:'left' ,totalRowText:"合计"},
+                        {field: 'fapiaoApplyId', title: '发票申请单号', minWidth: 150,align:'left'},
+                        {field: 'type', title: '购买方类型',
+                            templet: function (d) {
+                                if (d.type === 'INDIVIDUAL') {
+                                    return '<span style="color:red;">个人</span>'
+                                } else if (d.type === 'ORGANIZATION') {
+                                    return '<span style="color:green;">单位</span>'
+                                }else{
+                                    return ''
+                                }
+                            }, minWidth: 80,align:'center'},
+                        {field: 'name', title: '名称', minWidth: 120,align:'left'},
+                        {field: 'taxpayerId', title: '纳税人识别号', minWidth: 100,align:'left'},
+                        {field: 'address', title: '地址', minWidth: 200,align:'left'},
+                        {field: 'telephone', title: '电话', minWidth: 100,align:'left'},
+                        {field: 'bankName', title: '开户银行', minWidth: 100,align:'left', totalRow:true},
+                        {field: 'bankAccount', title: '银行账号', minWidth: 100,align:'left', totalRow:true},
+                        {field: 'state', title: '状态',
+                            templet: function (d) {
+                                if (d.state === 0) {
+                                    return '<span style="color:red;">待申请</span>'
+                                } else if (d.state === 1) {
+                                    return '<span style="color:green;">待开票</span>'
+                                }else if (d.state === 2) {
+                                    return '<span style="color:green;">已开票</span>'
+                                }else{
+                                    return ''
+                                }
+                            }, minWidth: 100,align:'center'},
+                    {field: 'status', title: '订单状态',
+                        templet: function (d) {
+                            if (d.status === 1) {
+                                return '<span style="color:red;">待支付</span>'
+                            } else if (d.status === 2) {
+                                return '<span style="color:green;">待发货</span>'
+                            }else if (d.status === 3) {
+                                return '<span style="color:green;">待收货</span>'
+                            }else if (d.status === 4) {
+                                return '<span style="color:green;">已完成</span>'
+                            }else if (d.status === 5) {
+                                return '<span style="color:green;">退款中</span>'
+                            }else if (d.status === 6) {
+                                return '<span style="color:green;">已退款</span>'
+                            }else if (d.status === 7) {
+                                return '<span style="color:green;">已取消</span>'
+                            }else{
+                                return ''
+                            }
+                        }, minWidth: 100,align:'center'},
+                        {title: '操作',
+                            templet: function (d) {
+                                if(d.status === 4){
+                                    return '<button class="layui-btn layui-btn-normal layui-btn-xs" lay-event="seeInvoiceDetail" shiro:hasPermission="user:update">开票信息</button>' +
+                                        '<button class="layui-btn layui-btn-normal layui-btn-xs" lay-event="updateInvoiceDetail" shiro:hasPermission="user:update">更新抬头</button>'
+                                }else{
+                                    return ''
+                                }
+                            },minWidth: 300,align:'center', fixed:'right'}
+                ]]
+            });
+        }
+
+        // 获取查询参数
+        function getQueryParams() {
+            return {
+                orderNo: $searchForm.find('input[name="orderNo"]').val().trim(),
+                payOrderNo: $searchForm.find('input[name="payOrderNo"]').val().trim(),
+                state: $searchForm.find("select[name='state']").val(),
+            };
+        }
+    })
+</script>
\ No newline at end of file
diff --git a/src/main/resources/wxP12/apiclient_key.pem b/src/main/resources/wxP12/apiclient_key.pem
new file mode 100644
index 0000000..a754832
--- /dev/null
+++ b/src/main/resources/wxP12/apiclient_key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDfoX/j+3GsXcRf
+vVGaTBtewpzyKhkJPXK/QElmEAaGFyjX3PkevbXm5HiL1HXtZnewaLnLOrBiiPyP
+PRuuRIyx5Yx7OK2MWGtzj5pByYnvAqWJD048kYdW2sMuF17ARlj+DzRxNNmySCZF
+EFI0QKJTz4CQTcYHPOJQXPR5kCzWXgueIWowdUQCRWQZM3xB8l0nhMA3dqMQjZXK
+cYbISrNfU5X0aynHhhwozk1onQG19a1o2mF+FCIpqViis6iJ0rVi9NRax+9mvA1x
+CAyqAmw2XrYd+uNbAHpxMLDr8A4Nu+e9U9I7Ns3HPuPkGUITTIdShfQWIdKYy+Ye
+tx5oTtnjAgMBAAECggEAD9feOqWeuQpvliTQejD4I2+ANPvciDYNQhVo3c022Rsc
+7qYeFBp2n6QCEk38CNSgmCPjPc2rn197HR7ELkV2/kd3+2bZjL7a5fX/hLeliQRT
++/DUpx/cSb/34fUP5mlVdrJXEcofviy0YWI1oLQPjAaAYjjQVcTvd6gOPeqD8q/X
+Fa3glF/bpm7xNJay7FSaSDywtvaDnhJmTo1norJNV0Sg7Gz5x0eKGa+kW/rm1z8Z
+kumhEdaK+yl4mZ47hz/R6xbUyh7BV3xLekC49nAduGgCfU8TQm1Gq8eQdxjmtJeJ
+WM7zFy2/1/2ZRMfJ9zdh4B/jiosmikpyxc5tCQIvaQKBgQDwXJascXQ7bLa6Pzmq
+Ddby3v+xTJBOsB/NbI275kHEMCNc3ExAhnbwrPcrWyRsWR7mKPClTiK2drFfbo7o
+vqEi+pC5vuShxv55riK7ehIcIttG9H0iKFMapJKYsF199JpGoYKyQ8pPtokdd3eH
+xYzc65S0/bW9gQBgixs0/DG9dQKBgQDuLj8GLpOkDcckuNgMl31cAv5qvUn8ICNv
+/DQPsCO6jWs1c8N7ru3mxVoUkKCDKMsLSak535Mzskg154M+236y8YesgOVfeZse
+ZmueUJNJ0STFKGmZMjTlHe5qWZoShknwC6U91/MUfiHUMAiERPZ3bYRCb1WxN0da
+/zmcl8UW9wKBgQDYlbW3oWvo+CcXYE1nrJzZsJOagbEvFokxo/V7MRpl3DKhRGj5
+Y7DdYh4+1RvW/d9X6eAeNHAXFpDxuz/O/adZS4sJtLd0B0na/0yQvoh/DfGk2D3B
+f7CSu2TVbWZxTu2NY4/PbkJo3cRj5viwQRSTJTyUcmFHZ2ydkftsHSv82QKBgQCZ
+uD0yxzLAaagCWk4N6mOc083Ro/MxBqj4aIRNL7gPeaAcsmcS1zZxfyOBLEp7/AL3
+eQX/9PyzG7ghGhcJpW3jHaEbw2DGOtzTRPTmC0jUFmgt2sQ0fEPJL4UlHEkk9YiY
+pVtzS4F102YP5SRKAZw0PfljSym6I1wpLZnIJ+eqxwKBgQDEtVO2Nja7AVl0n99Q
+DXW/mrtRZ6ToROXmzcJMiMEN24zstitaB+s01QAasz1VwdFZkZLBHFBzekBSkCJa
+xiwZOQXJhzMSL9MYGLTXo/Jhqlsb03923zO45ZFJKnlY7T0W33G0oEfCyAVsXEMM
+nemv1UBmk/fW7KrS2Eg46wanwg==
+-----END PRIVATE KEY-----
diff --git a/src/test/java/cc/mrbird/febs/ProfitTest.java b/src/test/java/cc/mrbird/febs/ProfitTest.java
index 9ee9405..8a8d677 100644
--- a/src/test/java/cc/mrbird/febs/ProfitTest.java
+++ b/src/test/java/cc/mrbird/febs/ProfitTest.java
@@ -4,6 +4,7 @@
 import cc.mrbird.febs.common.enumerates.FlowTypeEnum;
 import cc.mrbird.febs.common.enumerates.MoneyFlowTypeEnum;
 import cc.mrbird.febs.common.exception.FebsException;
+import cc.mrbird.febs.common.utils.AppContants;
 import cc.mrbird.febs.common.utils.HttpCurlUtil;
 import cc.mrbird.febs.common.utils.MallUtils;
 import cc.mrbird.febs.common.utils.RedisUtils;
@@ -11,19 +12,26 @@
 import cc.mrbird.febs.mall.entity.*;
 import cc.mrbird.febs.mall.mapper.*;
 import cc.mrbird.febs.mall.service.*;
+import cc.mrbird.febs.mall.vo.MallGoodsListVo;
 import cc.mrbird.febs.mall.vo.MallMemberCouponVo;
-import cc.mrbird.febs.pay.model.BrandWCPayRequestData;
-import cc.mrbird.febs.pay.model.HeaderDto;
+import cc.mrbird.febs.pay.model.*;
 import cc.mrbird.febs.pay.service.IXcxPayService;
 import cc.mrbird.febs.pay.service.WxFaPiaoService;
+import cc.mrbird.febs.pay.util.JCEUtil;
 import cc.mrbird.febs.pay.util.WechatConfigure;
 import cc.mrbird.febs.rabbit.consumer.AgentConsumer;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateUtil;
+import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier;
+import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
+import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
+import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
+import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
 import okhttp3.HttpUrl;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.httpclient.HttpClient;
@@ -35,15 +43,19 @@
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
 import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPatch;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.core.io.ResourceLoader;
+import org.springframework.util.Base64Utils;
 import org.springframework.web.client.RestTemplate;
 
 import javax.annotation.Resource;
@@ -51,7 +63,12 @@
 import java.math.BigDecimal;
 import java.net.URLConnection;
 import java.nio.charset.Charset;
-import java.security.KeyPair;
+import java.nio.charset.StandardCharsets;
+import java.security.*;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.util.*;
 import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
@@ -105,77 +122,238 @@
     private WxFaPiaoService wxFaPiaoService;
     @Autowired
     ResourceLoader resourceLoader;
+
     @Test
-    public void rankProfit() throws IOException {
+    public void redisTest() throws IOException {//配置开发选项
+        String obj = redisUtils.getString("mall_goods_json");//获取JSONARRAY字符串对象
+        System.out.println(obj);
+        JSONArray jsonArray = JSONUtil.parseArray(obj);//转换成JSONARRAY对象
+        List<MallGoodsListVo> mallGoodsListVos = JSONUtil.toList(jsonArray, MallGoodsListVo.class);//转换成对象集合
+        System.out.println(mallGoodsListVos.size());
+
+    }
+    @Test
+    public void rankProfit() throws IOException {//配置开发选项
 //        System.out.println(new ClassPathResource("wxP12/apiclient_cert.p12").getFile().exists());
 //        System.out.println(new File("src/main/resources/wxP12/apiclient_cert.p12").exists());
 //
 //        InputStream inputStream = new FileInputStream(file);
 //        System.out.println(resourceLoader.getResource("classpath:/wxP12/apiclient_cert.p12").exists());
-        KeyPair privateKey = wxFaPiaoService.getPrivateKey();
+        PrivateKey privateKey = wxFaPiaoService.getPrivateKeyV3();
         HeaderDto headerDto = new HeaderDto();
         headerDto.setCallback_url("https://api.blnka.cn/api/xcxPay/fapiaoCallBack");
-        headerDto.setShow_fapiao_cell(true);
+        headerDto.setShow_fapiao_cell(false);
         String parseObj = JSONUtil.parseObj(headerDto).toString();
         String baseUrl = "https://api.mch.weixin.qq.com";
         String canonicalUrl = "/v3/new-tax-control-fapiao/merchant/development-config";
-        String postStr = wxFaPiaoService.createAuthorization(
-                "POST",
-                canonicalUrl,
-                parseObj,
-                privateKey
-
-        );
-        // 创建httppost
+        String postStr = null;
         try {
-            HttpClient httpClient = new HttpClient();
-            PostMethod post = new PostMethod("https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/merchant/development-config");
-            post.setRequestHeader("Accept", "application/json");
-            post.setRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36");
-            post.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
-            post.setRequestHeader("Connection", "keep-alive");
-            post.setRequestHeader("Authorization",  "WECHATPAY2-SHA256-RSA2048 "+postStr);
-            RequestEntity entity = new StringRequestEntity(parseObj, "text/html", "utf-8");
-            post.setRequestEntity(entity);
-            httpClient.executeMethod(post);
-            String responseBodyAsString = post.getResponseBodyAsString();
-            cn.hutool.json.JSONObject maps = JSONUtil.parseObj(responseBodyAsString);
-            System.out.println(maps);
-        } catch (IOException e) {
+            postStr = wxFaPiaoService.createAuthorization(
+                    "PATCH",
+                    baseUrl+canonicalUrl,
+                    parseObj,
+                    privateKey
+            );
+        } catch (NoSuchAlgorithmException e) {
             e.printStackTrace();
         }
+
+        String token = AppContants.FP_TOKEN_HEADER_TYPE+postStr;
+        System.out.println("WECHATPAY2-SHA256-RSA2048 "+postStr);
+        String s = wxFaPiaoService.sendPatch(baseUrl + canonicalUrl, parseObj, token);
+        System.out.println(s);
+    }
+
+    @Test
+    public void rankProfit03() throws IOException {//创建电子发票卡券模板
+        FPCardTemplateDto fpCardTemplateDto = new FPCardTemplateDto();
+        fpCardTemplateDto.setCard_appid("wxad2fdb2fcad10fb2");
+
+        FPCardTemplateInformationDto fpCardTemplateInformationDto = new FPCardTemplateInformationDto();
+        fpCardTemplateInformationDto.setLogo_url("http://mmbiz.qpic.cn/mmbiz/iaL1LJM1mF9aRKPZJkmG8xXhiaHqkKSVMMWeN3hLut7X7hicFNjakmxibMLGWpXrEXB33367o7zHN0CwngnQY7zb7g/0");
+        fpCardTemplateInformationDto.setPayee_name("发票测试");
+
+        FPCardTemplateCustomCellDto fpCardTemplateCustomCellDto = new FPCardTemplateCustomCellDto();
+        fpCardTemplateCustomCellDto.setWords("电子发票");
+        fpCardTemplateCustomCellDto.setDescription("查看发票");
+        fpCardTemplateCustomCellDto.setJump_url("http://www.qq.com");
+        fpCardTemplateCustomCellDto.setMiniprogram_path("gh_86a091e50ad4@app");
+        fpCardTemplateCustomCellDto.setMiniprogram_user_name("pages/xxxPage");
+        fpCardTemplateInformationDto.setCustom_cell(fpCardTemplateCustomCellDto);
+
+        fpCardTemplateDto.setCard_template_information(fpCardTemplateInformationDto);
+
+        String parseObj = JSONUtil.parseObj(fpCardTemplateDto).toString();
+        System.out.println(parseObj);
+        String baseUrl = "https://api.mch.weixin.qq.com";
+        String canonicalUrl = "/v3/new-tax-control-fapiao/card-template";
+        PrivateKey privateKey = wxFaPiaoService.getPrivateKeyV3();
+        String postStr = null;
+        try {
+            postStr = wxFaPiaoService.createAuthorization(
+                    "POST",
+                    baseUrl+canonicalUrl,
+                    parseObj,
+                    privateKey
+            );
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        }
+        String token = AppContants.FP_TOKEN_HEADER_TYPE+postStr;
+
+        System.out.println("WECHATPAY2-SHA256-RSA2048 "+postStr);
+        String s = wxFaPiaoService.sendPost(baseUrl + canonicalUrl, parseObj, "WECHATPAY2-SHA256-RSA2048 "+postStr);
+        System.out.println(s);
+    }
+
+
+
+    @Test
+    public void rankProfit04() throws IOException {//获取用户填写的抬头
+        //初始化请求参数
+        Map<String, Object> params = new HashMap<>();
+        params.put("scene","WITH_WECHATPAY");
+        params.put("fapiao_apply_id","4200002059202401191880748568");
+
+        String baseUrl = "https://api.mch.weixin.qq.com";
+        String canonicalUrl = "/v3/new-tax-control-fapiao/user-title";
+
+        String urlparm = baseUrl+canonicalUrl;//有参数的get请求,加密是需要把参数加载进去URL
+        List<BasicNameValuePair> parameters = new ArrayList<>();
+        for (Map.Entry<String, Object> entry : params.entrySet()) {
+            parameters.add(new BasicNameValuePair(entry.getKey(), entry.getValue().toString()));
+        }
+        StringBuilder dataparm = new StringBuilder();
+        params.forEach((k, v) -> dataparm.append("&" + k + "=" + v));
+        String string = dataparm.toString();
+        if (!"".equals(string)) {
+            urlparm = baseUrl+canonicalUrl + "?" + dataparm.substring(1);
+        }
+        PrivateKey privateKey = wxFaPiaoService.getPrivateKeyV3();
+        String postStr = null;
+        try {
+            postStr = wxFaPiaoService.createAuthorization(
+                    "GET",
+                    urlparm,
+                    "",
+                    privateKey
+            );
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        }
+        String s = wxFaPiaoService.sendGet(baseUrl+canonicalUrl, params, "WECHATPAY2-SHA256-RSA2048 "+postStr);
+        System.out.println(s);
+    }
+
+    @Test
+    public void rankProfit2() throws IOException {//查询配置开发选项
+        PrivateKey privateKey = wxFaPiaoService.getPrivateKeyV3();
+        String baseUrl = "https://api.mch.weixin.qq.com";
+        String canonicalUrl = "/v3/new-tax-control-fapiao/merchant/development-config";
+        String postStr = null;
+        try {
+            postStr = wxFaPiaoService.createAuthorization(
+                    "GET",
+                    baseUrl+canonicalUrl,
+                    "",
+                    privateKey
+
+            );
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        }
+        System.out.println("WECHATPAY2-SHA256-RSA2048"+postStr);
+        String s = wxFaPiaoService.sendGet(baseUrl + canonicalUrl, new HashMap<>(), "WECHATPAY2-SHA256-RSA2048 "+postStr);
+
+        System.out.println(s);
+//        try {
+//            HttpClient httpClient = new HttpClient();
+//            GetMethod method = new GetMethod(baseUrl+canonicalUrl);
+//            method.setRequestHeader("Accept", "application/json");
+//            method.setRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36");
+//            method.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
+//            method.setRequestHeader("Connection", "keep-alive");
+//            method.setRequestHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 "+postStr);
+//            httpClient.executeMethod(method);
+//            System.out.println(method);
+//            String responseBodyAsString = method.getResponseBodyAsString();
+//            cn.hutool.json.JSONObject maps = JSONUtil.parseObj(responseBodyAsString);
+//            System.out.println(maps);
+//        } catch (IOException e) {
+//            e.printStackTrace();
+//        }
 
     }
 
     @Test
-    public void rankProfit2() throws IOException {
-        KeyPair privateKey = wxFaPiaoService.getPrivateKey();
+    public void rankProfit2_01() throws IOException, ParseException, GeneralSecurityException {//获取平台证书
+        JCEUtil.removeCryptographyRestrictions();
+        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+        PrivateKey privateKey = wxFaPiaoService.getPrivateKeyV3();
         String baseUrl = "https://api.mch.weixin.qq.com";
-        String canonicalUrl = "/v3/new-tax-control-fapiao/merchant/development-config";
-        String postStr = wxFaPiaoService.createAuthorization(
-                "GET",
-                canonicalUrl,
-                "",
-                privateKey
-
-        );
+        String canonicalUrl = "/v3/certificates";
+        String postStr = null;
         try {
-            HttpClient httpClient = new HttpClient();
-            GetMethod method = new GetMethod(baseUrl+canonicalUrl);
-            method.setRequestHeader("Accept", "application/json");
-            method.setRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36");
-            method.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
-            method.setRequestHeader("Connection", "keep-alive");
-            method.setRequestHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 "+postStr);
-            httpClient.executeMethod(method);
-            System.out.println(method);
-            String responseBodyAsString = method.getResponseBodyAsString();
+            postStr = wxFaPiaoService.createAuthorization(
+                    "GET",
+                    baseUrl+canonicalUrl,
+                    "",
+                    privateKey
+
+            );
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        }
+        String responseBodyAsString = wxFaPiaoService.sendGet(baseUrl + canonicalUrl, new HashMap<>(), "WECHATPAY2-SHA256-RSA2048 "+postStr);
+        //完成签名并执行请求
+        X509Certificate x509Certificate = null;
+        try {
             cn.hutool.json.JSONObject maps = JSONUtil.parseObj(responseBodyAsString);
             System.out.println(maps);
+
+            FPCertificateVo certificateVo = com.alibaba.fastjson.JSONObject.parseObject(responseBodyAsString, FPCertificateVo.class);
+            for (FPCertificates certificates : certificateVo.getData()) {
+                if (format.parse(certificates.getEffective_time()).before(new Date())
+                        && format.parse(certificates.getExpire_time()).after(new Date())) {
+                    FPEncryptCertificate encrypt_certificate = certificates.getEncrypt_certificate();
+                    //解密
+                    AesUtil aesUtil = new AesUtil("daL341aN5orDt13puXadsAf2rpuX12v3".getBytes("utf-8"));
+                    String pulicKey = aesUtil.decryptToString(
+                            encrypt_certificate.getAssociated_data().getBytes("utf-8"),
+                            encrypt_certificate.getNonce().getBytes("utf-8"),
+                            encrypt_certificate.getCiphertext());
+                    //获取平台证书
+                    final CertificateFactory cf = CertificateFactory.getInstance("X509");
+
+                    ByteArrayInputStream inputStream = new ByteArrayInputStream(pulicKey.getBytes(StandardCharsets.UTF_8));
+
+                    x509Certificate = (X509Certificate) cf.generateCertificate(inputStream);
+                    System.out.println(x509Certificate);
+                }
+            }
         } catch (IOException e) {
             e.printStackTrace();
         }
-
+        String timestamp = "1705548628";
+        String nonce = "sJBCGwYIM8WFJCpTE3gDGifC6VOyrPFY";
+        String body = "{\"id\":\"f23942b4-b128-56ef-9172-2da8204b5159\",\"create_time\":\"2024-01-18T11:30:28+08:00\",\"resource_type\":\"encrypt-resource\",\"event_type\":\"FAPIAO.USER_APPLIED\",\"summary\":\"用户已申请开票\",\"resource\":{\"original_type\":\"fapiao\",\"algorithm\":\"AEAD_AES_256_GCM\",\"ciphertext\":\"X7XjgHrepsnVpFqNgjl9EfRbJIdMUf15izFrtnvEf7So3SWlg6CFNofcQAJE+iL1XfkFk7DNNW0rILNbY1cGWGlAlKbbPhv5SFEakZVe9GZsSTd4EC5zTk9D6g+FTS6EQNxCkj4ut2WyYaSSqJjHgYaqdKzLNPSTGRVQnynafFE=\",\"associated_data\":\"fapiao\",\"nonce\":\"X7daAHdejNsm\"}}";
+        Map<String, Object> requestBody = JSONUtil.parseObj(body);
+        String signature = "WECHATPAY/SIGNTEST/9WwfXW/noMdzDPcOrFD51Bf5YIThXLTtLCl5hoGlSfIBdY2UpE+5eHLs0XyF7y2cQc4OlzTzDDxSMTp/p/8ZenE2hMBaQjxVrdbTtrOLA1h13/WjCNaTr1URTqQ5+IF5bLCxAW8BmZ0jEqpmI/HYR3wdK8/7W91c1zfKODYgkvJzxFzd8OWi2GtELE5tLamkOLyb0GEcsGww2DUOypO5HjNyITgsy9R00w3OH92UInCD8Z6c5BAsQNySqFK8N52y38AlIWTtKROHmJtu+kHbt+nMOU0kbB5bZcPAptsnVYpp9KMmjMrmyjiKtHZb2TGIWiN/L+bgceyN3g==";
+        //构造验签名串
+        String signatureStr = timestamp + "\n" + nonce + "\n" + com.alibaba.fastjson.JSONObject.toJSONString(requestBody) + "\n";
+        System.out.println(signature.length());
+        System.out.println(signature);
+        System.out.println(signatureStr.length());
+        System.out.println(signatureStr);
+        // 加载SHA256withRSA签名器
+        Signature signer = Signature.getInstance("SHA256withRSA");
+        // 用微信平台公钥对签名器进行初始化(调上一节中的获取平台证书方法)
+        signer.initVerify(x509Certificate);
+        // 把我们构造的验签名串更新到签名器中
+        signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
+        // 把请求头中微信服务器返回的签名用Base64解码 并使用签名器进行验证
+        boolean result = signer.verify(Base64Utils.decodeFromString(signature));
+        System.out.println(result);
     }
 
 

--
Gitblit v1.9.1