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> 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";//证书编号 } 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); } } 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") src/main/java/cc/mrbird/febs/mall/controller/ApiMallInvoiceController.java
New file @@ -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); } } 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"); } } src/main/java/cc/mrbird/febs/mall/dto/AdminMallInvoiceDto.java
New file @@ -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; } src/main/java/cc/mrbird/febs/mall/dto/ApiMallInvoiceDto.java
New file @@ -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; } src/main/java/cc/mrbird/febs/mall/entity/MallInvoice.java
New file @@ -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路径 } 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; 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; } src/main/java/cc/mrbird/febs/mall/mapper/MallInvoiceMapper.java
New file @@ -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); } 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); } 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(); } } src/main/java/cc/mrbird/febs/mall/service/MallInvoiceService.java
New file @@ -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); } src/main/java/cc/mrbird/febs/mall/service/impl/MallInvoiceServiceImpl.java
New file @@ -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(); } } src/main/java/cc/mrbird/febs/mall/vo/AdminInvoiceDetailVo.java
New file @@ -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;//订单编号 } src/main/java/cc/mrbird/febs/mall/vo/ApiMallInvoiceVo.java
New file @@ -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; } 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()); src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateCustomCellDto.java
New file @@ -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; } src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateDto.java
New file @@ -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; } src/main/java/cc/mrbird/febs/pay/model/FPCardTemplateInformationDto.java
New file @@ -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; } src/main/java/cc/mrbird/febs/pay/model/FPCertificateVo.java
New file @@ -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; } src/main/java/cc/mrbird/febs/pay/model/FPCertificates.java
New file @@ -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; } src/main/java/cc/mrbird/febs/pay/model/FPEncryptCertificate.java
New file @@ -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; } src/main/java/cc/mrbird/febs/pay/model/FPUserTitleDto.java
New file @@ -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; } 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; } 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); // // } } } src/main/java/cc/mrbird/febs/pay/util/JCEUtil.java
New file @@ -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); } } src/main/java/cc/mrbird/febs/pay/util/RequestBodyUtils.java
New file @@ -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); } } src/main/resources/mapper/modules/MallInvoiceMapper.xml
New file @@ -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> 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> src/main/resources/templates/febs/views/modules/order/invoiceDetail.html
New file @@ -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> src/main/resources/templates/febs/views/modules/order/invoiceList.html
New file @@ -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"></i> </div> <div class="layui-btn layui-btn-sm layui-btn-primary febs-button-green-plain table-action" id="reset"> <i class="layui-icon"></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"></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> src/main/resources/wxP12/apiclient_key.pem
New file @@ -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----- 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); }